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
12 changes: 12 additions & 0 deletions Assets/Tests/Editor/CliInstallationDetectorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,18 @@ public void BuildShellCliDetectionCommand_UsesShortVersionFlag()
Assert.That(command, Does.Not.Contain("uloop --version"));
}

[Test]
public void BuildShellCliDetectionCommandForShell_WhenRuntimeShellIsFish_UsesFishStatusSyntax()
{
// Verifies that command syntax follows the actual shell process, not PATH setup support.
string command = CliInstallationDetector.BuildShellCliDetectionCommandForShell(
"uloop",
"/opt/homebrew/bin/fish");

Assert.That(command, Does.Contain("set uloop_version_status $status"));
Assert.That(command, Does.Not.Contain("uloop_version_status=$?"));
}

[Test]
public void ParseShellCliInstallationOutput_WhenPathAndVersionExist_ReturnsDetection()
{
Expand Down
208 changes: 208 additions & 0 deletions Assets/Tests/Editor/CliPathSetupFlowTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
using System.Threading;
using System.Threading.Tasks;

using NUnit.Framework;
using UnityEngine;

using io.github.hatayama.UnityCliLoop.Application;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Test fixture that verifies the application-owned CLI PATH setup flow.
/// </summary>
public class CliPathSetupFlowTests
{
[Test]
public async Task EnsureCliVisibleFromShellAsync_WhenAlreadyVisibleDoesNotApplyProfile()
{
// Verifies that fresh shell visibility is the authority and no profile write happens when uloop is visible.
FakeCliInstallationDetector detector = new(true);
FakeNativeCliInstaller installer = new FakeNativeCliInstaller(
CreateSupportedPlan(),
CreateAppliedResult());
CliSetupApplicationService service = new(detector, installer);

CliPathSetupFlowResult result = await service.EnsureCliVisibleFromShellAsync(
RuntimePlatform.OSXEditor,
CancellationToken.None);

Assert.That(result.Status, Is.EqualTo(CliPathSetupFlowStatus.AlreadyVisible));
Assert.That(installer.ApplyCount, Is.EqualTo(0));
}

[Test]
public async Task EnsureCliVisibleFromShellAsync_WhenApplySucceedsRechecksShell()
{
// Verifies that PATH setup is only considered complete after a second fresh shell check passes.
FakeCliInstallationDetector detector = new(false, true);
FakeNativeCliInstaller installer = new FakeNativeCliInstaller(
CreateSupportedPlan(),
CreateAppliedResult());
CliSetupApplicationService service = new(detector, installer);

CliPathSetupFlowResult result = await service.EnsureCliVisibleFromShellAsync(
RuntimePlatform.OSXEditor,
CancellationToken.None);

Assert.That(result.Status, Is.EqualTo(CliPathSetupFlowStatus.AppliedAndVisible));
Assert.That(installer.ApplyCount, Is.EqualTo(1));
Assert.That(detector.VisibilityCheckCount, Is.EqualTo(2));
}

[Test]
public async Task EnsureCliVisibleFromShellAsync_WhenUnsupportedShellReturnsManualSetup()
{
// Verifies that unsupported shells never receive automatic profile edits.
FakeCliInstallationDetector detector = new(false);
FakeNativeCliInstaller installer = new FakeNativeCliInstaller(
CreateUnsupportedPlan(),
CreateAppliedResult());
CliSetupApplicationService service = new(detector, installer);

CliPathSetupFlowResult result = await service.EnsureCliVisibleFromShellAsync(
RuntimePlatform.OSXEditor,
CancellationToken.None);

Assert.That(result.Status, Is.EqualTo(CliPathSetupFlowStatus.ManualSetupRequired));
Assert.That(installer.ApplyCount, Is.EqualTo(0));
}

[Test]
public async Task EnsureCliVisibleFromShellAsync_WhenProfileApplyFailsReturnsFailure()
{
// Verifies that profile write failures become user-facing PATH setup failures.
FakeCliInstallationDetector detector = new(false);
CliPathSetupApplyResult applyResult = new CliPathSetupApplyResult(
false,
CliPathSetupApplyStatus.Failed,
"profile is read-only");
FakeNativeCliInstaller installer = new FakeNativeCliInstaller(CreateSupportedPlan(), applyResult);
CliSetupApplicationService service = new CliSetupApplicationService(detector, installer);

CliPathSetupFlowResult result = await service.EnsureCliVisibleFromShellAsync(
RuntimePlatform.OSXEditor,
CancellationToken.None);

Assert.That(result.Status, Is.EqualTo(CliPathSetupFlowStatus.Failed));
Assert.That(result.ErrorOutput, Does.Contain("profile is read-only"));
Assert.That(installer.ApplyCount, Is.EqualTo(1));
Assert.That(detector.VisibilityCheckCount, Is.EqualTo(1));
}

private static CliPathSetupPlan CreateUnsupportedPlan()
{
return new CliPathSetupPlan(
CliPathSetupShellKind.Unsupported,
"tcsh",
false,
"/Users/ExampleUser/.local/bin",
"/Users/ExampleUser/.local/bin",
"",
"",
"");
}

private static CliPathSetupPlan CreateSupportedPlan()
{
return new CliPathSetupPlan(
CliPathSetupShellKind.Zsh,
"zsh",
true,
"/Users/ExampleUser/.local/bin",
"$HOME/.local/bin",
"/Users/ExampleUser/.zshrc",
"export PATH=\"$HOME/.local/bin:$PATH\"",
"printf '\\n%s\\n' 'export PATH=\"$HOME/.local/bin:$PATH\"' >> '/Users/ExampleUser/.zshrc'");
}

private static CliPathSetupApplyResult CreateAppliedResult()
{
return new CliPathSetupApplyResult(true, CliPathSetupApplyStatus.Applied, "");
}

private sealed class FakeCliInstallationDetector : ICliInstallationDetector
{
private readonly bool[] _visibilityResults;

public FakeCliInstallationDetector(params bool[] visibilityResults)
{
_visibilityResults = visibilityResults;
}

public int VisibilityCheckCount { get; private set; }

public bool IsCliInstalled() => true;
public string GetCachedCliVersion() => "3.0.0-beta.9";
public string GetCachedCliExecutablePath() => "/Users/ExampleUser/.local/bin/uloop";
public bool IsCheckCompleted() => true;
public Task RefreshCliVersionAsync(CancellationToken ct) => Task.CompletedTask;
public Task ForceRefreshCliVersionAsync(CancellationToken ct) => Task.CompletedTask;
public void InvalidateCache() { }

public Task<bool> IsCliVisibleFromShellAsync(RuntimePlatform platform, CancellationToken ct)
{
bool result = _visibilityResults[
System.Math.Min(VisibilityCheckCount, _visibilityResults.Length - 1)];
VisibilityCheckCount++;
return Task.FromResult(result);
}
}

private sealed class FakeNativeCliInstaller : INativeCliInstaller
{
private readonly CliPathSetupPlan _plan;
private readonly CliPathSetupApplyResult _applyResult;

public FakeNativeCliInstaller(CliPathSetupPlan plan, CliPathSetupApplyResult applyResult)
{
_plan = plan;
_applyResult = applyResult;
}

public int ApplyCount { get; private set; }

public bool IsPackageOwnedCurrentUserInstallPath(string cliExecutablePath, RuntimePlatform platform)
{
return true;
}

public bool HasPackageOwnedCurrentUserInstall(RuntimePlatform platform)
{
return true;
}

public Task<CliInstallResult> InstallGlobalCliAsync(
RuntimePlatform platform,
string cliReleaseTag,
CancellationToken ct)
{
return Task.FromResult(new CliInstallResult(true, ""));
}

public Task<CliInstallResult> UninstallGlobalCliAsync(RuntimePlatform platform, CancellationToken ct)
{
return Task.FromResult(new CliInstallResult(true, ""));
}

public Task<CliPathSetupPlan> GetGlobalCliPathSetupPlanAsync(RuntimePlatform platform, CancellationToken ct)
{
return Task.FromResult(_plan);
}

public CliPathSetupApplyResult ApplyGlobalCliPathSetup(CliPathSetupPlan plan)
{
ApplyCount++;
return _applyResult;
}

public NativeCliInstallCommand GetGlobalCliInstallCommand(
RuntimePlatform platform,
string cliReleaseTag,
bool removeLegacyLaunchers)
{
return new NativeCliInstallCommand("sh", "-c true", "install");
}
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/CliPathSetupFlowTests.cs.meta

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

108 changes: 108 additions & 0 deletions Assets/Tests/Editor/CliPathSetupProfileResolverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using NUnit.Framework;
using UnityEngine;

using io.github.hatayama.UnityCliLoop.Application;
using io.github.hatayama.UnityCliLoop.Infrastructure;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Test fixture that verifies shell profile selection for CLI PATH setup.
/// </summary>
public class CliPathSetupProfileResolverTests
{
[Test]
public void ResolvePlan_WhenZshUsesZshrcInZdotdir()
{
// Verifies that zsh setup honors the explicit ZDOTDIR environment root without probing login hooks.
CliPathSetupPlan plan = CliPathSetupProfileResolver.ResolvePlan(
RuntimePlatform.OSXEditor,
"/bin/zsh",
"/Users/ExampleUser",
"/Users/ExampleUser/.config/zsh",
null,
"/Users/ExampleUser/.local/bin",
path => false);

Assert.That(plan.ShellKind, Is.EqualTo(CliPathSetupShellKind.Zsh));
Assert.That(plan.ConfigurationFilePath, Is.EqualTo("/Users/ExampleUser/.config/zsh/.zshrc"));
Assert.That(plan.ConfigurationLine, Is.EqualTo("export PATH=\"$HOME/.local/bin:$PATH\""));
}

[Test]
public void ResolvePlan_WhenBashProfileExistsUsesExistingProfile()
{
// Verifies that bash setup avoids creating .bash_profile when a login profile already exists.
CliPathSetupPlan plan = CliPathSetupProfileResolver.ResolvePlan(
RuntimePlatform.OSXEditor,
"/bin/bash",
"/Users/ExampleUser",
null,
null,
"/Users/ExampleUser/.local/bin",
path => path == "/Users/ExampleUser/.profile");

Assert.That(plan.ShellKind, Is.EqualTo(CliPathSetupShellKind.Bash));
Assert.That(plan.ConfigurationFilePath, Is.EqualTo("/Users/ExampleUser/.profile"));
Assert.That(plan.ConfigurationLine, Is.EqualTo("export PATH=\"$HOME/.local/bin:$PATH\""));
}

[Test]
public void ResolvePlan_WhenFishUsesXdgConfigHome()
{
// Verifies that fish setup writes config.fish under XDG_CONFIG_HOME.
CliPathSetupPlan plan = CliPathSetupProfileResolver.ResolvePlan(
RuntimePlatform.OSXEditor,
"/opt/homebrew/bin/fish",
"/Users/ExampleUser",
null,
"/Users/ExampleUser/Library/Application Support",
"/Users/ExampleUser/.local/bin",
path => false);

Assert.That(plan.ShellKind, Is.EqualTo(CliPathSetupShellKind.Fish));
Assert.That(
plan.ConfigurationFilePath,
Is.EqualTo("/Users/ExampleUser/Library/Application Support/fish/config.fish"));
Assert.That(plan.ConfigurationLine, Is.EqualTo("fish_add_path --move \"$HOME/.local/bin\""));
}

[Test]
public void ResolvePlan_WhenUnsupportedShellDisablesAutomaticApply()
{
// Verifies that unknown shells do not expose a command written for a different shell syntax.
CliPathSetupPlan plan = CliPathSetupProfileResolver.ResolvePlan(
RuntimePlatform.OSXEditor,
"/bin/tcsh",
"/Users/ExampleUser",
null,
null,
"/Users/ExampleUser/.local/bin",
path => false);

Assert.That(plan.ShellKind, Is.EqualTo(CliPathSetupShellKind.Unsupported));
Assert.That(plan.CanApplyAutomatically, Is.False);
Assert.That(plan.ManualCommand, Is.Empty);
}

[Test]
public void ResolvePlan_WhenInstallDirectoryIsMissingDoesNotUseExecutableNameAsDirectory()
{
// Verifies that missing install roots do not produce misleading PATH directory guidance.
CliPathSetupPlan plan = CliPathSetupProfileResolver.ResolvePlan(
RuntimePlatform.OSXEditor,
"/bin/zsh",
"/Users/ExampleUser",
null,
null,
"",
path => false);

Assert.That(plan.ShellKind, Is.EqualTo(CliPathSetupShellKind.Unsupported));
Assert.That(plan.CanApplyAutomatically, Is.False);
Assert.That(plan.InstallDirectory, Is.Empty);
Assert.That(plan.ProfileInstallDirectory, Is.Empty);
Assert.That(plan.ManualCommand, Is.Empty);
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/CliPathSetupProfileResolverTests.cs.meta

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

Loading
Loading