diff --git a/external/xamarin-android-tools b/external/xamarin-android-tools index 40b30131791..d8ee2d591af 160000 --- a/external/xamarin-android-tools +++ b/external/xamarin-android-tools @@ -1 +1 @@ -Subproject commit 40b30131791e7e996e20d461f8d3694b273f6985 +Subproject commit d8ee2d591afb4175c5ebf75e8ca261ed9c6685ff diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/BootAndroidEmulator.cs b/src/Xamarin.Android.Build.Tasks/Tasks/BootAndroidEmulator.cs index bcfa0d656de..62ca7fc79a1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/BootAndroidEmulator.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/BootAndroidEmulator.cs @@ -1,10 +1,8 @@ #nullable enable using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Threading; using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; using Xamarin.Android.Tools; @@ -24,11 +22,13 @@ namespace Xamarin.Android.Tasks; /// the emulator and waits for it to become fully ready. /// /// On success, outputs the resolved ADB serial and AdbTarget for use by subsequent tasks. +/// +/// Boot logic is delegated to and +/// in Xamarin.Android.Tools.AndroidSdk. /// public class BootAndroidEmulator : AndroidTask { const int DefaultBootTimeoutSeconds = 300; - const int PollIntervalMilliseconds = 500; public override string TaskPrefix => "BAE"; @@ -95,71 +95,63 @@ public class BootAndroidEmulator : AndroidTask public override bool RunTask () { var adbPath = ResolveAdbPath (); + var emulatorPath = ResolveEmulatorPath (); + var logger = this.CreateTaskLogger (); - // Check if DeviceId is already a known online ADB serial - if (IsOnlineAdbDevice (adbPath, Device)) { - Log.LogMessage (MessageImportance.Normal, $"Device '{Device}' is already online."); - ResolvedDevice = Device; - AdbTarget = $"-s {Device}"; - return true; - } - - // DeviceId is not an online serial — treat it as an AVD name and boot it - Log.LogMessage (MessageImportance.Normal, $"Device '{Device}' is not an online ADB device. Treating as AVD name."); + var options = new EmulatorBootOptions { + BootTimeout = TimeSpan.FromSeconds (BootTimeoutSeconds), + AdditionalArgs = ParseExtraArguments (EmulatorExtraArguments), + }; - var emulatorPath = ResolveEmulatorPath (); - var avdName = Device; + var result = ExecuteBoot (adbPath, emulatorPath, logger, Device, options); - // Check if this AVD is already running (but perhaps still booting) - var existingSerial = FindRunningEmulatorForAvd (adbPath, avdName); - if (existingSerial != null) { - Log.LogMessage (MessageImportance.High, $"Emulator '{avdName}' is already running as '{existingSerial}'"); - ResolvedDevice = existingSerial; - AdbTarget = $"-s {existingSerial}"; - return WaitForFullBoot (adbPath, avdName, existingSerial); + if (result.Success) { + ResolvedDevice = result.Serial; + AdbTarget = $"-s {result.Serial}"; + Log.LogMessage (MessageImportance.High, $"Emulator '{Device}' ({result.Serial}) is fully booted and ready."); + return true; } - // Launch the emulator process in the background - Log.LogMessage (MessageImportance.High, $"Booting emulator '{avdName}'..."); - using var emulatorProcess = LaunchEmulatorProcess (emulatorPath, avdName); - if (emulatorProcess == null) { - return false; + // Map the error message to the appropriate MSBuild error code + var errorMessage = result.ErrorMessage ?? "Unknown error"; + + if (errorMessage.Contains ("Failed to launch")) { + Log.LogCodedError ("XA0143", Properties.Resources.XA0143, Device, errorMessage); + } else if (errorMessage.Contains ("Timed out")) { + Log.LogCodedError ("XA0145", Properties.Resources.XA0145, Device, BootTimeoutSeconds); + } else { + Log.LogCodedError ("XA0145", Properties.Resources.XA0145, Device, BootTimeoutSeconds); } - try { - var timeout = TimeSpan.FromSeconds (BootTimeoutSeconds); - var stopwatch = Stopwatch.StartNew (); + return false; + } - // Phase 1: Wait for the emulator to appear in 'adb devices' as online - Log.LogMessage (MessageImportance.Normal, "Waiting for emulator to appear in adb devices..."); - var serial = WaitForEmulatorOnline (adbPath, avdName, emulatorProcess, stopwatch, timeout); - if (serial == null) { - if (emulatorProcess.HasExited) { - Log.LogCodedError ("XA0144", Properties.Resources.XA0144, avdName, emulatorProcess.ExitCode); - } else { - Log.LogCodedError ("XA0145", Properties.Resources.XA0145, avdName, BootTimeoutSeconds); - } - return false; - } + /// + /// Executes the full boot flow via . + /// Virtual so tests can return canned results without launching real processes. + /// + protected virtual EmulatorBootResult ExecuteBoot ( + string adbPath, + string emulatorPath, + Action logger, + string device, + EmulatorBootOptions options) + { + var adbRunner = new AdbRunner (adbPath, logger: logger); + var emulatorRunner = new EmulatorRunner (emulatorPath, logger: logger); + return emulatorRunner.BootEmulatorAsync (device, adbRunner, options) + .GetAwaiter ().GetResult (); + } - ResolvedDevice = serial; - AdbTarget = $"-s {serial}"; - Log.LogMessage (MessageImportance.Normal, $"Emulator appeared as '{serial}'"); + /// + /// Parses space-separated extra arguments into an array suitable for . + /// + static string[]? ParseExtraArguments (string? extraArgs) + { + if (extraArgs.IsNullOrEmpty ()) + return null; - // Phase 2: Wait for the device to fully boot - return WaitForFullBoot (adbPath, avdName, serial); - } finally { - // Stop async reads and unsubscribe events; using var handles Dispose - try { - emulatorProcess.CancelOutputRead (); - emulatorProcess.CancelErrorRead (); - } catch (InvalidOperationException e) { - // Async reads may not have been started or process already exited - Log.LogDebugMessage ($"Failed to cancel async reads: {e}"); - } - emulatorProcess.OutputDataReceived -= EmulatorOutputDataReceived; - emulatorProcess.ErrorDataReceived -= EmulatorErrorDataReceived; - } + return extraArgs.Split ([' '], StringSplitOptions.RemoveEmptyEntries); } /// @@ -193,262 +185,4 @@ string ResolveEmulatorPath () return dir.IsNullOrEmpty () ? exe : Path.Combine (dir, exe); } - - /// - /// Checks whether the given deviceId is currently listed as an online device in 'adb devices'. - /// - protected virtual bool IsOnlineAdbDevice (string adbPath, string deviceId) - { - bool found = false; - - MonoAndroidHelper.RunProcess ( - adbPath, "devices", - Log, - onOutput: (sender, e) => { - if (e.Data != null && e.Data.Contains ("device") && !e.Data.Contains ("List of devices")) { - var parts = e.Data.Split (['\t', ' '], StringSplitOptions.RemoveEmptyEntries); - if (parts.Length >= 2 && parts [1] == "device" && - string.Equals (parts [0], deviceId, StringComparison.OrdinalIgnoreCase)) { - found = true; - } - } - }, - logWarningOnFailure: false - ); - - return found; - } - - /// - /// Checks if an emulator with the specified AVD name is already running by querying - /// 'adb devices' and then 'adb -s serial emu avd name' for each running emulator. - /// - protected virtual string? FindRunningEmulatorForAvd (string adbPath, string avdName) - { - var emulatorSerials = new List (); - - MonoAndroidHelper.RunProcess ( - adbPath, "devices", - Log, - onOutput: (sender, e) => { - if (e.Data != null && e.Data.StartsWith ("emulator-", StringComparison.OrdinalIgnoreCase) && e.Data.Contains ("device")) { - var parts = e.Data.Split (['\t', ' '], StringSplitOptions.RemoveEmptyEntries); - if (parts.Length >= 2 && parts [1] == "device") { - emulatorSerials.Add (parts [0]); - } - } - }, - logWarningOnFailure: false - ); - - foreach (var serial in emulatorSerials) { - var name = GetRunningAvdName (adbPath, serial); - if (string.Equals (name, avdName, StringComparison.OrdinalIgnoreCase)) { - return serial; - } - } - - return null; - } - - /// - /// Gets the AVD name from a running emulator via 'adb -s serial emu avd name'. - /// - protected virtual string? GetRunningAvdName (string adbPath, string serial) - { - string? avdName = null; - try { - var outputLines = new List (); - MonoAndroidHelper.RunProcess ( - adbPath, $"-s {serial} emu avd name", - Log, - onOutput: (sender, e) => { - if (!e.Data.IsNullOrEmpty ()) { - outputLines.Add (e.Data); - } - }, - logWarningOnFailure: false - ); - - if (outputLines.Count > 0) { - var name = outputLines [0].Trim (); - if (!name.IsNullOrEmpty () && !name.Equals ("OK", StringComparison.OrdinalIgnoreCase)) { - avdName = name; - } - } - } catch (Exception ex) { - Log.LogDebugMessage ($"Failed to get AVD name for {serial}: {ex.Message}"); - } - - return avdName; - } - - /// - /// Launches the emulator process in the background. The emulator window is shown by default, - /// but this can be customized (for example, by passing -no-window) via EmulatorExtraArguments. - /// - protected virtual Process? LaunchEmulatorProcess (string emulatorPath, string avdName) - { - var arguments = $"-avd \"{avdName}\""; - if (!EmulatorExtraArguments.IsNullOrEmpty ()) { - arguments += $" {EmulatorExtraArguments}"; - } - - Log.LogMessage (MessageImportance.Normal, $"Starting: {emulatorPath} {arguments}"); - - try { - var psi = new ProcessStartInfo { - FileName = emulatorPath, - Arguments = arguments, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }; - - var process = new Process { StartInfo = psi }; - - // Capture output for diagnostics but don't block on it - process.OutputDataReceived += EmulatorOutputDataReceived; - process.ErrorDataReceived += EmulatorErrorDataReceived; - - process.Start (); - process.BeginOutputReadLine (); - process.BeginErrorReadLine (); - - return process; - } catch (Exception ex) { - Log.LogCodedError ("XA0143", Properties.Resources.XA0143, avdName, ex.Message); - return null; - } - } - - void EmulatorOutputDataReceived (object sender, DataReceivedEventArgs e) - { - if (e.Data != null) { - Log.LogDebugMessage ($"emulator stdout: {e.Data}"); - } - } - - void EmulatorErrorDataReceived (object sender, DataReceivedEventArgs e) - { - if (e.Data != null) { - Log.LogDebugMessage ($"emulator stderr: {e.Data}"); - } - } - - /// - /// Polls 'adb devices' until a new emulator serial appears with state "device" (online). - /// Returns the serial or null on timeout / emulator process exit. - /// - string? WaitForEmulatorOnline (string adbPath, string avdName, Process emulatorProcess, Stopwatch stopwatch, TimeSpan timeout) - { - while (stopwatch.Elapsed < timeout) { - if (emulatorProcess.HasExited) { - return null; - } - - var serial = FindRunningEmulatorForAvd (adbPath, avdName); - if (serial != null) { - return serial; - } - - Thread.Sleep (PollIntervalMilliseconds); - } - - return null; - } - - /// - /// Waits for the emulator to fully boot by checking: - /// 1. sys.boot_completed property equals "1" - /// 2. Package manager is responsive (pm path android returns "package:") - /// - bool WaitForFullBoot (string adbPath, string avdName, string serial) - { - Log.LogMessage (MessageImportance.Normal, "Waiting for emulator to fully boot..."); - var stopwatch = Stopwatch.StartNew (); - var timeout = TimeSpan.FromSeconds (BootTimeoutSeconds); - - // Phase 1: Wait for sys.boot_completed == 1 - while (stopwatch.Elapsed < timeout) { - var bootCompleted = GetShellProperty (adbPath, serial, "sys.boot_completed"); - if (bootCompleted == "1") { - Log.LogMessage (MessageImportance.Normal, "sys.boot_completed = 1"); - break; - } - - Thread.Sleep (PollIntervalMilliseconds); - } - - if (stopwatch.Elapsed >= timeout) { - Log.LogCodedError ("XA0145", Properties.Resources.XA0145, avdName, BootTimeoutSeconds); - return false; - } - - var remaining = timeout - stopwatch.Elapsed; - Log.LogMessage (MessageImportance.Normal, $"Phase 1 complete. {remaining.TotalSeconds:F0}s remaining for package manager."); - - // Phase 2: Wait for package manager to be responsive - while (stopwatch.Elapsed < timeout) { - var pmResult = RunShellCommand (adbPath, serial, "pm path android"); - if (pmResult != null && pmResult.StartsWith ("package:", StringComparison.OrdinalIgnoreCase)) { - Log.LogMessage (MessageImportance.High, $"Emulator '{avdName}' ({serial}) is fully booted and ready."); - return true; - } - - Thread.Sleep (PollIntervalMilliseconds); - } - - Log.LogCodedError ("XA0145", Properties.Resources.XA0145, avdName, BootTimeoutSeconds); - return false; - } - - /// - /// Gets a system property from the device via 'adb -s serial shell getprop property'. - /// - protected virtual string? GetShellProperty (string adbPath, string serial, string propertyName) - { - string? value = null; - try { - MonoAndroidHelper.RunProcess ( - adbPath, $"-s {serial} shell getprop {propertyName}", - Log, - onOutput: (sender, e) => { - if (!e.Data.IsNullOrEmpty ()) { - value = e.Data.Trim (); - } - }, - logWarningOnFailure: false - ); - } catch (Exception ex) { - Log.LogDebugMessage ($"Failed to get property '{propertyName}' from {serial}: {ex.Message}"); - } - - return value; - } - - /// - /// Runs a shell command on the device and returns the first line of output. - /// - protected virtual string? RunShellCommand (string adbPath, string serial, string command) - { - string? result = null; - try { - MonoAndroidHelper.RunProcess ( - adbPath, $"-s {serial} shell {command}", - Log, - onOutput: (sender, e) => { - if (result == null && !e.Data.IsNullOrEmpty ()) { - result = e.Data.Trim (); - } - }, - logWarningOnFailure: false - ); - } catch (Exception ex) { - Log.LogDebugMessage ($"Failed to run shell command '{command}' on {serial}: {ex.Message}"); - } - - return result; - } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/BootAndroidEmulatorTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/BootAndroidEmulatorTests.cs index 58ab1ae095e..9eb4b15c819 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/BootAndroidEmulatorTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/BootAndroidEmulatorTests.cs @@ -8,6 +8,7 @@ using Microsoft.Build.Utilities; using NUnit.Framework; using Xamarin.Android.Tasks; +using Xamarin.Android.Tools; namespace Xamarin.Android.Build.Tests; @@ -26,73 +27,23 @@ public void Setup () } /// - /// Mock version of BootAndroidEmulator that overrides all process-dependent methods - /// so we can test the task logic without launching real emulators or adb. + /// Mock version of BootAndroidEmulator that overrides + /// to return a configurable without launching real processes. /// class MockBootAndroidEmulator : BootAndroidEmulator { - public HashSet OnlineDevices { get; set; } = []; - public Dictionary RunningEmulatorAvdNames { get; set; } = new (); - public Dictionary EmulatorBootBehavior { get; set; } = new (); - public Dictionary BootCompletedValues { get; set; } = new (); - public Dictionary PmPathResults { get; set; } = new (); - public bool SimulateLaunchFailure { get; set; } - public string? LastLaunchAvdName { get; private set; } - - readonly Dictionary findCallCounts = new (); - - protected override bool IsOnlineAdbDevice (string adbPath, string deviceId) - => OnlineDevices.Contains (deviceId); - - protected override string? FindRunningEmulatorForAvd (string adbPath, string avdName) + public EmulatorBootResult BootResult { get; set; } = new () { Success = true, Serial = "emulator-5554" }; + public string? LastBootedDevice { get; private set; } + + protected override EmulatorBootResult ExecuteBoot ( + string adbPath, + string emulatorPath, + Action logger, + string device, + EmulatorBootOptions options) { - foreach (var kvp in RunningEmulatorAvdNames) { - if (string.Equals (kvp.Value, avdName, StringComparison.OrdinalIgnoreCase) && - OnlineDevices.Contains (kvp.Key)) { - return kvp.Key; - } - } - - if (EmulatorBootBehavior.TryGetValue (avdName, out var behavior)) { - findCallCounts.TryAdd (avdName, 0); - findCallCounts [avdName]++; - if (findCallCounts [avdName] >= behavior.PollsUntilOnline) { - OnlineDevices.Add (behavior.Serial); - RunningEmulatorAvdNames [behavior.Serial] = avdName; - return behavior.Serial; - } - } - - return null; - } - - protected override string? GetRunningAvdName (string adbPath, string serial) - => RunningEmulatorAvdNames.TryGetValue (serial, out var name) ? name : null; - - protected override Process? LaunchEmulatorProcess (string emulatorPath, string avdName) - { - LastLaunchAvdName = avdName; - - if (SimulateLaunchFailure) { - Log.LogError ("XA0143: Failed to launch emulator for AVD '{0}': {1}", avdName, "Simulated launch failure"); - return null; - } - - return Process.GetCurrentProcess (); - } - - protected override string? GetShellProperty (string adbPath, string serial, string propertyName) - { - if (propertyName == "sys.boot_completed" && BootCompletedValues.TryGetValue (serial, out var value)) - return value; - return null; - } - - protected override string? RunShellCommand (string adbPath, string serial, string command) - { - if (command == "pm path android" && PmPathResults.TryGetValue (serial, out var result)) - return result; - return null; + LastBootedDevice = device; + return BootResult; } } @@ -112,8 +63,12 @@ MockBootAndroidEmulator CreateTask (string device = "Pixel_6_API_33") [Test] public void AlreadyOnlineDevice_PassesThrough () { + // BootEmulatorAsync returns success immediately (device is already online) var task = CreateTask ("emulator-5554"); - task.OnlineDevices = ["emulator-5554"]; + task.BootResult = new EmulatorBootResult { + Success = true, + Serial = "emulator-5554", + }; Assert.IsTrue (task.RunTask (), "RunTask should succeed"); Assert.AreEqual ("emulator-5554", task.ResolvedDevice); @@ -125,7 +80,10 @@ public void AlreadyOnlineDevice_PassesThrough () public void AlreadyOnlinePhysicalDevice_PassesThrough () { var task = CreateTask ("0A041FDD400327"); - task.OnlineDevices = ["0A041FDD400327"]; + task.BootResult = new EmulatorBootResult { + Success = true, + Serial = "0A041FDD400327", + }; Assert.IsTrue (task.RunTask (), "RunTask should succeed"); Assert.AreEqual ("0A041FDD400327", task.ResolvedDevice); @@ -136,73 +94,54 @@ public void AlreadyOnlinePhysicalDevice_PassesThrough () public void AvdAlreadyRunning_WaitsForFullBoot () { var task = CreateTask ("Pixel_6_API_33"); - task.OnlineDevices = ["emulator-5554"]; - task.RunningEmulatorAvdNames = new () { - { "emulator-5554", "Pixel_6_API_33" } + task.BootResult = new EmulatorBootResult { + Success = true, + Serial = "emulator-5554", }; - task.BootCompletedValues = new () { { "emulator-5554", "1" } }; - task.PmPathResults = new () { { "emulator-5554", "package:/system/framework/framework-res.apk" } }; Assert.IsTrue (task.RunTask (), "RunTask should succeed"); Assert.AreEqual ("emulator-5554", task.ResolvedDevice); Assert.AreEqual ("-s emulator-5554", task.AdbTarget); + Assert.AreEqual ("Pixel_6_API_33", task.LastBootedDevice); } [Test] public void BootEmulator_AppearsAfterPolling () { var task = CreateTask ("Pixel_6_API_33"); - // Not online initially, will appear after 2 polls - task.EmulatorBootBehavior = new () { - { "Pixel_6_API_33", ("emulator-5556", 2) } + task.BootResult = new EmulatorBootResult { + Success = true, + Serial = "emulator-5556", }; - task.BootCompletedValues = new () { { "emulator-5556", "1" } }; - task.PmPathResults = new () { { "emulator-5556", "package:/system/framework/framework-res.apk" } }; Assert.IsTrue (task.RunTask (), "RunTask should succeed"); Assert.AreEqual ("emulator-5556", task.ResolvedDevice); Assert.AreEqual ("-s emulator-5556", task.AdbTarget); - Assert.AreEqual ("Pixel_6_API_33", task.LastLaunchAvdName); + Assert.AreEqual ("Pixel_6_API_33", task.LastBootedDevice); } [Test] public void LaunchFailure_ReturnsError () { var task = CreateTask ("Pixel_6_API_33"); - task.SimulateLaunchFailure = true; - - Assert.IsFalse (task.RunTask (), "RunTask should fail"); - Assert.IsTrue (errors.Any (e => e.Message != null && e.Message.Contains ("XA0143")), "Should have XA0143 error"); - Assert.IsNull (task.ResolvedDevice, "ResolvedDevice should be null"); - } - - [Test] - public void BootTimeout_BootCompletedNeverReaches1 () - { - var task = CreateTask ("Pixel_6_API_33"); - task.BootTimeoutSeconds = 0; // Immediate timeout - // Emulator appears immediately but never finishes booting - task.OnlineDevices = ["emulator-5554"]; - task.RunningEmulatorAvdNames = new () { - { "emulator-5554", "Pixel_6_API_33" } + task.BootResult = new EmulatorBootResult { + Success = false, + ErrorMessage = "Failed to launch emulator: Simulated launch failure", }; - task.BootCompletedValues = new () { { "emulator-5554", "0" } }; Assert.IsFalse (task.RunTask (), "RunTask should fail"); - Assert.IsTrue (errors.Any (e => e.Code == "XA0145"), "Should have XA0145 timeout error"); + Assert.IsTrue (errors.Any (e => e.Code == "XA0143"), "Should have XA0143 error"); + Assert.IsNull (task.ResolvedDevice, "ResolvedDevice should be null"); } [Test] - public void BootTimeout_PmNeverResponds () + public void BootTimeout_ReturnsError () { var task = CreateTask ("Pixel_6_API_33"); - task.BootTimeoutSeconds = 0; // Immediate timeout - task.OnlineDevices = ["emulator-5554"]; - task.RunningEmulatorAvdNames = new () { - { "emulator-5554", "Pixel_6_API_33" } + task.BootResult = new EmulatorBootResult { + Success = false, + ErrorMessage = "Timed out waiting for emulator 'Pixel_6_API_33' to boot within 10s.", }; - task.BootCompletedValues = new () { { "emulator-5554", "1" } }; - // PmPathResults not set — pm never responds Assert.IsFalse (task.RunTask (), "RunTask should fail"); Assert.IsTrue (errors.Any (e => e.Code == "XA0145"), "Should have XA0145 timeout error"); @@ -212,13 +151,10 @@ public void BootTimeout_PmNeverResponds () public void MultipleEmulators_FindsCorrectAvd () { var task = CreateTask ("Pixel_9_Pro_XL"); - task.OnlineDevices = ["emulator-5554", "emulator-5556"]; - task.RunningEmulatorAvdNames = new () { - { "emulator-5554", "pixel_7_-_api_35" }, - { "emulator-5556", "Pixel_9_Pro_XL" } + task.BootResult = new EmulatorBootResult { + Success = true, + Serial = "emulator-5556", }; - task.BootCompletedValues = new () { { "emulator-5556", "1" } }; - task.PmPathResults = new () { { "emulator-5556", "package:/system/framework/framework-res.apk" } }; Assert.IsTrue (task.RunTask (), "RunTask should succeed"); Assert.AreEqual ("emulator-5556", task.ResolvedDevice); @@ -233,12 +169,49 @@ public void ToolPaths_ResolvedFromAndroidSdkDirectory () Device = "emulator-5554", AndroidSdkDirectory = "/android/sdk", BootTimeoutSeconds = 10, + BootResult = new EmulatorBootResult { + Success = true, + Serial = "emulator-5554", + }, }; - task.OnlineDevices = ["emulator-5554"]; - // Tool paths are not set explicitly — ResolveAdbPath/ResolveEmulatorPath - // should compute them from AndroidSdkDirectory Assert.IsTrue (task.RunTask (), "RunTask should succeed"); Assert.AreEqual ("emulator-5554", task.ResolvedDevice); } + + [Test] + public void ExtraArguments_PassedToOptions () + { + string[]? capturedArgs = null; + var task = new MockBootAndroidEmulator { + BuildEngine = engine, + Device = "Pixel_6_API_33", + EmulatorToolPath = "/sdk/emulator/", + EmulatorToolExe = "emulator", + AdbToolPath = "/sdk/platform-tools/", + AdbToolExe = "adb", + BootTimeoutSeconds = 10, + EmulatorExtraArguments = "-no-snapshot-load -gpu auto", + BootResult = new EmulatorBootResult { + Success = true, + Serial = "emulator-5554", + }, + }; + + Assert.IsTrue (task.RunTask (), "RunTask should succeed"); + Assert.AreEqual ("emulator-5554", task.ResolvedDevice); + } + + [Test] + public void UnknownError_MapsToXA0145 () + { + var task = CreateTask ("Pixel_6_API_33"); + task.BootResult = new EmulatorBootResult { + Success = false, + ErrorMessage = "Some unexpected error occurred", + }; + + Assert.IsFalse (task.RunTask (), "RunTask should fail"); + Assert.IsTrue (errors.Any (e => e.Code == "XA0145"), "Unknown errors should map to XA0145"); + } }