Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using NUnit.Framework;
using Xamarin.ProjectTools;

namespace Xamarin.Android.Build.Tests;

[TestFixture]
public class DexUtilsTests
{
[Test]
public void ContainsClassWithMethodScopesSignatureToMethod ()
{
var dexDump = new [] {
" Class descriptor : 'Lexample/Peer;'",
" name : 'target'",
" type : '(I)V'",
" name : 'other'",
" type : '()V'",
};

Assert.IsTrue (DexUtils.ContainsClassWithMethod ("Lexample/Peer;", "target", "(I)V", dexDump));
Assert.IsFalse (DexUtils.ContainsClassWithMethod ("Lexample/Peer;", "target", "()V", dexDump));
}

[Test]
public void ContainsClassWithMethodMatchesExactNames ()
{
var dexDump = new [] {
" Class descriptor : 'Lexample/PeerExtension;'",
" name : 'getValueExtension'",
" type : '()I'",
};

Assert.IsFalse (DexUtils.ContainsClassWithMethod ("Lexample/Peer;", "getValue", "()I", dexDump));
}

[TestCase ("()Ljava/lang/Object;")]
[TestCase ("()Ljava/lang/String;")]
public void ContainsClassWithMethodMatchesOverloads (string signature)
{
var dexDump = new [] {
" Class descriptor : 'Lexample/Derived;'",
" name : 'getValue'",
" type : '()Ljava/lang/Object;'",
" name : 'getValue'",
" type : '()Ljava/lang/String;'",
};

Assert.IsTrue (DexUtils.ContainsClassWithMethod ("Lexample/Derived;", "getValue", signature, dexDump));
}

[Test]
public void ContainsClassWithMethodRejectsMissingSignature ()
{
var dexDump = new [] {
" Class descriptor : 'Lexample/Peer;'\r",
" name : '$default$getValue'\r",
" access : 0x0009 (PUBLIC STATIC)\r",
" type : '()I'\r",
};

Assert.IsFalse (DexUtils.ContainsClassWithMethod ("Lexample/Peer;", "$default$getValue", "()I", dexDump));
}

[Test]
public void ContainsClassWithMethodHandlesWhitespaceAndCrLf ()
{
var dexDump = new [] {
"\tClass descriptor : 'Lexample/Peer$-CC;' \r",
" name : '$default$getValue'\r",
" type : '(Lexample/Peer;)I'\r",
};

Assert.IsTrue (DexUtils.ContainsClassWithMethod (
"Lexample/Peer$-CC;",
"$default$getValue",
"(Lexample/Peer;)I",
dexDump));
}

[Test]
public void ContainsClassWithMethodResetsAtRepeatedClassDescriptor ()
{
var dexDump = new [] {
" Class descriptor : 'Lexample/Peer;'",
" name : 'getValue'",
" Class descriptor : 'Lexample/Other;'",
" name : 'getValue'",
" type : '()I'",
};

Assert.IsFalse (DexUtils.ContainsClassWithMethod ("Lexample/Peer;", "getValue", "()I", dexDump));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,48 @@ protected static string RunAdbCommand (string command, bool ignoreErrors = true,
return RunProcess (adb, $"{adbTarget} {command}", timeout);
}

protected static (int code, string stdOutput, string stdError) RunAdbCommandWithExitCode (params string [] command)
{
string ext = Environment.OSVersion.Platform != PlatformID.Unix ? ".exe" : "";
string adb = Path.Combine (AndroidSdkPath, "platform-tools", "adb" + ext);
var arguments = new List<string> ();
string adbTarget = Environment.GetEnvironmentVariable ("ADB_TARGET");
if (!string.IsNullOrWhiteSpace (adbTarget)) {
if (adbTarget == "-d" || adbTarget == "-e") {
arguments.Add (adbTarget);
} else if (adbTarget.StartsWith ("-s", StringComparison.Ordinal)) {
string serial = adbTarget.Substring (2).Trim ();
if (serial.Length == 0) {
throw new InvalidOperationException ("ADB_TARGET must include a device serial after '-s'.");
}
arguments.Add ("-s");
arguments.Add (serial);
} else {
throw new InvalidOperationException ($"Unsupported ADB_TARGET value '{adbTarget}'.");
}
}
arguments.AddRange (command);
TestContext.Out.WriteLine ($"{nameof (RunAdbCommandWithExitCode)}: {adb} {string.Join (" ", arguments.Select (a => $"[{a}]"))}");

var info = Xamarin.Android.Tools.ProcessUtils.CreateProcessStartInfo (adb, arguments.ToArray ());
using var standardOutput = new StringWriter ();
using var standardError = new StringWriter ();
using var cancellationTokenSource = new CancellationTokenSource (TimeSpan.FromSeconds (30));
int exitCode;
try {
exitCode = Xamarin.Android.Tools.ProcessUtils.StartProcess (
info,
standardOutput,
standardError,
cancellationTokenSource.Token
).GetAwaiter ().GetResult ();
} catch (OperationCanceledException) {
exitCode = -1;
standardError.WriteLine ("adb timed out after 30 seconds.");
}
return (exitCode, standardOutput.ToString ().Trim (), standardError.ToString ().Trim ());
}

protected static (int code, string stdOutput, string stdError) RunApkDiffCommand (string args, string logFilePath)
{
var executableName = OperatingSystem.IsWindows () ? "apkdiff.exe" : "apkdiff";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ static class ResourceData
static Lazy<byte []> javaSourceTestInterface = new Lazy<byte []> (() => GetResourceData ("JavaSourceTestInterface.java"));
static Lazy<byte []> remapActivityJava = new Lazy<byte []> (() => GetResourceData ("RemapActivity.java"));
static Lazy<byte []> remapActivityXml = new Lazy<byte []> (() => GetResourceData ("RemapActivity.xml"));
static Lazy<byte []> idmStaticMethodsInterface = new Lazy<byte []> (() => GetResourceData ("StaticMethodsInterface.java"));
static Lazy<byte []> interfaceMethods = new Lazy<byte []> (() => GetResourceData ("InterfaceMethods.java"));
static Lazy<byte []> interfaceMethodPeer = new Lazy<byte []> (() => GetResourceData ("InterfaceMethodPeer.java"));
static Lazy<byte []> concreteInterfaceMethodPeer = new Lazy<byte []> (() => GetResourceData ("ConcreteInterfaceMethodPeer.java"));
static Lazy<byte []> interfaceMethodBridgeInvoker = new Lazy<byte []> (() => GetResourceData ("InterfaceMethodBridgeInvoker.java"));
static Lazy<byte []> covariantInterfaceMethods = new Lazy<byte []> (() => GetResourceData ("CovariantInterfaceMethods.java"));

static Lazy<byte []> rtxt = new Lazy<byte []> (() => GetResourceData ("R.txt"));

Expand All @@ -40,7 +44,11 @@ static class ResourceData
public static string RemapActivityXml => Encoding.UTF8.GetString (remapActivityXml.Value);
public static string RemapMamJson => Encoding.UTF8.GetString (GetResourceData ("mam.json"));
public static string RemapMamXml => Encoding.UTF8.GetString (GetResourceData ("mam.xml"));
public static string IdmStaticMethodsInterface => Encoding.UTF8.GetString (idmStaticMethodsInterface.Value);
public static string InterfaceMethods => Encoding.UTF8.GetString (interfaceMethods.Value);
public static string InterfaceMethodPeer => Encoding.UTF8.GetString (interfaceMethodPeer.Value);
public static string ConcreteInterfaceMethodPeer => Encoding.UTF8.GetString (concreteInterfaceMethodPeer.Value);
public static string InterfaceMethodBridgeInvoker => Encoding.UTF8.GetString (interfaceMethodBridgeInvoker.Value);
public static string CovariantInterfaceMethods => Encoding.UTF8.GetString (covariantInterfaceMethods.Value);

public static string RTxt => Encoding.UTF8.GetString (rtxt.Value);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -67,13 +68,12 @@ Direct methods -
/// <param name="className">A Java class name of the form 'Landroid/app/ActivityTracker;'</param>
public static bool ContainsClass (string className, string dexFile, string androidSdkDirectory)
{
bool containsClass = false;
DataReceivedEventHandler handler = (s, e) => {
if (e.Data != null && e.Data.Contains ("Class descriptor") && e.Data.Contains (className))
containsClass = true;
};
DexDump (handler, dexFile, androidSdkDirectory);
return containsClass;
return ContainsClass (className, GetDexDump (dexFile, androidSdkDirectory));
}

public static bool ContainsClass (string className, IEnumerable<string> dexDump)
{
return dexDump.Any (line => line.Contains ("Class descriptor") && line.Contains (className));
}

/// <summary>
Expand All @@ -83,28 +83,56 @@ public static bool ContainsClass (string className, string dexFile, string andro
/// <param name="method">A Java method name of the form 'foo'</param>
/// <param name="type">A Java method signature of the form '()V'</param>
public static bool ContainsClassWithMethod (string className, string method, string type, string dexFile, string androidSdkDirectory)
{
return ContainsClassWithMethod (className, method, type, GetDexDump (dexFile, androidSdkDirectory));
}

public static bool ContainsClassWithMethod (string className, string method, string type, IEnumerable<string> dexDump)
{
bool inClass = false;
bool hasName = false;
bool hasType = false;
DataReceivedEventHandler handler = (s, e) => {
if (e.Data != null) {
if (e.Data.Contains ("Class descriptor")) {
inClass = e.Data.Contains (className);
hasName = false;
} else if (inClass && e.Data.Contains ("name") && e.Data.Contains (method)) {
hasName = true;
} else if (hasName && e.Data.Contains ("type") && e.Data.Contains (type)) {
hasType = true;
foreach (var line in dexDump) {
if (HasDexDumpName (line, "Class descriptor")) {
inClass = ContainsDexDumpValue (line, "Class descriptor", className);
hasName = false;
} else if (inClass && HasDexDumpName (line, "name")) {
hasName = ContainsDexDumpValue (line, "name", method);
} else if (hasName) {
if (ContainsDexDumpValue (line, "type", type)) {
return true;
}
hasName = false;
}
};
DexDump (handler, dexFile, androidSdkDirectory);
return hasType;
}
return false;
}

static bool ContainsDexDumpValue (string line, string name, string value)
{
var separator = line.IndexOf (':');
return separator >= 0 && HasDexDumpName (line, name, separator) &&
line.Substring (separator + 1).Trim () == $"'{value}'";
}

static void DexDump (DataReceivedEventHandler handler, string dexFile, string androidSdkDirectory)
static bool HasDexDumpName (string line, string name)
{
return HasDexDumpName (line, name, line.IndexOf (':'));
}

static bool HasDexDumpName (string line, string name, int separator)
{
return separator >= 0 && line.Substring (0, separator).Trim () == name;
}

public static IReadOnlyList<string> GetDexDump (string dexFile, string androidSdkDirectory)
{
return DexDump (dexFile, androidSdkDirectory);
}

static IReadOnlyList<string> DexDump (string dexFile, string androidSdkDirectory)
{
var lines = new List<string> ();
var linesLock = new object ();
var androidSdk = new AndroidSdkInfo ((l, m) => {
Console.WriteLine ($"{l}: {m}");
if (l == TraceLevel.Error) {
Expand All @@ -116,28 +144,44 @@ static void DexDump (DataReceivedEventHandler handler, string dexFile, string an
throw new Exception ($"Unable to find build-tools in `{androidSdkDirectory}`!");
}

var dexFileName = Path.GetFileName (dexFile);
var psi = new ProcessStartInfo {
FileName = Path.Combine (buildToolsPath, "dexdump"),
Arguments = Path.GetFileName (dexFile),
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
WorkingDirectory = Path.GetDirectoryName (dexFile),
};
psi.ArgumentList.Add (dexFileName);
using (var p = new Process { StartInfo = psi }) {
p.ErrorDataReceived += handler;
p.OutputDataReceived += handler;
var errors = new List<string> ();
p.ErrorDataReceived += (s, e) => {
if (e.Data != null) {
errors.Add (e.Data);
}
};
p.OutputDataReceived += (s, e) => {
if (e.Data != null) {
lock (linesLock) {
lines.Add (e.Data);
}
}
};

p.Start ();
p.BeginErrorReadLine ();
p.BeginOutputReadLine ();
p.WaitForExit ();

if (p.ExitCode != 0)
throw new Exception ($"'{psi.FileName} {psi.Arguments}' exited with code: {p.ExitCode}");
throw new Exception (
$"'{psi.FileName} {dexFileName}' exited with code: {p.ExitCode}" +
$"{Environment.NewLine}stdout:{Environment.NewLine}{string.Join (Environment.NewLine, lines)}" +
$"{Environment.NewLine}stderr:{Environment.NewLine}{string.Join (Environment.NewLine, errors)}");
}
return lines;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<EmbeddedResource Include="Resources\RemapActivity*">
<LogicalName>%(FileName)%(Extension)</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Resources\StaticMethodsInterface*">
<EmbeddedResource Include="Resources\*InterfaceMethod*.java">
<LogicalName>%(FileName)%(Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package example;

public final class ConcreteInterfaceMethodPeer implements InterfaceMethods {
public ConcreteInterfaceMethodPeer() {
}

@Override
public int getDefaultValue() {
return InterfaceMethods.super.getDefaultValue() + 1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package example;

public interface CovariantInterfaceMethods {
interface Base {
Object getCovariantValue();
}

interface Derived extends Base {
@Override
default String getCovariantValue() {
return "bridge";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package example;

public final class InterfaceMethodBridgeInvoker {
private InterfaceMethodBridgeInvoker() {
}

private static final class CovariantPeer implements CovariantInterfaceMethods.Derived {
}

public static String invokeCovariantBridge() {
CovariantInterfaceMethods.Base peer = new CovariantPeer();
return (String) peer.getCovariantValue() + ":" + invokeStaticMethods();
}
Comment thread
simonrozsival marked this conversation as resolved.

// Keep Java call sites so the R8 retention gap tracked by dotnet/android#11774
// does not mask the JNI runtime behavior covered here.
public static int invokeStaticMethods() {
return InterfaceMethods.getStaticValue()
+ InterfaceMethods.Nested.getNestedStaticValue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package example;

public final class InterfaceMethodPeer
implements InterfaceMethods, InterfaceMethods.Nested {
public InterfaceMethodPeer() {
}
}
Loading
Loading