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
87 changes: 87 additions & 0 deletions Assets/Tests/Editor/StaticFacadeStateGuardTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ public sealed class StaticFacadeStateGuardTests
"Packages/src/Editor/Infrastructure/Server/DomainReloadDetectionFileService.cs"
};

private static readonly string[] AsyncCancellationTokenGuardPaths = new string[]
{
"Packages/src/Editor/Infrastructure/SkillSetup/ToolSkillSynchronizer.cs",
"Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs",
"Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs",
"Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs"
};

private static readonly Regex DirectMutableStaticFieldPattern = new Regex(
@"\b(private|internal|public|protected)\s+static\s+(?!readonly\b)(?!event\b)(?!extern\b)[^(\r\n;=]*[;=]",
RegexOptions.Compiled);
Expand All @@ -79,6 +87,14 @@ public sealed class StaticFacadeStateGuardTests
@"\b(ServiceValue|RepositoryValue|RegistryValue|RegisteredUseCase)\b",
RegexOptions.Compiled);

private static readonly Regex AsyncMethodSignaturePattern = new Regex(
@"\b(?:(?:private|internal|public|protected)\s+)?(?:static\s+)?async\s+(?:void|Task(?:<[^()\n]+>)?)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^()\n]+>)?\s*\(([^)]*)\)",
RegexOptions.Compiled | RegexOptions.Singleline);

private static readonly Regex CancellationTokenCtParameterPattern = new Regex(
@"\bCancellationToken\s+ct\b",
RegexOptions.Compiled);

[Test]
public void MigratedFacadeFiles_WhenScanned_DoNotOwnMutableStaticState()
{
Expand Down Expand Up @@ -115,6 +131,35 @@ public void InstanceServices_WhenScanned_AreNotStaticClasses()
Assert.That(violations, Is.Empty, string.Join("\n", violations));
}

[Test]
public void RefactorTargets_WhenDeclaringAsyncMethods_RequireCancellationTokenCt()
{
// Tests that R2-5 refactor targets do not add async methods without the standard ct parameter.
List<string> violations = FindAsyncMethodsWithoutCancellationTokenCt();

Assert.That(violations, Is.Empty, string.Join("\n", violations));
}

[Test]
public void CancellationTokenCtParameterPattern_WhenNameOnlyStartsWithCt_DoesNotMatch()
{
// Tests that the async guard requires the exact ct parameter name.
Assert.That(CancellationTokenCtParameterPattern.IsMatch("CancellationToken ct"), Is.True);
Assert.That(CancellationTokenCtParameterPattern.IsMatch("CancellationToken cts"), Is.False);
}

[Test]
public void AsyncMethodSignaturePattern_WhenVisibilityIsImplicitAndReturnTypeIsNestedGeneric_Matches()
{
// Tests that the async guard does not skip implicit-private methods or nested generic Task returns.
string source = "async Task<List<string>> LoadAsync<T>(CancellationToken ct) => new List<string>();";
Match match = AsyncMethodSignaturePattern.Match(source);

Assert.That(match.Success, Is.True);
Assert.That(match.Groups[1].Value, Is.EqualTo("LoadAsync"));
Assert.That(match.Groups[2].Value, Is.EqualTo("CancellationToken ct"));
}

[Test]
public void MutableStaticFieldLine_WhenInitializerUsesTargetTypedNew_IsReported()
{
Expand Down Expand Up @@ -344,6 +389,48 @@ private static List<string> FindDirectStaticEventViolations()
return violations;
}

private static List<string> FindAsyncMethodsWithoutCancellationTokenCt()
{
List<string> violations = new();
string projectRoot = UnityCliLoopPathResolver.GetProjectRoot();

for (int pathIndex = 0; pathIndex < AsyncCancellationTokenGuardPaths.Length; pathIndex++)
{
string relativePath = AsyncCancellationTokenGuardPaths[pathIndex];
string absolutePath = Path.Combine(projectRoot, relativePath);
string source = File.ReadAllText(absolutePath);
MatchCollection matches = AsyncMethodSignaturePattern.Matches(source);
foreach (Match match in matches)
{
string parameterList = match.Groups[2].Value;
if (CancellationTokenCtParameterPattern.IsMatch(parameterList))
{
continue;
}

string methodName = match.Groups[1].Value;
int lineNumber = CountLinesBefore(source, match.Index) + 1;
violations.Add($"{relativePath}:{lineNumber}: {methodName}");
}
}

return violations;
}

private static int CountLinesBefore(string source, int index)
{
int lineCount = 0;
for (int charIndex = 0; charIndex < index; charIndex++)
{
if (source[charIndex] == '\n')
{
lineCount++;
}
}

return lineCount;
}

