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
94 changes: 94 additions & 0 deletions Assets/Tests/Editor/CliInstallProgressFormattingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;

using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.Presentation;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Test fixture that verifies CLI install progress text formatting.
/// </summary>
public class CliInstallProgressFormattingTests
{
[Test]
public void FormatStatusLine_WhenAnimationStepZero_UsesOneVisibleDotWithTransparentPadding()
{
// Verifies one visible dot keeps three-dot width via transparent rich-text padding.
string result = CliInstallProgressFormatting.FormatStatusLine(TimeSpan.Zero, 0);

Assert.That(result, Is.EqualTo("Installing.<color=#00000000>..</color> (0s)"));
}

[Test]
public void FormatStatusLine_WhenAnimationStepOne_UsesTwoVisibleDotsWithTransparentPadding()
{
// Verifies two visible dots keep three-dot width via one transparent padding dot.
string result = CliInstallProgressFormatting.FormatStatusLine(TimeSpan.Zero, 1);

Assert.That(result, Is.EqualTo("Installing..<color=#00000000>.</color> (0s)"));
}

[Test]
public void FormatStatusLine_WhenAnimationStepTwo_UsesThreeVisibleDotsWithoutPadding()
{
// Verifies three visible dots need no transparent padding and match the full status form.
string result = CliInstallProgressFormatting.FormatStatusLine(TimeSpan.Zero, 2);

Assert.That(result, Is.EqualTo("Installing... (0s)"));
}

[Test]
public void FormatStatusLine_WhenUnderOneMinute_ReturnsSecondsOnly()
{
// Verifies sub-minute elapsed time stays in the seconds-only format.
string result = CliInstallProgressFormatting.FormatStatusLine(TimeSpan.FromSeconds(59), 2);

Assert.That(result, Is.EqualTo("Installing... (59s)"));
}

[Test]
public void FormatStatusLine_WhenOverOneMinute_ReturnsMinutesAndSeconds()
{
// Verifies minute formatting pads seconds to two digits with fixed three-dot width.
string result = CliInstallProgressFormatting.FormatStatusLine(TimeSpan.FromSeconds(65), 2);

Assert.That(result, Is.EqualTo("Installing... (1m 05s)"));
}

[Test]
public void FormatStatusLine_WhenExactTensOfMinutes_PadsSeconds()
{
// Verifies exact minute boundaries still show zero-padded seconds.
string result = CliInstallProgressFormatting.FormatStatusLine(TimeSpan.FromSeconds(600), 2);

Assert.That(result, Is.EqualTo("Installing... (10m 00s)"));
}

[Test]
public void FormatStatusLine_WhenAnimationStepThree_LoopsBackToOneVisibleDot()
{
// Verifies animationStep wraps every three steps so the visible-dot cycle repeats.
string result = CliInstallProgressFormatting.FormatStatusLine(TimeSpan.Zero, 3);

Assert.That(result, Is.EqualTo("Installing.<color=#00000000>..</color> (0s)"));
}

[Test]
public void FormatDetailLine_WhenNullOrWhitespace_ReturnsEmpty()
{
// Verifies blank installer lines do not update the detail label.
Assert.That(CliInstallProgressFormatting.FormatDetailLine(null), Is.EqualTo(string.Empty));
Assert.That(CliInstallProgressFormatting.FormatDetailLine(" "), Is.EqualTo(string.Empty));
}

[Test]
public void FormatDetailLine_WhenPadded_TrimsWhitespace()
{
// Verifies surrounding whitespace is stripped from streamed installer output.
string result = CliInstallProgressFormatting.FormatDetailLine(" Downloading uloop ");

Assert.That(result, Is.EqualTo("Downloading uloop"));
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/CliInstallProgressFormattingTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Assets/Tests/Editor/CliPathSetupFlowTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -182,6 +183,7 @@ public Task<CliInstallResult> InstallGlobalCliAsync(
RuntimePlatform platform,
string dispatcherReleaseTag,
string dispatcherArchiveManifest,
IProgress<string> installProgress,
CancellationToken ct)
{
return Task.FromResult(new CliInstallResult(true, ""));
Expand Down
23 changes: 19 additions & 4 deletions Assets/Tests/Editor/CliSetupApplicationServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -25,11 +26,14 @@ public async Task InstallGlobalCliAsync_UsesPinnedDispatcherReleaseTag()
nativeCliInstaller,
new CliPinReaderService());

await service.InstallGlobalCliAsync(RuntimePlatform.OSXEditor, CancellationToken.None);
await service.InstallGlobalCliAsync(
RuntimePlatform.OSXEditor,
new Progress<string>(),
CancellationToken.None);

Assert.That(
nativeCliInstaller.InstalledVersion,
Is.EqualTo("dispatcher-v3.0.1-beta.6"));
Is.EqualTo(ExpectedDispatcherReleaseTag()));
}

