Android now can read apk files
This commit is contained in:
Родитель
21cb3c162a
Коммит
405864895b
|
@ -0,0 +1,54 @@
|
|||
using DotNetDevices.Android;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace DotNetDevices.Tests
|
||||
{
|
||||
public class AaptToolTests
|
||||
{
|
||||
public class ParseXmlTree
|
||||
{
|
||||
private static readonly XNamespace AndroidNamespace = "http://schemas.android.com/apk/res/android";
|
||||
|
||||
[Theory]
|
||||
[InlineData("TestData/Android/CompiledXmlDump.txt")]
|
||||
public void CanParse(string file)
|
||||
{
|
||||
var xmltree = File.ReadAllText(file);
|
||||
|
||||
var xdoc = AaptTool.ParseXmlTree(xmltree);
|
||||
|
||||
Assert.NotNull(xdoc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseIsValid()
|
||||
{
|
||||
var xmltree = File.ReadAllText("TestData/Android/CompiledXmlDump.txt");
|
||||
var xdoc = AaptTool.ParseXmlTree(xmltree);
|
||||
Assert.NotNull(xdoc);
|
||||
|
||||
var manifest = xdoc.Root;
|
||||
Assert.Equal(AndroidNamespace, manifest.GetNamespaceOfPrefix("android"));
|
||||
Assert.Equal("(type 0x10)0x1", manifest.Attribute(AndroidNamespace + "versionCode").Value);
|
||||
Assert.Equal("1.0.1.0", manifest.Attribute(AndroidNamespace + "versionName").Value);
|
||||
Assert.Equal("10", manifest.Attribute(AndroidNamespace + "compileSdkVersionCodename").Value);
|
||||
Assert.Equal("net.dot.devicetests", manifest.Attribute("package").Value);
|
||||
Assert.Equal("(type 0x10)0x1d", manifest.Attribute("platformBuildVersionCode").Value);
|
||||
|
||||
var usessdk = manifest.Element("uses-sdk");
|
||||
Assert.Equal("(type 0x10)0x13", usessdk.Attribute(AndroidNamespace + "minSdkVersion").Value);
|
||||
|
||||
var usespermissions = manifest.Elements("uses-permission").ToList();
|
||||
Assert.Equal(2, usespermissions.Count);
|
||||
Assert.Equal("android.permission.INTERNET", usespermissions[0].Attribute(AndroidNamespace + "name").Value);
|
||||
|
||||
var application = manifest.Element("application");
|
||||
Assert.Equal("@0x7f0c001b", application.Attribute(AndroidNamespace + "label").Value);
|
||||
Assert.Equal("android.app.Application", application.Attribute(AndroidNamespace + "name").Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
using DotNetDevices.Android;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace DotNetDevices.Tests
|
||||
{
|
||||
public class VirtualDeviceConfigTests
|
||||
{
|
||||
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")]
|
||||
public async Task CanCreateInstance(string file, string id, string name)
|
||||
{
|
||||
var config = new VirtualDeviceConfig(file);
|
||||
|
||||
var device = await config.CreateVirtualDeviceAsync();
|
||||
|
||||
Assert.NotNull(device);
|
||||
Assert.Equal(id, device.Id);
|
||||
Assert.Equal(name, device.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
AvdId=pixel_2_q_10_0_-_api_29
|
||||
PlayStore.enabled=true
|
||||
abi.type=x86
|
||||
avd.ini.displayname=Pixel 2 Q 10.0 - API 29
|
||||
avd.ini.encoding=UTF-8
|
||||
disk.dataPartition.size=6442450944
|
||||
fastboot.forceColdBoot=no
|
||||
fastboot.forceFastBoot=yes
|
||||
hw.accelerometer=yes
|
||||
hw.arc=no
|
||||
hw.audioInput=yes
|
||||
hw.battery=yes
|
||||
hw.camera.back=virtualscene
|
||||
hw.camera.front=emulated
|
||||
hw.cpu.arch=x86
|
||||
hw.cpu.ncore=4
|
||||
hw.dPad=no
|
||||
hw.device.hash2=MD5:55acbc835978f326788ed66a5cd4c9a7
|
||||
hw.device.manufacturer=Google
|
||||
hw.device.name=pixel_2
|
||||
hw.gps=yes
|
||||
hw.gpu.enabled=yes
|
||||
hw.gpu.mode=auto
|
||||
hw.keyboard=yes
|
||||
hw.lcd.density=420
|
||||
hw.lcd.height=1920
|
||||
hw.lcd.width=1080
|
||||
hw.mainKeys=no
|
||||
hw.ramSize=1536
|
||||
hw.sdCard=yes
|
||||
hw.sensors.orientation=yes
|
||||
hw.sensors.proximity=yes
|
||||
hw.trackBall=no
|
||||
image.sysdir.1=system-images\android-29\google_apis_playstore\x86\
|
||||
sdcard.size=512M
|
||||
showDeviceFrame=yes
|
||||
skin.name=pixel_2
|
||||
skin.path=C:\Android\android-sdk\skins\pixel_2
|
||||
tag.display=Google Play
|
||||
tag.id=google_apis_playstore
|
||||
vm.heapSize=256
|
|
@ -0,0 +1 @@
|
|||
AvdId=pixel_2_q_10_0_-_api_29
|
|
@ -0,0 +1,51 @@
|
|||
N: android=http://schemas.android.com/apk/res/android
|
||||
E: manifest (line=2)
|
||||
A: android:versionCode(0x0101021b)=(type 0x10)0x1
|
||||
A: android:versionName(0x0101021c)="1.0.1.0" (Raw: "1.0.1.0")
|
||||
A: android:installLocation(0x010102b7)=(type 0x10)0x0
|
||||
A: android:compileSdkVersion(0x01010572)=(type 0x10)0x1d
|
||||
A: android:compileSdkVersionCodename(0x01010573)="10" (Raw: "10")
|
||||
A: package="net.dot.devicetests" (Raw: "net.dot.devicetests")
|
||||
A: platformBuildVersionCode=(type 0x10)0x1d
|
||||
A: platformBuildVersionName=(type 0x10)0xa
|
||||
E: uses-sdk (line=3)
|
||||
A: android:minSdkVersion(0x0101020c)=(type 0x10)0x13
|
||||
A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1d
|
||||
E: uses-permission (line=4)
|
||||
A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
|
||||
E: uses-permission (line=5)
|
||||
A: android:name(0x01010003)="android.permission.READ_EXTERNAL_STORAGE" (Raw: "android.permission.READ_EXTERNAL_STORAGE")
|
||||
E: application (line=6)
|
||||
A: android:theme(0x01010000)=@0x7f0d00c7
|
||||
A: android:label(0x01010001)=@0x7f0c001b
|
||||
A: android:icon(0x01010002)=@0x7f07006f
|
||||
A: android:name(0x01010003)="android.app.Application" (Raw: "android.app.Application")
|
||||
A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
|
||||
A: android:allowBackup(0x01010280)=(type 0x12)0xffffffff
|
||||
A: android:appComponentFactory(0x0101057a)="androidx.core.app.CoreComponentFactory" (Raw: "androidx.core.app.CoreComponentFactory")
|
||||
E: activity (line=7)
|
||||
A: android:name(0x01010003)="crc645c25d208dccba52c.MainActivity" (Raw: "crc645c25d208dccba52c.MainActivity")
|
||||
A: android:configChanges(0x0101001f)=(type 0x11)0x480
|
||||
E: intent-filter (line=8)
|
||||
E: action (line=9)
|
||||
A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
|
||||
E: category (line=10)
|
||||
A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
|
||||
E: service (line=13)
|
||||
A: android:name(0x01010003)="crc64396a3fe5f8138e3f.KeepAliveService" (Raw: "crc64396a3fe5f8138e3f.KeepAliveService")
|
||||
E: receiver (line=14)
|
||||
A: android:name(0x01010003)="crc643f46942d9dd1fff9.PowerSaveModeBroadcastReceiver" (Raw: "crc643f46942d9dd1fff9.PowerSaveModeBroadcastReceiver")
|
||||
A: android:enabled(0x0101000e)=(type 0x12)0xffffffff
|
||||
A: android:exported(0x01010010)=(type 0x12)0x0
|
||||
E: provider (line=15)
|
||||
A: android:name(0x01010003)="mono.MonoRuntimeProvider" (Raw: "mono.MonoRuntimeProvider")
|
||||
A: android:exported(0x01010010)=(type 0x12)0x0
|
||||
A: android:authorities(0x01010018)="net.dot.devicetests.mono.MonoRuntimeProvider.__mono_init__" (Raw: "net.dot.devicetests.mono.MonoRuntimeProvider.__mono_init__")
|
||||
A: android:initOrder(0x0101001a)=(type 0x10)0x773593ff
|
||||
E: receiver (line=17)
|
||||
A: android:name(0x01010003)="mono.android.Seppuku" (Raw: "mono.android.Seppuku")
|
||||
E: intent-filter (line=18)
|
||||
E: action (line=19)
|
||||
A: android:name(0x01010003)="mono.android.intent.action.SEPPUKU" (Raw: "mono.android.intent.action.SEPPUKU")
|
||||
E: category (line=20)
|
||||
A: android:name(0x01010003)="mono.android.intent.category.SEPPUKU.net.dot.devicetests" (Raw: "mono.android.intent.category.SEPPUKU.net.dot.devicetests")
|
|
@ -1,14 +0,0 @@
|
|||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace DotNetDevices.Tests
|
||||
{
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
|
@ -9,12 +9,22 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="1.3.0"><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PackageReference Include="coverlet.collector" Version="1.3.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\dotnet-devices\dotnet-devices.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="TestData\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
using System;
|
||||
using DotNetDevices.Processes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
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;
|
||||
|
||||
namespace DotNetDevices.Android
|
||||
{
|
||||
public class AVDManager
|
||||
{
|
||||
private static Regex virtualDevicePathRegex = new Regex(@"\s*Path\:\s*(.+)");
|
||||
|
||||
private readonly ProcessRunner processRunner;
|
||||
private readonly ILogger? logger;
|
||||
private readonly string avdmanager;
|
||||
|
@ -60,16 +62,30 @@ namespace DotNetDevices.Android
|
|||
{
|
||||
logger?.LogInformation("Retrieving all the virtual devices...");
|
||||
|
||||
var args = $"list avd -c";
|
||||
var args = $"list avd";
|
||||
|
||||
var result = await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var avd = new List<VirtualDevice>(result.OutputCount);
|
||||
var avds = new List<VirtualDevice>();
|
||||
|
||||
foreach (var output in GetListResults(result))
|
||||
{
|
||||
avd.Add(new VirtualDevice(output));
|
||||
var pathMatch = virtualDevicePathRegex.Match(output);
|
||||
if (pathMatch.Success)
|
||||
{
|
||||
var path = pathMatch.Groups[1].Value;
|
||||
var configIniPath = Path.Combine(path, "config.ini");
|
||||
if (Directory.Exists(path) && File.Exists(configIniPath))
|
||||
{
|
||||
var config = new VirtualDeviceConfig(configIniPath, logger);
|
||||
var avd = await config.CreateVirtualDeviceAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
avds.Add(avd);
|
||||
}
|
||||
}
|
||||
}
|
||||
return avd;
|
||||
|
||||
return avds;
|
||||
}
|
||||
|
||||
public async Task DeleteVirtualDeviceAsync(string name, CancellationToken cancellationToken = default)
|
||||
|
@ -81,7 +97,7 @@ namespace DotNetDevices.Android
|
|||
await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task CreateVirtualDeviceAsync(string name, string package, VirtualDeviceCreateOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public async Task CreateVirtualDeviceAsync(string name, string package, CreateVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
logger?.LogInformation($"Creating virtual device '{name}'...");
|
||||
|
||||
|
@ -110,7 +126,7 @@ namespace DotNetDevices.Android
|
|||
await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetListResults(ProcessResult result)
|
||||
private static IEnumerable<string> GetListResults(ProcessResult result)
|
||||
{
|
||||
foreach (var output in result.GetOutput())
|
||||
{
|
||||
|
@ -135,17 +151,4 @@ namespace DotNetDevices.Android
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class VirtualDeviceCreateOptions
|
||||
{
|
||||
public string? Device { get; set; }
|
||||
|
||||
public bool Overwrite { get; set; }
|
||||
|
||||
public string? Path { get; set; }
|
||||
|
||||
public string? SharedSdCardPath { get; set; }
|
||||
|
||||
public string? NewSdCardSize { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
using DotNetDevices.Processes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace DotNetDevices.Android
|
||||
{
|
||||
public class AaptTool
|
||||
{
|
||||
private readonly static Regex xmltreeNamespaceRegex = new Regex(@"^N:\s*(?<ns>[^=]+)=(?<url>.*)$");
|
||||
private readonly static Regex xmltreeElementRegex = new Regex(@"^E:\s*((?<ns>[^:]+):)?(?<name>.*) \(line=\d+\)$");
|
||||
private readonly static Regex xmltreeAttributeRegex = new Regex(@"^A:\s*((?<ns>[^:]+):)?(?<name>[^(]+)(\(.*\))?=(?<value>.*)$");
|
||||
|
||||
private readonly ProcessRunner processRunner;
|
||||
private readonly ILogger? logger;
|
||||
private readonly string aapt;
|
||||
|
||||
public AaptTool(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.");
|
||||
}
|
||||
|
||||
public async Task<AndroidManifest> GetAndroidManifestAsync(string apk, CancellationToken cancellationToken = default)
|
||||
{
|
||||
logger?.LogInformation("Loading AndroidManifest.xml...");
|
||||
|
||||
var args = $"dump xmltree \"{apk}\" AndroidManifest.xml";
|
||||
|
||||
var result = await processRunner.RunAsync(aapt, args, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new AndroidManifest(ParseXmlTree(result.Output));
|
||||
}
|
||||
|
||||
public static XDocument ParseXmlTree(string xmltree)
|
||||
{
|
||||
var lines = xmltree.Split(new[] { "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var xdoc = new XDocument();
|
||||
|
||||
var stack = new Stack<ParsedElement>();
|
||||
stack.Push(new ParsedElement(xdoc, 0));
|
||||
|
||||
var namespaces = new Dictionary<string, XNamespace>();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
ParseXmlTreeLine(line, stack, namespaces);
|
||||
}
|
||||
|
||||
return xdoc;
|
||||
}
|
||||
|
||||
private static void ParseXmlTreeLine(string line, Stack<ParsedElement> stack, Dictionary<string, XNamespace> namespaces)
|
||||
{
|
||||
var trimmedLine = line.TrimStart();
|
||||
var indent = line.Length - trimmedLine.Length;
|
||||
|
||||
if (trimmedLine.StartsWith("N"))
|
||||
{
|
||||
var match = xmltreeNamespaceRegex.Match(trimmedLine);
|
||||
if (!match.Success)
|
||||
throw new Exception($"Invalid namespace: {line}");
|
||||
|
||||
var namespaceName = match.Groups["ns"].Value;
|
||||
if (!namespaces.ContainsKey(namespaceName))
|
||||
namespaces.Add(namespaceName, XNamespace.Get(match.Groups["url"].Value));
|
||||
}
|
||||
else if (trimmedLine.StartsWith("E"))
|
||||
{
|
||||
// pop out if the current line is higher than previous
|
||||
while (stack.Count > 0 && stack.Peek().Indent >= indent)
|
||||
stack.Pop();
|
||||
|
||||
var match = xmltreeElementRegex.Match(trimmedLine);
|
||||
if (!match.Success)
|
||||
throw new Exception($"Invalid element: {line}");
|
||||
|
||||
var element = new XElement(GetXName(match, namespaces, line));
|
||||
|
||||
// this is the first element, so add the namespaces to it
|
||||
if (stack.Count == 1)
|
||||
{
|
||||
foreach (var pair in namespaces)
|
||||
{
|
||||
element.Add(new XAttribute(XNamespace.Xmlns + pair.Key, pair.Value));
|
||||
}
|
||||
}
|
||||
|
||||
stack.Peek().Container.Add(element);
|
||||
stack.Push(new ParsedElement(element, indent));
|
||||
}
|
||||
else if (trimmedLine.StartsWith("A"))
|
||||
{
|
||||
var match = xmltreeAttributeRegex.Match(trimmedLine);
|
||||
if (!match.Success)
|
||||
throw new Exception($"Invalid attribute: {line}");
|
||||
|
||||
// TODO: parse the (type) and use the correct value
|
||||
|
||||
var value = match.Groups["value"].Value;
|
||||
var strMatch = Regex.Match(value, @"\""(?<value>.*)\""\s*\(Raw:.*\)");
|
||||
var xName = GetXName(match, namespaces, line);
|
||||
stack.Peek().Container.Add(strMatch.Success
|
||||
? new XAttribute(xName, strMatch.Groups["value"].Value)
|
||||
: new XAttribute(xName, value));
|
||||
}
|
||||
}
|
||||
|
||||
static XName GetXName(Match match, Dictionary<string, XNamespace> namespaces, string line)
|
||||
{
|
||||
var namespaceName = match.Groups["ns"].Value;
|
||||
if (!string.IsNullOrWhiteSpace(namespaceName) && !namespaces.ContainsKey(namespaceName))
|
||||
throw new Exception($"Unknown xml namespace: {namespaceName}.");
|
||||
|
||||
XName xName;
|
||||
try
|
||||
{
|
||||
xName = string.IsNullOrWhiteSpace(namespaceName)
|
||||
? XName.Get(match.Groups["name"].Value)
|
||||
: XName.Get(match.Groups["name"].Value, namespaces[namespaceName].ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new Exception($"Invalid attribute: {line}");
|
||||
}
|
||||
return xName;
|
||||
}
|
||||
|
||||
class ParsedElement
|
||||
{
|
||||
public ParsedElement(XContainer container, int indent)
|
||||
{
|
||||
Container = container;
|
||||
Indent = indent;
|
||||
}
|
||||
|
||||
public XContainer Container { get; }
|
||||
|
||||
public int Indent { get; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace DotNetDevices.Android
|
||||
{
|
||||
public class AndroidManifest
|
||||
{
|
||||
public AndroidManifest(XDocument xdoc)
|
||||
{
|
||||
Document = xdoc ?? throw new ArgumentNullException(nameof(xdoc));
|
||||
}
|
||||
|
||||
public XDocument Document { get; }
|
||||
|
||||
public string? PackageName => Document.Root?.Attribute("package")?.Value;
|
||||
}
|
||||
}
|
|
@ -63,6 +63,55 @@ namespace DotNetDevices.Android
|
|||
return null;
|
||||
}
|
||||
|
||||
public static string? FindBuildToolPath(string? sdkRoot, string tool, ILogger? logger)
|
||||
{
|
||||
var newSdkRoot = FindPath(sdkRoot, null, logger);
|
||||
if (newSdkRoot == null)
|
||||
{
|
||||
logger?.LogDebug($"Unable to resolve Android SDK root directory from '{sdkRoot}'.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var versions = Directory.GetDirectories(Path.Combine(newSdkRoot, "build-tools"));
|
||||
if (versions.Length == 0)
|
||||
{
|
||||
logger?.LogDebug($"Unable to locate any build tools in '{newSdkRoot}'.");
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger?.LogDebug($"Found {versions.Length} build tools versions in '{newSdkRoot}'.");
|
||||
}
|
||||
|
||||
var path = default(string);
|
||||
var latestSoFar = new Version();
|
||||
|
||||
foreach (var versionDir in versions)
|
||||
{
|
||||
var v = Path.GetFileName(versionDir);
|
||||
if (Version.TryParse(v, out var version) && version > latestSoFar)
|
||||
{
|
||||
var foundPath = FindFuzzyPath(Path.Combine(versionDir, tool));
|
||||
if (foundPath != null)
|
||||
{
|
||||
latestSoFar = version;
|
||||
path = foundPath;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger?.LogDebug($"Found invalid build tool version: '{v}'.");
|
||||
}
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
logger?.LogDebug($"Unable to find any build tools in '{newSdkRoot}'.");
|
||||
else
|
||||
logger?.LogDebug($"Found build tool '{path}'.");
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private static string? FindFuzzyPath(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
namespace DotNetDevices.Android
|
||||
{
|
||||
public class CreateVirtualDeviceOptions
|
||||
{
|
||||
public string? Device { get; set; }
|
||||
|
||||
public bool Overwrite { get; set; }
|
||||
|
||||
public string? Path { get; set; }
|
||||
|
||||
public string? SharedSdCardPath { get; set; }
|
||||
|
||||
public string? NewSdCardSize { get; set; }
|
||||
}
|
||||
}
|
|
@ -101,7 +101,7 @@ namespace DotNetDevices.Android
|
|||
var avd = new List<VirtualDevice>(result.OutputCount);
|
||||
foreach (var output in result.GetOutput())
|
||||
{
|
||||
avd.Add(new VirtualDevice(output));
|
||||
avd.Add(new VirtualDevice(output, output));
|
||||
}
|
||||
return avd;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
using DotNetDevices.Processes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DotNetDevices.Android
|
||||
{
|
||||
public class SDKManager
|
||||
{
|
||||
private readonly ProcessRunner processRunner;
|
||||
private readonly ILogger? logger;
|
||||
private readonly string sdkmanager;
|
||||
|
||||
public SDKManager(string? sdkRoot = null, ILogger? logger = null)
|
||||
{
|
||||
processRunner = new ProcessRunner(logger);
|
||||
this.logger = logger;
|
||||
|
||||
sdkmanager = AndroidSDK.FindPath(sdkRoot, Path.Combine("tools", "bin", "sdkmanager"), logger)
|
||||
?? throw new ArgumentException($"Unable to locate the SDK Manager. Make sure that ANDROID_HOME or ANDROID_SDK_ROOT is set.");
|
||||
}
|
||||
|
||||
public async Task InstallAsync(string package, CancellationToken cancellationToken = default)
|
||||
{
|
||||
logger?.LogInformation("Installing packages...");
|
||||
|
||||
var args = $"--install \"{package}\"";
|
||||
|
||||
await processRunner.RunAsync(sdkmanager, args, null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,13 +4,21 @@ namespace DotNetDevices.Android
|
|||
{
|
||||
public class VirtualDevice
|
||||
{
|
||||
public VirtualDevice(string name)
|
||||
private readonly string? configPath;
|
||||
|
||||
public VirtualDevice(string id, string name, string? configPath = null)
|
||||
{
|
||||
Id = id ?? throw new ArgumentNullException(nameof(id));
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
|
||||
this.configPath = configPath;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public override string ToString() => Name;
|
||||
public override string ToString() =>
|
||||
$"{Name}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DotNetDevices.Android
|
||||
{
|
||||
public class VirtualDeviceConfig
|
||||
{
|
||||
private readonly string configPath;
|
||||
private readonly ILogger? logger;
|
||||
|
||||
public Dictionary<string, string>? properties;
|
||||
|
||||
public VirtualDeviceConfig(string configPath, ILogger? logger = null)
|
||||
{
|
||||
this.configPath = configPath ?? throw new ArgumentNullException(nameof(configPath));
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<string, string>> GetPropertiesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (properties != null)
|
||||
return properties;
|
||||
|
||||
logger?.LogDebug($"Loading config.ini {configPath}...");
|
||||
|
||||
var contents = await File.ReadAllTextAsync(configPath, cancellationToken);
|
||||
properties = ParseConfig(contents);
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
public async Task<string?> GetStringValueAsync(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var props = await GetPropertiesAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
props.TryGetValue(key, out var value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public async Task<VirtualDevice> CreateVirtualDeviceAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var props = await GetPropertiesAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!props.TryGetValue("avdid", out var id))
|
||||
throw new Exception($"Invalid config.ini. Unable to find the virtual device ID.");
|
||||
|
||||
if (!props.TryGetValue("avd.ini.displayname", out var name))
|
||||
name = id;
|
||||
|
||||
return new VirtualDevice(id, name, configPath);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ParseConfig(string contents)
|
||||
{
|
||||
var lines = contents.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
Dictionary<string, string> props = new Dictionary<string, string>();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var pair = line.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (pair.Length == 2)
|
||||
{
|
||||
props[pair[0].ToLowerInvariant()] = pair[1];
|
||||
}
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -54,13 +54,14 @@ namespace DotNetDevices.Apple
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetStringValueAsync(string key, CancellationToken cancellationToken = default)
|
||||
public async Task<string?> GetStringValueAsync(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var xdoc = await GetDocumentAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var keyElements = xdoc.Descendants("key").Where(x => x.Value == key).ToArray();
|
||||
if (keyElements.Length == 0)
|
||||
throw new Exception($"Unable to find key {key}.");
|
||||
return null;
|
||||
|
||||
if (keyElements.Length > 1)
|
||||
throw new Exception($"Found multiple instances of key {key}.");
|
||||
|
||||
|
@ -71,7 +72,7 @@ namespace DotNetDevices.Apple
|
|||
return value.Value;
|
||||
}
|
||||
|
||||
public Task<string> GetBundleIdentifierAsync(CancellationToken cancellationToken = default) =>
|
||||
public Task<string?> GetBundleIdentifierAsync(CancellationToken cancellationToken = default) =>
|
||||
GetStringValueAsync("CFBundleIdentifier", cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
using System;
|
||||
using DotNetDevices.Android;
|
||||
using DotNetDevices.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.CommandLine.Rendering;
|
||||
using System.CommandLine.Rendering.Views;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetDevices.Android;
|
||||
using DotNetDevices.Apple;
|
||||
using DotNetDevices.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DotNetDevices.Commands
|
||||
{
|
||||
|
@ -23,15 +21,8 @@ namespace DotNetDevices.Commands
|
|||
{
|
||||
new Command("list", "List the virtual devices.")
|
||||
{
|
||||
new Option<string?>(new[] { "--sdk" }, "Whether or not to only include the available simulators."),
|
||||
new Option(new[] { "--available" }, "Whether or not to only include the available simulators."),
|
||||
new Option(new[] { "--booted" }, "Whether or not to only include the booted simulators."),
|
||||
new Option<SimulatorRuntime>(new[] { "--runtime" }, "The runtime to use when filtering."),
|
||||
new Option<string?>(new[] { "--version" }, description: "The runtime version to use when filtering. This could be in either <major> or <major>.<minor> version formats.",
|
||||
parseArgument: CommandLine.ParseVersion),
|
||||
new Option<string?>(new[] { "--sdk" }, "The path to the Android SDK directory."),
|
||||
CommandLine.CreateVerbosity(),
|
||||
new Argument<string?>("TERM", "The search term to use when filtering simulators. This could be any number of properties (UDID, runtime, version, availability, or state) as well as part of the simulator name.")
|
||||
{ Arity = ArgumentArity.ZeroOrOne },
|
||||
}.WithHandler(CommandHandler.Create(typeof(AndroidCommand).GetMethod(nameof(HandleListAsync))!)),
|
||||
new Command("create", "Create a new virtual device.")
|
||||
{
|
||||
|
@ -41,22 +32,23 @@ namespace DotNetDevices.Commands
|
|||
new Argument<string?>("NAME", "The name of the new virtual device."),
|
||||
new Argument<string?>("PACKAGE", "The package to use for the new virtual device."),
|
||||
}.WithHandler(CommandHandler.Create(typeof(AndroidCommand).GetMethod(nameof(HandleCreateAsync))!)),
|
||||
new Command("boot", "Boot a particular simulator.")
|
||||
new Command("boot", "Boot a particular virtual device.")
|
||||
{
|
||||
new Option<string?>(new[] { "--sdk" }, "Whether or not to only include the available simulators."),
|
||||
new Option<string?>(new[] { "--sdk" }, "The path to the Android SDK directory."),
|
||||
CommandLine.CreateVerbosity(),
|
||||
new Argument<string?>("NAME", "The UDID of the simulator to boot."),
|
||||
new Argument<string?>("NAME", "The name of the virtual device to boot."),
|
||||
}.WithHandler(CommandHandler.Create(typeof(AndroidCommand).GetMethod(nameof(HandleBootAsync))!)),
|
||||
new Command("install", "Download and install packages using the SDK Manager.")
|
||||
{
|
||||
new Option<string?>(new[] { "--sdk" }, "The path to the Android SDK directory."),
|
||||
CommandLine.CreateVerbosity(),
|
||||
new Argument<string?>("PACKAGE", "The package to install."),
|
||||
}.WithHandler(CommandHandler.Create(typeof(AndroidCommand).GetMethod(nameof(HandleInstallAsync))!)),
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task HandleListAsync(
|
||||
string? term = null,
|
||||
string? sdk = null,
|
||||
bool available = false,
|
||||
bool booted = false,
|
||||
SimulatorRuntime? runtime = null,
|
||||
string? version = null,
|
||||
string? verbosity = null,
|
||||
IConsole console = null!,
|
||||
CancellationToken cancellationToken = default)
|
||||
|
@ -64,49 +56,36 @@ namespace DotNetDevices.Commands
|
|||
var logger = console.CreateLogger(verbosity);
|
||||
var avdmanager = new AVDManager(sdk, logger);
|
||||
|
||||
var devices = await avdmanager.GetDevicesAsync();
|
||||
foreach (var device in devices)
|
||||
{
|
||||
logger?.LogInformation(" - " + device.ToString());
|
||||
}
|
||||
var devices = await avdmanager.GetVirtualDevicesAsync(cancellationToken);
|
||||
|
||||
var targets = await avdmanager.GetTargetsAsync();
|
||||
foreach (var target in targets)
|
||||
{
|
||||
logger?.LogInformation(" - " + target.ToString());
|
||||
}
|
||||
var filtered = (IEnumerable<VirtualDevice>)devices;
|
||||
|
||||
var avds = await avdmanager.GetVirtualDevicesAsync();
|
||||
foreach (var avd in avds)
|
||||
{
|
||||
logger?.LogInformation(" - " + avd.ToString());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await avdmanager.DeleteVirtualDeviceAsync("TESTING");
|
||||
}
|
||||
catch { }
|
||||
//try
|
||||
//{
|
||||
// await avdmanager.DeleteVirtualDeviceAsync("TESTING");
|
||||
//}
|
||||
//catch { }
|
||||
|
||||
try
|
||||
{
|
||||
await avdmanager.DeleteVirtualDeviceAsync("TESTED");
|
||||
}
|
||||
catch { }
|
||||
//try
|
||||
//{
|
||||
// await avdmanager.DeleteVirtualDeviceAsync("TESTED");
|
||||
//}
|
||||
//catch { }
|
||||
|
||||
await avdmanager.CreateVirtualDeviceAsync("TESTING", "system-images;android-28;google_apis_playstore;x86_64");
|
||||
//await avdmanager.CreateVirtualDeviceAsync("TESTING", "system-images;android-28;google_apis_playstore;x86_64");
|
||||
|
||||
await avdmanager.CreateVirtualDeviceAsync("TESTING", "system-images;android-28;google_apis_playstore;x86_64", new VirtualDeviceCreateOptions { Overwrite = true });
|
||||
//await avdmanager.CreateVirtualDeviceAsync("TESTING", "system-images;android-28;google_apis_playstore;x86_64", new VirtualDeviceCreateOptions { Overwrite = true });
|
||||
|
||||
await avdmanager.RenameVirtualDeviceAsync("TESTING", "TESTED");
|
||||
await avdmanager.MoveVirtualDeviceAsync("TESTED", "/Users/matthew/.android/avd/tested.avd");
|
||||
//await avdmanager.RenameVirtualDeviceAsync("TESTING", "TESTED");
|
||||
//await avdmanager.MoveVirtualDeviceAsync("TESTED", "/Users/matthew/.android/avd/tested.avd");
|
||||
|
||||
//await avdmanager.DeleteVirtualDeviceAsync("TESTING");
|
||||
|
||||
//term = term?.ToLowerInvariant()?.Trim();
|
||||
|
||||
//var simctl = new SimulatorControl(logger);
|
||||
//var simulators = await simctl.GetSimulatorsAsync(cancellationToken);
|
||||
|
||||
|
||||
//term = term?.ToLowerInvariant()?.Trim();
|
||||
|
||||
//var filtered = (IEnumerable<Simulator>)simulators;
|
||||
//if (!string.IsNullOrWhiteSpace(term))
|
||||
|
@ -142,20 +121,16 @@ namespace DotNetDevices.Commands
|
|||
// filtered = filtered.Where(s => s.Version.Major == versionMjor);
|
||||
//}
|
||||
|
||||
//var all = filtered.ToList();
|
||||
var all = filtered.ToList();
|
||||
|
||||
//logger.LogInformation($"Found {all.Count} simulator[s].");
|
||||
logger.LogInformation($"Found {all.Count} virtual device[s].");
|
||||
|
||||
//var table = new TableView<Simulator>();
|
||||
//table.AddColumn(s => s.Udid, "UDID");
|
||||
//table.AddColumn(s => s.Name, "Name");
|
||||
//table.AddColumn(s => s.Runtime, "Runtime");
|
||||
//table.AddColumn(s => s.Version, "Version");
|
||||
//table.AddColumn(s => s.Availability, "Availability");
|
||||
//table.AddColumn(s => s.State, "State");
|
||||
//table.Items = all;
|
||||
var table = new TableView<VirtualDevice>();
|
||||
table.AddColumn(s => s.Id, "Id");
|
||||
table.AddColumn(s => s.Name, "Name");
|
||||
table.Items = all;
|
||||
|
||||
//console.Append(new StackLayoutView { table });
|
||||
console.Append(new StackLayoutView { table });
|
||||
}
|
||||
|
||||
public static async Task HandleCreateAsync(
|
||||
|
@ -171,7 +146,7 @@ namespace DotNetDevices.Commands
|
|||
|
||||
var avdmanager = new AVDManager(sdk, logger);
|
||||
|
||||
var options = new VirtualDeviceCreateOptions
|
||||
var options = new CreateVirtualDeviceOptions
|
||||
{
|
||||
Overwrite = replace
|
||||
};
|
||||
|
@ -191,7 +166,7 @@ namespace DotNetDevices.Commands
|
|||
var emulator = new EmulatorManager(sdk, logger);
|
||||
|
||||
var avds = await emulator.GetVirtualDevicesAsync(cancellationToken);
|
||||
if (avds.All(a => !a.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
if (avds.All(a => !a.Id.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
logger.LogError($"No virtual device with name {name} was found.");
|
||||
return 1;
|
||||
|
@ -210,5 +185,21 @@ namespace DotNetDevices.Commands
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static async Task<int> HandleInstallAsync(
|
||||
string package,
|
||||
string? sdk = null,
|
||||
string? verbosity = null,
|
||||
IConsole console = null!,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var logger = console.CreateLogger(verbosity);
|
||||
|
||||
var sdkmanager = new SDKManager(sdk, logger);
|
||||
|
||||
await sdkmanager.InstallAsync(package, cancellationToken);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,237 @@
|
|||
using DotNetDevices.Android;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DotNetDevices.Commands
|
||||
{
|
||||
internal class AndroidTestCommand
|
||||
{
|
||||
private readonly string? sdkRoot;
|
||||
private readonly ILogger logger;
|
||||
private readonly AVDManager avdmanager;
|
||||
private readonly EmulatorManager emulator;
|
||||
private readonly AaptTool aapt;
|
||||
|
||||
public AndroidTestCommand(string? sdkRoot, ILogger logger)
|
||||
{
|
||||
this.sdkRoot = sdkRoot;
|
||||
this.logger = logger;
|
||||
|
||||
avdmanager = new AVDManager(sdkRoot, logger);
|
||||
emulator = new EmulatorManager(sdkRoot, logger);
|
||||
aapt = new AaptTool(sdkRoot, logger);
|
||||
}
|
||||
|
||||
public async Task RunTestsAsync(
|
||||
string app,
|
||||
string? deviceResults = null, // "TestResults.trx"
|
||||
string? outputResults = null, // "TestResults.trx"
|
||||
string? runtimeString = null,
|
||||
string? versionString = null,
|
||||
bool latest = false,
|
||||
string? deviceType = null,
|
||||
string? deviceName = null,
|
||||
bool reset = false,
|
||||
bool shutdown = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// validate app / bundle
|
||||
var androidManifest = await aapt.GetAndroidManifestAsync(app, cancellationToken);
|
||||
var packageName = androidManifest.PackageName;
|
||||
if (string.IsNullOrEmpty(packageName))
|
||||
throw new Exception("Unable to determine the package name for the app.");
|
||||
|
||||
logger.LogInformation($"Running tests on '{packageName}'...");
|
||||
|
||||
//// validate requested OS
|
||||
//var simulatorType = ParseSimulatorType(deviceType);
|
||||
//var runtime = ParseSimulatorRuntime(runtimeString);
|
||||
//var runtimeVersion = await ParseVersionAsync(versionString, runtime, cancellationToken);
|
||||
|
||||
//logger.LogInformation($"Looking for an available {simulatorType} ({runtimeVersion}) simulator...");
|
||||
//var available = await GetAvailableSimulatorsAsync(simulatorType, runtime, runtimeVersion, latest, cancellationToken);
|
||||
|
||||
//// first look for a booted device
|
||||
//var simulator = available.FirstOrDefault(s => s.State == SimulatorState.Booted) ?? available.FirstOrDefault();
|
||||
//logger.LogInformation($"Using simulator {simulator.Name} ({simulator.Runtime} {simulator.Version}): {simulator.Udid}");
|
||||
|
||||
//try
|
||||
//{
|
||||
// if (reset)
|
||||
// await simctl.EraseSimulatorAsync(simulator.Udid, true, cancellationToken);
|
||||
|
||||
// await simctl.InstallAppAsync(simulator.Udid, app, true, cancellationToken);
|
||||
|
||||
// try
|
||||
// {
|
||||
// var parser = new TestResultsParser();
|
||||
|
||||
// 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.");
|
||||
// }
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// await simctl.UninstallAppAsync(simulator.Udid, bundleId, false, cancellationToken);
|
||||
// }
|
||||
//}
|
||||
//finally
|
||||
//{
|
||||
// if (shutdown)
|
||||
// await simctl.ShutdownSimulatorAsync(simulator.Udid, cancellationToken);
|
||||
//}
|
||||
}
|
||||
|
||||
//private async Task<List<Simulator>> GetAvailableSimulatorsAsync(SimulatorType type, SimulatorRuntime runtime, Version version, bool useLatest = true, CancellationToken cancellationToken = default)
|
||||
//{
|
||||
// // load all simulators
|
||||
// var simulators = await simctl.GetSimulatorsAsync(cancellationToken);
|
||||
|
||||
// // find ones that can be used
|
||||
// var available = simulators
|
||||
// .Where(s => s.Availability == SimulatorAvailability.Available)
|
||||
// .Where(s => s.Runtime == runtime)
|
||||
// .Where(s => s.Type == type);
|
||||
// logger.LogDebug($"Found some available simulators:");
|
||||
// foreach (var sim in available)
|
||||
// {
|
||||
// logger.LogDebug($" {sim.Name} ({sim.Runtime} {sim.Version}): {sim.Udid}");
|
||||
// }
|
||||
|
||||
// // filter by version info
|
||||
// string matchingPattern;
|
||||
// if (useLatest)
|
||||
// {
|
||||
// var min = version;
|
||||
// var max = new Version(min.Major + 1, 0);
|
||||
// available = available.Where(s => s.Version >= min && s.Version < max);
|
||||
// matchingPattern = $"[{min}, {max})";
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// available = available.Where(s => s.Version == version);
|
||||
// matchingPattern = $"[{version}]";
|
||||
// }
|
||||
|
||||
// var matching = available.ToList();
|
||||
// if (matching.Count > 0)
|
||||
// {
|
||||
// logger.LogDebug($"Found matching simulators {matchingPattern}:");
|
||||
// foreach (var sim in matching)
|
||||
// {
|
||||
// logger.LogDebug($" {sim.Name} ({sim.Runtime} {sim.Version}): {sim.Udid}");
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// throw new Exception($"Unable to find any simulators that match version {matchingPattern}.");
|
||||
// }
|
||||
|
||||
// return matching;
|
||||
//}
|
||||
|
||||
//private async Task<Version> ParseVersionAsync(string? version, SimulatorRuntime os, CancellationToken cancellationToken = default)
|
||||
//{
|
||||
// var osVersion = version?.ToLowerInvariant().Trim();
|
||||
// if (!Version.TryParse(osVersion, out var numberVersion))
|
||||
// {
|
||||
// if (int.TryParse(osVersion, out var v))
|
||||
// numberVersion = new Version(v, 0);
|
||||
// else if (string.IsNullOrEmpty(osVersion) || osVersion == "default")
|
||||
// numberVersion = await simctl.GetDefaultVersionAsync(os, cancellationToken);
|
||||
// else
|
||||
// throw new Exception($"Unable to determine the version for {osVersion}.");
|
||||
// }
|
||||
|
||||
// return numberVersion;
|
||||
//}
|
||||
|
||||
//private static SimulatorRuntime ParseSimulatorRuntime(string? runtime)
|
||||
//{
|
||||
// var osName = runtime?.ToLowerInvariant()?.Trim();
|
||||
// var os = osName switch
|
||||
// {
|
||||
// null => SimulatorRuntime.iOS,
|
||||
// "" => SimulatorRuntime.iOS,
|
||||
// "ios" => SimulatorRuntime.iOS,
|
||||
// "watchos" => SimulatorRuntime.watchOS,
|
||||
// "tvos" => SimulatorRuntime.tvOS,
|
||||
// _ => throw new Exception($"Unable to determine the OS for {runtime}.")
|
||||
// };
|
||||
// return os;
|
||||
//}
|
||||
|
||||
//private static SimulatorType ParseSimulatorType(string? deviceType)
|
||||
//{
|
||||
// var deviceTypeName = deviceType?.ToLowerInvariant()?.Trim();
|
||||
// var device = deviceTypeName switch
|
||||
// {
|
||||
// // iPhone
|
||||
// null => SimulatorType.iPhone,
|
||||
// "" => SimulatorType.iPhone,
|
||||
// "iphone" => SimulatorType.iPhone,
|
||||
// "phone" => SimulatorType.iPhone,
|
||||
// // iPad
|
||||
// "ipad" => SimulatorType.iPad,
|
||||
// "tablet" => SimulatorType.iPad,
|
||||
// // iPod
|
||||
// "ipod" => SimulatorType.iPod,
|
||||
// // Apple TV
|
||||
// "tv" => SimulatorType.AppleTV,
|
||||
// "appletv" => SimulatorType.AppleTV,
|
||||
// // Apple Watch
|
||||
// "watch" => SimulatorType.AppleWatch,
|
||||
// "applewatch" => SimulatorType.AppleWatch,
|
||||
// //
|
||||
// _ => throw new Exception($"Unable to determine the simulator type for {deviceType}.")
|
||||
// };
|
||||
// return device;
|
||||
//}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
using System;
|
||||
using DotNetDevices.Apple;
|
||||
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;
|
||||
using DotNetDevices.Apple;
|
||||
using DotNetDevices.Testing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DotNetDevices.Commands
|
||||
{
|
||||
|
@ -38,6 +38,10 @@ namespace DotNetDevices.Commands
|
|||
// validate app / bundle
|
||||
var plist = new PList(Path.Combine(app, "Info.plist"), logger);
|
||||
var bundleId = await plist.GetBundleIdentifierAsync(cancellationToken);
|
||||
if (string.IsNullOrEmpty(bundleId))
|
||||
throw new Exception("Unable to determine the bundle identifer for the app.");
|
||||
|
||||
logger.LogInformation($"Running tests on '{bundleId}'...");
|
||||
|
||||
// validate requested OS
|
||||
var simulatorType = ParseSimulatorType(deviceType);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
using System;
|
||||
using DotNetDevices.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DotNetDevices.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DotNetDevices.Commands
|
||||
{
|
||||
|
@ -60,14 +60,27 @@ namespace DotNetDevices.Commands
|
|||
|
||||
try
|
||||
{
|
||||
// detect iOS .app files (directories)
|
||||
if (Path.GetExtension(app).Equals(".app", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// detect iOS .app files (directories)
|
||||
var cmd = new AppleTestCommand(logger);
|
||||
await cmd.RunTestsAsync(app, deviceResults, outputResults, runtime, version, latest, deviceType, deviceName, reset, shutdown, cancellationToken);
|
||||
}
|
||||
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
else if (Path.GetExtension(app).Equals(".apk", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// detect Android .apk files
|
||||
var cmd = new AndroidTestCommand(null, logger);
|
||||
await cmd.RunTestsAsync(app, deviceResults, outputResults, runtime, version, latest, deviceType, deviceName, reset, shutdown, cancellationToken);
|
||||
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError($"Unknown app package type: {app}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"profiles": {
|
||||
"dotnet-devices": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "test \"C:\\Projects\\dotnet-devices\\DeviceTests\\DeviceTests.Android\\bin\\Debug\\net.dot.devicetests-Signed.apk\""
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче