diff --git a/src/Microsoft.DotNet.XHarness.Android/AdbRunner.cs b/src/Microsoft.DotNet.XHarness.Android/AdbRunner.cs index c44ad9c9c..f324fa8bf 100644 --- a/src/Microsoft.DotNet.XHarness.Android/AdbRunner.cs +++ b/src/Microsoft.DotNet.XHarness.Android/AdbRunner.cs @@ -27,10 +27,10 @@ public class AdbRunner private readonly string _absoluteAdbExePath; private readonly ILogger _log; private readonly IAdbProcessManager _processManager; - private readonly Dictionary _commandList = new() + private readonly Dictionary _commandList = new() { - { "architecture", "shell getprop ro.product.cpu.abilist" }, - { "app", "shell pm list packages -3" } + { "architecture", new[] { "shell", "getprop", "ro.product.cpu.abilist" } }, + { "app", new[] { "shell", "pm", "list", "packages", "-3" } }, }; public int APIVersion => _api ?? GetAPIVersion(); @@ -85,7 +85,7 @@ public void SetActiveDevice(string? deviceSerialNumber) private int GetAPIVersion() { - var output = RunAdbCommand("shell getprop ro.build.version.sdk"); + var output = RunAdbCommand(new[] { "shell", "getprop", "ro.build.version.sdk" }); return int.Parse(output.StandardOutput); } @@ -113,22 +113,22 @@ private static string GetCliAdbExePath() public TimeSpan TimeToWaitForBootCompletion { get; set; } = TimeSpan.FromMinutes(5); - public string GetAdbVersion() => RunAdbCommand("version").StandardOutput; + public string GetAdbVersion() => RunAdbCommand(new[] { "version" }).StandardOutput; - public string GetAdbState() => RunAdbCommand("get-state").StandardOutput; + public string GetAdbState() => RunAdbCommand(new[] { "get-state" }).StandardOutput; - public string RebootAndroidDevice() => RunAdbCommand("reboot").StandardOutput; + public string RebootAndroidDevice() => RunAdbCommand(new[] { "reboot" }).StandardOutput; - public void ClearAdbLog() => RunAdbCommand("logcat -c"); + public void ClearAdbLog() => RunAdbCommand(new[] { "logcat", "-c" }); - public void EnableWifi(bool enable) => RunAdbCommand($"shell svc wifi {(enable ? "enable" : "disable")}"); + public void EnableWifi(bool enable) => RunAdbCommand(new[] { "shell", "svc", "wifi", enable ? "enable" : "disable" }); public void DumpAdbLog(string outputFilePath, string filterSpec = "") { // Workaround: Doesn't seem to have a flush() function and sometimes it doesn't have the full log on emulators. Thread.Sleep(3000); - var result = RunAdbCommand($"logcat -d {filterSpec}", TimeSpan.FromMinutes(2)); + var result = RunAdbCommand(new[] { "logcat", "-d", filterSpec }, TimeSpan.FromMinutes(2)); if (result.ExitCode != 0) { // Could throw here, but it would tear down a possibly otherwise acceptable execution. @@ -155,7 +155,7 @@ public void WaitForDevice() // (Returns instantly if device is ready) // This can fail if _currentDevice is unset if there are multiple devices. _log.LogInformation("Waiting for device to be available (max 5 minutes)"); - var result = RunAdbCommand("wait-for-device", TimeSpan.FromMinutes(5)); + var result = RunAdbCommand(new[] { "wait-for-device" }, TimeSpan.FromMinutes(5)); _log.LogDebug($"{result.StandardOutput}"); if (result.ExitCode != 0) { @@ -167,11 +167,11 @@ public void WaitForDevice() // to be '1' (as opposed to empty) to make subsequent automation happy. var began = DateTimeOffset.UtcNow; var waitingUntil = began.Add(TimeToWaitForBootCompletion); - var bootCompleted = RunAdbCommand($"shell getprop {AdbShellPropertyForBootCompletion}"); + var bootCompleted = RunAdbCommand(new[] { "shell", "getprop", AdbShellPropertyForBootCompletion }); while (!bootCompleted.StandardOutput.Trim().StartsWith("1") && DateTimeOffset.UtcNow < waitingUntil) { - bootCompleted = RunAdbCommand($"shell getprop {AdbShellPropertyForBootCompletion}"); + bootCompleted = RunAdbCommand(new[] { "shell", "getprop", AdbShellPropertyForBootCompletion }); _log.LogDebug($"{AdbShellPropertyForBootCompletion} = '{bootCompleted.StandardOutput.Trim()}'"); Thread.Sleep((int)TimeSpan.FromSeconds(10).TotalMilliseconds); } @@ -188,7 +188,7 @@ public void WaitForDevice() public void StartAdbServer() { - var result = RunAdbCommand("start-server"); + var result = RunAdbCommand(new[] { "start-server" }); _log.LogDebug($"{result.StandardOutput}"); if (result.ExitCode != 0) { @@ -198,7 +198,7 @@ public void StartAdbServer() public void KillAdbServer() { - var result = RunAdbCommand("kill-server"); + var result = RunAdbCommand(new[] { "kill-server" }); if (result.ExitCode != 0) { throw new Exception($"Error killing ADB Server. Std out:{result.StandardOutput} Std. Err: {result.StandardError}"); @@ -217,7 +217,7 @@ public int InstallApk(string apkPath) throw new FileNotFoundException($"Could not find {apkPath}", apkPath); } - var result = RunAdbCommand($"install \"{apkPath}\""); + var result = RunAdbCommand(new[] { "install", apkPath }); // Two possible retry scenarios, theoretically both can happen on the same run: @@ -227,7 +227,7 @@ public int InstallApk(string apkPath) _log.LogWarning($"Hit broken pipe error; Will make one attempt to restart ADB server, then retry the install"); KillAdbServer(); StartAdbServer(); - result = RunAdbCommand($"install \"{apkPath}\""); + result = RunAdbCommand(new[] { "install", apkPath }); } // 2. Installation cache on device is messed up; restarting the device reliably seems to unblock this (unless the device is actually full, if so this will error the same) @@ -236,7 +236,7 @@ public int InstallApk(string apkPath) _log.LogWarning($"It seems the package installation cache may be full on the device. We'll try to reboot it before trying one more time.{Environment.NewLine}Output:{result}"); RebootAndroidDevice(); WaitForDevice(); - result = RunAdbCommand($"install \"{apkPath}\""); + result = RunAdbCommand(new[] { "install", apkPath }); } // 3. Installation timed out or failed with exception; restarting the ADB server, reboot the device and give more time for installation @@ -248,7 +248,7 @@ public int InstallApk(string apkPath) StartAdbServer(); RebootAndroidDevice(); WaitForDevice(); - result = RunAdbCommand($"install \"{apkPath}\"", TimeSpan.FromMinutes(10)); + result = RunAdbCommand(new[] { "install", apkPath }, TimeSpan.FromMinutes(10)); } if (result.ExitCode != 0) @@ -271,7 +271,7 @@ public int UninstallApk(string apkName) } _log.LogInformation($"Attempting to remove apk '{apkName}': "); - var result = RunAdbCommand($"uninstall {apkName}"); + var result = RunAdbCommand(new[] { "uninstall", apkName }); // See note above in install() if (result.ExitCode == (int)AdbExitCodes.ADB_BROKEN_PIPE) @@ -280,7 +280,7 @@ public int UninstallApk(string apkName) KillAdbServer(); StartAdbServer(); - result = RunAdbCommand($"uninstall {apkName}"); + result = RunAdbCommand(new[] { "uninstall", apkName }); } if (result.ExitCode == (int)AdbExitCodes.SUCCESS) @@ -303,7 +303,7 @@ public int UninstallApk(string apkName) public int KillApk(string apkName) { _log.LogInformation($"Killing all running processes for '{apkName}': "); - var result = RunAdbCommand($"shell am kill --user all {apkName}"); + var result = RunAdbCommand(new[] { "shell", "am", "kill", "--user", "all", apkName}); if (result.ExitCode != (int)AdbExitCodes.SUCCESS) { _log.LogError($"Error:{Environment.NewLine}{result}"); @@ -331,7 +331,7 @@ public List PullFiles(string devicePath, string localPath) _log.LogInformation($"Attempting to pull contents of {devicePath} to {localPath}"); var copiedFiles = new List(); - var result = RunAdbCommand($"pull {devicePath} {tempFolder}"); + var result = RunAdbCommand(new[] { "pull", devicePath, tempFolder }); if (result.ExitCode != (int)AdbExitCodes.SUCCESS) { @@ -372,9 +372,20 @@ public List PullFiles(string devicePath, string localPath) { var devicesAndProperties = new Dictionary(); - string command = _commandList[property]; + IEnumerable GetAdbArguments(string deviceSerial) + { + var args = new List + { + "-s", + deviceSerial, + }; + + args.AddRange(_commandList[property]); - var result = RunAdbCommand("devices -l", TimeSpan.FromSeconds(30)); + return args; + } + + var result = RunAdbCommand(new[] { "devices", "-l" }, TimeSpan.FromSeconds(30)); string[] standardOutputLines = result.StandardOutput.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); // Retry up to 3 mins til we get output; if the ADB server isn't started the output will come from a child process and we'll miss it. @@ -386,7 +397,7 @@ public List PullFiles(string devicePath, string localPath) { _log.LogDebug($"Unexpected response from adb devices -l:{Environment.NewLine}Exit code={result.ExitCode}{Environment.NewLine}Std. Output: {result.StandardOutput} {Environment.NewLine}Std. Error: {result.StandardError}"); Thread.Sleep(10000); - result = RunAdbCommand("devices -l", TimeSpan.FromSeconds(30)); + result = RunAdbCommand(new[] { "devices", "-l" }, TimeSpan.FromSeconds(30)); standardOutputLines = result.StandardOutput.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); } @@ -402,7 +413,7 @@ public List PullFiles(string devicePath, string localPath) { var deviceSerial = lineParts[0]; - var shellResult = RunAdbCommand($"-s {deviceSerial} {command}", TimeSpan.FromSeconds(30)); + var shellResult = RunAdbCommand(GetAdbArguments(deviceSerial), TimeSpan.FromSeconds(30)); // Assumption: All Devices on a machine running Xharness should attempt to be online or disconnected. retriesLeft = 30; // Max 5 minutes (30 attempts * 10 second waits) @@ -411,7 +422,7 @@ public List PullFiles(string devicePath, string localPath) _log.LogWarning($"Device '{deviceSerial}' is offline; retrying up to one minute."); Thread.Sleep(10000); - shellResult = RunAdbCommand($"-s {deviceSerial} {command}", TimeSpan.FromSeconds(30)); + shellResult = RunAdbCommand(GetAdbArguments(deviceSerial), TimeSpan.FromSeconds(30)); } if (shellResult.ExitCode == (int)AdbExitCodes.SUCCESS) @@ -502,30 +513,28 @@ public Dictionary GetAllDevicesToUse(ILogger logger, IEnumerable public ProcessExecutionResults RunApkInstrumentation(string apkName, string? instrumentationClassName, Dictionary args, TimeSpan timeout) { string displayName = string.IsNullOrEmpty(instrumentationClassName) ? "{default}" : instrumentationClassName; - string appArguments = ""; - if (args.Count > 0) + + var adbArgs = new List { - foreach (string key in args.Keys) - { - appArguments = $"{appArguments} -e {key} {args[key]}"; - } - } + "shell", "am", "instrument" + }; + + adbArgs.AddRange(args.SelectMany(arg => new[] { "-e", arg.Key, arg.Value })); + adbArgs.Add("-w"); - string command = $"shell am instrument {appArguments} -w {apkName}"; if (string.IsNullOrEmpty(instrumentationClassName)) { _log.LogInformation($"Starting default instrumentation class on {apkName} (exit code 0 == success)"); + adbArgs.Add(apkName); } else { _log.LogInformation($"Starting instrumentation class '{instrumentationClassName}' on {apkName}"); - command = $"{command}/{instrumentationClassName}"; + adbArgs.Add($"{apkName}/{instrumentationClassName}"); } - _log.LogDebug($"Raw command: '{command}'"); - var stopWatch = new Stopwatch(); - stopWatch.Start(); - var result = RunAdbCommand(command, timeout); + var stopWatch = Stopwatch.StartNew(); + var result = RunAdbCommand(adbArgs, timeout); stopWatch.Stop(); if (result.ExitCode == (int)AdbExitCodes.INSTRUMENTATION_TIMEOUT) @@ -536,7 +545,9 @@ public ProcessExecutionResults RunApkInstrumentation(string apkName, string? ins { _log.LogInformation($"Running instrumentation class {displayName} took {stopWatch.Elapsed.TotalSeconds} seconds"); } + _log.LogDebug(result.ToString()); + return result; } @@ -544,16 +555,16 @@ public ProcessExecutionResults RunApkInstrumentation(string apkName, string? ins #region Process runner helpers - public ProcessExecutionResults RunAdbCommand(string command) => RunAdbCommand(command, TimeSpan.FromMinutes(5)); + public ProcessExecutionResults RunAdbCommand(IEnumerable arguments) => RunAdbCommand(arguments, TimeSpan.FromMinutes(5)); - public ProcessExecutionResults RunAdbCommand(string command, TimeSpan timeOut) + public ProcessExecutionResults RunAdbCommand(IEnumerable arguments, TimeSpan timeOut) { if (!File.Exists(_absoluteAdbExePath)) { throw new FileNotFoundException($"Provided path for adb.exe was not valid ('{_absoluteAdbExePath}')", _absoluteAdbExePath); } - return _processManager.Run(_absoluteAdbExePath, command, timeOut); + return _processManager.Run(_absoluteAdbExePath, arguments, timeOut); } #endregion diff --git a/src/Microsoft.DotNet.XHarness.Android/Execution/AdbProcessManager.cs b/src/Microsoft.DotNet.XHarness.Android/Execution/AdbProcessManager.cs index 514433d88..92f10f24a 100644 --- a/src/Microsoft.DotNet.XHarness.Android/Execution/AdbProcessManager.cs +++ b/src/Microsoft.DotNet.XHarness.Android/Execution/AdbProcessManager.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Text; -using System.Threading; +using Microsoft.DotNet.XHarness.Common.Utilities; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.XHarness.Android.Execution; @@ -18,15 +20,14 @@ public class AdbProcessManager : IAdbProcessManager /// public string DeviceSerial { get; set; } = string.Empty; - public ProcessExecutionResults Run(string adbExePath, string arguments) => Run(adbExePath, arguments, TimeSpan.FromMinutes(5)); - - public ProcessExecutionResults Run(string adbExePath, string arguments, TimeSpan timeOut) + public ProcessExecutionResults Run(string adbExePath, IEnumerable arguments, TimeSpan timeOut) { - string deviceSerialArgs = string.IsNullOrEmpty(DeviceSerial) ? string.Empty : $"-s {DeviceSerial}"; - - _log.LogDebug($"Executing command: '{adbExePath} {deviceSerialArgs} {arguments}'"); + if (!string.IsNullOrEmpty(DeviceSerial)) + { + arguments = arguments.Prepend(DeviceSerial).Prepend("-s"); + } - var processStartInfo = new ProcessStartInfo + var processStartInfo = new ProcessStartInfo() { CreateNoWindow = true, UseShellExecute = false, @@ -34,9 +35,17 @@ public ProcessExecutionResults Run(string adbExePath, string arguments, TimeSpan RedirectStandardOutput = true, RedirectStandardError = true, FileName = adbExePath, - Arguments = $"{deviceSerialArgs} {arguments}", }; + + foreach (var arg in arguments) + { + processStartInfo.ArgumentList.Add(arg); + } + + _log.LogDebug($"Executing command: '{adbExePath} {StringUtils.FormatArguments(processStartInfo.ArgumentList)}'"); + var p = new Process() { StartInfo = processStartInfo }; + var standardOut = new StringBuilder(); var standardErr = new StringBuilder(); @@ -63,7 +72,6 @@ public ProcessExecutionResults Run(string adbExePath, string arguments, TimeSpan }; p.Start(); - p.BeginOutputReadLine(); p.BeginErrorReadLine(); @@ -90,15 +98,15 @@ public ProcessExecutionResults Run(string adbExePath, string arguments, TimeSpan p.Close(); lock (standardOut) - lock (standardErr) + lock (standardErr) + { + return new ProcessExecutionResults() { - return new ProcessExecutionResults() - { - ExitCode = exitCode, - StandardOutput = standardOut.ToString(), - StandardError = standardErr.ToString(), - TimedOut = timedOut - }; - } + ExitCode = exitCode, + StandardOutput = standardOut.ToString(), + StandardError = standardErr.ToString(), + TimedOut = timedOut + }; + } } } diff --git a/src/Microsoft.DotNet.XHarness.Android/Execution/Api23AndOlderReportManager.cs b/src/Microsoft.DotNet.XHarness.Android/Execution/Api23AndOlderReportManager.cs index 239064852..4cb2a2fa2 100644 --- a/src/Microsoft.DotNet.XHarness.Android/Execution/Api23AndOlderReportManager.cs +++ b/src/Microsoft.DotNet.XHarness.Android/Execution/Api23AndOlderReportManager.cs @@ -23,7 +23,7 @@ public string DumpBugReport(AdbRunner runner, string outputFilePathWithoutFormat // give some time for bug report to be available Thread.Sleep(3000); - var result = runner.RunAdbCommand($"bugreport", TimeSpan.FromMinutes(5)); + var result = runner.RunAdbCommand(new[] { "bugreport" }, TimeSpan.FromMinutes(5)); if (result.ExitCode != 0) { diff --git a/src/Microsoft.DotNet.XHarness.Android/Execution/IAdbProcessManager.cs b/src/Microsoft.DotNet.XHarness.Android/Execution/IAdbProcessManager.cs index 6b4487bfc..72dc91817 100644 --- a/src/Microsoft.DotNet.XHarness.Android/Execution/IAdbProcessManager.cs +++ b/src/Microsoft.DotNet.XHarness.Android/Execution/IAdbProcessManager.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Text; namespace Microsoft.DotNet.XHarness.Android.Execution; @@ -30,10 +31,12 @@ public override string ToString() } -// interface that helps to manage the different processes in the app. +/// +/// Interface for calling the adb binary in a separate process. +/// public interface IAdbProcessManager { public string DeviceSerial { get; set; } - public ProcessExecutionResults Run(string filename, string arguments); - public ProcessExecutionResults Run(string filename, string arguments, TimeSpan timeout); + + public ProcessExecutionResults Run(string filename, IEnumerable arguments, TimeSpan timeout); } diff --git a/src/Microsoft.DotNet.XHarness.Android/Execution/NewReportManager.cs b/src/Microsoft.DotNet.XHarness.Android/Execution/NewReportManager.cs index 5fa8003f6..9c8834542 100644 --- a/src/Microsoft.DotNet.XHarness.Android/Execution/NewReportManager.cs +++ b/src/Microsoft.DotNet.XHarness.Android/Execution/NewReportManager.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.XHarness.Android.Execution; @@ -21,7 +17,7 @@ public string DumpBugReport(AdbRunner runner, string outputFilePathWithoutFormat // give some time for bug report to be available Thread.Sleep(3000); - var result = runner.RunAdbCommand($"bugreport {outputFilePathWithoutFormat}.zip", TimeSpan.FromMinutes(5)); + var result = runner.RunAdbCommand(new[] { "bugreport", $"{outputFilePathWithoutFormat}.zip" }, TimeSpan.FromMinutes(5)); if (result.ExitCode != 0) { diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Android/AndroidAdbCommandArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Android/AndroidAdbCommandArguments.cs new file mode 100644 index 000000000..49f17bf1f --- /dev/null +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Android/AndroidAdbCommandArguments.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.DotNet.XHarness.CLI.CommandArguments.Android; + +internal class AndroidAdbCommandArguments : XHarnessCommandArguments +{ + public TimeoutArgument Timeout { get; set; } = new(TimeSpan.FromMinutes(1)); + + protected override IEnumerable GetArguments() => new[] + { + Timeout, + }; +} diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Android/AndroidGetStateCommandArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Android/AndroidGetStateCommandArguments.cs index b78f56d7b..1eb750b2d 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Android/AndroidGetStateCommandArguments.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Android/AndroidGetStateCommandArguments.cs @@ -8,10 +8,5 @@ namespace Microsoft.DotNet.XHarness.CLI.CommandArguments.Android; internal class AndroidGetStateCommandArguments : XHarnessCommandArguments { - public ShowAdbPathArgument ShowAdbPath { get; set; } = new(); - - protected override IEnumerable GetArguments() => new Argument[] - { - ShowAdbPath - }; + protected override IEnumerable GetArguments() => System.Array.Empty(); } diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidAdbCommand.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidAdbCommand.cs new file mode 100644 index 000000000..8e629f602 --- /dev/null +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidAdbCommand.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.DotNet.XHarness.Android; +using Microsoft.DotNet.XHarness.CLI.CommandArguments.Android; +using Microsoft.DotNet.XHarness.Common; +using Microsoft.DotNet.XHarness.Common.CLI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.XHarness.CLI.Commands.Android; + +internal class AndroidAdbCommand : XHarnessCommand +{ + private const string Description = "Invoke bundled adb with given arguments"; + protected override string CommandUsage { get; } = "android adb [OPTIONS] -- [ADB ARGUMENTS]"; + protected override string CommandDescription => Description; + + protected override AndroidAdbCommandArguments Arguments { get; } = new(); + + public AndroidAdbCommand() : base(TargetPlatform.Android, "adb", false, new ServiceCollection(), Description) + { + } + + protected override Task InvokeInternal(ILogger logger) + { + if (!PassThroughArguments.Any()) + { + logger.LogError("Please provide delimeter '--' followed by arguments for ADB:" + Environment.NewLine + + $" {CommandUsage}" + Environment.NewLine + + $"Example:" + Environment.NewLine + + $" android adb --timeout 00:01:30 -- devices -l"); + + return Task.FromResult(ExitCode.INVALID_ARGUMENTS); + } + + var runner = new AdbRunner(logger); + + try + { + var result = runner.RunAdbCommand(PassThroughArguments, Arguments.Timeout); + + Console.Write(result.StandardOutput); + Console.Error.Write(result.StandardError); + + return Task.FromResult((ExitCode)result.ExitCode); + } + catch (Exception toLog) + { + logger.LogCritical(toLog, $"Error: {toLog.Message}"); + return Task.FromResult(ExitCode.GENERAL_FAILURE); + } + } +} diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidCommandSet.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidCommandSet.cs index 872c13c12..63387bae6 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidCommandSet.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidCommandSet.cs @@ -21,6 +21,7 @@ public AndroidCommandSet() : base("android") Add(new AndroidInstallCommand()); Add(new AndroidRunCommand()); Add(new AndroidUninstallCommand()); + Add(new AndroidAdbCommand()); Add(new AndroidGetStateCommand()); } } diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidGetStateCommand.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidGetStateCommand.cs index afc5a5ce5..77696695e 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidGetStateCommand.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidGetStateCommand.cs @@ -27,12 +27,6 @@ protected override Task InvokeInternal(ILogger logger) { var runner = new AdbRunner(logger); - if (Arguments.ShowAdbPath) - { - Console.WriteLine(runner.AdbExePath); - return Task.FromResult(ExitCode.SUCCESS); - } - logger.LogInformation("Getting state of ADB and attached Android device(s)"); try { diff --git a/src/Microsoft.DotNet.XHarness.CLI/Program.cs b/src/Microsoft.DotNet.XHarness.CLI/Program.cs index 280306acd..dd124855c 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Program.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Program.cs @@ -34,12 +34,12 @@ public static int Main(string[] args) if (args.Length > 0) { #if !DEBUG - if (args[0] == "apple" && !RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - // Otherwise the command would just not be found - Console.Error.WriteLine("The 'apple' command is not available on non-OSX platforms!"); - return (int)ExitCode.INVALID_ARGUMENTS; - } + if (args[0] == "apple" && !RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // Otherwise the command would just not be found + Console.Error.WriteLine("The 'apple' command is not available on non-OSX platforms!"); + return (int)ExitCode.INVALID_ARGUMENTS; + } #endif // Mono.Options wouldn't allow "--" so we will temporarily rename it and parse it ourselves later @@ -99,20 +99,19 @@ private static bool IsOutputSensitive(string[] args) switch (args[0]) { case "apple": - return args[1] == "device"; - - case "android": - if (args[1] == "device") + return args[1] switch { - return true; - } + "device" => true, + _ => false, + }; - if (args[1] == "state" && args.Contains("--adb")) + case "android": + return args[1] switch { - return true; - } - - return false; + "device" => true, + "adb" => true, + _ => false, + }; } return false; diff --git a/tests/Microsoft.DotNet.XHarness.Android.Tests/AdbRunnerTests.cs b/tests/Microsoft.DotNet.XHarness.Android.Tests/AdbRunnerTests.cs index b56ab6cdb..9de4f0001 100644 --- a/tests/Microsoft.DotNet.XHarness.Android.Tests/AdbRunnerTests.cs +++ b/tests/Microsoft.DotNet.XHarness.Android.Tests/AdbRunnerTests.cs @@ -8,8 +8,8 @@ using System.IO; using System.Linq; using System.Text; -using System.Threading; using Microsoft.DotNet.XHarness.Android.Execution; +using Microsoft.DotNet.XHarness.Common.Utilities; using Microsoft.Extensions.Logging; using Moq; @@ -39,13 +39,13 @@ public AdbRunnerTests() // Fake ADB executable since its path is checked Directory.CreateDirectory(s_scratchAndOutputPath); - File.Create(s_adbPath).Close(); + File.WriteAllText(s_adbPath, string.Empty); // Mock to check the args ADB actually gets called with _processManager.Setup(pm => pm.Run( It.IsAny(), // process, not checking the value to match any call - It.IsAny(), // same - It.IsAny())).Returns((string p, string a, TimeSpan t) => CallFakeProcessManager(p, a, t)); + It.IsAny>(), // same + It.IsAny())).Returns((string p, IEnumerable a, TimeSpan t) => CallFakeProcessManager(p, a.ToArray(), t)); } public void Dispose() @@ -61,7 +61,7 @@ public void GetAdbState() { var runner = new AdbRunner(_mainLog.Object, _processManager.Object, s_adbPath); string result = runner.GetAdbState(); - _processManager.Verify(pm => pm.Run(s_adbPath, "get-state", TimeSpan.FromMinutes(5)), Times.Once); + VerifyAdbCall("get-state"); Assert.Equal("device", result); } @@ -70,7 +70,7 @@ public void ClearAdbLog() { var runner = new AdbRunner(_mainLog.Object, _processManager.Object, s_adbPath); runner.ClearAdbLog(); - _processManager.Verify(pm => pm.Run(s_adbPath, "logcat -c", TimeSpan.FromMinutes(5)), Times.Once); + VerifyAdbCall("logcat", "-c"); } [Fact] public void DumpAdbLog() @@ -78,7 +78,7 @@ public void DumpAdbLog() var runner = new AdbRunner(_mainLog.Object, _processManager.Object, s_adbPath); string pathToDumpLogTo = Path.Join(s_scratchAndOutputPath, $"{Path.GetRandomFileName()}.log"); runner.DumpAdbLog(pathToDumpLogTo); - _processManager.Verify(pm => pm.Run(s_adbPath, "logcat -d ", TimeSpan.FromMinutes(2)), Times.Once); + VerifyAdbCall("logcat", "-d", ""); Assert.Equal("Sample LogCat Output", File.ReadAllText(pathToDumpLogTo)); } @@ -90,7 +90,7 @@ public void DumpBugReport() runner.SetActiveDevice(string.Empty); string pathToDumpBugReport = Path.Join(s_scratchAndOutputPath, Path.GetRandomFileName()); runner.DumpBugReport(pathToDumpBugReport); - _processManager.Verify(pm => pm.Run(s_adbPath, $"bugreport {pathToDumpBugReport}.zip", TimeSpan.FromMinutes(5)), Times.Once); + VerifyAdbCall("bugreport", $"{pathToDumpBugReport}.zip"); Assert.Equal("Sample BugReport Output", File.ReadAllText($"{pathToDumpBugReport}.zip")); } @@ -107,8 +107,8 @@ public void WaitForDevice() s_bootCompleteCheckTimes = 0; // Force simulating device is offline runner.SetActiveDevice(null); runner.WaitForDevice(); - _processManager.Verify(pm => pm.Run(s_adbPath, "wait-for-device", TimeSpan.FromMinutes(5)), Times.Exactly(2)); - _processManager.Verify(pm => pm.Run(s_adbPath, "shell getprop sys.boot_completed", TimeSpan.FromMinutes(5)), Times.Exactly(4)); + VerifyAdbCall(Times.Exactly(2), "wait-for-device"); + VerifyAdbCall(Times.Exactly(4), "shell", "getprop", "sys.boot_completed"); } [Fact] @@ -116,15 +116,15 @@ public void ListDevicesAndArchitectures() { var runner = new AdbRunner(_mainLog.Object, _processManager.Object, s_adbPath); var result = runner.GetAttachedDevicesWithProperties("architecture"); - _processManager.Verify(pm => pm.Run(s_adbPath, "devices -l", TimeSpan.FromSeconds(30)), Times.Once); + VerifyAdbCall("devices", "-l"); // Ensure it called, parsed the four random device names and found all four architectures foreach (var fakeDeviceInfo in _fakeDeviceList.Keys) { - _processManager.Verify(pm => pm.Run(s_adbPath, $"-s {fakeDeviceInfo.Item1} shell getprop ro.product.cpu.abilist", TimeSpan.FromSeconds(30)), Times.Once); + VerifyAdbCall("-s", fakeDeviceInfo.Item1, "shell", "getprop", "ro.product.cpu.abilist"); Assert.Equal(fakeDeviceInfo.Item2, result[fakeDeviceInfo.Item1]); - } + Assert.Equal(4, result.Count); } @@ -133,7 +133,7 @@ public void StartAdbServer() { var runner = new AdbRunner(_mainLog.Object, _processManager.Object, s_adbPath); runner.StartAdbServer(); - _processManager.Verify(pm => pm.Run(s_adbPath, "start-server", TimeSpan.FromMinutes(5)), Times.Once); + VerifyAdbCall("start-server"); } [Fact] @@ -141,7 +141,7 @@ public void KillAdbServer() { var runner = new AdbRunner(_mainLog.Object, _processManager.Object, s_adbPath); runner.KillAdbServer(); - _processManager.Verify(pm => pm.Run(s_adbPath, "kill-server", TimeSpan.FromMinutes(5)), Times.Once); + VerifyAdbCall("kill-server"); } [Fact] @@ -151,7 +151,7 @@ public void InstallApk() string fakeApkPath = Path.Join(s_scratchAndOutputPath, $"{Path.GetRandomFileName()}.apk"); File.Create(fakeApkPath).Close(); int exitCode = runner.InstallApk(fakeApkPath); - _processManager.Verify(pm => pm.Run(s_adbPath, $"install \"{fakeApkPath}\"", TimeSpan.FromMinutes(5)), Times.Once); + VerifyAdbCall("install", fakeApkPath); Assert.Equal(0, exitCode); } @@ -161,7 +161,7 @@ public void UninstallApk() var runner = new AdbRunner(_mainLog.Object, _processManager.Object, s_adbPath); string fakeApkName = $"{Path.GetRandomFileName()}"; int exitCode = runner.UninstallApk(fakeApkName); - _processManager.Verify(pm => pm.Run(s_adbPath, $"uninstall {fakeApkName}", TimeSpan.FromMinutes(5)), Times.Once); + VerifyAdbCall("uninstall", fakeApkName); Assert.Equal(0, exitCode); } @@ -171,7 +171,7 @@ public void KillApk() var runner = new AdbRunner(_mainLog.Object, _processManager.Object, s_adbPath); string fakeApkName = $"{Path.GetRandomFileName()}"; int exitCode = runner.KillApk(fakeApkName); - _processManager.Verify(pm => pm.Run(s_adbPath, $"shell am kill --user all {fakeApkName}", TimeSpan.FromMinutes(5)), Times.Once); + VerifyAdbCall("shell", "am", "kill", "--user", "all", fakeApkName); Assert.Equal(0, exitCode); } @@ -181,7 +181,7 @@ public void GetDeviceToUse() var requiredArchitecture = "x86_64"; var runner = new AdbRunner(_mainLog.Object, _processManager.Object, s_adbPath); var result = runner.GetDeviceToUse(_mainLog.Object, new[] { requiredArchitecture }, "architecture"); - _processManager.Verify(pm => pm.Run(s_adbPath, "devices -l", TimeSpan.FromSeconds(30)), Times.Once); + VerifyAdbCall("devices", "-l"); Assert.True(_fakeDeviceList.ContainsKey(new Tuple(result, requiredArchitecture))); } @@ -190,7 +190,7 @@ public void RebootAndroidDevice() { var runner = new AdbRunner(_mainLog.Object, _processManager.Object, s_adbPath); string result = runner.RebootAndroidDevice(); - _processManager.Verify(pm => pm.Run(s_adbPath, "reboot", TimeSpan.FromMinutes(5)), Times.Once); + VerifyAdbCall("reboot"); } [Theory] @@ -217,19 +217,20 @@ public void RunInstrumentation(string instrumentationName) if (string.IsNullOrEmpty(instrumentationName)) { - _processManager.Verify(pm => pm.Run(s_adbPath, $"shell am instrument -e arg1 value1 -e arg2 value2 -w {fakeApkName}", TimeSpan.FromSeconds(123)), Times.Once); - _processManager.Verify(pm => pm.Run(s_adbPath, $"shell am instrument -w {fakeApkName}", TimeSpan.FromSeconds(456)), Times.Once); + VerifyAdbCall("shell", "am", "instrument", "-e", "arg1", "value1", "-e", "arg2", "value2", "-w", fakeApkName); + VerifyAdbCall("shell", "am", "instrument", "-w", fakeApkName); } else { - _processManager.Verify(pm => pm.Run(s_adbPath, $"shell am instrument -e arg1 value1 -e arg2 value2 -w {fakeApkName}/{instrumentationName}", TimeSpan.FromSeconds(123)), Times.Once); - _processManager.Verify(pm => pm.Run(s_adbPath, $"shell am instrument -w {fakeApkName}/{instrumentationName}", TimeSpan.FromSeconds(456)), Times.Once); + VerifyAdbCall("shell", "am", "instrument", "-e", "arg1", "value1", "-e", "arg2", "value2", "-w", $"{fakeApkName}/{instrumentationName}"); + VerifyAdbCall("shell", "am", "instrument", "-w", $"{fakeApkName}/{instrumentationName}"); } } #endregion #region Helper Functions + // Generates a list of fake devices, one per supported architecture so we can test AdbRunner's parsing of the output. // As with most of these tests, if adb.exe changes, this will break (we are locked into specific version) private static Dictionary, int> InitializeFakeDeviceList() @@ -245,51 +246,53 @@ private static Dictionary, int> InitializeFakeDeviceList() return values; } - private ProcessExecutionResults CallFakeProcessManager(string process, string arguments, TimeSpan timeout) + private ProcessExecutionResults CallFakeProcessManager(string process, string[] arguments, TimeSpan timeout) { if (Debugger.IsAttached) { - Debug.WriteLine($"Fake ADB Process Manager invoked with args: '{process} {arguments}' (timeout = {timeout.TotalSeconds})"); + Debug.WriteLine($"Fake ADB Process Manager invoked with args: '{process} {StringUtils.FormatArguments(arguments)}' (timeout = {timeout.TotalSeconds})"); } bool timedOut = false; int exitCode = 0; string stdOut = ""; string stdErr = ""; - - string[] allArgs = arguments.Split(' ', StringSplitOptions.RemoveEmptyEntries); - int argStart = 0; - if (allArgs[0] == "-s") + if (arguments[0] == "-s") { - s_currentDeviceSerial = allArgs[1]; + s_currentDeviceSerial = arguments[1]; argStart = 2; } - switch (allArgs[argStart].ToLowerInvariant()) + switch (arguments[argStart].ToLowerInvariant()) { case "get-state": stdOut = "device"; exitCode = 0; break; + case "devices": var s = new StringBuilder(); int transportId = 1; s.AppendLine("List of devices attached"); + foreach (var device in _fakeDeviceList) { string offlineMsg = _fakeDeviceList[device.Key]++ > 4 ? "offline" : "online"; s.AppendLine($"{device.Key.Item1} {offlineMsg} transportid:{transportId++}"); } + stdOut = s.ToString(); break; + case "shell": - if ($"{allArgs[argStart + 1]} {allArgs[argStart + 2]}".Equals("getprop ro.product.cpu.abilist")) + if ($"{arguments[argStart + 1]} {arguments[argStart + 2]}".Equals("getprop ro.product.cpu.abilist")) { stdOut = _fakeDeviceList.Keys.Where(k => k.Item1 == s_currentDeviceSerial).Single().Item2; } - if ($"{allArgs[argStart + 1]} {allArgs[argStart + 2]}".Equals("getprop sys.boot_completed")) + + if ($"{arguments[argStart + 1]} {arguments[argStart + 2]}".Equals("getprop sys.boot_completed")) { // Newline is strange, but this is actually what it looks like if (s_bootCompleteCheckTimes > 0) @@ -303,32 +306,36 @@ private ProcessExecutionResults CallFakeProcessManager(string process, string ar } s_bootCompleteCheckTimes++; } - if ($"{allArgs[argStart + 1]} {allArgs[argStart + 2]}".Equals("getprop ro.build.version.sdk")) + + if ($"{arguments[argStart + 1]} {arguments[argStart + 2]}".Equals("getprop ro.build.version.sdk")) { stdOut = $"29{Environment.NewLine}"; } + exitCode = 0; break; + case "logcat": - if (allArgs[argStart + 1].Equals("-c")) { }; // Do nothing - if (allArgs[argStart + 1].Equals("-d")) + if (arguments[argStart + 1].Equals("-d")) { stdOut = "Sample LogCat Output"; } + break; + case "bugreport": - var outputPath = allArgs[argStart + 1]; + var outputPath = arguments[argStart + 1]; File.WriteAllText(outputPath, "Sample BugReport Output"); break; + case "install": case "reboot": case "uninstall": case "wait-for-device": case "start-server": case "kill-server": - // No output needed, but pretend to wait a little - Thread.Sleep(1000); break; + default: throw new InvalidOperationException($"Fake ADB doesn't know how to handle argument: {arguments}"); } @@ -342,5 +349,14 @@ private ProcessExecutionResults CallFakeProcessManager(string process, string ar }; } + private void VerifyAdbCall(params string[] arguments) => VerifyAdbCall(Times.Once(), arguments); + + private void VerifyAdbCall(Times occurence, params string[] arguments) + { + _processManager.Verify( + x => x.Run(s_adbPath, It.Is>(args => Enumerable.SequenceEqual(arguments, args)), It.IsAny()), + occurence); + } + #endregion }