[Test]
Expand Down Expand Up @@ -61,7 +65,7 @@ public void GetGlobalCliInstallCommand_UsesPinnedDispatcherReleaseTag()
Assert.That(commandResult.Success, Is.True, commandResult.ErrorOutput);
Assert.That(
commandResult.Command.ManualCommand,
Is.EqualTo("install dispatcher-v3.0.1-beta.6"));
Is.EqualTo($"install {ExpectedDispatcherReleaseTag()}"));
}

[Test]
Expand All @@ -74,7 +78,10 @@ public async Task InstallGlobalCliAsync_WhenBootstrapPinIsUnavailableFailsWithou
nativeCliInstaller,
new FailingBootstrapPinReader());

CliInstallResult result = await service.InstallGlobalCliAsync(RuntimePlatform.OSXEditor, CancellationToken.None);
CliInstallResult result = await service.InstallGlobalCliAsync(
RuntimePlatform.OSXEditor,
new Progress<string>(),
CancellationToken.None);

Assert.That(result.Success, Is.False);
Assert.That(result.ErrorOutput, Does.Contain("bootstrap pin"));
Expand All @@ -88,6 +95,13 @@ private static string ExpectedMinimumDispatcherVersion()
return result.Pin.MinimumDispatcherVersion;
}

private static string ExpectedDispatcherReleaseTag()
{
DispatcherBootstrapPinLoadResult result = new CliPinReaderService().LoadDispatcherBootstrapPin();
Assert.That(result.Success, Is.True, result.ErrorMessage);
return result.DispatcherReleaseTag;
}

private sealed class FakeCliInstallationDetector : ICliInstallationDetector
{
private readonly string[] _versions;
Expand Down Expand Up @@ -158,6 +172,7 @@ public Task<CliInstallResult> InstallGlobalCliAsync(
RuntimePlatform platform,
string dispatcherReleaseTag,
string dispatcherArchiveManifest,
IProgress<string> installProgress,
CancellationToken ct)
{
InstalledVersion = dispatcherReleaseTag;
Expand Down
3 changes: 3 additions & 0 deletions Assets/Tests/Editor/CliSetupSectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ private static VisualElement CreateRootElement()
root.Add(new Label { name = "cli-status-label" });
root.Add(new Button { name = "refresh-cli-version-button" });
root.Add(new Button { name = "install-cli-button" });
VisualElement installProgress = new() { name = "cli-install-progress" };
installProgress.Add(new Label { name = "cli-install-progress-label" });
root.Add(installProgress);
root.Add(new EnumField { name = "skills-target-field" });
root.Add(new Button { name = "refresh-skills-state-button" });
root.Add(new VisualElement { name = "group-skills-row" });
Expand Down
51 changes: 49 additions & 2 deletions Assets/Tests/Editor/NativeCliInstallerTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -301,7 +302,8 @@ public void RunInstallCommand_WhenInstallerExecutableIsMissingReturnsFailure()
CliInstallResult result = NativeCliSetupCommandRunner.RunInstallCommand(
command,
CancellationToken.None,
1000);
1000,
static _ => { });

Assert.That(result.Success, Is.False);
Assert.That(result.ErrorOutput, Does.Contain("Failed to start release CLI installer"));
Expand All @@ -316,12 +318,41 @@ public void RunInstallCommand_WhenInstallerDoesNotExitReturnsFailure()
CliInstallResult result = NativeCliSetupCommandRunner.RunInstallCommand(
command,
CancellationToken.None,
50);
50,
static _ => { });

Assert.That(result.Success, Is.False);
Assert.That(result.ErrorOutput, Does.Contain("timed out"));
}

[Test]
public void RunInstallCommand_StreamsOutputLinesToCallback()
{
// Verifies that installer stdout and stderr lines reach the progress callback so the editor UI can stream them.
NativeCliInstallCommand command = BuildEchoInstallCommand();
List<string> receivedLines = new();

CliInstallResult result = NativeCliSetupCommandRunner.RunInstallCommand(
command,
CancellationToken.None,
5000,
line =>
{
lock (receivedLines)
{
receivedLines.Add(line);
}
});

Assert.That(result.Success, Is.True, result.ErrorOutput);
lock (receivedLines)
{
Assert.That(receivedLines, Does.Contain("line1"));
Assert.That(receivedLines, Does.Contain("line2"));
Assert.That(receivedLines, Does.Contain("err1"));
}
}

