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
126 changes: 9 additions & 117 deletions Assets/Tests/Editor/NativeCliInstallerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -298,148 +298,40 @@ public void RunUninstallCommand_WhenCanceledReportsUninstallCommand()
}

[Test]
public async Task WaitForUninstallTargetRemovalAsync_WhenDeferredRemovalCompletesReturnsSuccess()
public async Task WaitForUninstallCompletionAsync_WhenTargetRemainsReportsTimeout()
{
// Verifies that Settings uninstall waits for deferred launcher self-removal before refreshing CLI status.
int remainingExistingChecks = 2;
int delayCount = 0;

CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallTargetRemovalAsync(
"C:\\Users\\ExampleUser\\AppData\\Local\\Programs\\uloop\\bin\\uloop.exe",
CancellationToken.None,
1000,
100,
executablePath => remainingExistingChecks-- > 0,
(delayMs, ct) =>
{
delayCount++;
return Task.CompletedTask;
});

Assert.That(result.Success, Is.True, result.ErrorOutput);
Assert.That(delayCount, Is.EqualTo(2));
}

[Test]
public async Task WaitForUninstallTargetRemovalAsync_WhenTargetRemainsReturnsFailure()
{
// Verifies that delayed launcher removal failures do not recache a stale installed CLI.
int delayCount = 0;

CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallTargetRemovalAsync(
"C:\\Users\\ExampleUser\\AppData\\Local\\Programs\\uloop\\bin\\uloop.exe",
CancellationToken.None,
250,
100,
executablePath => true,
(delayMs, ct) =>
{
delayCount++;
return Task.CompletedTask;
});

Assert.That(result.Success, Is.False);
Assert.That(result.ErrorOutput, Does.Contain("Timed out waiting for uLoop CLI uninstall"));
Assert.That(delayCount, Is.EqualTo(3));
}

[Test]
public async Task WaitForUninstallCompletionAsync_OnWindowsWaitsForUserPathRemoval()
{
// Verifies that Settings uninstall waits until Windows User PATH no longer resolves the native CLI directory.
string installDirectory = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin";
string userPath = installDirectory + ";C:\\npm";
// Verifies uninstall completion reports the launcher path when deferred self-removal times out.
string targetPath = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin\\uloop.exe";
int delayCount = 0;

CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync(
installDirectory + "\\uloop.exe",
installDirectory,
RuntimePlatform.WindowsEditor,
CancellationToken.None,
1000,
100,
executablePath => false,
true,
(name, target) => userPath,
(delayMs, ct) =>
{
delayCount++;
userPath = "C:\\npm";
return Task.CompletedTask;
});

Assert.That(result.Success, Is.True, result.ErrorOutput);
Assert.That(delayCount, Is.EqualTo(1));
}

[Test]
public async Task WaitForUninstallCompletionAsync_OnWindowsFailsWhenUserPathRemains()
{
// Verifies that uninstall cannot report success while Windows User PATH still contains the native CLI directory.
string installDirectory = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin";
int delayCount = 0;

CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync(
installDirectory + "\\uloop.exe",
installDirectory,
RuntimePlatform.WindowsEditor,
targetPath,
CancellationToken.None,
250,
100,
executablePath => false,
true,
(name, target) => installDirectory + ";C:\\npm",
executablePath => true,
(delayMs, ct) =>
{
delayCount++;
return Task.CompletedTask;
});

Assert.That(result.Success, Is.False);
Assert.That(result.ErrorOutput, Does.Contain("Windows User PATH"));
Assert.That(result.ErrorOutput, Does.Not.Contain("\\uloop.exe"));
Assert.That(result.ErrorOutput, Does.Contain(targetPath));
Assert.That(delayCount, Is.EqualTo(3));
}

[Test]
public async Task WaitForUninstallCompletionAsync_WhenUserPathRemovalIsNotRequiredReportsTargetTimeoutOnly()
{
// Verifies fallback uninstall failures do not claim ownership of Windows User PATH cleanup.
string installDirectory = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin";

CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync(
installDirectory + "\\uloop.exe",
installDirectory,
RuntimePlatform.WindowsEditor,
CancellationToken.None,
250,
100,
executablePath => true,
false,
(name, target) => installDirectory + ";C:\\npm",
(delayMs, ct) => Task.CompletedTask);

Assert.That(result.Success, Is.False);
Assert.That(result.ErrorOutput, Does.Contain("\\uloop.exe"));
Assert.That(result.ErrorOutput, Does.Not.Contain("Windows User PATH"));
}

[Test]
public async Task WaitForUninstallCompletionAsync_WhenUserPathRemovalIsNotRequiredIgnoresUserPath()
public async Task WaitForUninstallCompletionAsync_WhenTargetIsRemovedReturnsSuccess()
{
// Verifies fallback launchers can complete uninstall without owning Windows User PATH cleanup.
string installDirectory = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin";
// Verifies uninstall completion succeeds as soon as deferred launcher self-removal finishes.

CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync(
installDirectory + "\\uloop.exe",
installDirectory,
RuntimePlatform.WindowsEditor,
"C:\\Users\\ExampleUser\\Programs\\uloop\\bin\\uloop.exe",
CancellationToken.None,
250,
100,
executablePath => false,
false,
(name, target) => installDirectory + ";C:\\npm",
(delayMs, ct) => Task.CompletedTask);

Assert.That(result.Success, Is.True, result.ErrorOutput);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,24 +220,6 @@ internal static string GetPathEnvironmentVariableName(RuntimePlatform platform)
: CliConstants.POSIX_PATH_ENVIRONMENT_VARIABLE;
}

internal static bool DoesUserPathContainInstallDirectory(
string installDirectory,
RuntimePlatform platform,
Func<string, EnvironmentVariableTarget, string> getEnvironmentVariable)
{
UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty");
UnityEngine.Debug.Assert(getEnvironmentVariable != null, "getEnvironmentVariable must not be null");

if (platform != RuntimePlatform.WindowsEditor)
{
return false;
}

string pathVariableName = GetPathEnvironmentVariableName(platform);
string currentUserPath = getEnvironmentVariable(pathVariableName, EnvironmentVariableTarget.User);
return DoesPathContainInstallDirectory(currentUserPath, installDirectory, platform);
}

