Repository navigation
fix: Pause point status, await, and clear find a marker whichever path names the file #3220
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| using System; | ||
|
|
||
| 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 that status, extend, and clear reach a source pause point whichever path form the | ||
| /// query names its file by, while a named marker is looked up by its id as given. | ||
| /// </summary> | ||
| public sealed class PausePointQueryScriptPathFormTests | ||
| { | ||
| // An embedded package whose folder name differs from its package name, so the folder path | ||
| // and the asset path of the same script differ. | ||
| private const string PackageAssetPath = | ||
| "Packages/io.github.hatayama.uloop.hotreload-package-fixture/Runtime/HotReloadPackageFixture.cs"; | ||
| private const string PackagePhysicalPath = | ||
| "Packages/uloop-hotreload-package-fixture/Runtime/HotReloadPackageFixture.cs"; | ||
| private const string AssetsScriptPath = "Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs"; | ||
| private const string Line = ":16"; | ||
| private const string ProjectRootToken = "<root>/"; | ||
|
|
||
| [SetUp] | ||
| public void SetUp() | ||
| { | ||
| UloopPausePointRegistry.ConfigureForTests(new FakePauseController(), () => DateTime.UtcNow); | ||
| } | ||
|
|
||
| [TearDown] | ||
| public void TearDown() | ||
| { | ||
| UloopPausePointRegistry.ResetForTests(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// What: a status query naming the file by its folder path, its absolute path, or a ./ path | ||
| /// finds the marker enabled under the asset path. | ||
| /// </summary> | ||
| [TestCase(PackageAssetPath, PackagePhysicalPath)] | ||
| [TestCase(PackageAssetPath, ProjectRootToken + PackagePhysicalPath)] | ||
| [TestCase(PackageAssetPath, "./" + PackagePhysicalPath)] | ||
| [TestCase(PackageAssetPath, "./" + PackageAssetPath)] | ||
| [TestCase(AssetsScriptPath, "./" + AssetsScriptPath)] | ||
| [TestCase(AssetsScriptPath, ProjectRootToken + AssetsScriptPath)] | ||
| [TestCase(AssetsScriptPath, "Assets/Tests/../Tests/Editor/PausePointQueryScriptPathFormTests.cs")] | ||
| public void Status_ByAnotherPathForm_FindsTheMarkerEnabledByTheAssetPath(string enabledPath, string queriedPath) | ||
| { | ||
| string markerId = enabledPath + Line; | ||
| UloopPausePointRegistry.Enable(markerId, 30); | ||
|
|
||
| PausePointStatusResponse response = PausePointStatusBridgeCommand.Execute(IdParams(queriedPath + Line)); | ||
|
|
||
| Assert.That(response.Status, Is.EqualTo(UloopPausePointStatus.Enabled)); | ||
| Assert.That(response.Id, Is.EqualTo(markerId)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// What: the extend an await sends names the file as the user typed it and still extends the marker. | ||
| /// </summary> | ||
| [Test] | ||
| public void Extend_ByTheFolderPath_ExtendsTheMarkerEnabledByTheAssetPath() | ||
| { | ||
| string markerId = PackageAssetPath + Line; | ||
| UloopPausePointRegistry.Enable(markerId, 1); | ||
| JObject parameters = IdParams(PackagePhysicalPath + Line); | ||
| parameters["MinimumRemainingSeconds"] = 60; | ||
|
|
||
| PausePointStatusResponse response = PausePointStatusBridgeCommand.Extend(parameters); | ||
|
|
||
| Assert.That(response.Id, Is.EqualTo(markerId)); | ||
| Assert.That(response.RemainingMilliseconds, Is.GreaterThan(30000)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// What: the clear sent after an await times out removes the marker when it names the folder path. | ||
| /// </summary> | ||
| [Test] | ||
| public void BridgeClear_ByTheFolderPath_ClearsTheMarkerEnabledByTheAssetPath() | ||
| { | ||
| string markerId = PackageAssetPath + Line; | ||
| UloopPausePointRegistry.Enable(markerId, 30); | ||
|
|
||
| PausePointStatusBridgeCommand.Clear(IdParams(PackagePhysicalPath + Line)); | ||
|
|
||
| Assert.That(UloopPausePointRegistry.IsArmed(markerId), Is.False); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// What: clear-pause-point --file with the folder path removes the marker enabled under the asset path. | ||
| /// </summary> | ||
| [Test] | ||
| public void ToolClear_ByTheFolderPath_ClearsTheMarkerEnabledByTheAssetPath() | ||
| { | ||
| string markerId = PackageAssetPath + Line; | ||
| UloopPausePointRegistry.Enable(markerId, 30); | ||
|
|
||
| new PausePointUseCase().Clear(new ClearPausePointSchema { Id = PackagePhysicalPath + Line }); | ||
|
|
||
| Assert.That(UloopPausePointRegistry.IsArmed(markerId), Is.False); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// What: a named marker is looked up by the id as given, even when the id reads like a path | ||
| /// and line that a rewrite would change. | ||
| /// </summary> | ||
| [TestCase("./jump")] | ||
| [TestCase("./jump:1")] | ||
| [TestCase("Enemy\\Attack:2")] | ||
| public void Status_NamedMarker_IsLookedUpByItsIdAsGiven(string markerId) | ||
| { | ||
| UloopPausePointRegistry.Enable(markerId, 30); | ||
|
|
||
| PausePointStatusResponse response = PausePointStatusBridgeCommand.Execute(IdParams(markerId)); | ||
|
|
||
| Assert.That(response.Status, Is.EqualTo(UloopPausePointStatus.Enabled)); | ||
| Assert.That(response.Id, Is.EqualTo(markerId)); | ||
| } | ||
|
|
||
| private static JObject IdParams(string id) | ||
| { | ||
| string resolved = id.StartsWith(ProjectRootToken, StringComparison.Ordinal) | ||
| ? UnityCliLoopPathResolver.GetProjectRoot().Replace('\\', '/').TrimEnd('/') + "/" | ||
| + id.Substring(ProjectRootToken.Length) | ||
| : id; | ||
| return new JObject { ["Id"] = resolved }; | ||
| } | ||
|
|
||
| private sealed class FakePauseController : IUloopPausePointPauseController | ||
| { | ||
| public bool IsPlaying => true; | ||
| public bool IsPaused => false; | ||
|
|
||
| public void Pause() | ||
| { | ||
| } | ||
|
|
||
| public void Resume() | ||
| { | ||
| } | ||
| } | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| using System; | ||
| using System.IO; | ||
|
|
||
| using UnityEngine; | ||
|
|
||
| namespace io.github.hatayama.UnityCliLoop.FirstPartyTools | ||
| { | ||
| /// <summary> | ||
| /// The id of a pause point set at a source location, <c><asset path>:<line></c>: built | ||
| /// once when the marker is enabled, and rebuilt from whatever path form a later query names | ||
| /// the file by, so every form reaches the same marker. | ||
| /// </summary> | ||
| internal static class SourcePausePointId | ||
| { | ||
| internal static string Build(string assetPath, int line) | ||
| { | ||
| Debug.Assert(!string.IsNullOrEmpty(assetPath), "assetPath must not be empty."); | ||
| return assetPath + ":" + line; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns the id a query names its marker by. An id already registered, such as a named | ||
| /// marker, comes back as given. Otherwise an id in the source form has its path rewritten | ||
| /// to the asset path; any other id, and a source id whose path is already the asset path, | ||
| /// comes back unchanged. Must be called on the Unity main thread. | ||
| /// </summary> | ||
| internal static string ToMarkerId(string id, string projectRoot, Func<string, bool> isRegistered) | ||
| { | ||
| Debug.Assert(!string.IsNullOrEmpty(id), "id must not be empty."); | ||
| Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be empty."); | ||
| Debug.Assert(isRegistered != null, "isRegistered must not be null."); | ||
|
|
||
| // Why first: a named marker may itself read like <path>:<line>, and the rewrite would | ||
| // send its query to an id nobody enabled. | ||
| if (isRegistered(id)) | ||
| { | ||
| return id; | ||
| } | ||
|
|
||
| int separator = id.LastIndexOf(':'); | ||
| if (separator <= 0 || !int.TryParse(id.Substring(separator + 1), out int line)) | ||
| { | ||
| return id; | ||
| } | ||
|
|
||
| string path = id.Substring(0, separator); | ||
| // Why skip the Package Manager for an Assets path: await polls status every second, and | ||
| // such a path is already its asset path. | ||
| if (RewriteLeavesUnchanged(path)) | ||
| { | ||
| return id; | ||
| } | ||
|
|
||
| string assetPath = ScriptPathNormalizer.ToAssetPath(path, projectRoot, ScriptPackageRoots.ReadCurrent()); | ||
| return assetPath == path ? id : Build(assetPath, line); | ||
| } | ||
|
Comment on lines
+44
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: git diff --unified=80 60c9a931554213356ef59fd36aede030c040486a 44e0a1d938da27b687f7119b03f2582dd7926c88 -- Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs
printf '\\n--- file ---\\n'
nl -ba Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs
printf '\\n--- related symbols ---\\n'
rg -n -F -- 'SourcePausePointId' Packages/src
rg -n -F -- 'ToAssetPath(' Packages/src/Editor/FirstPartyTools/Common/ScriptPath
rg -n -F -- 'ScriptPackageRoots' Packages/src/Editor/FirstPartyToolsRepository: hatayama/unity-cli-loop Length of output: 7818 🏁 Script executed: printf '%s\\n' '--- ScriptPathNormalizer ---'
nl -ba Packages/src/Editor/FirstPartyTools/Common/ScriptPath/ScriptPathNormalizer.cs
printf '%s\\n' '--- ScriptPackageRoots ---'
nl -ba Packages/src/Editor/FirstPartyTools/Common/ScriptPath/ScriptPackageRoots.cs
printf '%s\\n' '--- PausePointUseCase relevant range ---'
nl -ba Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs | sed -n '130,260p'
printf '%s\\n' '--- status bridge relevant range ---'
nl -ba Packages/src/Editor/Infrastructure/Api/PausePointStatusBridgeCommand.cs | sed -n '90,150p'
printf '%s\\n' '--- related tests and source-location API ---'
rg -n -i --glob '*Test*' --glob '*.cs' 'ToAssetPath|SourcePausePointId|Set.*PausePoint|source location|package root|Packages/' Packages/tests Packages/src/Editor/FirstPartyTools/PausePoint Packages/src/Editor/Infrastructure/Api 2>/dev/nullRepository: hatayama/unity-cli-loop Length of output: 26136 🏁 Script executed: printf '%s\\n' '--- focused source-path tests/files ---'
rg --files Packages | rg '(Test|Tests|test|tests).*(ScriptPath|PausePoint)|((ScriptPath|PausePoint).*(Test|Tests|test|tests))'
printf '%s\\n' '--- all normalization test references ---'
rg -n --glob '*.cs' --glob '*.json' --glob '*.md' 'ScriptPathNormalizer|ScriptPackageRoot|SourcePausePointId|enable-pause-point|pause-point-status|--file.*--line' Packages cli
printf '%s\\n' '--- enable tool schema and entrypoint ---'
nl -ba Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs | sed -n '1,130p'
nl -ba Packages/src/Editor/FirstPartyTools/PausePoint/PausePointTools.cs | sed -n '1,130p'
printf '%s\\n' '--- package path types and package metadata/config docs ---'
rg -n --glob '*.cs' 'class ScriptPackageRoot|struct ScriptPackageRoot|new ScriptPackageRoot|resolvedPath|assetPath' Packages/src/Editor/FirstPartyTools/Common/ScriptPath Packages/src/Editor/FirstPartyTools/PausePointRepository: hatayama/unity-cli-loop Length of output: 41543 🏁 Script executed: printf '%s\\n' '--- CLI source-location ID construction ---'
rg -n --glob '*.go' --glob '*.cs' --glob '*.ts' --glob '*.json' 'pause-point-status|clear-pause-point|await-pause-point|File.*Line|file.*line|SourcePausePoint' cli Packages/src/Editor
printf '%s\\n' '--- cli files ---'
rg --files cli | rg '(pause|PausePoint|pausepoint)'
printf '%s\\n' '--- package asset-path evidence and matching tools ---'
rg -n --glob '*.cs' --glob '*.md' --glob '*.json' 'project-relative path|Assets/\\.\\.\\.|Packages/<package-id>|Library/PackageCache|any form|resolvedPath' Packages/src/Editor Packages/src/Documentation~ cliRepository: hatayama/unity-cli-loop Length of output: 45653 🏁 Script executed: printf '%s\\n' '--- CLI file-line ID helper ---'
nl -ba cli/project-runner/internal/projectrunner/pause_point_file_line_id.go
printf '%s\\n' '--- status/await file-line option flow ---'
rg -n -F -- 'composePausePointFileLineID' cli/project-runner/internal/projectrunner
rg -n -F -- 'applyPausePointFileLine' cli/project-runner/internal/projectrunner
nl -ba cli/project-runner/internal/projectrunner/pause_point_status_options.go
nl -ba cli/project-runner/internal/projectrunner/pause_point_clear_file_line.go
printf '%s\\n' '--- Unity status bridge dispatch and contract ---'
nl -ba Packages/src/Editor/Infrastructure/Api/PausePointStatusBridgeCommand.cs | sed -n '1,105p'
printf '%s\\n' '--- published path form contract ---'
nl -ba Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/troubleshooting.md | sed -n '80,96p'
nl -ba Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md | sed -n '28,42p;88,102p'Repository: hatayama/unity-cli-loop Length of output: 25567 🏁 Script executed: printf '%s\\n' '--- bridge method bindings ---'
rg -n -F -- 'PausePointStatusBridgeCommand.Execute' Packages/src/Editor
rg -n -F -- 'PausePointStatusBridgeCommand.Clear' Packages/src/Editor
rg -n -F -- 'PausePointStatusBridgeCommand.Extend' Packages/src/Editor
printf '%s\\n' '--- registry lookup implementation ---'
rg -n -F -- 'GetStatus(string id)' Packages/src/Runtime
rg -n -F -- 'Clear(string id' Packages/src/Runtime/PausePoints
nl -ba Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs | sed -n '90,190p'
nl -ba Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs | sed -n '250,355p'
printf '%s\\n' '--- package root and query path guards ---'
nl -ba Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs | sed -n '26,57p'
nl -ba Packages/src/Editor/FirstPartyTools/Common/ScriptPath/ScriptPathNormalizer.cs | sed -n '20,72p'
nl -ba Packages/src/Editor/FirstPartyTools/Common/ScriptPath/ScriptPackageRoots.cs | sed -n '17,35p'Repository: hatayama/unity-cli-loop Length of output: 19198 🌐 Web query:
💡 Result: Normalize project-relative registry package paths before the early return. When 🤖 Prompt for AI Agents |
||
|
|
||
| // Why Packages/ is not skipped: a package's folder path also starts with it, and only the | ||
| // package roots tell the two apart. Why . and .. segments are not skipped: enable folds them | ||
| // away, while the CLI sends them as typed. | ||
| private static bool RewriteLeavesUnchanged(string path) | ||
| { | ||
| return !Path.IsPathRooted(path) | ||
| && path.IndexOf('\\') < 0 | ||
| && !path.StartsWith("Packages/", StringComparison.Ordinal) | ||
| && !HasDotSegment(path); | ||
| } | ||
|
|
||
| private static bool HasDotSegment(string path) | ||
| { | ||
| foreach (string segment in path.Split('/')) | ||
| { | ||
| if (segment == "." || segment == "..") | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
| } | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.