[Test]
public void RunUninstallCommand_WhenCanceledReportsUninstallCommand()
{
Expand Down Expand Up @@ -445,6 +476,22 @@ private static NativeCliInstallCommand BuildLongRunningInstallCommand()
"sleep 5");
}

private static NativeCliInstallCommand BuildEchoInstallCommand()
{
if (UnityEngine.Application.platform == RuntimePlatform.WindowsEditor)
{
return new NativeCliInstallCommand(
"powershell",
"-NoProfile -ExecutionPolicy Bypass -Command \"Write-Output 'line1'; Write-Output 'line2'; [Console]::Error.WriteLine('err1')\"",
"Write-Output line1; Write-Output line2; [Console]::Error.WriteLine('err1')");
}

return new NativeCliInstallCommand(
"/bin/sh",
"-c \"echo line1; echo line2; echo err1 1>&2\"",
"echo line1; echo line2; echo err1 1>&2");
}

[Test]
public void BuildPathWithInstallDirectory_OnWindowsMovesExistingNativeInstallDirToFront()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Task<CliInstallResult> InstallGlobalCliAsync(
RuntimePlatform platform,
string dispatcherReleaseTag,
string dispatcherArchiveManifest,
IProgress<string> installProgress,
CancellationToken ct);
Task<CliInstallResult> UninstallGlobalCliAsync(RuntimePlatform platform, CancellationToken ct);
Task<CliPathSetupPlan> GetGlobalCliPathSetupPlanAsync(RuntimePlatform platform, CancellationToken ct);
Expand Down Expand Up @@ -198,8 +199,12 @@ public bool IsCliVersionEqual(string leftVersion, string rightVersion)
return CliVersionComparer.IsVersionEqual(leftVersion, rightVersion);
}

public async Task<CliInstallResult> InstallGlobalCliAsync(RuntimePlatform platform, CancellationToken ct)
public async Task<CliInstallResult> InstallGlobalCliAsync(
RuntimePlatform platform,
IProgress<string> installProgress,
CancellationToken ct)
{
Debug.Assert(installProgress != null, "installProgress must not be null");
ct.ThrowIfCancellationRequested();

DispatcherBootstrapPinLoadResult bootstrapPin = _cliPinReader.LoadDispatcherBootstrapPin();
Expand All @@ -212,6 +217,7 @@ public async Task<CliInstallResult> InstallGlobalCliAsync(RuntimePlatform platfo
platform,
bootstrapPin.DispatcherReleaseTag,
bootstrapPin.ArchiveManifest,
installProgress,
ct);
_cliInstallationDetector.InvalidateCache();
return result;
Expand Down
16 changes: 14 additions & 2 deletions Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ public static async Task<CliInstallResult> InstallAsync(
RuntimePlatform platform,
string dispatcherReleaseTag,
string dispatcherArchiveManifest,
IProgress<string> installProgress,
CancellationToken ct)
{
UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(dispatcherReleaseTag), "dispatcherReleaseTag must not be null or empty");
UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(dispatcherArchiveManifest), "dispatcherArchiveManifest must not be null or empty");
UnityEngine.Debug.Assert(installProgress != null, "installProgress must not be null");
ct.ThrowIfCancellationRequested();

string installDirectory = NativeCliInstallPathResolver.GetInstallDirectoryForCurrentUser(platform);
Expand All @@ -56,7 +58,11 @@ public static async Task<CliInstallResult> InstallAsync(
dispatcherArchiveManifest,
true);
CliInstallResult result = await Task.Run(
() => NativeCliSetupCommandRunner.RunInstallCommand(command, ct, INSTALL_PROCESS_TIMEOUT_MS),
() => NativeCliSetupCommandRunner.RunInstallCommand(
command,
ct,
INSTALL_PROCESS_TIMEOUT_MS,
line => installProgress.Report(line)),
ct);

if (result.Success)
Expand Down Expand Up @@ -184,10 +190,16 @@ public Task<CliInstallResult> InstallGlobalCliAsync(
RuntimePlatform platform,
string dispatcherReleaseTag,
string dispatcherArchiveManifest,
IProgress<string> installProgress,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
return NativeCliInstaller.InstallAsync(platform, dispatcherReleaseTag, dispatcherArchiveManifest, ct);
return NativeCliInstaller.InstallAsync(
platform,
dispatcherReleaseTag,
dispatcherArchiveManifest,
installProgress,
ct);
}

public Task<CliInstallResult> UninstallGlobalCliAsync(RuntimePlatform platform, CancellationToken ct)
Expand Down
Loading
Loading