private static List<string> FindStaticClassViolations(string[] relativePaths)
{
List<string> violations = new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ private async Task<SimulateKeyboardResponse> ExecutePress(
else if (pressWasApplied)
{
InputSimulationWaitOutcome releaseOutcome =
await ReleaseKeyStateIfPossible(keyboard, key).ConfigureAwait(false);
await ReleaseKeyStateIfPossible(keyboard, key, CancellationToken.None).ConfigureAwait(false);
if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
{
waitOutcome = InputSimulationWaitOutcome.TimedOut;
Expand Down Expand Up @@ -310,7 +310,7 @@ private async Task<SimulateKeyboardResponse> ExecuteKeyDown(Keyboard keyboard, K
else if (keyDownApplied && !committed)
{
InputSimulationWaitOutcome rollbackOutcome =
await RollbackHeldKey(keyboard, key, keyName).ConfigureAwait(false);
await RollbackHeldKey(keyboard, key, keyName, CancellationToken.None).ConfigureAwait(false);
if (rollbackOutcome == InputSimulationWaitOutcome.TimedOut)
{
waitOutcome = InputSimulationWaitOutcome.TimedOut;
Expand Down Expand Up @@ -357,7 +357,7 @@ private async Task<SimulateKeyboardResponse> ExecuteKeyUp(Keyboard keyboard, Key
}

InputSimulationWaitOutcome releaseOutcome =
await ReleaseKeyStateIfPossible(keyboard, key).ConfigureAwait(false);
await ReleaseKeyStateIfPossible(keyboard, key, CancellationToken.None).ConfigureAwait(false);

if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
{
Expand Down Expand Up @@ -499,26 +499,33 @@ await EditorFrameWaiter.WaitFramesOrTimeoutAsync(
CancellationToken.None).ConfigureAwait(false);
}

private static async Task<InputSimulationWaitOutcome> RollbackHeldKey(Keyboard keyboard, Key key, string keyName)
private static async Task<InputSimulationWaitOutcome> RollbackHeldKey(
Keyboard keyboard,
Key key,
string keyName,
CancellationToken ct)
{
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None);
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
InputSimulationWaitOutcome releaseOutcome =
await ReleaseKeyStateIfPossible(keyboard, key).ConfigureAwait(false);
await ReleaseKeyStateIfPossible(keyboard, key, ct).ConfigureAwait(false);
if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
{
ScheduleTimedOutHeldKeyCleanup(keyboard, key, keyName, false);
return releaseOutcome;
}

await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None);
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
KeyboardKeyState.SetKeyUp(key);
SimulateKeyboardOverlayState.RemoveHeldKey(keyName);
return releaseOutcome;
}

private static async Task<InputSimulationWaitOutcome> ReleaseKeyStateIfPossible(Keyboard keyboard, Key key)
private static async Task<InputSimulationWaitOutcome> ReleaseKeyStateIfPossible(
Keyboard keyboard,
Key key,
CancellationToken ct)
{
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None);
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
if (!CanInjectKeyboardState(keyboard))
{
return InputSimulationWaitOutcome.Completed;
Expand All @@ -532,7 +539,7 @@ private static async Task<InputSimulationWaitOutcome> ReleaseKeyStateIfPossible(

InputSimulationWaitOutcome releaseOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(
() => KeyboardKeyState.SetKeyState(keyboard, key, false),
CancellationToken.None).ConfigureAwait(false);
ct).ConfigureAwait(false);
if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
{
ScheduleReleaseKeyStateImmediately(keyboard, key);
Expand Down Expand Up @@ -581,7 +588,7 @@ private static async Task CleanupTimedOutPressAsync(
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
if (pressWasApplied)
{
await ReleaseKeyStateIfPossible(keyboard, key).ConfigureAwait(false);
await ReleaseKeyStateIfPossible(keyboard, key, ct).ConfigureAwait(false);
}

KeyboardKeyState.UnregisterTransientKey(key);
Expand All @@ -603,7 +610,7 @@ private static async Task CleanupTimedOutHeldKeyAsync(
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
if (keyWasApplied)
{
await ReleaseKeyStateIfPossible(keyboard, key).ConfigureAwait(false);
await ReleaseKeyStateIfPossible(keyboard, key, ct).ConfigureAwait(false);
}

KeyboardKeyState.SetKeyUp(key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ private async Task<SimulateMouseInputResponse> ExecuteClick(
else if (pressWasApplied)
{
InputSimulationWaitOutcome releaseOutcome =
await ReleaseButtonIfPossible(mouse, button).ConfigureAwait(false);
await ReleaseButtonIfPossible(mouse, button, CancellationToken.None).ConfigureAwait(false);
if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
{
waitOutcome = InputSimulationWaitOutcome.TimedOut;
Expand Down Expand Up @@ -325,7 +325,7 @@ private async Task<SimulateMouseInputResponse> ExecuteLongPress(
else if (pressWasApplied)
{
InputSimulationWaitOutcome releaseOutcome =
await ReleaseButtonIfPossible(mouse, button).ConfigureAwait(false);
await ReleaseButtonIfPossible(mouse, button, CancellationToken.None).ConfigureAwait(false);
if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
{
waitOutcome = InputSimulationWaitOutcome.TimedOut;
Expand Down Expand Up @@ -635,9 +635,12 @@ private static List<UnityCliLoopPausePointHit> CollectPausePointHits()
return hits;
}

private static async Task<InputSimulationWaitOutcome> ReleaseButtonIfPossible(Mouse mouse, RuntimeMouseButton button)
private static async Task<InputSimulationWaitOutcome> ReleaseButtonIfPossible(
Mouse mouse,
RuntimeMouseButton button,
CancellationToken ct)
{
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None);
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
if (!CanInjectMouseState(mouse))
{
return InputSimulationWaitOutcome.Completed;
Expand All @@ -651,7 +654,7 @@ private static async Task<InputSimulationWaitOutcome> ReleaseButtonIfPossible(Mo

InputSimulationWaitOutcome releaseOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(
() => MouseInputState.SetButtonState(mouse, button, false),
CancellationToken.None).ConfigureAwait(false);
ct).ConfigureAwait(false);
if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
{
ScheduleReleaseButtonImmediately(mouse, button);
Expand Down Expand Up @@ -714,7 +717,7 @@ private static async Task CleanupTimedOutButtonAsync(
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
if (pressWasApplied)
{
await ReleaseButtonIfPossible(mouse, button).ConfigureAwait(false);
await ReleaseButtonIfPossible(mouse, button, ct).ConfigureAwait(false);
}

MouseInputState.SetButtonUp(button);
Expand Down
Loading
Loading