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
207 changes: 206 additions & 1 deletion src/Cli/Microsoft.Maui.Cli.UnitTests/AndroidCommandsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public void InstallCommand_HasCorrectOptions()
var installCommand = androidCommand.Subcommands.First(c => c.Name == "install");

// Assert
Assert.Contains(installCommand.Options, o => o.Name == "--sdk-path");
Assert.Contains(installCommand.Options, o => o.Name == "--sdk-install-path");
Assert.Contains(installCommand.Options, o => o.Name == "--jdk-path");
Assert.Contains(installCommand.Options, o => o.Name == "--jdk-version");
Assert.Contains(installCommand.Options, o => o.Name == "--packages");
Expand Down Expand Up @@ -238,6 +238,151 @@ public void AndroidCommand_HasAllSubcommands()
Assert.Contains(androidCommand.Subcommands, c => c.Name == "emulator");
}

[Fact]
public void AndroidCommand_HasSdkAndJdkOptions()
{
// Arrange
var androidCommand = AndroidCommands.Create();

// Assert
Assert.Contains(androidCommand.Options, o => o.Name == "--sdk");
Assert.Contains(androidCommand.Options, o => o.Name == "--jdk");
}

[Fact]
public void SdkOption_IsRecursive()
{
// The --sdk option should be available to all subcommands
Assert.True(AndroidCommands.SdkOption.Recursive);
}

[Fact]
public void JdkOption_IsRecursive()
{
// The --jdk option should be available to all subcommands
Assert.True(AndroidCommands.JdkOption.Recursive);
}

[Fact]
public void SdkOption_ParsesOnSubcommand()
{
// Arrange
var rootCommand = Program.BuildRootCommand();

// Act — parse --sdk on a nested subcommand
var parseResult = rootCommand.Parse("android --sdk /custom/sdk sdk list");

// Assert
Assert.Empty(parseResult.Errors);
var sdkValue = parseResult.GetValue(AndroidCommands.SdkOption);
Assert.Equal("/custom/sdk", sdkValue);
}

[Fact]
public void JdkOption_ParsesOnSubcommand()
{
// Arrange
var rootCommand = Program.BuildRootCommand();

// Act
var parseResult = rootCommand.Parse("android --jdk /custom/jdk jdk check");

// Assert
Assert.Empty(parseResult.Errors);
var jdkValue = parseResult.GetValue(AndroidCommands.JdkOption);
Assert.Equal("/custom/jdk", jdkValue);
}

[Fact]
public void SdkAndJdkOptions_ParseTogether()
{
// Arrange
var rootCommand = Program.BuildRootCommand();

// Act
var parseResult = rootCommand.Parse("android --sdk /my/sdk --jdk /my/jdk emulator list");

// Assert
Comment thread
rmarinho marked this conversation as resolved.
Assert.Empty(parseResult.Errors);
Assert.Equal("/my/sdk", parseResult.GetValue(AndroidCommands.SdkOption));
Assert.Equal("/my/jdk", parseResult.GetValue(AndroidCommands.JdkOption));
}

[Fact]
public async Task SdkOption_OverridesProviderSdkPath()
{
var tempSdk = Path.Combine(Path.GetTempPath(), "maui-test-sdk-" + Path.GetRandomFileName());
Directory.CreateDirectory(tempSdk);
try
{
var fakeAndroid = new FakeAndroidProvider
{
IsSdkInstalled = true,
SdkPath = "/original/sdk",
LicensesAccepted = true
};

var testProvider = ServiceConfiguration.CreateTestServiceProvider(androidProvider: fakeAndroid);
try
{
Program.Services = testProvider;

var rootCommand = Program.BuildRootCommand();
var parseResult = rootCommand.Parse($"android --sdk {tempSdk} install --json");
await parseResult.InvokeAsync();

// The provider's SdkPath should have been overridden
Assert.Equal(tempSdk, fakeAndroid.SdkPath);
}
finally
{
Program.ResetServices();
}
}
finally
{
if (Directory.Exists(tempSdk))
Directory.Delete(tempSdk, recursive: true);
}
}

[Fact]
public async Task JdkOption_OverridesProviderJdkPath()
{
var tempJdk = Path.Combine(Path.GetTempPath(), "maui-test-jdk-" + Path.GetRandomFileName());
Directory.CreateDirectory(tempJdk);
try
{
var fakeAndroid = new FakeAndroidProvider
{
IsSdkInstalled = true,
LicensesAccepted = true
};

var testProvider = ServiceConfiguration.CreateTestServiceProvider(androidProvider: fakeAndroid);
try
{
Program.Services = testProvider;

var rootCommand = Program.BuildRootCommand();
var parseResult = rootCommand.Parse($"android --jdk {tempJdk} install --json");
await parseResult.InvokeAsync();

// The provider's JdkPath should have been overridden
Assert.Equal(tempJdk, fakeAndroid.JdkPath);
}
finally
{
Program.ResetServices();
}
}
finally
{
if (Directory.Exists(tempJdk))
Directory.Delete(tempJdk, recursive: true);
}
}

