Repository navigation
feat(pause-point): report parameters that cannot be captured #2572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
d0f0990
feat(pause-point): collect the parameters capture cannot box
hatayama f178c71
feat(pause-point): report the not-capturable parameters on enable and…
hatayama 8b67ea1
docs(pause-point): explain which parameters can never be captured
hatayama 8bf2c7d
fix(pause-point): assert the not-capturable list instead of coalescin…
hatayama b700bc4
fix(pause-point): give advice that works for all three not-capturable…
hatayama 85f7fc0
test(pause-point): pass the not-capturable list from the warnings-cha…
hatayama File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
233 changes: 233 additions & 0 deletions
233
Assets/Tests/Editor/PausePointNotCapturableVariablesTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,233 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
|
|
||
| using Newtonsoft.Json; | ||
| using Newtonsoft.Json.Linq; | ||
| using NUnit.Framework; | ||
|
|
||
| using io.github.hatayama.UnityCliLoop.FirstPartyTools; | ||
| using io.github.hatayama.UnityCliLoop.Infrastructure; | ||
| using io.github.hatayama.UnityCliLoop.Runtime; | ||
| using io.github.hatayama.UnityCliLoop.ToolContracts; | ||
|
|
||
| namespace io.github.hatayama.UnityCliLoop.Tests.Editor | ||
| { | ||
| /// <summary> | ||
| /// Verifies the enable-time warning and the registry-to-response path for parameters that | ||
| /// capture cannot box. | ||
| /// </summary> | ||
| [TestFixture] | ||
| public sealed class PausePointNotCapturableVariablesTests | ||
| { | ||
| private const string ByRefEntry = "accumulator (ref/out/in parameter cannot be boxed)"; | ||
| private const string RefStructEntry = "scratch (ref struct cannot be boxed)"; | ||
|
|
||
| private const string ExpectedWarningForTwoEntries = | ||
| "Parameters not captured because they cannot be boxed: " | ||
| + "accumulator (ref/out/in parameter cannot be boxed), scratch (ref struct cannot be boxed). " | ||
| + "Copy the value it refers to into a plain local (dereference a pointer, ToArray() a span), " | ||
| + "or use --snapshot-timing post-line on the line that consumes it."; | ||
|
|
||
| /// <summary> | ||
| /// What: a non-empty list produces the warning naming every entry with its reason. | ||
| /// </summary> | ||
| [Test] | ||
| public void BuildNotCapturableParametersWarningOrEmpty_WithEntries_NamesThemAndTheWorkaround() | ||
| { | ||
| string warning = PausePointNotCapturableWarnings.BuildNotCapturableParametersWarningOrEmpty( | ||
| new[] { ByRefEntry, RefStructEntry }); | ||
|
|
||
| Assert.That(warning, Is.EqualTo(ExpectedWarningForTwoEntries)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// What: an empty list produces no warning, so a fully capturable method stays quiet. | ||
| /// </summary> | ||
| [Test] | ||
| public void BuildNotCapturableParametersWarningOrEmpty_WithEmptyList_ReturnsEmpty() | ||
| { | ||
| string warning = PausePointNotCapturableWarnings.BuildNotCapturableParametersWarningOrEmpty( | ||
| Array.Empty<string>()); | ||
|
|
||
| Assert.That(warning, Is.Empty); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// What: a null list produces no warning instead of throwing. | ||
| /// </summary> | ||
| [Test] | ||
| public void BuildNotCapturableParametersWarningOrEmpty_WithNull_ReturnsEmpty() | ||
| { | ||
| string warning = PausePointNotCapturableWarnings.BuildNotCapturableParametersWarningOrEmpty(null); | ||
|
|
||
| Assert.That(warning, Is.Empty); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// What: SetNotCapturableVariables is visible on the next status snapshot. | ||
| /// </summary> | ||
| [Test] | ||
| public void SetNotCapturableVariables_WhenStored_AppearsInStatusSnapshot() | ||
| { | ||
| UloopPausePointRegistry.ConfigureForTests(new FakeNotCapturablePauseController(), () => DateTime.UtcNow); | ||
| try | ||
| { | ||
| const string id = "Assets/Scripts/Enemy.cs:42"; | ||
| UloopPausePointRegistry.Enable(id, 30); | ||
| UloopPausePointRegistry.SetNotCapturableVariables(id, new[] { ByRefEntry }); | ||
|
|
||
| UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus(id); | ||
|
|
||
| Assert.That(snapshot.NotCapturableVariables, Is.EqualTo(new[] { ByRefEntry })); | ||
| } | ||
| finally | ||
| { | ||
| UloopPausePointRegistry.ResetForTests(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// What: clearing with an empty list drops a previously stored exclusion list, so a | ||
| /// discarded resolution never leaves a stale list behind. | ||
| /// </summary> | ||
| [Test] | ||
| public void SetNotCapturableVariables_WhenClearedWithEmptyList_DropsPreviousEntries() | ||
| { | ||
| UloopPausePointRegistry.ConfigureForTests(new FakeNotCapturablePauseController(), () => DateTime.UtcNow); | ||
| try | ||
| { | ||
| const string id = "Assets/Scripts/Enemy.cs:42"; | ||
| UloopPausePointRegistry.Enable(id, 30); | ||
| UloopPausePointRegistry.SetNotCapturableVariables(id, new[] { ByRefEntry }); | ||
| UloopPausePointRegistry.SetNotCapturableVariables(id, Array.Empty<string>()); | ||
|
|
||
| UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus(id); | ||
|
|
||
| Assert.That(snapshot.NotCapturableVariables, Is.Empty); | ||
| } | ||
| finally | ||
| { | ||
| UloopPausePointRegistry.ResetForTests(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// What: the status response carries the stored entries through FromSnapshot. | ||
| /// </summary> | ||
| [Test] | ||
| public void StatusResponseFromSnapshot_WithStoredEntries_CarriesThem() | ||
| { | ||
| UloopPausePointRegistry.ConfigureForTests(new FakeNotCapturablePauseController(), () => DateTime.UtcNow); | ||
| try | ||
| { | ||
| const string id = "Assets/Scripts/Enemy.cs:42"; | ||
| UloopPausePointRegistry.Enable(id, 30); | ||
| UloopPausePointRegistry.SetNotCapturableVariables(id, new[] { ByRefEntry }); | ||
|
|
||
| PausePointStatusResponse response = | ||
| PausePointStatusResponse.FromSnapshot(UloopPausePointRegistry.GetStatus(id)); | ||
|
|
||
| Assert.That(response.NotCapturableVariables, Is.EqualTo(new[] { ByRefEntry })); | ||
| } | ||
| finally | ||
| { | ||
| UloopPausePointRegistry.ResetForTests(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// What: with nothing to report the status response omits the field from its JSON, so the | ||
| /// shared contract shape stays unchanged for fully capturable methods. | ||
| /// </summary> | ||
| [Test] | ||
| public void StatusResponseFromSnapshot_WithNoEntries_OmitsFieldFromJson() | ||
| { | ||
| UloopPausePointRegistry.ConfigureForTests(new FakeNotCapturablePauseController(), () => DateTime.UtcNow); | ||
| try | ||
| { | ||
| const string id = "Assets/Scripts/Enemy.cs:42"; | ||
| UloopPausePointRegistry.Enable(id, 30); | ||
|
|
||
| PausePointStatusResponse response = | ||
| PausePointStatusResponse.FromSnapshot(UloopPausePointRegistry.GetStatus(id)); | ||
| string json = JsonConvert.SerializeObject( | ||
| response, | ||
| Formatting.None, | ||
| UnityCliLoopJsonResponseSerializerSettings.Settings); | ||
|
|
||
| Assert.That(JObject.Parse(json).ContainsKey("NotCapturableVariables"), Is.False); | ||
| } | ||
| finally | ||
| { | ||
| UloopPausePointRegistry.ResetForTests(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// What: the enable response carries the stored entries through FromSnapshot. | ||
| /// </summary> | ||
| [Test] | ||
| public void EnableResponseFromSnapshot_WithStoredEntries_CarriesThem() | ||
| { | ||
| UloopPausePointRegistry.ConfigureForTests(new FakeNotCapturablePauseController(), () => DateTime.UtcNow); | ||
| try | ||
| { | ||
| const string id = "Assets/Scripts/Enemy.cs:42"; | ||
| UloopPausePointRegistry.Enable(id, 30); | ||
| UloopPausePointRegistry.SetNotCapturableVariables(id, new[] { ByRefEntry, RefStructEntry }); | ||
|
|
||
| PausePointResponse response = | ||
| PausePointResponse.FromSnapshot(UloopPausePointRegistry.GetStatus(id)); | ||
|
|
||
| Assert.That( | ||
| response.NotCapturableVariables, | ||
| Is.EqualTo(new[] { ByRefEntry, RefStructEntry })); | ||
| } | ||
| finally | ||
| { | ||
| UloopPausePointRegistry.ResetForTests(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// What: with nothing to report the enable response omits the field from its JSON. | ||
| /// </summary> | ||
| [Test] | ||
| public void EnableResponseFromSnapshot_WithNoEntries_OmitsFieldFromJson() | ||
| { | ||
| UloopPausePointRegistry.ConfigureForTests(new FakeNotCapturablePauseController(), () => DateTime.UtcNow); | ||
| try | ||
| { | ||
| const string id = "Assets/Scripts/Enemy.cs:42"; | ||
| UloopPausePointRegistry.Enable(id, 30); | ||
|
|
||
| PausePointResponse response = | ||
| PausePointResponse.FromSnapshot(UloopPausePointRegistry.GetStatus(id)); | ||
| string json = JsonConvert.SerializeObject( | ||
| response, | ||
| Formatting.None, | ||
| UnityCliLoopJsonResponseSerializerSettings.Settings); | ||
|
|
||
| Assert.That(JObject.Parse(json).ContainsKey("NotCapturableVariables"), Is.False); | ||
| } | ||
| finally | ||
| { | ||
| UloopPausePointRegistry.ResetForTests(); | ||
| } | ||
| } | ||
|
|
||
| private sealed class FakeNotCapturablePauseController : IUloopPausePointPauseController | ||
| { | ||
| public bool IsPlaying => true; | ||
| public bool IsPaused => false; | ||
|
|
||
| public void Pause() | ||
| { | ||
| } | ||
|
|
||
| public void Resume() | ||
| { | ||
| } | ||
| } | ||
| } | ||
| } |
11 changes: 11 additions & 0 deletions
11
Assets/Tests/Editor/PausePointNotCapturableVariablesTests.cs.meta
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
Assets/Tests/Editor/SourcePausePointNotCapturableParameterFixture.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| namespace io.github.hatayama.UnityCliLoop.Tests.Editor | ||
| { | ||
| // Read back through Mono.Cecil from Library/ScriptAssemblies/UnityCLILoop.Tests.Editor.dll, | ||
| // so this fixture must stay in the UnityCLILoop.Tests.Editor assembly: a nested test asmdef | ||
| // compiles into a different dll that the Cecil test does not open. | ||
| internal sealed class SourcePausePointNotCapturableParameterFixture | ||
| { | ||
| // ref/out/in and ref-struct parameters exist here specifically to verify that capture | ||
| // reports them as not capturable instead of dropping them silently. | ||
| public int Combine( | ||
| int value, | ||
| ref int accumulator, | ||
| out int doubled, | ||
| in int multiplier, | ||
| System.Span<int> scratch) | ||
| { | ||
| doubled = value * 2; | ||
| accumulator += doubled; | ||
| scratch[0] = accumulator; | ||
| return scratch[0] * multiplier; | ||
| } | ||
|
|
||
| // The leading parameter is byref here specifically so skipFirstParameter is observable: | ||
| // skipping it changes the reported list, which a fixture with a capturable first | ||
| // parameter could never show. | ||
| public int CombineLeadingByRef(ref int accumulator, int value, in int multiplier) | ||
| { | ||
| accumulator += value; | ||
| return accumulator * multiplier; | ||
| } | ||
|
|
||
| // The capturable-only counterpart, so the empty not-capturable list is asserted against | ||
| // a method that really has nothing to report. | ||
| public int Add(int left, int right) | ||
| { | ||
| return left + right; | ||
| } | ||
| } | ||
| } |
11 changes: 11 additions & 0 deletions
11
Assets/Tests/Editor/SourcePausePointNotCapturableParameterFixture.cs.meta
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
--snapshot-timing post-linedoes not expose the excluded parameter itself, so arming its consuming line cannot generally observe aref, pointer, or span value. Tell users to assign a boxable copy to a plain local and capture that local; reserve post-line for statements that produce such a local.Prompt for AI agents