private static string GetPathSeparator(RuntimePlatform platform)
{
return platform == RuntimePlatform.WindowsEditor
Expand Down Expand Up @@ -272,42 +254,6 @@ private static string NormalizePathForComparison(string path, RuntimePlatform pl
return normalizedPath.TrimEnd('\\', '/').Replace('/', '\\');
}

private static bool DoesPathContainInstallDirectory(
string currentPath,
string installDirectory,
RuntimePlatform platform)
{
UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty");

string normalizedPath = currentPath ?? "";
if (string.IsNullOrEmpty(normalizedPath))
{
return false;
}

string separator = GetPathSeparator(platform);
string[] entries = normalizedPath.Split(
new[] { separator },
StringSplitOptions.RemoveEmptyEntries);
string normalizedInstallDirectory = NormalizePathForComparison(installDirectory, platform);
StringComparison comparison = GetPathComparison(platform);
foreach (string entry in entries)
{
if (string.IsNullOrWhiteSpace(entry))
{
continue;
}

string normalizedEntry = NormalizePathForComparison(entry, platform);
if (string.Equals(normalizedEntry, normalizedInstallDirectory, comparison))
{
return true;
}
}

return false;
}

private static string GetGlobalCliInstallFileName(RuntimePlatform platform)
{
return platform == RuntimePlatform.WindowsEditor
Expand Down
4 changes: 0 additions & 4 deletions Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,10 @@ public static async Task<CliInstallResult> UninstallAsync(RuntimePlatform platfo
string installPath = NativeCliInstallPathResolver.GetGlobalCliInstallPath(installDirectory, platform);
CliInstallResult removalResult = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync(
installPath,
installDirectory,
platform,
ct,
NativeCliUninstallCompletionWaiter.UNINSTALL_COMPLETION_TIMEOUT_MS,
NativeCliSetupCommandRunner.INSTALL_PROCESS_WAIT_SLICE_MS,
File.Exists,
false,
Environment.GetEnvironmentVariable,
Task.Delay);
if (!removalResult.Success)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,69 +15,25 @@ internal static class NativeCliUninstallCompletionWaiter
{
internal const int UNINSTALL_COMPLETION_TIMEOUT_MS = 30000;

internal static async Task<CliInstallResult> WaitForUninstallTargetRemovalAsync(
string targetPath,
CancellationToken ct,
int timeoutMs,
int pollMs,
Func<string, bool> fileExists,
Func<int, CancellationToken, Task> delayAsync)
{
UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(targetPath), "targetPath must not be null or empty");
UnityEngine.Debug.Assert(timeoutMs > 0, "timeoutMs must be greater than zero");
UnityEngine.Debug.Assert(pollMs > 0, "pollMs must be greater than zero");
UnityEngine.Debug.Assert(fileExists != null, "fileExists must not be null");
UnityEngine.Debug.Assert(delayAsync != null, "delayAsync must not be null");

int elapsedMs = 0;
while (fileExists(targetPath))
{
ct.ThrowIfCancellationRequested();
if (elapsedMs >= timeoutMs)
{
return new CliInstallResult(
false,
$"Timed out waiting for uLoop CLI uninstall to remove {targetPath}.");
}

int delayMs = Math.Min(pollMs, timeoutMs - elapsedMs);
await delayAsync(delayMs, ct);
elapsedMs += delayMs;
}

return new CliInstallResult(true, "");
}

internal static async Task<CliInstallResult> WaitForUninstallCompletionAsync(
string targetPath,
string installDirectory,
RuntimePlatform platform,
CancellationToken ct,
int timeoutMs,
int pollMs,
Func<string, bool> fileExists,
bool requireUserPathRemoval,
Func<string, EnvironmentVariableTarget, string> getEnvironmentVariable,
Func<int, CancellationToken, Task> delayAsync)
{
UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(targetPath), "targetPath must not be null or empty");
UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty");
UnityEngine.Debug.Assert(timeoutMs > 0, "timeoutMs must be greater than zero");
UnityEngine.Debug.Assert(pollMs > 0, "pollMs must be greater than zero");
UnityEngine.Debug.Assert(fileExists != null, "fileExists must not be null");
UnityEngine.Debug.Assert(getEnvironmentVariable != null, "getEnvironmentVariable must not be null");
UnityEngine.Debug.Assert(delayAsync != null, "delayAsync must not be null");

int elapsedMs = 0;
while (true)
{
bool targetStillExists = fileExists(targetPath);
bool userPathStillContainsInstallDirectory = ShouldWaitForUserPathRemoval(
requireUserPathRemoval,
installDirectory,
platform,
getEnvironmentVariable);
if (!targetStillExists && !userPathStillContainsInstallDirectory)
if (!targetStillExists)
{
return new CliInstallResult(true, "");
}
Expand All @@ -87,58 +43,13 @@ internal static async Task<CliInstallResult> WaitForUninstallCompletionAsync(
{
return new CliInstallResult(
false,
BuildUninstallCompletionTimeoutFailure(
targetPath,
installDirectory,
platform,
targetStillExists,
userPathStillContainsInstallDirectory));
$"Timed out waiting for uLoop CLI uninstall to remove {targetPath}.");
}

int delayMs = Math.Min(pollMs, timeoutMs - elapsedMs);
await delayAsync(delayMs, ct);
elapsedMs += delayMs;
}
}

private static bool ShouldWaitForUserPathRemoval(
bool requireUserPathRemoval,
string installDirectory,
RuntimePlatform platform,
Func<string, EnvironmentVariableTarget, string> getEnvironmentVariable)
{
UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty");
UnityEngine.Debug.Assert(getEnvironmentVariable != null, "getEnvironmentVariable must not be null");

return requireUserPathRemoval
&& NativeCliInstallPathResolver.DoesUserPathContainInstallDirectory(
installDirectory,
platform,
getEnvironmentVariable);
}

private static string BuildUninstallCompletionTimeoutFailure(
string targetPath,
string installDirectory,
RuntimePlatform platform,
bool targetStillExists,
bool userPathStillContainsInstallDirectory)
{
UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(targetPath), "targetPath must not be null or empty");
UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty");
UnityEngine.Debug.Assert(targetStillExists || userPathStillContainsInstallDirectory, "at least one uninstall cleanup condition must still be pending");

if (platform != RuntimePlatform.WindowsEditor || !userPathStillContainsInstallDirectory)
{
return $"Timed out waiting for uLoop CLI uninstall to remove {targetPath}.";
}

if (!targetStillExists)
{
return $"Timed out waiting for uLoop CLI uninstall to remove {installDirectory} from Windows User PATH.";
}

return $"Timed out waiting for uLoop CLI uninstall to remove {targetPath} and remove {installDirectory} from Windows User PATH.";
}
}
}
Loading