diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DexUtilsTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DexUtilsTests.cs new file mode 100644 index 00000000000..032a9d880ea --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DexUtilsTests.cs @@ -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)); + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs index 5c0fd14fc3c..83a0cbdefc9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs @@ -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 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"; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/ResourceData.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/ResourceData.cs index c016bca7564..54fda810d46 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/ResourceData.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/ResourceData.cs @@ -22,7 +22,11 @@ static class ResourceData static Lazy javaSourceTestInterface = new Lazy (() => GetResourceData ("JavaSourceTestInterface.java")); static Lazy remapActivityJava = new Lazy (() => GetResourceData ("RemapActivity.java")); static Lazy remapActivityXml = new Lazy (() => GetResourceData ("RemapActivity.xml")); - static Lazy idmStaticMethodsInterface = new Lazy (() => GetResourceData ("StaticMethodsInterface.java")); + static Lazy interfaceMethods = new Lazy (() => GetResourceData ("InterfaceMethods.java")); + static Lazy interfaceMethodPeer = new Lazy (() => GetResourceData ("InterfaceMethodPeer.java")); + static Lazy concreteInterfaceMethodPeer = new Lazy (() => GetResourceData ("ConcreteInterfaceMethodPeer.java")); + static Lazy interfaceMethodBridgeInvoker = new Lazy (() => GetResourceData ("InterfaceMethodBridgeInvoker.java")); + static Lazy covariantInterfaceMethods = new Lazy (() => GetResourceData ("CovariantInterfaceMethods.java")); static Lazy rtxt = new Lazy (() => GetResourceData ("R.txt")); @@ -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); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/DexUtils.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/DexUtils.cs index 0caea353a90..6b0cb93a4c6 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/DexUtils.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/DexUtils.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -67,13 +68,12 @@ Direct methods - /// A Java class name of the form 'Landroid/app/ActivityTracker;' 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 dexDump) + { + return dexDump.Any (line => line.Contains ("Class descriptor") && line.Contains (className)); } /// @@ -83,28 +83,56 @@ public static bool ContainsClass (string className, string dexFile, string andro /// A Java method name of the form 'foo' /// A Java method signature of the form '()V' 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 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 GetDexDump (string dexFile, string androidSdkDirectory) + { + return DexDump (dexFile, androidSdkDirectory); + } + + static IReadOnlyList DexDump (string dexFile, string androidSdkDirectory) + { + var lines = new List (); + var linesLock = new object (); var androidSdk = new AndroidSdkInfo ((l, m) => { Console.WriteLine ($"{l}: {m}"); if (l == TraceLevel.Error) { @@ -116,9 +144,9 @@ 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, @@ -126,9 +154,21 @@ static void DexDump (DataReceivedEventHandler handler, string dexFile, string an 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 (); + 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 (); @@ -136,8 +176,12 @@ static void DexDump (DataReceivedEventHandler handler, string dexFile, string an 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; } } } diff --git a/tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj b/tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj index 9c773480c70..3af1afb0317 100644 --- a/tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj +++ b/tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj @@ -33,7 +33,7 @@ %(FileName)%(Extension) - + %(FileName)%(Extension) diff --git a/tests/MSBuildDeviceIntegration/Resources/ConcreteInterfaceMethodPeer.java b/tests/MSBuildDeviceIntegration/Resources/ConcreteInterfaceMethodPeer.java new file mode 100644 index 00000000000..533d8e0eb30 --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Resources/ConcreteInterfaceMethodPeer.java @@ -0,0 +1,11 @@ +package example; + +public final class ConcreteInterfaceMethodPeer implements InterfaceMethods { + public ConcreteInterfaceMethodPeer() { + } + + @Override + public int getDefaultValue() { + return InterfaceMethods.super.getDefaultValue() + 1; + } +} diff --git a/tests/MSBuildDeviceIntegration/Resources/CovariantInterfaceMethods.java b/tests/MSBuildDeviceIntegration/Resources/CovariantInterfaceMethods.java new file mode 100644 index 00000000000..ed44fe08799 --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Resources/CovariantInterfaceMethods.java @@ -0,0 +1,14 @@ +package example; + +public interface CovariantInterfaceMethods { + interface Base { + Object getCovariantValue(); + } + + interface Derived extends Base { + @Override + default String getCovariantValue() { + return "bridge"; + } + } +} diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceMethodBridgeInvoker.java b/tests/MSBuildDeviceIntegration/Resources/InterfaceMethodBridgeInvoker.java new file mode 100644 index 00000000000..3efaf3cf4fa --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceMethodBridgeInvoker.java @@ -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(); + } + + // 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(); + } +} diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceMethodPeer.java b/tests/MSBuildDeviceIntegration/Resources/InterfaceMethodPeer.java new file mode 100644 index 00000000000..0601fcdfb61 --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceMethodPeer.java @@ -0,0 +1,7 @@ +package example; + +public final class InterfaceMethodPeer + implements InterfaceMethods, InterfaceMethods.Nested { + public InterfaceMethodPeer() { + } +} diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceMethods.java b/tests/MSBuildDeviceIntegration/Resources/InterfaceMethods.java new file mode 100644 index 00000000000..55952308dbd --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceMethods.java @@ -0,0 +1,21 @@ +package example; + +public interface InterfaceMethods { + static int getStaticValue() { + return 11; + } + + default int getDefaultValue() { + return 22; + } + + interface Nested { + static int getNestedStaticValue() { + return 33; + } + + default int getNestedDefaultValue() { + return 44; + } + } +} diff --git a/tests/MSBuildDeviceIntegration/Resources/StaticMethodsInterface.java b/tests/MSBuildDeviceIntegration/Resources/StaticMethodsInterface.java deleted file mode 100644 index 1b8e157b758..00000000000 --- a/tests/MSBuildDeviceIntegration/Resources/StaticMethodsInterface.java +++ /dev/null @@ -1,7 +0,0 @@ -package example; - -public interface StaticMethodsInterface { - static int getValue() { - return 3; - } -} diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 6e46aacf34a..1ee3300d3ae 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -2249,57 +2249,233 @@ public void TypeAndMemberRemapping ([Values] bool isRelease, [Values (AndroidRun ); } + static IEnumerable GetInterfaceMethodDesugaringData () + { + foreach (var typemapImplementation in new [] { "llvm-ir", "trimmable" }) { + foreach (var useR8 in new [] { false, true }) { + foreach (var apiNative in new [] { true, false }) { + yield return CreateTestCase ( + typemapImplementation, + AndroidRuntime.CoreCLR, + apiNative, + useR8); + } + } + } + + foreach (var apiNative in new [] { true, false }) { + yield return CreateTestCase ( + "trimmable", + AndroidRuntime.NativeAOT, + apiNative, + true); + } + + static TestCaseData CreateTestCase ( + string typemapImplementation, + AndroidRuntime runtime, + bool apiNative, + bool useR8) + { + var typemapName = typemapImplementation.Replace ("-", "_"); + var apiName = apiNative ? "Native" : "Desugared"; + var dexToolName = useR8 ? "R8" : "D8"; + return new TestCaseData (typemapImplementation, runtime, apiNative, useR8) + .SetName ($"InterfaceMethods_{typemapName}_{runtime}_{apiName}_{dexToolName}"); + } + } + [Test] - public void SupportDesugaringStaticInterfaceMethods ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) + [TestCaseSource (nameof (GetInterfaceMethodDesugaringData))] + public void InterfaceMethodsMatchDesugaring ( + string typemapImplementation, + AndroidRuntime runtime, + bool apiNative, + bool useR8) { const bool isRelease = true; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } - // TODO: fix for NativeAOT, if possible. Currently fails with: - // - // Process: com.xamarin.supportdesugaringstaticinterfacemethods_nativeaot, PID: 13888 - // java.lang.NoSuchMethodError: no static method "Lexample/StaticMethodsInterface;.getValue()I" - if (runtime == AndroidRuntime.NativeAOT) { - Assert.Ignore ("Currently broken on NativeAOT"); - } - - var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime)) { + var packageSuffix = $"interfacemethods_{typemapImplementation.Replace ("-", "")}_{apiNative}_{useR8}"; + var packageName = PackageUtils.MakePackageName (runtime, packageSuffix).ToLowerInvariant (); + var proj = new XamarinAndroidApplicationProject (packageName: packageName) { IsRelease = true, - EnableDefaultItems = true, OtherBuildItems = { - new AndroidItem.AndroidJavaSource ("StaticMethodsInterface.java") { + new AndroidItem.AndroidJavaSource ("InterfaceMethods.java") { Encoding = new UTF8Encoding (encoderShouldEmitUTF8Identifier: false), - TextContent = () => ResourceData.IdmStaticMethodsInterface, + TextContent = () => ResourceData.InterfaceMethods, Metadata = { { "Bind", "True" }, }, }, + new AndroidItem.AndroidJavaSource ("InterfaceMethodPeer.java") { + Encoding = new UTF8Encoding (encoderShouldEmitUTF8Identifier: false), + TextContent = () => ResourceData.InterfaceMethodPeer, + Metadata = { + { "Bind", "True" }, + }, + }, + new AndroidItem.AndroidJavaSource ("ConcreteInterfaceMethodPeer.java") { + Encoding = new UTF8Encoding (encoderShouldEmitUTF8Identifier: false), + TextContent = () => ResourceData.ConcreteInterfaceMethodPeer, + Metadata = { + { "Bind", "True" }, + }, + }, + // Binding this covariant bridge is a separate generator gap, so execute it through JNI. + new AndroidItem.AndroidJavaSource ("InterfaceMethodBridgeInvoker.java") { + Encoding = new UTF8Encoding (encoderShouldEmitUTF8Identifier: false), + TextContent = () => ResourceData.InterfaceMethodBridgeInvoker, + Metadata = { + { "Bind", "False" }, + }, + }, + new AndroidItem.AndroidJavaSource ("CovariantInterfaceMethods.java") { + Encoding = new UTF8Encoding (encoderShouldEmitUTF8Identifier: false), + TextContent = () => ResourceData.CovariantInterfaceMethods, + Metadata = { + { "Bind", "False" }, + }, + }, }, }; proj.SetRuntime (runtime); - - // Note: To properly test, static interface default methods (Java 8+) must be compiled correctly. - // With $(SupportedOSPlatformVersion) >= 24, D8 handles them natively without desugaring. + proj.SetRuntimeIdentifiers (new [] { DeviceAbi }); + proj.SetProperty ("AndroidTypeMapImplementation", typemapImplementation); + proj.SetProperty ("AndroidLinkTool", useR8 ? "r8" : ""); + proj.SetDefaultTargetDevice (); proj.SupportedOSPlatformVersion = "24"; + if (!apiNative) { + // .NET 11 rejects minSdk < 24. Rewrite the validated manifest only for this fixture + // so D8/R8 still emits the pre-API 24 companion-class form. + proj.Imports.Add (new Import (() => "ForceInterfaceMethodDesugaring.targets") { + TextContent = () => """ + + + + + + """, + }); + } + proj.MainActivity = proj.DefaultMainActivity.Replace ( + "//${AFTER_ONCREATE}", + """ + using var peer = new Example.InterfaceMethodPeer (); + Example.IInterfaceMethods interfacePeer = peer; + Example.IInterfaceMethods.INested nestedPeer = peer; + using var concretePeer = new Example.ConcreteInterfaceMethodPeer (); + using var bridgeType = new Java.Interop.JniType ("example/InterfaceMethodBridgeInvoker"); + var bridgeMethod = bridgeType.GetStaticMethod ("invokeCovariantBridge", "()Ljava/lang/String;"); + var bridgeResult = Java.Interop.JniEnvironment.StaticMethods.CallStaticObjectMethod (bridgeType.PeerReference, bridgeMethod); + var bridgeValue = Java.Interop.JniEnvironment.Strings.ToString ( + ref bridgeResult, Java.Interop.JniObjectReferenceOptions.CopyAndDispose); + Console.WriteLine ( + $"INTERFACE_METHOD_RESULTS " + + $"{Example.IInterfaceMethods.StaticValue}:" + + $"{interfacePeer.DefaultValue}:" + + $"{concretePeer.DefaultValue}:" + + $"{Example.IInterfaceMethods.INested.NestedStaticValue}:" + + $"{nestedPeer.NestedDefaultValue}:" + + $"{bridgeValue}"); + """); + using var builder = CreateApkBuilder (packageName: packageName); + try { + CleanupInterfaceMethodPackage (proj.PackageName); + Assert.IsTrue (builder.Build (proj), "`dotnet build` should succeed"); - proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", @" - Console.WriteLine ($""# jonp static interface default method invocation; IStaticMethodsInterface.Value={Example.IStaticMethodsInterface.Value}""); -"); - var builder = CreateApkBuilder (); - Assert.IsTrue (builder.Build (proj), "`dotnet build` should succeed"); - RunProjectAndAssert (proj, builder); - var appStartupLogcatFile = Path.Combine (Root, builder.ProjectDirectory, "logcat.log"); - bool didLaunch = WaitForActivityToStart (proj.PackageName, "MainActivity", appStartupLogcatFile, ActivityStartTimeoutInSeconds); - Assert.IsTrue (didLaunch, "MainActivity should have launched!"); - var logcatOutput = File.ReadAllText (appStartupLogcatFile); + var dexFile = builder.Output.GetIntermediaryPath (Path.Combine ("android", "bin", "classes.dex")); + FileAssert.Exists (dexFile); + AssertInterfaceMethodDexShape (dexFile, apiNative); - StringAssert.Contains ( - "IStaticMethodsInterface.Value=3", + RunProjectAndAssert (proj, builder); + var appStartupLogcatFile = Path.Combine (Root, builder.ProjectDirectory, "logcat.log"); + bool didLaunch = WaitForActivityToStart ( + proj.PackageName, + "MainActivity", + appStartupLogcatFile, + ActivityStartTimeoutInSeconds); + Assert.IsTrue (didLaunch, "MainActivity should have launched!"); + var logcatOutput = File.ReadAllText (appStartupLogcatFile); + + StringAssert.Contains ( + "INTERFACE_METHOD_RESULTS 11:22:23:33:44:bridge:44", logcatOutput, - "Was IStaticMethodsInterface.Value executed?" - ); + "Managed and Java static, default, nested, and covariant bridge interface methods should all execute." + ); + } finally { + TryCleanupInterfaceMethodPackage (proj.PackageName); + } + } + + static void CleanupInterfaceMethodPackage (string packageName) + { + RunAdbCommandWithExitCode ("shell", "am", "force-stop", packageName); + RunAdbCommandWithExitCode ("uninstall", packageName); + var (exitCode, standardOutput, standardError) = RunAdbCommandWithExitCode ( + "shell", + "pm", + "list", + "packages", + packageName); + Assert.AreEqual (0, exitCode, $"Failed to query installed packages: {standardError}"); + var installedPackages = standardOutput.Split ( + new [] { '\r', '\n' }, + StringSplitOptions.RemoveEmptyEntries); + CollectionAssert.DoesNotContain ( + installedPackages, + $"package:{packageName}", + $"{packageName} should not remain installed."); + } + + static void TryCleanupInterfaceMethodPackage (string packageName) + { + try { + CleanupInterfaceMethodPackage (packageName); + } catch (Exception ex) { + TestContext.WriteLine ($"Final cleanup for '{packageName}' failed: {ex}"); + } + } + + void AssertInterfaceMethodDexShape (string dexFile, bool apiNative) + { + const string interfaceClass = "Lexample/InterfaceMethods;"; + const string nestedInterfaceClass = "Lexample/InterfaceMethods$Nested;"; + const string covariantInterfaceClass = "Lexample/CovariantInterfaceMethods$Derived;"; + var dexDump = DexUtils.GetDexDump (dexFile, AndroidSdkPath); + + if (apiNative) { + Assert.IsTrue (DexUtils.ContainsClassWithMethod (interfaceClass, "getStaticValue", "()I", dexDump)); + Assert.IsTrue (DexUtils.ContainsClassWithMethod (interfaceClass, "getDefaultValue", "()I", dexDump)); + Assert.IsTrue (DexUtils.ContainsClassWithMethod (nestedInterfaceClass, "getNestedStaticValue", "()I", dexDump)); + Assert.IsTrue (DexUtils.ContainsClassWithMethod (nestedInterfaceClass, "getNestedDefaultValue", "()I", dexDump)); + Assert.IsTrue (DexUtils.ContainsClassWithMethod (covariantInterfaceClass, "getCovariantValue", + "()Ljava/lang/Object;", dexDump)); + Assert.IsTrue (DexUtils.ContainsClassWithMethod (covariantInterfaceClass, "getCovariantValue", + "()Ljava/lang/String;", dexDump)); + Assert.IsFalse (DexUtils.ContainsClass ("Lexample/InterfaceMethods$-CC;", dexDump)); + Assert.IsFalse (DexUtils.ContainsClass ("Lexample/InterfaceMethods$Nested$-CC;", dexDump)); + Assert.IsFalse (DexUtils.ContainsClass ("Lexample/CovariantInterfaceMethods$Derived$-CC;", dexDump)); + } else { + Assert.IsFalse (DexUtils.ContainsClassWithMethod (interfaceClass, "getStaticValue", "()I", dexDump)); + Assert.IsTrue (DexUtils.ContainsClassWithMethod ("Lexample/InterfaceMethods$-CC;", "getStaticValue", + "()I", dexDump)); + Assert.IsTrue (DexUtils.ContainsClassWithMethod ("Lexample/InterfaceMethods$-CC;", "$default$getDefaultValue", + "(Lexample/InterfaceMethods;)I", dexDump)); + Assert.IsTrue (DexUtils.ContainsClassWithMethod ("Lexample/InterfaceMethods$Nested$-CC;", "getNestedStaticValue", + "()I", dexDump)); + Assert.IsTrue (DexUtils.ContainsClassWithMethod ("Lexample/InterfaceMethods$Nested$-CC;", "$default$getNestedDefaultValue", + "(Lexample/InterfaceMethods$Nested;)I", dexDump)); + Assert.IsTrue (DexUtils.ContainsClassWithMethod ("Lexample/CovariantInterfaceMethods$Derived$-CC;", + "$default$getCovariantValue", + "(Lexample/CovariantInterfaceMethods$Derived;)Ljava/lang/Object;", dexDump)); + } } [Test]