diff --git a/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj b/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj index 9b81f49..5b1fa07 100644 --- a/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj +++ b/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj @@ -25,8 +25,10 @@ prompt 4 None - + armeabi-v7a;x86;x86_64;arm64-v8a 1G + false + true true diff --git a/DeviceTests/DeviceTests.Android/MainActivity.cs b/DeviceTests/DeviceTests.Android/MainActivity.cs index 7adb290..7c20f5f 100644 --- a/DeviceTests/DeviceTests.Android/MainActivity.cs +++ b/DeviceTests/DeviceTests.Android/MainActivity.cs @@ -1,31 +1,30 @@ -using System.IO; -using System.Reflection; -using Android.App; -using Android.Content.PM; -using Android.OS; -using Android.Runtime; -using Xunit.Runners.UI; -using Environment = System.Environment; - -namespace DeviceTests.Droid -{ - [Activity(MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] - public class MainActivity : RunnerActivity - { - protected override void OnCreate(Bundle bundle) - { - AddExecutionAssembly(Assembly.GetExecutingAssembly()); - - AddTestAssembly(typeof(SharedTests).Assembly); - - var path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); - - ResultChannel = new CombinedResultChannel(Path.Combine(path, "TestResults.trx")); - - AutoStart = true; - TerminateAfterExecution = true; - - base.OnCreate(bundle); - } - } -} +using Android.App; +using Android.Content.PM; +using Android.OS; +using System.IO; +using System.Reflection; +using Xunit.Runners.UI; +using Environment = System.Environment; + +namespace DeviceTests.Droid +{ + [Activity(MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] + public class MainActivity : RunnerActivity + { + protected override void OnCreate(Bundle bundle) + { + AddExecutionAssembly(Assembly.GetExecutingAssembly()); + + AddTestAssembly(typeof(SharedTests).Assembly); + + var path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + + ResultChannel = new CombinedResultChannel(Path.Combine(path, "TestResults.trx")); + + AutoStart = true; + TerminateAfterExecution = true; + + base.OnCreate(bundle); + } + } +} diff --git a/dotnet-devices.Tests/Android/AaptToolTests.cs b/dotnet-devices.Tests/Android/AaptToolTests.cs index 5ce9709..1e5bd58 100644 --- a/dotnet-devices.Tests/Android/AaptToolTests.cs +++ b/dotnet-devices.Tests/Android/AaptToolTests.cs @@ -18,7 +18,7 @@ namespace DotNetDevices.Tests { var xmltree = File.ReadAllText(file); - var xdoc = AaptTool.ParseXmlTree(xmltree); + var xdoc = Aapt.ParseXmlTree(xmltree); Assert.NotNull(xdoc); } @@ -27,7 +27,7 @@ namespace DotNetDevices.Tests public void ParseIsValid() { var xmltree = File.ReadAllText("TestData/Android/CompiledXmlDump.txt"); - var xdoc = AaptTool.ParseXmlTree(xmltree); + var xdoc = Aapt.ParseXmlTree(xmltree); Assert.NotNull(xdoc); var manifest = xdoc.Root; diff --git a/dotnet-devices.Tests/Android/VirtualDeviceConfigTests.cs b/dotnet-devices.Tests/Android/VirtualDeviceConfigTests.cs index 52209ce..4ca1700 100644 --- a/dotnet-devices.Tests/Android/VirtualDeviceConfigTests.cs +++ b/dotnet-devices.Tests/Android/VirtualDeviceConfigTests.cs @@ -10,8 +10,8 @@ namespace DotNetDevices.Tests public class CreateVirtualDeviceAsync { [Theory] - [InlineData("TestData/Android/AvdConfigIni_Normal.txt", "pixel_2_q_10_0_-_api_29", "Pixel 2 Q 10.0 - API 29")] - [InlineData("TestData/Android/AvdConfigIni_Tiny.txt", "pixel_2_q_10_0_-_api_29", "pixel_2_q_10_0_-_api_29")] + [InlineData("TestData/Android/AvdConfigIni_Normal.avd", "pixel_2_q_10_0_-_api_29", "Pixel 2 Q 10.0 - API 29")] + [InlineData("TestData/Android/AvdConfigIni_Tiny.avd", "pixel_2_q_10_0_-_api_29", "pixel_2_q_10_0_-_api_29")] public async Task CanCreateInstance(string file, string id, string name) { var config = new VirtualDeviceConfig(file); @@ -26,7 +26,7 @@ namespace DotNetDevices.Tests [Fact] public async Task CanReadTV() { - var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_TV.txt"); + var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_TV.avd"); var device = await config.CreateVirtualDeviceAsync(); @@ -39,7 +39,7 @@ namespace DotNetDevices.Tests [Fact] public async Task CanReadWear() { - var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Wear.txt"); + var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Wear.avd"); var device = await config.CreateVirtualDeviceAsync(); @@ -52,7 +52,7 @@ namespace DotNetDevices.Tests [Fact] public async Task CanReadTablet() { - var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Tablet.txt"); + var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Tablet.avd"); var device = await config.CreateVirtualDeviceAsync(); @@ -65,7 +65,7 @@ namespace DotNetDevices.Tests [Fact] public async Task CanReadGeneric() { - var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Generic.txt"); + var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Generic.avd"); var device = await config.CreateVirtualDeviceAsync(); @@ -78,7 +78,7 @@ namespace DotNetDevices.Tests [Fact] public async Task CanReadPhone() { - var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Phone.txt"); + var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Phone.avd"); var device = await config.CreateVirtualDeviceAsync(); diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Generic.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Generic.avd/config.ini similarity index 100% rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_Generic.txt rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_Generic.avd/config.ini diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Normal.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Normal.avd/config.ini similarity index 100% rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_Normal.txt rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_Normal.avd/config.ini diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Phone.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Phone.avd/config.ini similarity index 100% rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_Phone.txt rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_Phone.avd/config.ini diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_TV.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_TV.avd/config.ini similarity index 100% rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_TV.txt rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_TV.avd/config.ini diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tablet.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tablet.avd/config.ini similarity index 100% rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tablet.txt rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tablet.avd/config.ini diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tiny.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tiny.avd/config.ini similarity index 100% rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tiny.txt rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tiny.avd/config.ini diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Wear.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Wear.avd/config.ini similarity index 100% rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_Wear.txt rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_Wear.avd/config.ini diff --git a/dotnet-devices/Android/AVDManager.cs b/dotnet-devices/Android/AVDManager.cs index e8de93e..e30f6bc 100644 --- a/dotnet-devices/Android/AVDManager.cs +++ b/dotnet-devices/Android/AVDManager.cs @@ -11,7 +11,8 @@ namespace DotNetDevices.Android { public class AVDManager { - private static Regex virtualDevicePathRegex = new Regex(@"\s*Path\:\s*(.+)"); + private static readonly Regex virtualDevicePathRegex = new Regex(@"\s*Path\:\s*(.+)"); + private static readonly string[] userDataFiles = { "userdata-qemu.img", "userdata-qemu.img.qcow2" }; private readonly ProcessRunner processRunner; private readonly ILogger? logger; @@ -58,7 +59,21 @@ namespace DotNetDevices.Android return targets; } - public async Task> GetVirtualDeviceNamesAsync(CancellationToken cancellationToken = default) + public async Task ResetVirtualDeviceAsync(string id, CancellationToken cancellationToken) + { + logger?.LogInformation($"Resetting virtual device '{id}'..."); + + var avdPath = await GetVirtualDevicePathAsync(id, cancellationToken).ConfigureAwait(false); + + foreach (var file in userDataFiles) + { + var f = Path.Combine(avdPath, file); + if (File.Exists(f)) + File.Delete(f); + } + } + + public async Task> GetVirtualDeviceIdsAsync(CancellationToken cancellationToken = default) { logger?.LogInformation("Retrieving all the virtual devices..."); @@ -77,7 +92,6 @@ namespace DotNetDevices.Android if (Directory.Exists(path)) { var avd = Path.GetFileNameWithoutExtension(path); - avds.Add(avd); } } @@ -86,6 +100,29 @@ namespace DotNetDevices.Android return avds; } + public async Task GetVirtualDevicePathAsync(string id, CancellationToken cancellationToken = default) + { + logger?.LogInformation("Retrieving all the virtual devices..."); + + var args = $"list avd"; + + var result = await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false); + + foreach (var output in GetListResults(result)) + { + var pathMatch = virtualDevicePathRegex.Match(output); + if (pathMatch.Success) + { + var path = pathMatch.Groups[1].Value; + var avdId = Path.GetFileNameWithoutExtension(path); + if (avdId.Equals(id, StringComparison.OrdinalIgnoreCase) && Directory.Exists(path)) + return path; + } + } + + throw new Exception($"Virtual device '{id}' does not exist."); + } + public async Task> GetVirtualDevicesAsync(CancellationToken cancellationToken = default) { logger?.LogInformation("Retrieving all the virtual devices..."); @@ -105,7 +142,7 @@ namespace DotNetDevices.Android var configIniPath = Path.Combine(path, "config.ini"); if (Directory.Exists(path) && File.Exists(configIniPath)) { - var config = new VirtualDeviceConfig(configIniPath, logger); + var config = new VirtualDeviceConfig(path, logger); var avd = await config.CreateVirtualDeviceAsync(cancellationToken).ConfigureAwait(false); avds.Add(avd); @@ -116,11 +153,11 @@ namespace DotNetDevices.Android return avds; } - public async Task DeleteVirtualDeviceAsync(string name, CancellationToken cancellationToken = default) + public async Task DeleteVirtualDeviceAsync(string id, CancellationToken cancellationToken = default) { - logger?.LogInformation($"Deleting virtual device '{name}'..."); + logger?.LogInformation($"Deleting virtual device '{id}'..."); - var args = $"delete avd --name \"{name}\""; + var args = $"delete avd --name \"{id}\""; try { @@ -133,7 +170,7 @@ namespace DotNetDevices.Android bool WasExisting(ProcessResult result) { - var expected = $"Error: There is no Android Virtual Device named '{name}'."; + var expected = $"Error: There is no Android Virtual Device with ID '{id}'."; foreach (var output in result.GetErrorOutput()) { @@ -145,11 +182,11 @@ namespace DotNetDevices.Android } } - public async Task CreateVirtualDeviceAsync(string name, string package, CreateVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default) + public async Task CreateVirtualDeviceAsync(string id, string package, CreateVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default) { - logger?.LogInformation($"Creating virtual device '{name}'..."); + logger?.LogInformation($"Creating virtual device '{id}'..."); - var args = $"create avd --name \"{name}\" --package \"{package}\""; + var args = $"create avd --name \"{id}\" --package \"{package}\""; if (options?.Overwrite == true) args += " --force"; @@ -164,7 +201,7 @@ namespace DotNetDevices.Android bool WasExisting(ProcessResult result) { - var expected = $"Error: Android Virtual Device '{name}' already exists."; + var expected = $"Error: Android Virtual Device '{id}' already exists."; foreach (var output in result.GetErrorOutput()) { @@ -176,20 +213,20 @@ namespace DotNetDevices.Android } } - public async Task RenameVirtualDeviceAsync(string name, string newName, CancellationToken cancellationToken = default) + public async Task RenameVirtualDeviceAsync(string id, string newId, CancellationToken cancellationToken = default) { - logger?.LogInformation($"Renaming virtual device '{name}'..."); + logger?.LogInformation($"Renaming virtual device '{id}'..."); - var args = $"move avd --name \"{name}\" --rename \"{newName}\""; + var args = $"move avd --name \"{id}\" --rename \"{newId}\""; await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false); } - public async Task MoveVirtualDeviceAsync(string name, string newPath, CancellationToken cancellationToken = default) + public async Task MoveVirtualDeviceAsync(string id, string newPath, CancellationToken cancellationToken = default) { - logger?.LogInformation($"Moving virtual device '{name}'..."); + logger?.LogInformation($"Moving virtual device '{id}'..."); - var args = $"move avd --name \"{name}\" --path \"{newPath}\""; + var args = $"move avd --name \"{id}\" --path \"{newPath}\""; await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false); } diff --git a/dotnet-devices/Android/AaptTool.cs b/dotnet-devices/Android/Aapt.cs similarity index 98% rename from dotnet-devices/Android/AaptTool.cs rename to dotnet-devices/Android/Aapt.cs index cc866fd..25a0c69 100644 --- a/dotnet-devices/Android/AaptTool.cs +++ b/dotnet-devices/Android/Aapt.cs @@ -9,7 +9,7 @@ using System.Xml.Linq; namespace DotNetDevices.Android { - public class AaptTool + public class Aapt { private readonly static Regex xmltreeNamespaceRegex = new Regex(@"^N:\s*(?[^=]+)=(?.*)$"); private readonly static Regex xmltreeElementRegex = new Regex(@"^E:\s*((?[^:]+):)?(?.*) \(line=\d+\)$"); @@ -19,13 +19,13 @@ namespace DotNetDevices.Android private readonly ILogger? logger; private readonly string aapt; - public AaptTool(string? sdkRoot = null, ILogger? logger = null) + public Aapt(string? sdkRoot = null, ILogger? logger = null) { - processRunner = new ProcessRunner(logger); this.logger = logger; - aapt = AndroidSDK.FindBuildToolPath(sdkRoot, "aapt", logger) ?? throw new ArgumentException($"Unable to locate aapt. Make sure that ANDROID_HOME or ANDROID_SDK_ROOT is set."); + + processRunner = new ProcessRunner(logger); } public async Task GetAndroidManifestAsync(string apk, CancellationToken cancellationToken = default) diff --git a/dotnet-devices/Android/Adb.cs b/dotnet-devices/Android/Adb.cs new file mode 100644 index 0000000..013ec5d --- /dev/null +++ b/dotnet-devices/Android/Adb.cs @@ -0,0 +1,380 @@ +using DotNetDevices.Processes; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace DotNetDevices.Android +{ + public class Adb + { + private readonly ProcessRunner processRunner; + private readonly ILogger? logger; + private readonly string adb; + + public Adb(string? sdkRoot = null, ILogger? logger = null) + { + this.logger = logger; + adb = AndroidSDK.FindPath(sdkRoot, "platform-tools/adb", logger) + ?? throw new ArgumentException($"Unable to locate adb. Make sure that ANDROID_HOME or ANDROID_SDK_ROOT is set."); + + processRunner = new ProcessRunner(logger); + } + + public async Task GetVirtualDeviceWithIdAsync(string avdId, CancellationToken cancellationToken = default) + { + var devices = await GetDevicesAsync(cancellationToken).ConfigureAwait(false); + foreach (var device in devices) + { + var deviceAvdId = await GetVirtualDeviceIdAsync(device.Serial, cancellationToken).ConfigureAwait(false); + if (deviceAvdId?.Equals(avdId, StringComparison.OrdinalIgnoreCase) == true) + return device; + } + + return null; + } + + public async IAsyncEnumerable GetVirtualDevicesWithIdAsync(string avdId, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (avdId == null) + throw new ArgumentNullException(nameof(avdId)); + + var devices = await GetDevicesAsync(cancellationToken).ConfigureAwait(false); + foreach (var device in devices) + { + var deviceAvdId = await GetVirtualDeviceIdAsync(device.Serial, cancellationToken).ConfigureAwait(false); + if (deviceAvdId?.Equals(avdId, StringComparison.OrdinalIgnoreCase) == true) + yield return device; + } + } + + public async Task> GetDevicesAsync(CancellationToken cancellationToken = default) + { + logger?.LogInformation("Searching for conected devices..."); + + return await GetDevicesNoLoggingAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task GetDeviceAsync(string serial, CancellationToken cancellationToken = default) + { + if (serial == null) + throw new ArgumentNullException(nameof(serial)); + + logger?.LogInformation($"Searching for conected device '{serial}'..."); + + var devices = await GetDevicesNoLoggingAsync(cancellationToken).ConfigureAwait(false); + + return devices.FirstOrDefault(d => d.Serial.Equals(serial, StringComparison.OrdinalIgnoreCase)); + } + + public async Task GetVirtualDeviceIdAsync(string serial, CancellationToken cancellationToken = default) + { + if (serial == null) + throw new ArgumentNullException(nameof(serial)); + + logger?.LogInformation($"Reading virtual device ID for '{serial}'..."); + + await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false); + + var args = $"-s \"{serial}\" emu avd name"; + var result = await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false); + + var nonEmptyLines = result.GetOutput() + .Where(l => !string.IsNullOrWhiteSpace(l)) + .ToArray(); + if (nonEmptyLines.Length == 0) + return null; + + if (nonEmptyLines.Length < 2 || !nonEmptyLines[1].Trim().Equals("OK", StringComparison.OrdinalIgnoreCase)) + throw new Exception($"Unable to read virtual device ID."); + + return nonEmptyLines[0].Trim(); + } + + public async Task LogcatAsync(string serial, LogcatOptions? options = null, CancellationToken cancellationToken = default) + { + if (serial == null) + throw new ArgumentNullException(nameof(serial)); + + logger?.LogInformation($"Starting logcat for '{serial}'..."); + + await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false); + + try + { + var dump = options?.DumpOnly == true + ? "-d" + : string.Empty; + + var args = $"-s \"{serial}\" logcat {dump}"; + return await processRunner.RunAsync(adb, args, Wrap(options?.HandleOutput), cancellationToken).ConfigureAwait(false); + } + catch (ProcessResultException ex) when (ex.InnerException is OperationCanceledException && ex.ProcessResult != null) + { + return ex.ProcessResult; + } + + static Func? Wrap(Action? handle) + { + if (handle == null) + return null; + + return o => + { + handle(o); + return true; + }; + } + } + + public async Task ClearLogcatAsync(string serial, CancellationToken cancellationToken = default) + { + if (serial == null) + throw new ArgumentNullException(nameof(serial)); + + logger?.LogInformation($"Clearing logcat for '{serial}'..."); + + await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false); + + var args = $"-s \"{serial}\" logcat --clear"; + await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false); + } + + public async Task LaunchActivityAsync(string serial, string activity, CancellationToken cancellationToken = default) + { + if (serial == null) + throw new ArgumentNullException(nameof(serial)); + if (activity == null) + throw new ArgumentNullException(nameof(activity)); + + logger?.LogInformation($"Launching activity '{activity}' on device '{serial}'..."); + + await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false); + + var args = $"-s \"{serial}\" shell am start -n \"{activity}\""; + await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false); + } + + public async Task PullFileAsync(string serial, string packageName, string sourceFileName, string destFileName, bool overwrite, CancellationToken cancellationToken = default) + { + if (serial == null) + throw new ArgumentNullException(nameof(serial)); + if (packageName == null) + throw new ArgumentNullException(nameof(packageName)); + if (sourceFileName == null) + throw new ArgumentNullException(nameof(sourceFileName)); + if (destFileName == null) + throw new ArgumentNullException(nameof(destFileName)); + + logger?.LogInformation($"Pulling file '{sourceFileName}' on device '{serial}' to '{destFileName}'..."); + + if (File.Exists(destFileName)) + { + if (!overwrite) + throw new IOException($"File {destFileName} already exists."); + File.Delete(destFileName); + } + + var guid = Guid.NewGuid().ToString(); + + var command = $"cp \"{sourceFileName}\" \"/sdcard/Download/{guid}\""; + await RunCommandAsAppAsync(serial, packageName, command, cancellationToken).ConfigureAwait(false); + + var args = $"-s \"{serial}\" pull \"/sdcard/Download/{guid}\" \"{destFileName}\""; + await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false); + + command = $"rm \"/sdcard/Download/{guid}\""; + await RunCommandAsAppAsync(serial, packageName, command, cancellationToken).ConfigureAwait(false); + } + + public async Task InstallAppAsync(string serial, string appPath, InstallAppOptions? options = default, CancellationToken cancellationToken = default) + { + if (serial == null) + throw new ArgumentNullException(nameof(serial)); + if (appPath == null) + throw new ArgumentNullException(nameof(appPath)); + if (!File.Exists(appPath)) + throw new FileNotFoundException($"Unable to find the app '{appPath}'.", appPath); + + logger?.LogInformation($"Installing '{appPath}' on virtual device '{serial}'..."); + + if (options?.SkipSharedRuntimeValidation != true && HasSharedRuntime(appPath)) + throw new Exception("Installing apps that rely on the Mono Shared Runtime is not supported. Change the project configuration or use a Release build."); + + await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false); + + var args = $"-s \"{serial}\" install \"{appPath}\""; + await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false); + } + + public async Task UninstallAppAsync(string serial, string packageName, CancellationToken cancellationToken) + { + if (serial == null) + throw new ArgumentNullException(nameof(serial)); + if (packageName == null) + throw new ArgumentNullException(nameof(packageName)); + + logger?.LogInformation($"Uninstalling '{packageName}' on virtual device '{serial}'..."); + + await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false); + + var args = $"-s \"{serial}\" uninstall \"{packageName}\""; + await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false); + } + + public async Task ShutdownVirtualDeviceAsync(string serial, CancellationToken cancellationToken = default) + { + logger?.LogInformation($"Shutting down virtual device with serial '{serial}'..."); + + await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false); + + var args = $"-s \"{serial}\" emu kill"; + await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false); + + await EnsureShutdownAsync(serial, cancellationToken); + } + + public async Task GetDataDirectoryAsync(string serial, string packageName, CancellationToken cancellationToken = default) + { + if (serial == null) + throw new ArgumentNullException(nameof(serial)); + if (packageName == null) + throw new ArgumentNullException(nameof(packageName)); + + logger?.LogInformation($"Retrieving data path for app '{packageName}' on device '{serial}'..."); + + var command = $"pwd"; + var result = await RunCommandAsAppAsync(serial, packageName, command, cancellationToken).ConfigureAwait(false); + var packageDataRoot = result.Output.Trim(); + + return $"{packageDataRoot}/files"; + } + + public async Task PathExistsAsync(string serial, string packageName, string path, CancellationToken cancellationToken = default) + { + if (serial == null) + throw new ArgumentNullException(nameof(serial)); + if (packageName == null) + throw new ArgumentNullException(nameof(packageName)); + if (path == null) + throw new ArgumentNullException(nameof(path)); + + logger?.LogInformation($"Check for '{path}' for app '{packageName}' on device '{serial}'..."); + + try + { + var command = $"ls \"{path}\""; + var result = await RunCommandAsAppAsync(serial, packageName, command, cancellationToken).ConfigureAwait(false); + + return true; + } + catch (ProcessResultException ex) when (ex.ProcessResult.OutputCount == 1 && ex.ProcessResult.Output.Contains("No such file or directory")) + { + return false; + } + } + + public async Task RunCommandAsAppAsync(string serial, string packageName, string command, CancellationToken cancellationToken = default) + { + if (serial == null) + throw new ArgumentNullException(nameof(serial)); + if (packageName == null) + throw new ArgumentNullException(nameof(packageName)); + if (command == null) + throw new ArgumentNullException(nameof(command)); + + logger?.LogInformation($"Running command '{command}' as app '{packageName}' on device '{serial}'..."); + + command = $"run-as \"{packageName}\" {command}"; + return await RunCommandAsync(serial, command, cancellationToken).ConfigureAwait(false); + } + + public async Task RunCommandAsync(string serial, string command, CancellationToken cancellationToken = default) + { + if (serial == null) + throw new ArgumentNullException(nameof(serial)); + if (command == null) + throw new ArgumentNullException(nameof(command)); + + logger?.LogInformation($"Running command '{command}' on device '{serial}'..."); + + await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false); + + var args = $"-s \"{serial}\" shell {command}"; + return await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false); + } + + private bool HasSharedRuntime(string appPath) + { + using var archive = ZipFile.OpenRead(appPath); + + var entries = archive.Entries; + + var hasMonodroid = entries.Any(x => x.Name.EndsWith("libmonodroid.so")); + var hasRuntime = entries.Any(x => x.Name.EndsWith("mscorlib.dll")); + var hasEnterpriseBundle = entries.Any(x => x.Name.EndsWith("libmonodroid_bundle_app.so")); + + return hasMonodroid && !hasRuntime && !hasEnterpriseBundle; + } + + private async Task EnsureShutdownAsync(string serial, CancellationToken cancellationToken) + { + while (await IsDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); + } + } + + private async Task EnsureDeviceVisibleAsync(string serial, CancellationToken cancellationToken) + { + var foundDevice = await IsDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false); + if (!foundDevice) + throw new Exception($"Unable to find virtual device '{serial}'."); + } + + private async Task IsDeviceVisibleAsync(string serial, CancellationToken cancellationToken) + { + var devices = await GetDevicesNoLoggingAsync(cancellationToken).ConfigureAwait(false); + return devices.Any(d => d.Serial.Equals(serial, StringComparison.OrdinalIgnoreCase)); + } + + private async Task> GetDevicesNoLoggingAsync(CancellationToken cancellationToken) + { + logger?.LogDebug("Searching for conected devices..."); + + var args = $"devices"; + + var result = await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false); + + var devices = new List(); + + foreach (var line in result.GetOutput()) + { + if (!line.Contains('\t')) + continue; + + var parts = line.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 2) + continue; + + var serial = parts[0].Trim(); + var state = parts[1].Trim().ToLowerInvariant() switch + { + "device" => ConnectedDeviceState.Connected, + "offline" => ConnectedDeviceState.Disconnected, + "no device" => ConnectedDeviceState.Unknown, + _ => ConnectedDeviceState.Unknown + }; + devices.Add(new ConnectedDevice(serial, state)); + } + + return devices; + } + } +} diff --git a/dotnet-devices/Android/AndroidManifest.cs b/dotnet-devices/Android/AndroidManifest.cs index 2c8d91a..56a655c 100644 --- a/dotnet-devices/Android/AndroidManifest.cs +++ b/dotnet-devices/Android/AndroidManifest.cs @@ -1,10 +1,13 @@ using System; +using System.Linq; using System.Xml.Linq; namespace DotNetDevices.Android { public class AndroidManifest { + private static readonly XNamespace xmlnsAndroid = "http://schemas.android.com/apk/res/android"; + public AndroidManifest(XDocument xdoc) { Document = xdoc ?? throw new ArgumentNullException(nameof(xdoc)); @@ -12,6 +15,20 @@ namespace DotNetDevices.Android public XDocument Document { get; } - public string? PackageName => Document.Root?.Attribute("package")?.Value; + public string? PackageName => + Document.Root + ?.Attribute("package")?.Value; + + public string? MainLauncherActivity => + Document.Root + ?.Element("application") + ?.Elements("activity") + ?.FirstOrDefault(a => + a?.Element("intent-filter") + ?.Element("action")?.Attribute(xmlnsAndroid + "name")?.Value == "android.intent.action.MAIN" && + a?.Element("intent-filter") + ?.Element("category")?.Attribute(xmlnsAndroid + "name")?.Value == "android.intent.category.LAUNCHER") + ?.Attribute(xmlnsAndroid + "name") + ?.Value; } } diff --git a/dotnet-devices/Android/ConnectedDevice.cs b/dotnet-devices/Android/ConnectedDevice.cs new file mode 100644 index 0000000..2e94a4d --- /dev/null +++ b/dotnet-devices/Android/ConnectedDevice.cs @@ -0,0 +1,28 @@ +using System; +using System.Text.RegularExpressions; + +namespace DotNetDevices.Android +{ + public class ConnectedDevice + { + private static readonly Regex emulatorPortRegex = new Regex(@"emulator-(\d+)"); + + public ConnectedDevice(string serial, ConnectedDeviceState state) + { + Serial = serial ?? throw new ArgumentNullException(nameof(serial)); + State = state; + + var match = emulatorPortRegex.Match(Serial); + if (match.Success && int.TryParse(match.Groups[1].Value, out var newPort)) + Port = newPort; + else + Port = -1; + } + + public string Serial { get; } + + public ConnectedDeviceState State { get; } + + public int Port { get; } + } +} diff --git a/dotnet-devices/Android/ConnectedDeviceState.cs b/dotnet-devices/Android/ConnectedDeviceState.cs new file mode 100644 index 0000000..dc9499a --- /dev/null +++ b/dotnet-devices/Android/ConnectedDeviceState.cs @@ -0,0 +1,9 @@ +namespace DotNetDevices.Android +{ + public enum ConnectedDeviceState + { + Unknown, + Disconnected, + Connected, + } +} diff --git a/dotnet-devices/Android/EmulatorManager.cs b/dotnet-devices/Android/EmulatorManager.cs index a95f422..5b1d20b 100644 --- a/dotnet-devices/Android/EmulatorManager.cs +++ b/dotnet-devices/Android/EmulatorManager.cs @@ -1,11 +1,11 @@ -using System; +using DotNetDevices.Processes; +using Microsoft.Extensions.Logging; +using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using DotNetDevices.Processes; -using Microsoft.Extensions.Logging; namespace DotNetDevices.Android { @@ -27,14 +27,14 @@ namespace DotNetDevices.Android ?? throw new ArgumentException($"Unable to locate the Android Emulator. Make sure that ANDROID_HOME or ANDROID_SDK_ROOT is set."); } - public async Task BootVirtualDeviceAsync(string name, BootVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default) + public async Task BootVirtualDeviceAsync(string avdId, BootVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default) { - if (name == null) - throw new ArgumentNullException(nameof(name)); + if (avdId == null) + throw new ArgumentNullException(nameof(avdId)); - logger?.LogInformation($"Booting virtual device '{name}'..."); + logger?.LogInformation($"Booting virtual device '{avdId}'..."); - var args = $"-avd {name} -verbose"; + var args = $"-avd {avdId} -verbose"; if (options?.NoWindow == true) args += " -no-boot-anim -no-window"; if (options?.NoSnapshots == true) @@ -77,7 +77,7 @@ namespace DotNetDevices.Android return true; } - bool IsAlreadyLaunched(ProcessResultException ex) + static bool IsAlreadyLaunched(ProcessResultException ex) { foreach (var output in ex.ProcessResult.GetOutput()) { diff --git a/dotnet-devices/Android/InstallAppOptions.cs b/dotnet-devices/Android/InstallAppOptions.cs new file mode 100644 index 0000000..2ce50a7 --- /dev/null +++ b/dotnet-devices/Android/InstallAppOptions.cs @@ -0,0 +1,7 @@ +namespace DotNetDevices.Android +{ + public class InstallAppOptions + { + public bool SkipSharedRuntimeValidation { get; set; } = false; + } +} diff --git a/dotnet-devices/Android/LogcatOptions.cs b/dotnet-devices/Android/LogcatOptions.cs new file mode 100644 index 0000000..7206142 --- /dev/null +++ b/dotnet-devices/Android/LogcatOptions.cs @@ -0,0 +1,12 @@ +using DotNetDevices.Processes; +using System; + +namespace DotNetDevices.Android +{ + public class LogcatOptions + { + public bool DumpOnly { get; set; } + + public Action? HandleOutput { get; set; } + } +} diff --git a/dotnet-devices/Android/VirtualDevice.cs b/dotnet-devices/Android/VirtualDevice.cs index e6be798..02f6ca8 100644 --- a/dotnet-devices/Android/VirtualDevice.cs +++ b/dotnet-devices/Android/VirtualDevice.cs @@ -4,14 +4,14 @@ namespace DotNetDevices.Android { public class VirtualDevice { - public VirtualDevice(string id, string name, string package, VirtualDeviceType type, int apiLevel, string? configPath) + public VirtualDevice(string id, string name, string package, VirtualDeviceType type, int apiLevel, string? avdPath) { Id = id ?? throw new ArgumentNullException(nameof(id)); Name = name ?? throw new ArgumentNullException(nameof(name)); Package = package ?? throw new ArgumentNullException(nameof(package)); Type = type; ApiLevel = apiLevel; - ConfigPath = configPath; + AvdPath = avdPath; } public string Id { get; } @@ -24,7 +24,7 @@ namespace DotNetDevices.Android public int ApiLevel { get; } - public string? ConfigPath { get; } + public string? AvdPath { get; } public Version Version => ApiLevel switch diff --git a/dotnet-devices/Android/VirtualDeviceConfig.cs b/dotnet-devices/Android/VirtualDeviceConfig.cs index ae6dc75..c6f334a 100644 --- a/dotnet-devices/Android/VirtualDeviceConfig.cs +++ b/dotnet-devices/Android/VirtualDeviceConfig.cs @@ -14,13 +14,16 @@ namespace DotNetDevices.Android private readonly string configPath; private readonly ILogger? logger; + private readonly string? avdPath; public Dictionary? properties; - public VirtualDeviceConfig(string configPath, ILogger? logger = null) + public VirtualDeviceConfig(string avdPath, ILogger? logger = null) { - this.configPath = configPath ?? throw new ArgumentNullException(nameof(configPath)); + this.avdPath = avdPath ?? throw new ArgumentNullException(nameof(avdPath)); this.logger = logger; + + configPath = Path.Combine(avdPath, "config.ini"); } public async Task> GetPropertiesAsync(CancellationToken cancellationToken = default) @@ -50,10 +53,7 @@ namespace DotNetDevices.Android var props = await GetPropertiesAsync(cancellationToken).ConfigureAwait(false); if (!props.TryGetValue("avdid", out var id)) - { - var avdDir = Path.GetDirectoryName(configPath); - id = Path.GetFileNameWithoutExtension(avdDir); - } + id = Path.GetFileNameWithoutExtension(avdPath); if (string.IsNullOrEmpty(id)) throw new Exception($"Invalid config.ini. Unable to find the virtual device ID."); @@ -77,7 +77,7 @@ namespace DotNetDevices.Android if (!TryGetType(props, out var type)) type = VirtualDeviceType.Unknown; - return new VirtualDevice(id, name, package, type, apiLevel, configPath); + return new VirtualDevice(id, name, package, type, apiLevel, avdPath); } private static bool TryGetType(IReadOnlyDictionary props, out VirtualDeviceType value) diff --git a/dotnet-devices/Apple/LaunchAppOptions.cs b/dotnet-devices/Apple/LaunchAppOptions.cs index 03467d1..155c2aa 100644 --- a/dotnet-devices/Apple/LaunchAppOptions.cs +++ b/dotnet-devices/Apple/LaunchAppOptions.cs @@ -7,8 +7,6 @@ namespace DotNetDevices.Apple { public bool CaptureOutput { get; set; } = false; - public bool BootSimulator { get; set; } = false; - public Action? HandleOutput { get; set; } } } diff --git a/dotnet-devices/Apple/SimulatorControl.cs b/dotnet-devices/Apple/SimulatorControl.cs index 08aead7..d3b31bf 100644 --- a/dotnet-devices/Apple/SimulatorControl.cs +++ b/dotnet-devices/Apple/SimulatorControl.cs @@ -1,13 +1,13 @@ -using System; +using DotNetDevices.Processes; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using DotNetDevices.Processes; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace DotNetDevices.Apple { @@ -183,7 +183,7 @@ namespace DotNetDevices.Apple await EnsureShutdownAsync(udid, cancellationToken).ConfigureAwait(false); } - public async Task EraseSimulatorAsync(string udid, bool shutdown = false, CancellationToken cancellationToken = default) + public async Task EraseSimulatorAsync(string udid, CancellationToken cancellationToken = default) { if (udid == null) throw new ArgumentNullException(nameof(udid)); @@ -194,14 +194,11 @@ namespace DotNetDevices.Apple if (sim == null) throw new Exception($"Unable to find simulator {udid}."); - if (shutdown && sim.State != SimulatorState.Shutdown) - await ShutdownSimulatorAsync(udid, cancellationToken).ConfigureAwait(false); - var args = $"simctl erase \"{udid}\""; await processRunner.RunAsync(xcrun, args, null, cancellationToken).ConfigureAwait(false); } - public async Task InstallAppAsync(string udid, string appPath, bool boot = false, CancellationToken cancellationToken = default) + public async Task InstallAppAsync(string udid, string appPath, CancellationToken cancellationToken = default) { if (udid == null) throw new ArgumentNullException(nameof(udid)); @@ -216,9 +213,6 @@ namespace DotNetDevices.Apple if (sim == null) throw new Exception($"Unable to find simulator {udid}."); - if (boot && sim.State != SimulatorState.Booted) - await BootSimulatorAsync(udid, cancellationToken).ConfigureAwait(false); - var args = $"simctl install \"{udid}\" \"{appPath}\""; await processRunner.RunAsync(xcrun, args, null, cancellationToken).ConfigureAwait(false); } @@ -236,9 +230,6 @@ namespace DotNetDevices.Apple if (sim == null) throw new Exception($"Unable to find simulator {udid}."); - if (options?.BootSimulator == true && sim.State != SimulatorState.Booted) - await BootSimulatorAsync(udid, cancellationToken).ConfigureAwait(false); - var console = options?.CaptureOutput == true ? "--console" : string.Empty; @@ -289,7 +280,7 @@ namespace DotNetDevices.Apple await processRunner.RunAsync(xcrun, args, null, cancellationToken).ConfigureAwait(false); } - public async Task UninstallAppAsync(string udid, string appBundleId, bool boot = false, CancellationToken cancellationToken = default) + public async Task UninstallAppAsync(string udid, string appBundleId, CancellationToken cancellationToken = default) { if (udid == null) throw new ArgumentNullException(nameof(udid)); @@ -302,14 +293,8 @@ namespace DotNetDevices.Apple if (sim == null) throw new Exception($"Unable to find simulator {udid}."); - if (boot && sim.State != SimulatorState.Booted) - await BootSimulatorAsync(udid, cancellationToken).ConfigureAwait(false); - var args = $"simctl uninstall \"{udid}\" \"{appBundleId}\""; await processRunner.RunAsync(xcrun, args, null, cancellationToken).ConfigureAwait(false); - - if (boot && sim.State != SimulatorState.Booted) - await ShutdownSimulatorAsync(udid, cancellationToken).ConfigureAwait(false); } private async Task EnsureBootedAsync(string udid, CancellationToken cancellationToken) @@ -317,7 +302,7 @@ namespace DotNetDevices.Apple while ((await GetSimulatorNoLoggingAsync(udid, cancellationToken).ConfigureAwait(false))!.State != SimulatorState.Booted) { cancellationToken.ThrowIfCancellationRequested(); - await Task.Delay(1000).ConfigureAwait(false); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); } } @@ -326,7 +311,7 @@ namespace DotNetDevices.Apple while ((await GetSimulatorNoLoggingAsync(udid, cancellationToken).ConfigureAwait(false))!.State != SimulatorState.Shutdown) { cancellationToken.ThrowIfCancellationRequested(); - await Task.Delay(1000).ConfigureAwait(false); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet-devices/Commands/AndroidCommand.cs b/dotnet-devices/Commands/AndroidCommand.cs index 62832e9..7705685 100644 --- a/dotnet-devices/Commands/AndroidCommand.cs +++ b/dotnet-devices/Commands/AndroidCommand.cs @@ -133,7 +133,7 @@ namespace DotNetDevices.Commands if (!replace) { - var devices = await avdmanager.GetVirtualDeviceNamesAsync(cancellationToken); + var devices = await avdmanager.GetVirtualDeviceIdsAsync(cancellationToken); if (devices.Any(d => d.Equals(name, StringComparison.OrdinalIgnoreCase))) { logger.LogInformation($"Virtual device already exists."); @@ -160,7 +160,7 @@ namespace DotNetDevices.Commands var avdmanager = new AVDManager(sdk, logger); - var devices = await avdmanager.GetVirtualDeviceNamesAsync(cancellationToken); + var devices = await avdmanager.GetVirtualDeviceIdsAsync(cancellationToken); if (devices.All(d => !d.Equals(name, StringComparison.OrdinalIgnoreCase))) { logger.LogInformation($"Virtual device does not exist."); diff --git a/dotnet-devices/Commands/AndroidTestCommand.cs b/dotnet-devices/Commands/AndroidTestCommand.cs index 26e5c60..6a01b46 100644 --- a/dotnet-devices/Commands/AndroidTestCommand.cs +++ b/dotnet-devices/Commands/AndroidTestCommand.cs @@ -1,7 +1,9 @@ using DotNetDevices.Android; +using DotNetDevices.Testing; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -14,7 +16,8 @@ namespace DotNetDevices.Commands private readonly ILogger logger; private readonly AVDManager avdmanager; private readonly EmulatorManager emulator; - private readonly AaptTool aapt; + private readonly Aapt aapt; + private readonly Adb adb; public AndroidTestCommand(string? sdkRoot, ILogger logger) { @@ -23,7 +26,8 @@ namespace DotNetDevices.Commands avdmanager = new AVDManager(sdkRoot, logger); emulator = new EmulatorManager(sdkRoot, logger); - aapt = new AaptTool(sdkRoot, logger); + aapt = new Aapt(sdkRoot, logger); + adb = new Adb(sdkRoot, logger); } public async Task RunTestsAsync( @@ -44,8 +48,12 @@ namespace DotNetDevices.Commands var packageName = androidManifest.PackageName; if (string.IsNullOrEmpty(packageName)) throw new Exception("Unable to determine the package name for the app."); + var activityName = androidManifest.MainLauncherActivity; + if (string.IsNullOrEmpty(activityName)) + throw new Exception("Unable to determine the main launcher activity name for the app."); logger.LogInformation($"Running tests on '{packageName}'..."); + logger.LogInformation($"Detected main launcher activity '{activityName}'."); // validate requested OS var avdRuntime = ParseDeviceRuntime(runtimeString); @@ -61,74 +69,93 @@ namespace DotNetDevices.Commands var avd = available.FirstOrDefault(); logger.LogInformation($"Using virtual device {avd.Name} ({avd.Runtime} {avd.Version}): {avd.Id}"); + string? serial = null; try { - // if (reset) - // await simctl.EraseSimulatorAsync(simulator.Udid, true, cancellationToken); + if (reset) + { + var bootedDevice = await adb.GetVirtualDeviceWithIdAsync(avd.Id, cancellationToken); + if (bootedDevice != null) + await adb.ShutdownVirtualDeviceAsync(bootedDevice.Serial); - // await simctl.InstallAppAsync(simulator.Udid, app, true, cancellationToken); + await avdmanager.ResetVirtualDeviceAsync(avd.Id, cancellationToken); + } + + var bootOptions = new BootVirtualDeviceOptions + { + WipeData = reset, + }; + var port = await emulator.BootVirtualDeviceAsync(avd.Id, bootOptions, cancellationToken); + if (port == -1) + { + // device was already booted, so find it + var bootedDevice = await adb.GetVirtualDeviceWithIdAsync(avd.Id, cancellationToken); + if (bootedDevice == null) + throw new Exception($"Virtual device '{avd.Name}' was already booted, but was not able to be found."); + + serial = bootedDevice.Serial; + logger.LogDebug($"Virutal device was already booted, found serial '{serial}'"); + } + else + { + serial = $"emulator-{port}"; + logger.LogDebug($"Virtual device was booted to port {port}, assuming serial '{serial}'"); + } + + var installOptions = new InstallAppOptions + { + SkipSharedRuntimeValidation = false, + }; + await adb.InstallAppAsync(serial, app, installOptions, cancellationToken); try { - // var parser = new TestResultsParser(); + await adb.ClearLogcatAsync(serial, cancellationToken); - // var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var parser = new TestResultsParser(); - // var launched = await simctl.LaunchAppAsync(simulator.Udid, bundleId, new LaunchAppOptions - // { - // CaptureOutput = true, - // BootSimulator = true, - // HandleOutput = output => - // { - // parser.ParseTestOutput( - // output, - // line => logger?.LogWarning(line), - // async () => - // { - // try - // { - // // wait a few seconds before terminating - // await Task.Delay(1000, cts.Token); + var logcatTask = adb.LogcatAsync(serial, new LogcatOptions + { + HandleOutput = output => + { + parser.ParseTestOutput( + output, + line => logger?.LogWarning(line), + () => throw new TaskCanceledException()); + } + }, cancellationToken); - // await simctl.TerminateAppAsync(simulator.Udid, bundleId, cts.Token); - // } - // catch (OperationCanceledException) - // { - // // we expected this - // } - // }); - // }, - // }, cancellationToken); + await adb.LaunchActivityAsync(serial, $"{packageName}/{activityName}", cancellationToken); - // cts.Cancel(); + await logcatTask; - // if (deviceResults != null) - // { - // var dest = outputResults ?? Path.GetFileName(deviceResults); + if (deviceResults != null) + { + var dest = outputResults ?? Path.GetFileName(deviceResults); - // logger.LogInformation($"Copying test results from simulator to {dest}..."); + logger.LogInformation($"Copying test results from virtual device to {dest}..."); - // var dataPath = await simctl.GetDataDirectoryAsync(simulator.Udid, bundleId, cancellationToken); - // var results = Path.Combine(dataPath, "Documents", deviceResults); - // if (File.Exists(results)) - // File.Copy(results, dest, true); - // else - // logger.LogInformation($"No test results found."); - // } - // else - // { - // logger.LogInformation($"Unable to determine the test results file."); - // } + var dataPath = await adb.GetDataDirectoryAsync(serial, packageName, cancellationToken); + var results = Path.Combine(dataPath, deviceResults).Replace("\\", "/"); + if (await adb.PathExistsAsync(serial, packageName, results, cancellationToken)) + await adb.PullFileAsync(serial, packageName, results, dest, true, cancellationToken); + else + logger.LogInformation($"No test results found."); + } + else + { + logger.LogInformation($"Unable to determine the test results file."); + } } finally { - // await simctl.UninstallAppAsync(simulator.Udid, bundleId, false, cancellationToken); + await adb.UninstallAppAsync(serial, packageName, cancellationToken); } } finally { - // if (shutdown) - // await simctl.ShutdownSimulatorAsync(simulator.Udid, cancellationToken); + if (shutdown && serial != null) + await adb.ShutdownVirtualDeviceAsync(serial, cancellationToken); } } diff --git a/dotnet-devices/Commands/AppleTestCommand.cs b/dotnet-devices/Commands/AppleTestCommand.cs index 95756b3..df6358d 100644 --- a/dotnet-devices/Commands/AppleTestCommand.cs +++ b/dotnet-devices/Commands/AppleTestCommand.cs @@ -58,9 +58,14 @@ namespace DotNetDevices.Commands try { if (reset) - await simctl.EraseSimulatorAsync(simulator.Udid, true, cancellationToken); + { + await simctl.ShutdownSimulatorAsync(simulator.Udid, cancellationToken); + await simctl.EraseSimulatorAsync(simulator.Udid, cancellationToken); + } - await simctl.InstallAppAsync(simulator.Udid, app, true, cancellationToken); + await simctl.BootSimulatorAsync(simulator.Udid, cancellationToken); + + await simctl.InstallAppAsync(simulator.Udid, app, cancellationToken); try { @@ -71,7 +76,6 @@ namespace DotNetDevices.Commands var launched = await simctl.LaunchAppAsync(simulator.Udid, bundleId, new LaunchAppOptions { CaptureOutput = true, - BootSimulator = true, HandleOutput = output => { parser.ParseTestOutput( @@ -116,7 +120,7 @@ namespace DotNetDevices.Commands } finally { - await simctl.UninstallAppAsync(simulator.Udid, bundleId, false, cancellationToken); + await simctl.UninstallAppAsync(simulator.Udid, bundleId, cancellationToken); } } finally