// --- Handler-level tests for the JSON/non-Spectre 'android install' license preflight. ---
// These exercise the behavior added in PR #106: fail fast when the SDK is already
// installed and licenses aren't accepted, but don't block on a fresh machine where
Expand Down Expand Up @@ -326,4 +471,64 @@ public async Task InstallCommand_Json_Proceeds_WhenAcceptLicensesFlagPassed()
Assert.Equal(0, exitCode);
Assert.Single(fake.InstallCalls);
}

[Fact]
public void GetAndroidProvider_RejectsWhitespaceOnlySdkPath()
{
var rootCommand = Program.BuildRootCommand();
var parseResult = rootCommand.Parse("android --sdk \" \" sdk list");

// Whitespace-only should be treated as empty and not applied
Assert.Empty(parseResult.Errors);
// GetAndroidProvider uses IsNullOrWhiteSpace, so " " is ignored
}

[Fact]
public void GetAndroidProvider_ThrowsForNonexistentSdkPath()
{
var fakeAndroid = new FakeAndroidProvider { IsSdkInstalled = true };
var testProvider = ServiceConfiguration.CreateTestServiceProvider(androidProvider: fakeAndroid);
try
{
Program.Services = testProvider;
var rootCommand = Program.BuildRootCommand();
var parseResult = rootCommand.Parse("android --sdk /nonexistent/path/that/does/not/exist sdk list");

Assert.Throws<DirectoryNotFoundException>(() => AndroidCommands.GetAndroidProvider(parseResult));
}
finally
{
Program.ResetServices();
}
}

[Fact]
public void GetAndroidProvider_ThrowsForNonexistentJdkPath()
{
var fakeAndroid = new FakeAndroidProvider { IsSdkInstalled = true };
var testProvider = ServiceConfiguration.CreateTestServiceProvider(androidProvider: fakeAndroid);
try
{
Program.Services = testProvider;
var rootCommand = Program.BuildRootCommand();
var parseResult = rootCommand.Parse("android --jdk /nonexistent/jdk/path sdk list");

Assert.Throws<DirectoryNotFoundException>(() => AndroidCommands.GetAndroidProvider(parseResult));
}
finally
{
Program.ResetServices();
}
}

[Fact]
public void InstallCommand_HasSdkInstallPathOption()
{
// Verify the option was renamed from --sdk-path to --sdk-install-path
var androidCommand = AndroidCommands.Create();
var installCommand = androidCommand.Subcommands.First(c => c.Name == "install");

Assert.Contains(installCommand.Options, o => o.Name == "--sdk-install-path");
Assert.DoesNotContain(installCommand.Options, o => o.Name == "--sdk-path");
}
}
93 changes: 93 additions & 0 deletions src/Cli/Microsoft.Maui.Cli.UnitTests/AndroidProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,96 @@ private static int ExtractApiLevel(string systemImagePath)
return 0;
}
}

