Android now runs tests
This commit is contained in:
Родитель
c0e5600a11
Коммит
de64e0276c
|
@ -25,8 +25,10 @@
|
|||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<AndroidLinkMode>None</AndroidLinkMode>
|
||||
<AndroidSupportedAbis />
|
||||
<AndroidSupportedAbis>armeabi-v7a;x86;x86_64;arm64-v8a</AndroidSupportedAbis>
|
||||
<JavaMaximumHeapSize>1G</JavaMaximumHeapSize>
|
||||
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Android.App;
|
||||
using Android.App;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Xunit.Runners.UI;
|
||||
using Environment = System.Environment;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<IEnumerable<string>> 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<IEnumerable<string>> 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<string> 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<IEnumerable<VirtualDevice>> 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);
|
||||
}
|
||||
|
|
|
@ -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*(?<ns>[^=]+)=(?<url>.*)$");
|
||||
private readonly static Regex xmltreeElementRegex = new Regex(@"^E:\s*((?<ns>[^:]+):)?(?<name>.*) \(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<AndroidManifest> GetAndroidManifestAsync(string apk, CancellationToken cancellationToken = default)
|
|
@ -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<ConnectedDevice?> 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<ConnectedDevice> 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<IEnumerable<ConnectedDevice>> GetDevicesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
logger?.LogInformation("Searching for conected devices...");
|
||||
|
||||
return await GetDevicesNoLoggingAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<ConnectedDevice?> 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<string?> 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<ProcessResult> 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<ProcessOutput, bool>? Wrap(Action<ProcessOutput>? 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<string> 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<bool> 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<ProcessResult> 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<ProcessResult> 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<bool> 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<IEnumerable<ConnectedDevice>> 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<ConnectedDevice>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace DotNetDevices.Android
|
||||
{
|
||||
public enum ConnectedDeviceState
|
||||
{
|
||||
Unknown,
|
||||
Disconnected,
|
||||
Connected,
|
||||
}
|
||||
}
|
|
@ -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<int> BootVirtualDeviceAsync(string name, BootVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public async Task<int> 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())
|
||||
{
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
namespace DotNetDevices.Android
|
||||
{
|
||||
public class InstallAppOptions
|
||||
{
|
||||
public bool SkipSharedRuntimeValidation { get; set; } = false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
using DotNetDevices.Processes;
|
||||
using System;
|
||||
|
||||
namespace DotNetDevices.Android
|
||||
{
|
||||
public class LogcatOptions
|
||||
{
|
||||
public bool DumpOnly { get; set; }
|
||||
|
||||
public Action<ProcessOutput>? HandleOutput { get; set; }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -14,13 +14,16 @@ namespace DotNetDevices.Android
|
|||
|
||||
private readonly string configPath;
|
||||
private readonly ILogger? logger;
|
||||
private readonly string? avdPath;
|
||||
|
||||
public Dictionary<string, string>? 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<IReadOnlyDictionary<string, string>> 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<string, string> props, out VirtualDeviceType value)
|
||||
|
|
|
@ -7,8 +7,6 @@ namespace DotNetDevices.Apple
|
|||
{
|
||||
public bool CaptureOutput { get; set; } = false;
|
||||
|
||||
public bool BootSimulator { get; set; } = false;
|
||||
|
||||
public Action<ProcessOutput>? HandleOutput { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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);
|
||||
|
||||
// await simctl.InstallAppAsync(simulator.Udid, app, true, cancellationToken);
|
||||
|
||||
try
|
||||
if (reset)
|
||||
{
|
||||
// var parser = new TestResultsParser();
|
||||
var bootedDevice = await adb.GetVirtualDeviceWithIdAsync(avd.Id, cancellationToken);
|
||||
if (bootedDevice != null)
|
||||
await adb.ShutdownVirtualDeviceAsync(bootedDevice.Serial);
|
||||
|
||||
// var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
// 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);
|
||||
|
||||
// await simctl.TerminateAppAsync(simulator.Udid, bundleId, cts.Token);
|
||||
// }
|
||||
// catch (OperationCanceledException)
|
||||
// {
|
||||
// // we expected this
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
// }, cancellationToken);
|
||||
|
||||
// cts.Cancel();
|
||||
|
||||
// if (deviceResults != null)
|
||||
// {
|
||||
// var dest = outputResults ?? Path.GetFileName(deviceResults);
|
||||
|
||||
// logger.LogInformation($"Copying test results from simulator 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.");
|
||||
// }
|
||||
await avdmanager.ResetVirtualDeviceAsync(avd.Id, cancellationToken);
|
||||
}
|
||||
finally
|
||||
|
||||
var bootOptions = new BootVirtualDeviceOptions
|
||||
{
|
||||
// await simctl.UninstallAppAsync(simulator.Udid, bundleId, false, cancellationToken);
|
||||
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
|
||||
{
|
||||
await adb.ClearLogcatAsync(serial, cancellationToken);
|
||||
|
||||
var parser = new TestResultsParser();
|
||||
|
||||
var logcatTask = adb.LogcatAsync(serial, new LogcatOptions
|
||||
{
|
||||
HandleOutput = output =>
|
||||
{
|
||||
parser.ParseTestOutput(
|
||||
output,
|
||||
line => logger?.LogWarning(line),
|
||||
() => throw new TaskCanceledException());
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
await adb.LaunchActivityAsync(serial, $"{packageName}/{activityName}", cancellationToken);
|
||||
|
||||
await logcatTask;
|
||||
|
||||
if (deviceResults != null)
|
||||
{
|
||||
var dest = outputResults ?? Path.GetFileName(deviceResults);
|
||||
|
||||
logger.LogInformation($"Copying test results from virtual device to {dest}...");
|
||||
|
||||
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
|
||||
{
|
||||
// if (shutdown)
|
||||
// await simctl.ShutdownSimulatorAsync(simulator.Udid, cancellationToken);
|
||||
await adb.UninstallAppAsync(serial, packageName, cancellationToken);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (shutdown && serial != null)
|
||||
await adb.ShutdownVirtualDeviceAsync(serial, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче