Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions Assets/Tests/Editor/HotReload/HotReloadPausePointContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,79 @@ public async Task Enable_OnHotReloadedLocalFunctionBody_CapturesInstanceAsThis()
FormatCaptured(status));
}

/// <summary>
/// What: a pause point inside a local function of a hot-reloaded method, where a lambda
/// capturing the loop variable turns the loop scope into a closure class, captures the
/// instance as "this" with its fields and the variables of every enclosing scope, although
/// the instance and the method parameter travel in a by-ref closure struct that the loop
/// closure class does not link to.
/// </summary>
[Test]
public async Task Enable_OnHotReloadedLocalFunctionOfLoopClosure_CapturesInstanceAndEnclosingScopes()
{
string onDisk = File.ReadAllText(ResolveFixtureAbsolutePath());
const string compiledBody =
"public int ComputeWithPrivate(int delta)\n {\n"
+ " return _secret + delta;\n"
+ " }";
Assert.That(onDisk, Does.Contain(compiledBody), "Precondition: ComputeWithPrivate body must exist.");
string editedSource = onDisk.Replace(
compiledBody,
"public int ComputeWithPrivate(int delta)\n {\n"
+ " int total = 0;\n"
+ " for (int j = 0; j < 1; j++)\n {\n"
+ " Func<int> read = () => j;\n"
+ " int Local()\n {\n"
+ " int loopSum = _secret + delta + j;\n"
+ " return loopSum;\n"
+ " }\n\n"
+ " total += Local() + read();\n"
+ " }\n\n"
+ " return total;\n"
+ " }",
StringComparison.Ordinal);
int enableLine = FindLineNumber(editedSource, "return loopSum;");
Assert.That(enableLine, Is.GreaterThan(0));

await HotReloadFromEditedSourceAsync(editedSource, "ContractLoopClosureLocalFunction.cs");

PausePointResponse enable = new PausePointUseCase().Enable(new EnablePausePointSchema
{
File = FixtureProjectRelativePath,
Line = enableLine,
TimeoutSeconds = 30,
Mode = UloopPausePointCaptureMode.Continuous
});
Assert.That(enable.Success, Is.True, enable.Message + " / " + enable.RecommendedNextAction);
Assert.That(enable.RetargetedToHotReloadPatch, Is.True);
Assert.That(enable.ResolvedLine, Is.GreaterThan(0));

HotReloadE2EFixture fixture = new HotReloadE2EFixture();
Assert.That(fixture.ComputeWithPrivate(5), Is.EqualTo(fixture.SecretForAssert + 5));

UloopPausePointSnapshot status = UloopPausePointRegistry.GetStatus(enable.Id);
Assert.That(status.IsHit, Is.True);
Assert.That(status.CapturedVariables.Any(v => v.Name == "this"), Is.True, FormatCaptured(status));
Assert.That(
status.CapturedVariables.Any(v => v.Name == "_secret" && v.Scope == UloopCapturedVariableScope.InstanceField),
Is.True,
FormatCaptured(status));
Assert.That(
status.CapturedVariables.Any(v => v.Name == "delta" && v.Value == "5"),
Is.True,
FormatCaptured(status));
Assert.That(
status.CapturedVariables.Any(v => v.Name == "j" && v.Value == "0"),
Is.True,
FormatCaptured(status));
Assert.That(
status.CapturedVariables.Any(v => v.Name == HotReloadShimMethodLookup.ShimReceiverParameterName),
Is.False,
FormatCaptured(status));
// Read from the registry: the enable response turns an empty list into null.
Assert.That(status.NotCapturableVariables, Is.Empty);
}

/// <summary>
/// What: RevertAll after enable on a hot-reloaded async body restores instrumentation
/// onto the compiled MoveNext and hits with the original (non-edited) return value.
Expand Down
8 changes: 4 additions & 4 deletions Assets/Tests/Editor/PausePointToolModeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ public void Hit_WhenMarkerHasMaxPreviewElementsOverride_UsesOverrideForCollectio
List<int> values = Enumerable.Range(0, maxPreviewElements + 5).ToList();

SourcePausePointCapture.Capture(
"jump", null, Array.Empty<object>(), new object[] { "scores", values });
"jump", null, Array.Empty<object>(), Array.Empty<object>(), new object[] { "scores", values });

UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus("jump");
Assert.That(snapshot.CapturedVariables.Single().Value.Split(',').Length, Is.EqualTo(maxPreviewElements));
Expand All @@ -314,7 +314,7 @@ public void Capture_WhenHitWhenDoesNotMatch_SkipsCaptureAndRecordsSkipCount()
parseResult.Condition);

SourcePausePointCapture.Capture(
"jump", null, Array.Empty<object>(), new object[] { "speed", 5 });
"jump", null, Array.Empty<object>(), Array.Empty<object>(), new object[] { "speed", 5 });

UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus("jump");

Expand Down Expand Up @@ -344,7 +344,7 @@ public void Capture_WhenHitWhenEvaluationErrors_RecordsErrorAndCapturesFrame()
parseResult.Condition);

SourcePausePointCapture.Capture(
"jump", null, Array.Empty<object>(), new object[] { "speed", 5 });
"jump", null, Array.Empty<object>(), Array.Empty<object>(), new object[] { "speed", 5 });

UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus("jump");

Expand Down Expand Up @@ -494,7 +494,7 @@ public void Hit_WhenMaxCallerFramesIsZero_ReturnsEmptyCallerFramesArray()
"jump", 30, UloopPausePointCaptureMode.SingleShot, 20, 10, 0);

SourcePausePointCapture.Capture(
"jump", null, Array.Empty<object>(), Array.Empty<object>());
"jump", null, Array.Empty<object>(), Array.Empty<object>(), Array.Empty<object>());

UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus("jump");
Assert.That(snapshot.CallerFrames, Is.Empty);
Expand Down
Loading
Loading