public class AndroidProviderOverrideTests : IDisposable
{
readonly string _originalSdkDir;
readonly string _overrideSdkDir;

sealed class StubJdkManager : IJdkManager
{
public string? DetectedJdkPath { get; init; } = Path.GetTempPath();
public int? DetectedJdkVersion { get; init; } = 17;
public bool IsInstalled => true;
public Task<HealthCheck> CheckHealthAsync(CancellationToken ct = default) =>
Task.FromResult(new HealthCheck { Category = "android", Name = "JDK", Status = CheckStatus.Ok });
public Task InstallAsync(int? version = null, string? installPath = null, CancellationToken ct = default) =>
Task.CompletedTask;
public Task InstallAsync(int? version, string? installPath, Action<double, string>? onProgress, CancellationToken ct = default) =>
Task.CompletedTask;
public IEnumerable<int> GetAvailableVersions() => JdkManager.SupportedInstallVersions;
}

public AndroidProviderOverrideTests()
{
_originalSdkDir = Path.Combine(Path.GetTempPath(), "maui-test-orig-" + Path.GetRandomFileName());
_overrideSdkDir = Path.Combine(Path.GetTempPath(), "maui-test-ovr-" + Path.GetRandomFileName());
Directory.CreateDirectory(_originalSdkDir);
Directory.CreateDirectory(_overrideSdkDir);

// Create platform-tools/adb in the override SDK so Adb resolves successfully there
var platformToolsDir = Path.Combine(_overrideSdkDir, "platform-tools");
Directory.CreateDirectory(platformToolsDir);
var adbName = OperatingSystem.IsWindows() ? "adb.exe" : "adb";
File.WriteAllText(Path.Combine(platformToolsDir, adbName), "stub");
}

public void Dispose()
{
if (Directory.Exists(_originalSdkDir))
Directory.Delete(_originalSdkDir, recursive: true);
if (Directory.Exists(_overrideSdkDir))
Directory.Delete(_overrideSdkDir, recursive: true);
}

[Fact]
public void OverrideSdkPath_UpdatesSdkPathProperty()
{
var provider = new AndroidProvider(new StubJdkManager());

provider.OverrideSdkPath(_overrideSdkDir);

Assert.Equal(_overrideSdkDir, provider.SdkPath);
}

[Fact]
public void OverrideSdkPath_RebuildsMakesAdbAvailable()
{
// Construct with the empty original SDK dir (no platform-tools/adb)
Environment.SetEnvironmentVariable("ANDROID_HOME", _originalSdkDir);
try
{
var provider = new AndroidProvider(new StubJdkManager());

// Override to SDK dir that has platform-tools/adb
provider.OverrideSdkPath(_overrideSdkDir);

// After override, the provider should report the new SDK path and be installed
Assert.Equal(_overrideSdkDir, provider.SdkPath);
Assert.True(provider.IsSdkInstalled);
}
finally
{
Environment.SetEnvironmentVariable("ANDROID_HOME", null);
}
}

[Fact]
public void OverrideJdkPath_UpdatesJdkPathProperty()
{
var tempJdk = Path.Combine(Path.GetTempPath(), "maui-test-jdk-" + Path.GetRandomFileName());
Directory.CreateDirectory(tempJdk);
try
{
var provider = new AndroidProvider(new StubJdkManager());
provider.OverrideJdkPath(tempJdk);

Assert.Equal(tempJdk, provider.JdkPath);
}
finally
{
if (Directory.Exists(tempJdk))
Directory.Delete(tempJdk, recursive: true);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ public Task InstallSdkToolsAsync(string targetPath, Action<string, int, string>?
return Task.CompletedTask;
}

public void OverrideSdkPath(string path) => SdkPath = path;

public void OverrideJdkPath(string path) => JdkPath = path;

public void Dispose()
{
Disposed = true;
Expand Down
10 changes: 5 additions & 5 deletions src/Cli/Microsoft.Maui.Cli/Commands/AndroidCommands.Emulator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ static Command CreateEmulatorCommand()
var listCommand = new Command("list", "List available emulators");
listCommand.SetAction(async (ParseResult parseResult, CancellationToken cancellationToken) =>
{
var androidProvider = Program.AndroidProvider;
var androidProvider = GetAndroidProvider(parseResult);

var useJson = parseResult.GetValue(GlobalOptions.JsonOption);
var formatter = Program.GetFormatter(parseResult);
Expand Down Expand Up @@ -67,7 +67,7 @@ static Command CreateEmulatorCommand()
};
createCommand.SetAction(async (ParseResult parseResult, CancellationToken cancellationToken) =>
{
var androidProvider = Program.AndroidProvider;
var androidProvider = GetAndroidProvider(parseResult);

var useJson = parseResult.GetValue(GlobalOptions.JsonOption);
var dryRun = parseResult.GetValue(GlobalOptions.DryRunOption);
Expand Down Expand Up @@ -220,7 +220,7 @@ static Command CreateEmulatorCommand()
};
startCommand.SetAction(async (ParseResult parseResult, CancellationToken cancellationToken) =>
{
var androidProvider = Program.AndroidProvider;
var androidProvider = GetAndroidProvider(parseResult);

var useJson = parseResult.GetValue(GlobalOptions.JsonOption);
var dryRun = parseResult.GetValue(GlobalOptions.DryRunOption);
Expand Down Expand Up @@ -317,7 +317,7 @@ await spectre.StatusAsync($"Starting {name}...", async () =>
};
stopCommand.SetAction(async (ParseResult parseResult, CancellationToken cancellationToken) =>
{
var androidProvider = Program.AndroidProvider;
var androidProvider = GetAndroidProvider(parseResult);

var useJson = parseResult.GetValue(GlobalOptions.JsonOption);
var dryRun = parseResult.GetValue(GlobalOptions.DryRunOption);
Expand Down Expand Up @@ -435,7 +435,7 @@ await spectreStop2.StatusAsync($"Stopping {name}...", async () =>
};
deleteCommand.SetAction(async (ParseResult parseResult, CancellationToken cancellationToken) =>
{
var androidProvider = Program.AndroidProvider;
var androidProvider = GetAndroidProvider(parseResult);

var useJson = parseResult.GetValue(GlobalOptions.JsonOption);
var dryRun = parseResult.GetValue(GlobalOptions.DryRunOption);
Expand Down
Loading
Loading