diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs
index b5512e330fe..0a24f8b438d 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using System.Security.Cryptography;
+using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using NUnit.Framework;
@@ -36,6 +37,56 @@ public void Build_WithTrimmableTypeMap_Succeeds ([Values] bool isRelease, [Value
AssertTrimmableTypeMapOutputs (intermediateDir);
}
+ [TestCase ("llvm-ir", AndroidRuntime.CoreCLR)]
+ [TestCase ("trimmable", AndroidRuntime.CoreCLR)]
+ [TestCase ("trimmable", AndroidRuntime.NativeAOT)]
+ public void Build_JavaSourceSemanticParityFixture_Compiles (
+ string typeMapImplementation,
+ AndroidRuntime runtime)
+ {
+ bool isRelease = runtime == AndroidRuntime.NativeAOT;
+ if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) {
+ return;
+ }
+
+ var fixtureProject = Path.Combine (
+ XABuildPaths.TopDirectory,
+ "tests",
+ "Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests",
+ "JavaSourceParityFixture",
+ "JavaSourceParityFixture.csproj");
+ var proj = new XamarinAndroidApplicationProject {
+ IsRelease = isRelease,
+ References = {
+ new BuildItem.ProjectReference (fixtureProject, "JavaSourceParityFixture"),
+ new BuildItem.Reference ("Mono.Android.Export"),
+ },
+ };
+ proj.SetRuntime (runtime);
+ proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation);
+ proj.MainActivity = proj.DefaultMainActivity.Replace (
+ "//${AFTER_ONCREATE}",
+ "System.GC.KeepAlive (new UserApp.JavaSourceParity.SemanticPeer ());");
+ proj.AndroidJavaSources.Add (new AndroidItem.AndroidJavaSource ("com\\example\\parity\\Base.java") {
+ Encoding = Encoding.ASCII,
+ TextContent = () => """
+ package com.example.parity;
+
+ public class Base {
+ public Base () {}
+ public Base (int value) {}
+ public java.lang.String getValue () { return ""; }
+ }
+ """,
+ });
+ using var builder = CreateApkBuilder ();
+ Assert.IsTrue (builder.Build (proj), $"{runtime}/{typeMapImplementation} should compile the shared semantic parity fixture.");
+
+ var compiledClass = builder.Output.GetIntermediaryPath (
+ Path.Combine ("android", "bin", "classes", "com", "example", "parity", "SemanticPeer.class"));
+ FileAssert.Exists (compiledClass, $"{runtime}/{typeMapImplementation} should compile the generated SemanticPeer Java source.");
+ }
+
[Test]
public void Build_PublishAotProject_UsesTrimmableTypeMapForCoreClrDebug ()
{
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/JavaSourceParityFixture/JavaSourceParityFixture.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/JavaSourceParityFixture/JavaSourceParityFixture.csproj
new file mode 100644
index 00000000000..2ff286dbdac
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/JavaSourceParityFixture/JavaSourceParityFixture.csproj
@@ -0,0 +1,48 @@
+
+
+
+
+ $(DotNetTargetFramework)
+ latest
+ enable
+ false
+ Library
+ true
+ ..\..\..\product.snk
+ ..\..\..\bin\Test$(Configuration)\
+
+
+
+
+
+
+
+
+ <_MonoAndroidRefCandidate
+ Condition=" '%(AndroidBuildApiLevel.Unstable)' == 'false' "
+ Include="$(MSBuildThisFileDirectory)..\..\..\bin\$(Configuration)\lib\packs\Microsoft.Android.Ref.%(AndroidBuildApiLevel.Identity)\*\ref\net*\Mono.Android.dll"
+ />
+ <_JavaInteropRefCandidate
+ Condition=" '%(AndroidBuildApiLevel.Unstable)' == 'false' "
+ Include="$(MSBuildThisFileDirectory)..\..\..\bin\$(Configuration)\lib\packs\Microsoft.Android.Ref.%(AndroidBuildApiLevel.Identity)\*\ref\net*\Java.Interop.dll"
+ />
+
+
+ <_MonoAndroidRefAssembly>@(_MonoAndroidRefCandidate, ';')
+ <_MonoAndroidRefAssembly>$(_MonoAndroidRefAssembly.Split(';')[0])
+ <_JavaInteropRefAssembly>@(_JavaInteropRefCandidate, ';')
+ <_JavaInteropRefAssembly>$(_JavaInteropRefAssembly.Split(';')[0])
+
+
+
+ $(_MonoAndroidRefAssembly)
+
+
+
+
+ $(_JavaInteropRefAssembly)
+
+
+
+
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/JavaSourceParityFixture/JavaSourceParityTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/JavaSourceParityFixture/JavaSourceParityTypes.cs
new file mode 100644
index 00000000000..d5728b94e94
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/JavaSourceParityFixture/JavaSourceParityTypes.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Runtime.Versioning;
+using Android.Runtime;
+using Java.Interop;
+
+[assembly: SupportedOSPlatform ("android24.0")]
+
+namespace UserApp.JavaSourceParity;
+
+[Register ("com/example/parity/Base", DoNotGenerateAcw = true)]
+public class Base : Java.Lang.Object
+{
+ protected Base (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
+
+ [Register (".ctor", "()V", "")]
+ public Base () { }
+
+ [Register (".ctor", "(I)V", "")]
+ public Base (int value) { }
+
+ [Register ("getValue", "()Ljava/lang/String;", "GetGetValueHandler")]
+ public virtual string GetValue () => "";
+}
+
+[Register ("com/example/parity/SemanticPeer")]
+public class SemanticPeer : Base, Android.Views.View.IOnClickListener, Android.Views.View.IOnLongClickListener
+{
+ public SemanticPeer () { }
+
+ public SemanticPeer (int value) : base (value) { }
+
+ public override string GetValue () => "derived";
+
+ public void OnClick (Android.Views.View? view) { }
+
+ public bool OnLongClick (Android.Views.View? view) => true;
+
+ [Export ("checkedExport", Throws = new [] { typeof (Java.IO.IOException) })]
+ protected int CheckedExport (string value) => value.Length;
+
+ [ExportField ("STATIC_LABEL")]
+ public static string GetStaticLabel () => "static";
+
+ [ExportField ("LABEL")]
+ public string GetLabel () => "instance";
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/JavaSourceSemanticParityTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/JavaSourceSemanticParityTests.cs
new file mode 100644
index 00000000000..608d78069f7
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/JavaSourceSemanticParityTests.cs
@@ -0,0 +1,454 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using Java.Interop.Tools.Cecil;
+using Java.Interop.Tools.JavaCallableWrappers;
+using Java.Interop.Tools.JavaCallableWrappers.Adapters;
+using Java.Interop.Tools.TypeNameMappings;
+using Mono.Cecil;
+using Xamarin.Android.Tools.Bytecode;
+using Xunit;
+using CecilAssemblyDefinition = Mono.Cecil.AssemblyDefinition;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests;
+
+public partial class ScannerComparisonTests
+{
+ const string SemanticPeerManagedName = "UserApp.JavaSourceParity.SemanticPeer";
+ const string SemanticPeerJavaName = "com/example/parity/SemanticPeer";
+
+ sealed record JavaSemanticModel (
+ string Name,
+ string BaseName,
+ ClassAccessFlags Modifiers,
+ IReadOnlyList Interfaces,
+ IReadOnlyList Constructors,
+ IReadOnlyList Methods,
+ IReadOnlyList Fields
+ );
+
+ [Fact]
+ public void GeneratedJava_HasSemanticParityAndCompiles ()
+ {
+ var assemblyPaths = JavaSourceParityAssemblyPaths;
+
+ var legacySource = GenerateLegacyJava (assemblyPaths [0]);
+ var trimmableSource = GenerateTrimmableJava (assemblyPaths);
+
+ var legacyModel = CompileAndReadSemanticModel ("legacy", legacySource);
+ var trimmableModel = CompileAndReadSemanticModel ("trimmable", trimmableSource);
+
+ AssertFixtureCoverage (legacyModel);
+ AssertSemanticEquality (legacyModel, trimmableModel);
+ }
+
+ static string[] JavaSourceParityAssemblyPaths {
+ get {
+ var testDirectory = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location);
+ if (testDirectory is null) {
+ throw new InvalidOperationException ("Could not determine the integration test output directory.");
+ }
+
+ var paths = new List {
+ Path.Combine (testDirectory, "JavaSourceParityFixture.dll"),
+ Path.Combine (testDirectory, "Mono.Android.dll"),
+ };
+ var javaInteropPath = Path.Combine (testDirectory, "Java.Interop.dll");
+ if (File.Exists (javaInteropPath)) {
+ paths.Add (javaInteropPath);
+ }
+ foreach (var path in paths) {
+ if (!File.Exists (path)) {
+ throw new InvalidOperationException ($"Required fixture dependency '{path}' was not found.");
+ }
+ }
+ return paths.ToArray ();
+ }
+ }
+
+ static string GenerateLegacyJava (string assemblyPath)
+ {
+ var cache = new TypeDefinitionCache ();
+ var resolver = new DefaultAssemblyResolver ();
+ var assemblyDirectory = Path.GetDirectoryName (assemblyPath);
+ if (assemblyDirectory is null) {
+ throw new InvalidOperationException ($"Could not determine the directory for '{assemblyPath}'.");
+ }
+ resolver.AddSearchDirectory (assemblyDirectory);
+
+ var runtimeDirectory = Path.GetDirectoryName (typeof (object).Assembly.Location);
+ if (runtimeDirectory is not null) {
+ resolver.AddSearchDirectory (runtimeDirectory);
+ }
+
+ var readerParameters = new ReaderParameters { AssemblyResolver = resolver };
+ using var assembly = CecilAssemblyDefinition.ReadAssembly (assemblyPath, readerParameters);
+ var type = assembly.MainModule.GetType (SemanticPeerManagedName);
+ if (type is null) {
+ throw new InvalidOperationException ($"Could not find '{SemanticPeerManagedName}' in '{assemblyPath}'.");
+ }
+
+ var wrapper = CecilImporter.CreateType (type, cache);
+ using var writer = new StringWriter ();
+ wrapper.Generate (writer, new CallableWrapperWriterOptions {
+ CodeGenerationTarget = JavaPeerStyle.XAJavaInterop1,
+ });
+ return writer.ToString ();
+ }
+
+ static string GenerateTrimmableJava (IReadOnlyList assemblyPaths)
+ {
+ using var scanner = new JavaPeerScanner ();
+ var peReaders = new List ();
+ var assemblies = new List<(string Name, PEReader Reader)> ();
+ List peers;
+ try {
+ foreach (var path in assemblyPaths) {
+ var peReader = new PEReader (File.OpenRead (path));
+ peReaders.Add (peReader);
+ var metadataReader = peReader.GetMetadataReader ();
+ assemblies.Add ((metadataReader.GetString (metadataReader.GetAssemblyDefinition ().Name), peReader));
+ }
+ peers = scanner.Scan (assemblies);
+ } finally {
+ foreach (var peReader in peReaders) {
+ peReader.Dispose ();
+ }
+ }
+
+ var peer = peers.SingleOrDefault (p => p.ManagedTypeName == SemanticPeerManagedName);
+ if (peer is null) {
+ throw new InvalidOperationException ($"Could not scan '{SemanticPeerManagedName}'.");
+ }
+
+ using var writer = new StringWriter ();
+ new JcwJavaSourceGenerator ().Generate (peer, writer);
+ return writer.ToString ();
+ }
+
+ static JavaSemanticModel CompileAndReadSemanticModel (string name, string generatedSource)
+ {
+ var root = Path.Combine (Path.GetTempPath (), $"jcw-semantic-parity-{name}-{Guid.NewGuid ():N}");
+ var sourceDirectory = Path.Combine (root, "src");
+ var classesDirectory = Path.Combine (root, "classes");
+ Directory.CreateDirectory (sourceDirectory);
+ Directory.CreateDirectory (classesDirectory);
+
+ try {
+ var sourceFiles = WriteJavaSourceSet (sourceDirectory, generatedSource);
+ CompileJavaSourceSet (name, sourceFiles, classesDirectory, generatedSource);
+
+ var classPath = Path.Combine (classesDirectory, SemanticPeerJavaName + ".class");
+ Assert.True (File.Exists (classPath), $"javac did not produce '{classPath}'.");
+ using var stream = File.OpenRead (classPath);
+ return CreateSemanticModel (new ClassFile (stream));
+ } finally {
+ Directory.Delete (root, recursive: true);
+ }
+ }
+
+ static List WriteJavaSourceSet (string sourceDirectory, string generatedSource)
+ {
+ var sources = new Dictionary {
+ [SemanticPeerJavaName + ".java"] = generatedSource,
+ ["com/example/parity/Base.java"] = """
+ package com.example.parity;
+
+ public class Base {
+ public Base () {}
+ public Base (int value) {}
+ public java.lang.String getValue () { return ""; }
+ }
+ """,
+ ["android/view/View.java"] = """
+ package android.view;
+
+ public class View {
+ public interface OnClickListener {
+ void onClick (View view);
+ }
+ public interface OnLongClickListener {
+ boolean onLongClick (View view);
+ boolean onLongClickUseDefaultHapticFeedback (View view);
+ }
+ }
+ """,
+ ["mono/android/IGCUserPeer.java"] = """
+ package mono.android;
+
+ public interface IGCUserPeer {
+ void monodroidAddReference (java.lang.Object value);
+ void monodroidClearReferences ();
+ }
+ """,
+ ["mono/android/Runtime.java"] = """
+ package mono.android;
+
+ public final class Runtime {
+ public static void register (java.lang.String managedName, java.lang.Class> javaType, java.lang.String methods) {}
+ public static void registerNatives (java.lang.Class> javaType) {}
+ }
+ """,
+ ["mono/android/TypeManager.java"] = """
+ package mono.android;
+
+ public final class TypeManager {
+ public static void Activate (java.lang.String managedName, java.lang.String signature, java.lang.Object instance, java.lang.Object[] arguments) {}
+ }
+ """,
+ };
+
+ var paths = new List ();
+ foreach (var source in sources) {
+ var path = Path.Combine (sourceDirectory, source.Key);
+ var directory = Path.GetDirectoryName (path);
+ if (directory is null) {
+ throw new InvalidOperationException ($"Could not determine the directory for '{path}'.");
+ }
+ Directory.CreateDirectory (directory);
+ File.WriteAllText (path, source.Value);
+ paths.Add (path);
+ }
+ return paths;
+ }
+
+ static void CompileJavaSourceSet (string name, IReadOnlyList sourceFiles, string classesDirectory, string generatedSource)
+ {
+ var compilerPath = GetJavaCompilerPath ();
+ var startInfo = new ProcessStartInfo {
+ FileName = compilerPath,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ };
+ startInfo.ArgumentList.Add ("-d");
+ startInfo.ArgumentList.Add (classesDirectory);
+ foreach (var sourceFile in sourceFiles) {
+ startInfo.ArgumentList.Add (sourceFile);
+ }
+
+ using var process = Process.Start (startInfo);
+ if (process is null) {
+ throw new InvalidOperationException ($"Could not start '{compilerPath}'.");
+ }
+ var standardOutput = process.StandardOutput.ReadToEndAsync ();
+ var standardError = process.StandardError.ReadToEndAsync ();
+ if (!process.WaitForExit ((int) TimeSpan.FromMinutes (1).TotalMilliseconds)) {
+ process.Kill (entireProcessTree: true);
+ process.WaitForExit ();
+ var timeoutOutput = standardOutput.GetAwaiter ().GetResult ();
+ var timeoutError = standardError.GetAwaiter ().GetResult ();
+ throw new TimeoutException (
+ $"javac timed out compiling the {name} Java source set.{Environment.NewLine}" +
+ $"stdout:{Environment.NewLine}{timeoutOutput}{Environment.NewLine}" +
+ $"stderr:{Environment.NewLine}{timeoutError}");
+ }
+
+ var output = standardOutput.GetAwaiter ().GetResult ();
+ var error = standardError.GetAwaiter ().GetResult ();
+ Assert.True (process.ExitCode == 0,
+ $"javac failed compiling the {name} Java source set.{Environment.NewLine}" +
+ $"stdout:{Environment.NewLine}{output}{Environment.NewLine}" +
+ $"stderr:{Environment.NewLine}{error}{Environment.NewLine}" +
+ $"generated source:{Environment.NewLine}{generatedSource}");
+ }
+
+ static string GetJavaCompilerPath ()
+ {
+ var attribute = typeof (ScannerComparisonTests).Assembly
+ .GetCustomAttributes ()
+ .SingleOrDefault (a => a.Key == "JavaCPath");
+ if (attribute is null || attribute.Value.IsNullOrEmpty ()) {
+ throw new InvalidOperationException ("The JavaCPath assembly metadata value is missing.");
+ }
+ return attribute.Value;
+ }
+
+ static JavaSemanticModel CreateSemanticModel (ClassFile classFile)
+ {
+ // Classfiles preserve declaration semantics while discarding source formatting.
+ var constructors = classFile.Methods
+ .Where (method => method.IsConstructor)
+ .Select (FormatMethod)
+ .OrderBy (method => method, StringComparer.Ordinal)
+ .ToArray ();
+ var methods = classFile.Methods
+ .Where (IsSemanticMethod)
+ .Select (FormatMethod)
+ .OrderBy (method => method, StringComparer.Ordinal)
+ .ToArray ();
+ var fields = classFile.Fields
+ .Where (field => field.Name != "refList" && !field.Name.StartsWith ("__md_", StringComparison.Ordinal))
+ .Select (FormatField)
+ .OrderBy (field => field, StringComparer.Ordinal)
+ .ToArray ();
+ var interfaces = classFile.Interfaces
+ .Select (iface => iface.Name.Value)
+ .ToArray ();
+
+ return new JavaSemanticModel (
+ classFile.ThisClass.Name.Value,
+ classFile.SuperClass.Name.Value,
+ GetDeclarationModifiers (classFile.AccessFlags),
+ interfaces,
+ constructors,
+ methods,
+ fields
+ );
+ }
+
+ static bool IsSemanticMethod (Xamarin.Android.Tools.Bytecode.MethodInfo method)
+ {
+ if (method.Name is "" or "" ||
+ method.AccessFlags.HasFlag (MethodAccessFlags.Native) ||
+ method.Name is "monodroidAddReference" or "monodroidClearReferences" ||
+ method.Name.StartsWith ("__md_", StringComparison.Ordinal)) {
+ return false;
+ }
+ return true;
+ }
+
+ static string FormatMethod (Xamarin.Android.Tools.Bytecode.MethodInfo method)
+ {
+ var throws = method.GetThrows ()
+ .Select (type => type.BinaryName)
+ .OrderBy (type => type, StringComparer.Ordinal);
+ return $"{GetVisibility (method.AccessFlags)}|" +
+ $"static={method.AccessFlags.HasFlag (MethodAccessFlags.Static)}|" +
+ $"abstract={method.AccessFlags.HasFlag (MethodAccessFlags.Abstract)}|" +
+ $"bridge={method.AccessFlags.HasFlag (MethodAccessFlags.Bridge)}|" +
+ $"synthetic={method.AccessFlags.HasFlag (MethodAccessFlags.Synthetic)}|" +
+ $"{method.Name}|{method.Descriptor}|" +
+ $"final={method.AccessFlags.HasFlag (MethodAccessFlags.Final)}|" +
+ $"synchronized={method.AccessFlags.HasFlag (MethodAccessFlags.Synchronized)}|" +
+ $"varargs={method.AccessFlags.HasFlag (MethodAccessFlags.Varargs)}|" +
+ $"strict={method.AccessFlags.HasFlag (MethodAccessFlags.Strict)}|" +
+ $"throws={string.Join (",", throws)}";
+ }
+
+ static string FormatField (Xamarin.Android.Tools.Bytecode.FieldInfo field)
+ {
+ return $"{GetVisibility (field.AccessFlags)}|" +
+ $"static={field.AccessFlags.HasFlag (FieldAccessFlags.Static)}|" +
+ $"final={field.AccessFlags.HasFlag (FieldAccessFlags.Final)}|" +
+ $"{field.Name}|{field.Descriptor}|" +
+ $"volatile={field.AccessFlags.HasFlag (FieldAccessFlags.Volatile)}|" +
+ $"transient={field.AccessFlags.HasFlag (FieldAccessFlags.Transient)}|" +
+ $"synthetic={field.AccessFlags.HasFlag (FieldAccessFlags.Synthetic)}|" +
+ $"enum={field.AccessFlags.HasFlag (FieldAccessFlags.Enum)}";
+ }
+
+ static ClassAccessFlags GetDeclarationModifiers (ClassAccessFlags flags)
+ {
+ return flags & (
+ ClassAccessFlags.Public |
+ ClassAccessFlags.Private |
+ ClassAccessFlags.Protected |
+ ClassAccessFlags.Static |
+ ClassAccessFlags.Final |
+ ClassAccessFlags.Interface |
+ ClassAccessFlags.Abstract |
+ ClassAccessFlags.Synthetic |
+ ClassAccessFlags.Annotation |
+ ClassAccessFlags.Enum
+ );
+ }
+
+ static string GetVisibility (MethodAccessFlags flags)
+ {
+ if (flags.HasFlag (MethodAccessFlags.Public)) {
+ return "public";
+ }
+ if (flags.HasFlag (MethodAccessFlags.Protected)) {
+ return "protected";
+ }
+ if (flags.HasFlag (MethodAccessFlags.Private)) {
+ return "private";
+ }
+ return "package";
+ }
+
+ static string GetVisibility (FieldAccessFlags flags)
+ {
+ if (flags.HasFlag (FieldAccessFlags.Public)) {
+ return "public";
+ }
+ if (flags.HasFlag (FieldAccessFlags.Protected)) {
+ return "protected";
+ }
+ if (flags.HasFlag (FieldAccessFlags.Private)) {
+ return "private";
+ }
+ return "package";
+ }
+
+ static void AssertFixtureCoverage (JavaSemanticModel model)
+ {
+ Assert.Equal (SemanticPeerJavaName, model.Name);
+ Assert.Equal ("com/example/parity/Base", model.BaseName);
+ Assert.Equal (ClassAccessFlags.Public, model.Modifiers);
+ Assert.Equal (new [] {
+ "mono/android/IGCUserPeer",
+ "android/view/View$OnClickListener",
+ "android/view/View$OnLongClickListener",
+ }, model.Interfaces);
+ const string publicInstanceMethod = "public|static=False|abstract=False|bridge=False|synthetic=False|";
+ const string protectedInstanceMethod = "protected|static=False|abstract=False|bridge=False|synthetic=False|";
+ const string ordinaryMethodModifiers = "final=False|synchronized=False|varargs=False|strict=False|";
+ Assert.Contains (
+ publicInstanceMethod + "|()V|" + ordinaryMethodModifiers + "throws=",
+ model.Constructors
+ );
+ Assert.Contains (
+ publicInstanceMethod + "|(I)V|" + ordinaryMethodModifiers + "throws=",
+ model.Constructors
+ );
+ Assert.Contains (
+ model.Methods,
+ method => method.Contains (
+ publicInstanceMethod + "getValue|()Ljava/lang/String;",
+ StringComparison.Ordinal
+ )
+ );
+ Assert.Contains (
+ model.Methods,
+ method => method.Contains (
+ publicInstanceMethod + "onClick|(Landroid/view/View;)V",
+ StringComparison.Ordinal
+ )
+ );
+ Assert.Contains (
+ model.Methods,
+ method => method.Contains (
+ publicInstanceMethod + "onLongClick|(Landroid/view/View;)Z",
+ StringComparison.Ordinal
+ )
+ );
+ Assert.Contains (
+ model.Methods,
+ method => method.Contains (
+ protectedInstanceMethod + "checkedExport|(Ljava/lang/String;)I|" +
+ ordinaryMethodModifiers + "throws=java/io/IOException",
+ StringComparison.Ordinal
+ )
+ );
+ Assert.Contains (model.Fields, field => field.Contains ("public|static=True|final=False|STATIC_LABEL|Ljava/lang/String;", StringComparison.Ordinal));
+ Assert.Contains (model.Fields, field => field.Contains ("public|static=False|final=False|LABEL|Ljava/lang/String;", StringComparison.Ordinal));
+ }
+
+ static void AssertSemanticEquality (JavaSemanticModel expected, JavaSemanticModel actual)
+ {
+ Assert.Equal (expected.Name, actual.Name);
+ Assert.Equal (expected.BaseName, actual.BaseName);
+ Assert.Equal (expected.Modifiers, actual.Modifiers);
+ Assert.Equal (expected.Interfaces, actual.Interfaces);
+ Assert.Equal (expected.Constructors, actual.Constructors);
+ Assert.Equal (expected.Methods, actual.Methods);
+ Assert.Equal (expected.Fields, actual.Fields);
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj
index 96df7a6693b..5e220be6d41 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj
@@ -22,11 +22,13 @@
+
+
@@ -36,6 +38,8 @@
ReferenceOutputAssembly=false because we don't compile against it — we just need the DLL on disk. -->
+