From 6e32d8dcff6042c2605d4b5b1b6f39e4d3a579a8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 14 Feb 2026 00:55:49 +0100 Subject: [PATCH 1/8] [TrimmableTypeMap] Integration parity test slice --- ...k.TrimmableTypeMap.IntegrationTests.csproj | 56 + .../MockBuildEngine.cs | 22 + .../ScannerComparisonTests.cs | 1086 +++++++++++++++++ .../UserTypesFixture/UserTypes.cs | 176 +++ .../UserTypesFixture/UserTypesFixture.csproj | 45 + 5 files changed, 1385 insertions(+) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MockBuildEngine.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj 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 new file mode 100644 index 00000000000..35c76b21bbc --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj @@ -0,0 +1,56 @@ + + + + $(DotNetTargetFramework) + latest + enable + false + Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests + true + ..\..\product.snk + ..\..\bin\Test$(Configuration) + + + + + + + + + + + + + + + + + + + + + + + + + + <_MonoAndroidRefCandidate Include="$(MSBuildThisFileDirectory)..\..\bin\$(Configuration)\lib\packs\Microsoft.Android.Ref.*\*\ref\net*\Mono.Android.dll" /> + + + <_MonoAndroidRefAssembly>@(_MonoAndroidRefCandidate, ';') + <_MonoAndroidRefAssembly>$(_MonoAndroidRefAssembly.Split(';')[0]) + + + + $(_MonoAndroidRefAssembly) + + + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MockBuildEngine.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MockBuildEngine.cs new file mode 100644 index 00000000000..d6c8c19d1eb --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MockBuildEngine.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections; +using Microsoft.Build.Framework; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; + +/// +/// Minimal IBuildEngine implementation for use with TaskLoggingHelper in tests. +/// +sealed class MockBuildEngine : IBuildEngine +{ + public bool ContinueOnError => false; + public int LineNumberOfTaskNode => 0; + public int ColumnNumberOfTaskNode => 0; + public string ProjectFileOfTaskNode => ""; + + public bool BuildProjectFile (string projectFileName, string [] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => true; + public void LogCustomEvent (CustomBuildEventArgs e) { } + public void LogErrorEvent (BuildErrorEventArgs e) { } + public void LogMessageEvent (BuildMessageEventArgs e) { } + public void LogWarningEvent (BuildWarningEventArgs e) { } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs new file mode 100644 index 00000000000..8ee29061de0 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -0,0 +1,1086 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Java.Interop.Tools.Cecil; +using Java.Interop.Tools.TypeNameMappings; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using Xamarin.Android.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; + +public class ScannerComparisonTests +{ + readonly ITestOutputHelper output; + + public ScannerComparisonTests (ITestOutputHelper output) + { + this.output = output; + } + + record TypeMapEntry (string JavaName, string ManagedName, bool SkipInJavaToManaged); + + record MethodEntry (string JniName, string JniSignature, string? Connector); + + record TypeMethodGroup (string ManagedName, List Methods); + + static (List entries, Dictionary> methodsByJavaName) RunLegacyScanner (string assemblyPath) + { + var cache = new TypeDefinitionCache (); + var resolver = new DefaultAssemblyResolver (); + resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); + + var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location); + if (runtimeDir != null) { + resolver.AddSearchDirectory (runtimeDir); + } + + var readerParams = new ReaderParameters { AssemblyResolver = resolver }; + using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams); + + var scanner = new Xamarin.Android.Tasks.XAJavaTypeScanner ( + Xamarin.Android.Tools.AndroidTargetArch.Arm64, + new TaskLoggingHelper (new MockBuildEngine (), "test"), + cache + ); + + var javaTypes = scanner.GetJavaTypes (assembly); + var (dataSets, _) = Xamarin.Android.Tasks.TypeMapCecilAdapter.GetDebugNativeEntries ( + javaTypes, cache, needUniqueAssemblies: false + ); + + var entries = dataSets.JavaToManaged + .Select (e => new TypeMapEntry (e.JavaName, e.ManagedName, e.SkipInJavaToManaged)) + .OrderBy (e => e.JavaName, StringComparer.Ordinal) + .ThenBy (e => e.ManagedName, StringComparer.Ordinal) + .ToList (); + + // Extract method-level [Register] attributes from each TypeDefinition. + // Use the raw javaTypes list to get ALL types — multiple managed types + // can map to the same JNI name (aliases). + var methodsByJavaName = new Dictionary> (); + foreach (var typeDef in javaTypes) { + var javaName = GetCecilJavaName (typeDef); + if (javaName == null) { + continue; + } + + // Cecil uses '/' for nested types, SRM uses '+' (CLR format) — normalize + var managedName = $"{typeDef.FullName.Replace ('/', '+')}, {typeDef.Module.Assembly.Name.Name}"; + var methods = ExtractMethodRegistrations (typeDef); + + if (!methodsByJavaName.TryGetValue (javaName, out var groups)) { + groups = new List (); + methodsByJavaName [javaName] = groups; + } + + groups.Add (new TypeMethodGroup ( + managedName, + methods.OrderBy (m => m.JniName, StringComparer.Ordinal) + .ThenBy (m => m.JniSignature, StringComparer.Ordinal) + .ToList () + )); + } + + // Some types appear in dataSets.JavaToManaged (the typemap) but not in + // javaTypes (the raw list). Include them with empty method lists so the + // comparison covers all types known to the legacy scanner. + foreach (var entry in dataSets.JavaToManaged) { + if (methodsByJavaName.ContainsKey (entry.JavaName)) { + continue; + } + + methodsByJavaName [entry.JavaName] = new List { + new TypeMethodGroup (entry.ManagedName, new List ()) + }; + } + + return (entries, methodsByJavaName); + } + + static string? GetCecilJavaName (TypeDefinition typeDef) + { + if (!typeDef.HasCustomAttributes) { + return null; + } + + foreach (var attr in typeDef.CustomAttributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { + continue; + } + + if (attr.ConstructorArguments.Count > 0) { + return ((string) attr.ConstructorArguments [0].Value).Replace ('.', '/'); + } + } + + return null; + } + + static List ExtractMethodRegistrations (TypeDefinition typeDef) + { + var methods = new List (); + + // Collect [Register] from methods directly + foreach (var method in typeDef.Methods) { + if (!method.HasCustomAttributes) { + continue; + } + + foreach (var attr in method.CustomAttributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { + continue; + } + + if (attr.ConstructorArguments.Count < 2) { + continue; + } + + var jniMethodName = (string) attr.ConstructorArguments [0].Value; + var jniSignature = (string) attr.ConstructorArguments [1].Value; + var connector = attr.ConstructorArguments.Count > 2 + ? (string) attr.ConstructorArguments [2].Value + : null; + + methods.Add (new MethodEntry (jniMethodName, jniSignature, connector)); + } + } + + // Collect [Register] from properties (attribute is on the property, not the getter/setter) + if (typeDef.HasProperties) { + foreach (var prop in typeDef.Properties) { + if (!prop.HasCustomAttributes) { + continue; + } + + foreach (var attr in prop.CustomAttributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { + continue; + } + + if (attr.ConstructorArguments.Count < 2) { + continue; + } + + var jniMethodName = (string) attr.ConstructorArguments [0].Value; + var jniSignature = (string) attr.ConstructorArguments [1].Value; + var connector = attr.ConstructorArguments.Count > 2 + ? (string) attr.ConstructorArguments [2].Value + : null; + + methods.Add (new MethodEntry (jniMethodName, jniSignature, connector)); + } + } + } + + return methods; + } + + static (List entries, Dictionary> methodsByJavaName) RunNewScanner (string[] assemblyPaths) + { + var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]); + using var scanner = new JavaPeerScanner (); + var allPeers = scanner.Scan (assemblyPaths); + var peers = allPeers.Where (p => p.AssemblyName == primaryAssemblyName).ToList (); + + var entries = peers + .Select (p => new TypeMapEntry ( + p.JavaName, + $"{p.ManagedTypeName}, {p.AssemblyName}", + p.IsInterface || p.IsGenericDefinition + )) + .OrderBy (e => e.JavaName, StringComparer.Ordinal) + .ThenBy (e => e.ManagedName, StringComparer.Ordinal) + .ToList (); + + var methodsByJavaName = new Dictionary> (); + foreach (var peer in peers) { + var managedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + + if (!methodsByJavaName.TryGetValue (peer.JavaName, out var groups)) { + groups = new List (); + methodsByJavaName [peer.JavaName] = groups; + } + + groups.Add (new TypeMethodGroup ( + managedName, + peer.MarshalMethods + .Select (m => new MethodEntry (m.JniName, m.JniSignature, m.Connector)) + .OrderBy (m => m.JniName, StringComparer.Ordinal) + .ThenBy (m => m.JniSignature, StringComparer.Ordinal) + .ToList () + )); + } + + return (entries, methodsByJavaName); + } + + [Fact] + public void ExactTypeMap_MonoAndroid () + { + var (legacy, _) = RunLegacyScanner (MonoAndroidAssemblyPath); + var (newEntries, _) = RunNewScanner (AllAssemblyPaths); + output.WriteLine ($"Legacy: {legacy.Count} entries, New: {newEntries.Count} entries"); + AssertTypeMapMatch (legacy, newEntries); + } + + [Fact] + public void ExactMarshalMethods_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + var (_, legacyMethods) = RunLegacyScanner (assemblyPath); + var (_, newMethods) = RunNewScanner (AllAssemblyPaths); + + var legacyTypeCount = legacyMethods.Values.Sum (g => g.Count); + var newTypeCount = newMethods.Values.Sum (g => g.Count); + var legacyMethodCount = legacyMethods.Values.Sum (g => g.Sum (t => t.Methods.Count)); + var newMethodCount = newMethods.Values.Sum (g => g.Sum (t => t.Methods.Count)); + output.WriteLine ($"Legacy: {legacyTypeCount} type groups across {legacyMethods.Count} JNI names, {legacyMethodCount} total methods"); + output.WriteLine ($"New: {newTypeCount} type groups across {newMethods.Count} JNI names, {newMethodCount} total methods"); + + var allJavaNames = new HashSet (legacyMethods.Keys); + allJavaNames.UnionWith (newMethods.Keys); + + var missingTypes = new List (); + var extraTypes = new List (); + var missingMethods = new List (); + var extraMethods = new List (); + var connectorMismatches = new List (); + + foreach (var javaName in allJavaNames.OrderBy (n => n)) { + var inLegacy = legacyMethods.TryGetValue (javaName, out var legacyGroups); + var inNew = newMethods.TryGetValue (javaName, out var newGroups); + + if (inLegacy && !inNew) { + foreach (var g in legacyGroups!) { + missingTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)"); + } + continue; + } + + if (!inLegacy && inNew) { + foreach (var g in newGroups!) { + extraTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)"); + } + continue; + } + + // Both scanners found this JNI name — compare managed types within it + var legacyByManaged = legacyGroups!.ToDictionary (g => g.ManagedName, g => g.Methods); + var newByManaged = newGroups!.ToDictionary (g => g.ManagedName, g => g.Methods); + + foreach (var managedName in legacyByManaged.Keys.Except (newByManaged.Keys)) { + missingTypes.Add ($"{javaName} → {managedName} ({legacyByManaged [managedName].Count} methods)"); + } + + foreach (var managedName in newByManaged.Keys.Except (legacyByManaged.Keys)) { + extraTypes.Add ($"{javaName} → {managedName} ({newByManaged [managedName].Count} methods)"); + } + + // For managed types present in both, compare their method sets + foreach (var managedName in legacyByManaged.Keys.Intersect (newByManaged.Keys)) { + var legacyMethodList = legacyByManaged [managedName]; + var newMethodList = newByManaged [managedName]; + + var legacySet = new HashSet<(string name, string sig)> ( + legacyMethodList.Select (m => (m.JniName, m.JniSignature)) + ); + var newSet = new HashSet<(string name, string sig)> ( + newMethodList.Select (m => (m.JniName, m.JniSignature)) + ); + + foreach (var m in legacySet.Except (newSet)) { + missingMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}"); + } + + foreach (var m in newSet.Except (legacySet)) { + extraMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}"); + } + + // For methods in both, compare connector strings + var legacyByKey = legacyMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + var newByKey = newMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + + foreach (var key in legacyByKey.Keys.Intersect (newByKey.Keys)) { + var lc = legacyByKey [key].Connector ?? ""; + var nc = newByKey [key].Connector ?? ""; + if (lc != nc) { + connectorMismatches.Add ($"{javaName} [{managedName}]: {key.JniName}{key.JniSignature} legacy='{lc}' new='{nc}'"); + } + } + } + } + + LogDiffs ("MANAGED TYPES MISSING from new scanner", missingTypes); + LogDiffs ("MANAGED TYPES EXTRA in new scanner", extraTypes); + LogDiffs ("METHODS MISSING from new scanner", missingMethods); + LogDiffs ("METHODS EXTRA in new scanner", extraMethods); + LogDiffs ("CONNECTOR MISMATCHES", connectorMismatches); + + Assert.Empty (missingTypes); + Assert.Empty (extraTypes); + Assert.Empty (missingMethods); + Assert.Empty (extraMethods); + Assert.Empty (connectorMismatches); + } + + [Fact] + public void ScannerDiagnostics_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (new [] { assemblyPath }); + + var interfaces = peers.Count (p => p.IsInterface); + var abstracts = peers.Count (p => p.IsAbstract); + var generics = peers.Count (p => p.IsGenericDefinition); + var withMethods = peers.Count (p => p.MarshalMethods.Count > 0); + var totalMethods = peers.Sum (p => p.MarshalMethods.Count); + var withConstructors = peers.Count (p => p.MarshalMethods.Any (m => m.IsConstructor)); + var withBase = peers.Count (p => p.BaseJavaName != null); + var withInterfaces = peers.Count (p => p.ImplementedInterfaceJavaNames.Count > 0); + + output.WriteLine ($"Total types: {peers.Count}"); + output.WriteLine ($"Interfaces: {interfaces}"); + output.WriteLine ($"Abstract classes: {abstracts}"); + output.WriteLine ($"Generic defs: {generics}"); + output.WriteLine ($"With marshal methods: {withMethods} ({totalMethods} total methods)"); + output.WriteLine ($"With constructors: {withConstructors}"); + output.WriteLine ($"With base Java: {withBase}"); + output.WriteLine ($"With interfaces: {withInterfaces}"); + + // Mono.Android.dll should have thousands of types + Assert.True (peers.Count > 3000, $"Expected >3000 types, got {peers.Count}"); + Assert.True (interfaces > 500, $"Expected >500 interfaces, got {interfaces}"); + Assert.True (totalMethods > 10000, $"Expected >10000 marshal methods, got {totalMethods}"); + } + + [Fact] + public void ExactBaseJavaNames_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + var (legacyData, _) = BuildLegacyTypeData (assemblyPath); + var newData = BuildNewTypeData (AllAssemblyPaths); + + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var mismatches = new List (); + int compared = 0; + + foreach (var managedName in allManagedNames.OrderBy (n => n)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + compared++; + + if (legacy.BaseJavaName != newInfo.BaseJavaName) { + // Legacy ToJniName can't resolve bases for open generic types (returns null). + // Our scanner resolves them correctly. Accept this known difference. + if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && managedName.Contains ('`')) { + continue; + } + + // Invokers share JNI names with their base class. Legacy ToJniName + // self-reference filter discards the base (baseJni == javaName), but + // our scanner correctly resolves it. Accept legacy=null, new=valid + // for DoNotGenerateAcw types. + if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && newInfo.DoNotGenerateAcw) { + continue; + } + + // Legacy ToJniName(System.Object) returns "java/lang/Object" as a fallback, + // making Java.Lang.Object/Throwable appear to have themselves as base. + // Our scanner correctly returns null. Accept legacy=self, new=null. + if (legacy.BaseJavaName != null && newInfo.BaseJavaName == null && + legacy.BaseJavaName == legacy.JavaName) { + continue; + } + + mismatches.Add ($"{managedName}: legacy='{legacy.BaseJavaName ?? "(null)"}' new='{newInfo.BaseJavaName ?? "(null)"}'"); + } + } + + output.WriteLine ($"Compared BaseJavaName for {compared} types"); + + LogDiffs ("BASE JAVA NAME MISMATCHES", mismatches); + + Assert.Empty (mismatches); + } + + [Fact] + public void ExactImplementedInterfaces_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + var (legacyData, _) = BuildLegacyTypeData (assemblyPath); + var newData = BuildNewTypeData (AllAssemblyPaths); + + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var missingInterfaces = new List (); + var extraInterfaces = new List (); + int compared = 0; + + foreach (var managedName in allManagedNames.OrderBy (n => n)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + compared++; + + var legacySet = new HashSet (legacy.ImplementedInterfaces, StringComparer.Ordinal); + var newSet = new HashSet (newInfo.ImplementedInterfaces, StringComparer.Ordinal); + + foreach (var iface in legacySet.Except (newSet)) { + missingInterfaces.Add ($"{managedName}: missing '{iface}'"); + } + + foreach (var iface in newSet.Except (legacySet)) { + extraInterfaces.Add ($"{managedName}: extra '{iface}'"); + } + } + + output.WriteLine ($"Compared ImplementedInterfaces for {compared} types"); + + LogDiffs ("INTERFACES MISSING from new scanner", missingInterfaces); + LogDiffs ("INTERFACES EXTRA in new scanner", extraInterfaces); + + Assert.Empty (missingInterfaces); + Assert.Empty (extraInterfaces); + } + + [Fact] + public void ExactActivationCtors_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + var (legacyData, _) = BuildLegacyTypeData (assemblyPath); + var newData = BuildNewTypeData (AllAssemblyPaths); + + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var presenceMismatches = new List (); + var declaringTypeMismatches = new List (); + var styleMismatches = new List (); + int compared = 0; + int withActivationCtor = 0; + + foreach (var managedName in allManagedNames.OrderBy (n => n)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + compared++; + + if (legacy.HasActivationCtor != newInfo.HasActivationCtor) { + presenceMismatches.Add ($"{managedName}: legacy.has={legacy.HasActivationCtor} new.has={newInfo.HasActivationCtor}"); + continue; + } + + if (!legacy.HasActivationCtor) { + continue; + } + + withActivationCtor++; + + if (legacy.ActivationCtorDeclaringType != newInfo.ActivationCtorDeclaringType) { + declaringTypeMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorDeclaringType}' new='{newInfo.ActivationCtorDeclaringType}'"); + } + + if (legacy.ActivationCtorStyle != newInfo.ActivationCtorStyle) { + styleMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorStyle}' new='{newInfo.ActivationCtorStyle}'"); + } + } + + output.WriteLine ($"Compared ActivationCtor for {compared} types ({withActivationCtor} have activation ctors)"); + + LogDiffs ("ACTIVATION CTOR PRESENCE MISMATCHES", presenceMismatches); + LogDiffs ("ACTIVATION CTOR DECLARING TYPE MISMATCHES", declaringTypeMismatches); + LogDiffs ("ACTIVATION CTOR STYLE MISMATCHES", styleMismatches); + + Assert.Empty (presenceMismatches); + Assert.Empty (declaringTypeMismatches); + Assert.Empty (styleMismatches); + } + + [Fact] + public void ExactJavaConstructors_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + var (legacyData, _) = BuildLegacyTypeData (assemblyPath); + var newData = BuildNewTypeData (AllAssemblyPaths); + + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var missingCtors = new List (); + var extraCtors = new List (); + int compared = 0; + int totalCtors = 0; + + foreach (var managedName in allManagedNames.OrderBy (n => n)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + compared++; + + var legacySet = new HashSet (legacy.JavaConstructorSignatures, StringComparer.Ordinal); + var newSet = new HashSet (newInfo.JavaConstructorSignatures, StringComparer.Ordinal); + totalCtors += newSet.Count; + + foreach (var sig in legacySet.Except (newSet)) { + missingCtors.Add ($"{managedName}: missing '{sig}'"); + } + + foreach (var sig in newSet.Except (legacySet)) { + extraCtors.Add ($"{managedName}: extra '{sig}'"); + } + } + + output.WriteLine ($"Compared JavaConstructors for {compared} types ({totalCtors} total constructors)"); + + LogDiffs ("JAVA CONSTRUCTORS MISSING from new scanner", missingCtors); + LogDiffs ("JAVA CONSTRUCTORS EXTRA in new scanner", extraCtors); + + Assert.Empty (missingCtors); + Assert.Empty (extraCtors); + } + + [Fact] + public void ExactTypeFlags_MonoAndroid () + { + var assemblyPath = MonoAndroidAssemblyPath; + + var (legacyData, _) = BuildLegacyTypeData (assemblyPath); + var newData = BuildNewTypeData (AllAssemblyPaths); + + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var interfaceMismatches = new List (); + var abstractMismatches = new List (); + var genericMismatches = new List (); + var acwMismatches = new List (); + int compared = 0; + + foreach (var managedName in allManagedNames.OrderBy (n => n)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + compared++; + + if (legacy.IsInterface != newInfo.IsInterface) { + interfaceMismatches.Add ($"{managedName}: legacy={legacy.IsInterface} new={newInfo.IsInterface}"); + } + + if (legacy.IsAbstract != newInfo.IsAbstract) { + abstractMismatches.Add ($"{managedName}: legacy={legacy.IsAbstract} new={newInfo.IsAbstract}"); + } + + if (legacy.IsGenericDefinition != newInfo.IsGenericDefinition) { + genericMismatches.Add ($"{managedName}: legacy={legacy.IsGenericDefinition} new={newInfo.IsGenericDefinition}"); + } + + if (legacy.DoNotGenerateAcw != newInfo.DoNotGenerateAcw) { + acwMismatches.Add ($"{managedName}: legacy={legacy.DoNotGenerateAcw} new={newInfo.DoNotGenerateAcw}"); + } + } + + output.WriteLine ($"Compared type flags for {compared} types"); + + LogDiffs ("IsInterface MISMATCHES", interfaceMismatches); + LogDiffs ("IsAbstract MISMATCHES", abstractMismatches); + LogDiffs ("IsGenericDefinition MISMATCHES", genericMismatches); + LogDiffs ("DoNotGenerateAcw MISMATCHES", acwMismatches); + + Assert.Empty (interfaceMismatches); + Assert.Empty (abstractMismatches); + Assert.Empty (genericMismatches); + Assert.Empty (acwMismatches); + } + + + record TypeComparisonData ( + string ManagedName, + string JavaName, + string? BaseJavaName, + IReadOnlyList ImplementedInterfaces, + bool HasActivationCtor, + string? ActivationCtorDeclaringType, + string? ActivationCtorStyle, + IReadOnlyList JavaConstructorSignatures, + bool IsInterface, + bool IsAbstract, + bool IsGenericDefinition, + bool DoNotGenerateAcw + ); + + static (Dictionary perType, List entries) BuildLegacyTypeData (string assemblyPath) + { + var cache = new TypeDefinitionCache (); + var resolver = new DefaultAssemblyResolver (); + resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); + + var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location); + if (runtimeDir != null) { + resolver.AddSearchDirectory (runtimeDir); + } + + var readerParams = new ReaderParameters { AssemblyResolver = resolver }; + using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams); + + var scanner = new Xamarin.Android.Tasks.XAJavaTypeScanner ( + Xamarin.Android.Tools.AndroidTargetArch.Arm64, + new TaskLoggingHelper (new MockBuildEngine (), "test"), + cache + ); + + var javaTypes = scanner.GetJavaTypes (assembly); + var (dataSets, _) = Xamarin.Android.Tasks.TypeMapCecilAdapter.GetDebugNativeEntries ( + javaTypes, cache, needUniqueAssemblies: false + ); + + var entries = dataSets.JavaToManaged + .Select (e => new TypeMapEntry (e.JavaName, e.ManagedName, e.SkipInJavaToManaged)) + .OrderBy (e => e.JavaName, StringComparer.Ordinal) + .ThenBy (e => e.ManagedName, StringComparer.Ordinal) + .ToList (); + + var perType = new Dictionary (StringComparer.Ordinal); + + foreach (var typeDef in javaTypes) { + var javaName = GetCecilJavaName (typeDef); + if (javaName == null) { + continue; + } + + // Cecil uses '/' for nested types, SRM uses '+' — normalize + var managedName = $"{typeDef.FullName.Replace ('/', '+')}, {typeDef.Module.Assembly.Name.Name}"; + + // Base Java name + string? baseJavaName = null; + var baseType = typeDef.GetBaseType (cache); + if (baseType != null) { + var baseJni = JavaNativeTypeManager.ToJniName (baseType, cache); + // Filter self-references: ToJniName can return the type's own JNI name + // (e.g., Java.Lang.Object → System.Object → "java/lang/Object"). + if (baseJni != null && baseJni != javaName) { + baseJavaName = baseJni; + } + } + + // Implemented interfaces (only Java peer interfaces with [Register]) + var implementedInterfaces = new List (); + if (typeDef.HasInterfaces) { + foreach (var ifaceImpl in typeDef.Interfaces) { + var ifaceDef = cache.Resolve (ifaceImpl.InterfaceType); + if (ifaceDef == null) { + continue; + } + var ifaceRegs = CecilExtensions.GetTypeRegistrationAttributes (ifaceDef); + var ifaceReg = ifaceRegs.FirstOrDefault (); + if (ifaceReg != null) { + implementedInterfaces.Add (ifaceReg.Name.Replace ('.', '/')); + } + } + } + implementedInterfaces.Sort (StringComparer.Ordinal); + + // Activation constructor + bool hasActivationCtor = false; + string? activationCtorDeclaringType = null; + string? activationCtorStyle = null; + FindLegacyActivationCtor (typeDef, cache, out hasActivationCtor, out activationCtorDeclaringType, out activationCtorStyle); + + // Java constructors: [Register("", sig, ...)] on .ctor methods + var javaCtorSignatures = new List (); + foreach (var method in typeDef.Methods) { + if (!method.IsConstructor || method.IsStatic || !method.HasCustomAttributes) { + continue; + } + foreach (var attr in method.CustomAttributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { + continue; + } + if (attr.ConstructorArguments.Count >= 2) { + var regName = (string) attr.ConstructorArguments [0].Value; + if (regName == "" || regName == ".ctor") { + javaCtorSignatures.Add ((string) attr.ConstructorArguments [1].Value); + } + } + } + } + javaCtorSignatures.Sort (StringComparer.Ordinal); + + // Type flags + var isInterface = typeDef.IsInterface; + var isAbstract = typeDef.IsAbstract && !typeDef.IsInterface; + var isGenericDefinition = typeDef.HasGenericParameters; + var doNotGenerateAcw = GetCecilDoNotGenerateAcw (typeDef); + + perType [managedName] = new TypeComparisonData ( + managedName, + javaName, + baseJavaName, + implementedInterfaces, + hasActivationCtor, + activationCtorDeclaringType, + activationCtorStyle, + javaCtorSignatures, + isInterface, + isAbstract, + isGenericDefinition, + doNotGenerateAcw + ); + } + + return (perType, entries); + } + + static void FindLegacyActivationCtor (TypeDefinition typeDef, TypeDefinitionCache cache, + out bool found, out string? declaringType, out string? style) + { + found = false; + declaringType = null; + style = null; + + // Walk from current type up through base types + TypeDefinition? current = typeDef; + while (current != null) { + foreach (var method in current.Methods) { + if (!method.IsConstructor || method.IsStatic || method.Parameters.Count != 2) { + continue; + } + + var p0 = method.Parameters [0].ParameterType.FullName; + var p1 = method.Parameters [1].ParameterType.FullName; + + if (p0 == "System.IntPtr" && p1 == "Android.Runtime.JniHandleOwnership") { + found = true; + declaringType = $"{current.FullName.Replace ('/', '+')}, {current.Module.Assembly.Name.Name}"; + style = "XamarinAndroid"; + return; + } + + if ((p0 == "Java.Interop.JniObjectReference&" || p0 == "Java.Interop.JniObjectReference") && + p1 == "Java.Interop.JniObjectReferenceOptions") { + found = true; + declaringType = $"{current.FullName.Replace ('/', '+')}, {current.Module.Assembly.Name.Name}"; + style = "JavaInterop"; + return; + } + } + + current = current.GetBaseType (cache); + } + } + + static bool GetCecilDoNotGenerateAcw (TypeDefinition typeDef) + { + if (!typeDef.HasCustomAttributes) { + return false; + } + + foreach (var attr in typeDef.CustomAttributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { + continue; + } + if (attr.HasProperties) { + foreach (var prop in attr.Properties.Where (p => p.Name == "DoNotGenerateAcw")) { + if (prop.Argument.Value is bool val) { + return val; + } + } + } + // [Register] found but DoNotGenerateAcw not set — defaults to false + return false; + } + + return false; + } + + static Dictionary BuildNewTypeData (string[] assemblyPaths) + { + var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]); + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (assemblyPaths); + + var perType = new Dictionary (StringComparer.Ordinal); + + foreach (var peer in peers) { + // Only include types from the primary assembly + if (peer.AssemblyName != primaryAssemblyName) { + continue; + } + + var managedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + + // Map ActivationCtor + bool hasActivationCtor = peer.ActivationCtor != null; + string? activationCtorDeclaringType = null; + string? activationCtorStyle = null; + if (peer.ActivationCtor != null) { + activationCtorDeclaringType = $"{peer.ActivationCtor.DeclaringTypeName}, {peer.ActivationCtor.DeclaringAssemblyName}"; + activationCtorStyle = peer.ActivationCtor.Style.ToString (); + } + + // Java constructor signatures (sorted) — derived from constructor marshal methods + var javaCtorSignatures = peer.MarshalMethods + .Where (m => m.IsConstructor) + .Select (m => m.JniSignature) + .OrderBy (s => s, StringComparer.Ordinal) + .ToList (); + + // Implemented interfaces (sorted) + var implementedInterfaces = peer.ImplementedInterfaceJavaNames + .OrderBy (i => i, StringComparer.Ordinal) + .ToList (); + + perType [managedName] = new TypeComparisonData ( + managedName, + peer.JavaName, + peer.BaseJavaName, + implementedInterfaces, + hasActivationCtor, + activationCtorDeclaringType, + activationCtorStyle, + javaCtorSignatures, + peer.IsInterface, + peer.IsAbstract && !peer.IsInterface, // Match legacy: isAbstract excludes interfaces + peer.IsGenericDefinition, + peer.DoNotGenerateAcw + ); + } + + return perType; + } + + static string MonoAndroidAssemblyPath { + get { + // Compile-time check: this ensures the Mono.Android reference is properly configured. + // It's never actually evaluated at runtime — it just validates the build setup. + _ = nameof (Java.Lang.Object); + + // At runtime, find the Mono.Android.dll copy in the test output directory. + var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location)!; + var path = Path.Combine (testDir, "Mono.Android.dll"); + + if (!File.Exists (path)) { + throw new InvalidOperationException ( + $"Mono.Android.dll not found at '{path}'. " + + "Ensure Mono.Android is built (bin/Debug/lib/packs/Microsoft.Android.Ref.*)."); + } + + return path; + } + } + + static string[] AllAssemblyPaths { + get { + var monoAndroidPath = MonoAndroidAssemblyPath; + var dir = Path.GetDirectoryName (monoAndroidPath)!; + var javaInteropPath = Path.Combine (dir, "Java.Interop.dll"); + + if (!File.Exists (javaInteropPath)) { + return new [] { monoAndroidPath }; + } + + return new [] { monoAndroidPath, javaInteropPath }; + } + } + + static string NormalizeCrc64 (string javaName) + { + if (javaName.StartsWith ("crc64", StringComparison.Ordinal)) { + int slash = javaName.IndexOf ('/'); + if (slash > 0) { + return "crc64.../" + javaName.Substring (slash + 1); + } + } + return javaName; + } + + static string? UserTypesFixturePath { + get { + var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location)!; + var path = Path.Combine (testDir, "UserTypesFixture.dll"); + return File.Exists (path) ? path : null; + } + } + + static string[]? AllUserTypesAssemblyPaths { + get { + var fixturePath = UserTypesFixturePath; + if (fixturePath == null) { + return null; + } + + var dir = Path.GetDirectoryName (fixturePath)!; + var monoAndroidPath = Path.Combine (dir, "Mono.Android.dll"); + var javaInteropPath = Path.Combine (dir, "Java.Interop.dll"); + + var paths = new List { fixturePath }; + if (File.Exists (monoAndroidPath)) { + paths.Add (monoAndroidPath); + } + if (File.Exists (javaInteropPath)) { + paths.Add (javaInteropPath); + } + return paths.ToArray (); + } + } + + [Fact] + public void ExactTypeMap_UserTypesFixture () + { + var paths = AllUserTypesAssemblyPaths; + Assert.NotNull (paths); + + var fixturePath = paths! [0]; + var (legacy, _) = RunLegacyScanner (fixturePath); + var (newEntries, _) = RunNewScanner (paths); + + output.WriteLine ($"UserTypesFixture: Legacy={legacy.Count} entries, New={newEntries.Count} entries"); + + // Normalize CRC64 hashes — the two scanners use different polynomials + var legacyNormalized = legacy.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); + var newNormalized = newEntries.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); + + AssertTypeMapMatch (legacyNormalized, newNormalized); + } + + [Fact] + public void ExactMarshalMethods_UserTypesFixture () + { + var paths = AllUserTypesAssemblyPaths; + Assert.NotNull (paths); + + var fixturePath = paths! [0]; + var (_, legacyMethods) = RunLegacyScanner (fixturePath); + var (_, newMethods) = RunNewScanner (paths); + + // Normalize CRC64 hashes in method group keys + var legacyNormalized = legacyMethods + .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); + var newNormalized = newMethods + .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); + + output.WriteLine ($"UserTypesFixture: Legacy={legacyNormalized.Count} types with methods, New={newNormalized.Count}"); + + // Only compare types that the legacy scanner found (it skips user types without [Register]) + var missing = new List (); + var methodMismatches = new List (); + + foreach (var javaName in legacyNormalized.Keys.OrderBy (n => n)) { + if (!newNormalized.TryGetValue (javaName, out var newGroups)) { + missing.Add (javaName); + continue; + } + + var legacyGroups = legacyNormalized [javaName]; + + foreach (var legacyGroup in legacyGroups) { + var newGroup = newGroups.FirstOrDefault (g => g.ManagedName == legacyGroup.ManagedName); + if (newGroup == null) { + missing.Add ($"{javaName} → {legacyGroup.ManagedName}"); + continue; + } + + // Legacy test helper only extracts [Register] methods, not [Export] methods. + // When legacy has 0 methods (from the typemap fallback path) but new has some, + // the new scanner is correct — it handles [Export] too. Skip comparison. + if (legacyGroup.Methods.Count == 0) { + continue; + } + + if (legacyGroup.Methods.Count != newGroup.Methods.Count) { + methodMismatches.Add ($"{javaName}/{legacyGroup.ManagedName}: legacy={legacyGroup.Methods.Count} methods, new={newGroup.Methods.Count}"); + continue; + } + + for (int i = 0; i < legacyGroup.Methods.Count; i++) { + var lm = legacyGroup.Methods [i]; + var nm = newGroup.Methods [i]; + if (lm.JniName != nm.JniName || lm.JniSignature != nm.JniSignature) { + methodMismatches.Add ($"{javaName}: [{i}] legacy=({lm.JniName}, {lm.JniSignature}) new=({nm.JniName}, {nm.JniSignature})"); + } + } + } + } + + LogDiffs ("MISSING from new scanner", missing); + LogDiffs ("METHOD MISMATCHES", methodMismatches); + + Assert.Empty (missing); + Assert.Empty (methodMismatches); + } + + void AssertTypeMapMatch (List legacy, List newEntries) + { + var legacyMap = legacy.GroupBy (e => e.JavaName).ToDictionary (g => g.Key, g => g.ToList ()); + var newMap = newEntries.GroupBy (e => e.JavaName).ToDictionary (g => g.Key, g => g.ToList ()); + + var allJavaNames = new HashSet (legacyMap.Keys); + allJavaNames.UnionWith (newMap.Keys); + + var missing = new List (); + var extra = new List (); + var managedNameMismatches = new List (); + var skipMismatches = new List (); + + foreach (var javaName in allJavaNames.OrderBy (n => n)) { + var inLegacy = legacyMap.TryGetValue (javaName, out var legacyEntries); + var inNew = newMap.TryGetValue (javaName, out var newEntriesForName); + + if (inLegacy && !inNew) { + foreach (var e in legacyEntries!) + missing.Add ($"{e.JavaName} → {e.ManagedName} (skip={e.SkipInJavaToManaged})"); + continue; + } + + if (!inLegacy && inNew) { + foreach (var e in newEntriesForName!) + extra.Add ($"{e.JavaName} → {e.ManagedName} (skip={e.SkipInJavaToManaged})"); + continue; + } + + var le = legacyEntries!.OrderBy (e => e.ManagedName).First (); + var ne = newEntriesForName!.OrderBy (e => e.ManagedName).First (); + + if (le.ManagedName != ne.ManagedName) + managedNameMismatches.Add ($"{javaName}: legacy='{le.ManagedName}' new='{ne.ManagedName}'"); + + if (le.SkipInJavaToManaged != ne.SkipInJavaToManaged) + skipMismatches.Add ($"{javaName}: legacy.skip={le.SkipInJavaToManaged} new.skip={ne.SkipInJavaToManaged}"); + } + + LogDiffs ("MISSING", missing); + LogDiffs ("EXTRA", extra); + LogDiffs ("MANAGED NAME MISMATCHES", managedNameMismatches); + LogDiffs ("SKIP FLAG MISMATCHES", skipMismatches); + + Assert.Empty (missing); + Assert.Empty (extra); + Assert.Empty (managedNameMismatches); + Assert.Empty (skipMismatches); + } + + void LogDiffs (string label, List items) + { + if (items.Count == 0) return; + output.WriteLine ($"\n--- {label} ({items.Count}) ---"); + foreach (var item in items) output.WriteLine ($" {item}"); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs new file mode 100644 index 00000000000..291137278cb --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs @@ -0,0 +1,176 @@ +// User-type test fixture assembly that references REAL Mono.Android. +// Exercises edge cases that MCW binding assemblies don't have: +// - User types extending Java peers without [Register] +// - Component attributes ([Activity], [Service], etc.) +// - [Export] methods +// - Nested user types +// - Generic user types + +using System; +using System.Runtime.Versioning; +using Android.App; +using Android.Content; +using Android.Runtime; +using Java.Interop; + +[assembly: SupportedOSPlatform ("android21.0")] + +// --- User Activity with explicit Name --- + +namespace UserApp +{ + [Activity (Name = "com.example.userapp.MainActivity", MainLauncher = true, Label = "User App")] + public class MainActivity : Activity + { + protected override void OnCreate (Android.OS.Bundle? savedInstanceState) + { + base.OnCreate (savedInstanceState); + } + } + + // Activity WITHOUT explicit Name — should get CRC64-based JNI name + [Activity (Label = "Settings")] + public class SettingsActivity : Activity + { + } + + // Simple Activity subclass — no attributes at all, just extends a Java peer + public class PlainActivity : Activity + { + } +} + +// --- Services --- + +namespace UserApp.Services +{ + [Service (Name = "com.example.userapp.MyBackgroundService")] + public class MyBackgroundService : Android.App.Service + { + public override Android.OS.IBinder? OnBind (Android.Content.Intent? intent) => null; + } + + // Service without explicit Name + [Service] + public class UnnamedService : Android.App.Service + { + public override Android.OS.IBinder? OnBind (Android.Content.Intent? intent) => null; + } +} + +// --- BroadcastReceiver --- + +namespace UserApp.Receivers +{ + [BroadcastReceiver (Name = "com.example.userapp.BootReceiver", Exported = false)] + public class BootReceiver : BroadcastReceiver + { + public override void OnReceive (Context? context, Intent? intent) + { + } + } +} + +// --- Application with BackupAgent --- + +namespace UserApp +{ + public class MyBackupAgent : Android.App.Backup.BackupAgent + { + public override void OnBackup (Android.OS.ParcelFileDescriptor? oldState, + Android.App.Backup.BackupDataOutput? data, + Android.OS.ParcelFileDescriptor? newState) + { + } + + public override void OnRestore (Android.App.Backup.BackupDataInput? data, + int appVersionCode, + Android.OS.ParcelFileDescriptor? newState) + { + } + } + + [Application (Name = "com.example.userapp.MyApp", BackupAgent = typeof (MyBackupAgent))] + public class MyApp : Application + { + public MyApp (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +// --- Nested types --- + +namespace UserApp.Nested +{ + [Register ("com/example/userapp/OuterClass")] + public class OuterClass : Java.Lang.Object + { + // Nested class inheriting from Java peer — no [Register] + public class InnerHelper : Java.Lang.Object + { + } + + // Deeply nested + public class MiddleClass : Java.Lang.Object + { + public class DeepHelper : Java.Lang.Object + { + } + } + } +} + +// --- Plain Java.Lang.Object subclasses (no attributes) --- + +namespace UserApp.Models +{ + // These should all get CRC64-based JNI names + public class UserModel : Java.Lang.Object + { + } + + public class DataManager : Java.Lang.Object + { + } +} + +// --- Explicit [Register] on user type --- + +namespace UserApp +{ + [Register ("com/example/userapp/CustomView")] + public class CustomView : Android.Views.View + { + protected CustomView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +// --- Interface implementation --- + +namespace UserApp.Listeners +{ + public class MyClickListener : Java.Lang.Object, Android.Views.View.IOnClickListener + { + public void OnClick (Android.Views.View? v) + { + } + } +} + +// --- [Export] method --- + +namespace UserApp +{ + public class ExportedMethodHolder : Java.Lang.Object + { + [Export ("doWork")] + public void DoWork () + { + } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj new file mode 100644 index 00000000000..bba3496f276 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj @@ -0,0 +1,45 @@ + + + + + $(DotNetTargetFramework) + latest + enable + false + Library + true + ..\..\..\product.snk + + ..\..\..\bin\Test$(Configuration)\ + + + + + + + + + + <_MonoAndroidRefCandidate Include="$(MSBuildThisFileDirectory)..\..\..\bin\$(Configuration)\lib\packs\Microsoft.Android.Ref.*\*\ref\net*\Mono.Android.dll" /> + <_JavaInteropRefCandidate Include="$(MSBuildThisFileDirectory)..\..\..\bin\$(Configuration)\lib\packs\Microsoft.Android.Ref.*\*\ref\net*\Java.Interop.dll" /> + + + <_MonoAndroidRefAssembly>@(_MonoAndroidRefCandidate, ';') + <_MonoAndroidRefAssembly>$(_MonoAndroidRefAssembly.Split(';')[0]) + <_JavaInteropRefAssembly>@(_JavaInteropRefCandidate, ';') + <_JavaInteropRefAssembly>$(_JavaInteropRefAssembly.Split(';')[0]) + + + + $(_MonoAndroidRefAssembly) + + + + + $(_JavaInteropRefAssembly) + + + + + From 947cb5403226eb41f83b5eb92f824df9e04f6b50 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 16 Feb 2026 17:46:09 +0100 Subject: [PATCH 2/8] [TrimmableTypeMap] Wire integration tests into solution and CI Add IntegrationTests and UserTypesFixture projects to Xamarin.Android.sln. Add CI step to run integration tests and publish results. Add InternalsVisibleTo for the integration test assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Xamarin.Android.sln | 14 +++++++++++ .../yaml-templates/build-windows-steps.yaml | 14 +++++++++++ .../Properties/AssemblyInfo.cs | 1 + .../ScannerComparisonTests.cs | 25 +++++++++++-------- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/Xamarin.Android.sln b/Xamarin.Android.sln index d5554ea849a..48ee13d6a66 100644 --- a/Xamarin.Android.sln +++ b/Xamarin.Android.sln @@ -65,6 +65,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.Trimm EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestFixtures", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests\TestFixtures\TestFixtures.csproj", "{C5A44686-3469-45A7-B6AB-2798BA0625BC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj", "{A14CB0A1-7A05-4F27-88B2-383798CE1DEE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserTypesFixture", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests\UserTypesFixture\UserTypesFixture.csproj", "{2498F8A0-AA04-40EF-8691-59BBD2396B4D}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "class-parse", "external\Java.Interop\tools\class-parse\class-parse.csproj", "{38C762AB-8FD1-44DE-9855-26AAE7129DC3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "logcat-parse", "external\Java.Interop\tools\logcat-parse\logcat-parse.csproj", "{7387E151-48E3-4885-B2CA-A74434A34045}" @@ -249,6 +253,14 @@ Global {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Debug|AnyCPU.Build.0 = Debug|Any CPU {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Release|AnyCPU.ActiveCfg = Release|Any CPU {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Release|AnyCPU.Build.0 = Release|Any CPU + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Release|AnyCPU.Build.0 = Release|Any CPU + {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Release|AnyCPU.Build.0 = Release|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Debug|AnyCPU.Build.0 = Debug|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Release|AnyCPU.ActiveCfg = Release|Any CPU @@ -418,6 +430,8 @@ Global {53E4ABF0-1085-45F9-B964-DCAE4B819998} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} {F9CD012E-67AC-4A4E-B2A7-252387F91256} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} {C5A44686-3469-45A7-B6AB-2798BA0625BC} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} + {2498F8A0-AA04-40EF-8691-59BBD2396B4D} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} {38C762AB-8FD1-44DE-9855-26AAE7129DC3} = {864062D3-A415-4A6F-9324-5820237BA058} {7387E151-48E3-4885-B2CA-A74434A34045} = {864062D3-A415-4A6F-9324-5820237BA058} {8A6CB07C-E493-4A4F-AB94-038645A27118} = {E351F97D-EA4F-4E7F-AAA0-8EBB1F2A4A62} diff --git a/build-tools/automation/yaml-templates/build-windows-steps.yaml b/build-tools/automation/yaml-templates/build-windows-steps.yaml index 48544d84d7a..76ea70b7f36 100644 --- a/build-tools/automation/yaml-templates/build-windows-steps.yaml +++ b/build-tools/automation/yaml-templates/build-windows-steps.yaml @@ -92,6 +92,20 @@ steps: testResultsFiles: "$(Agent.TempDirectory)/trimmable-typemap-tests/*.trx" testRunTitle: Microsoft.Android.Sdk.TrimmableTypeMap.Tests +- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml@self + parameters: + command: test + project: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.dll + arguments: --logger trx --results-directory $(Agent.TempDirectory)/trimmable-typemap-integration-tests + displayName: Test Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests $(XA.Build.Configuration) + +- task: PublishTestResults@2 + displayName: publish Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests results + condition: always() + inputs: + testResultsFormat: VSTest + testResultsFiles: "$(Agent.TempDirectory)/trimmable-typemap-integration-tests/*.trx" + testRunTitle: Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests - task: BatchScript@1 displayName: Test dotnet-local.cmd - create template inputs: diff --git a/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs b/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs index 0bd860a35e2..e66435ccc40 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs @@ -20,3 +20,4 @@ [assembly: InternalsVisibleTo ("Xamarin.Android.Build.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] [assembly: InternalsVisibleTo ("MSBuildDeviceIntegration, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] +[assembly: InternalsVisibleTo ("Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index 8ee29061de0..6133eaeafca 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -251,7 +251,7 @@ public void ExactMarshalMethods_MonoAndroid () var extraMethods = new List (); var connectorMismatches = new List (); - foreach (var javaName in allJavaNames.OrderBy (n => n)) { + foreach (var javaName in allJavaNames.OrderBy (n => n, StringComparer.Ordinal)) { var inLegacy = legacyMethods.TryGetValue (javaName, out var legacyGroups); var inNew = newMethods.TryGetValue (javaName, out var newGroups); @@ -378,7 +378,7 @@ public void ExactBaseJavaNames_MonoAndroid () var mismatches = new List (); int compared = 0; - foreach (var managedName in allManagedNames.OrderBy (n => n)) { + foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { var legacy = legacyData [managedName]; var newInfo = newData [managedName]; @@ -433,7 +433,7 @@ public void ExactImplementedInterfaces_MonoAndroid () var extraInterfaces = new List (); int compared = 0; - foreach (var managedName in allManagedNames.OrderBy (n => n)) { + foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { var legacy = legacyData [managedName]; var newInfo = newData [managedName]; @@ -477,7 +477,7 @@ public void ExactActivationCtors_MonoAndroid () int compared = 0; int withActivationCtor = 0; - foreach (var managedName in allManagedNames.OrderBy (n => n)) { + foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { var legacy = legacyData [managedName]; var newInfo = newData [managedName]; @@ -530,7 +530,7 @@ public void ExactJavaConstructors_MonoAndroid () int compared = 0; int totalCtors = 0; - foreach (var managedName in allManagedNames.OrderBy (n => n)) { + foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { var legacy = legacyData [managedName]; var newInfo = newData [managedName]; @@ -575,7 +575,7 @@ public void ExactTypeFlags_MonoAndroid () var acwMismatches = new List (); int compared = 0; - foreach (var managedName in allManagedNames.OrderBy (n => n)) { + foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { var legacy = legacyData [managedName]; var newInfo = newData [managedName]; @@ -874,7 +874,8 @@ static string MonoAndroidAssemblyPath { _ = nameof (Java.Lang.Object); // At runtime, find the Mono.Android.dll copy in the test output directory. - var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location)!; + var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location) + ?? throw new InvalidOperationException ("Could not determine test assembly directory."); var path = Path.Combine (testDir, "Mono.Android.dll"); if (!File.Exists (path)) { @@ -890,7 +891,8 @@ static string MonoAndroidAssemblyPath { static string[] AllAssemblyPaths { get { var monoAndroidPath = MonoAndroidAssemblyPath; - var dir = Path.GetDirectoryName (monoAndroidPath)!; + var dir = Path.GetDirectoryName (monoAndroidPath) + ?? throw new InvalidOperationException ("Could not determine Mono.Android directory."); var javaInteropPath = Path.Combine (dir, "Java.Interop.dll"); if (!File.Exists (javaInteropPath)) { @@ -914,7 +916,8 @@ static string NormalizeCrc64 (string javaName) static string? UserTypesFixturePath { get { - var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location)!; + var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location) + ?? throw new InvalidOperationException ("Could not determine test assembly directory."); var path = Path.Combine (testDir, "UserTypesFixture.dll"); return File.Exists (path) ? path : null; } @@ -983,7 +986,7 @@ public void ExactMarshalMethods_UserTypesFixture () var missing = new List (); var methodMismatches = new List (); - foreach (var javaName in legacyNormalized.Keys.OrderBy (n => n)) { + foreach (var javaName in legacyNormalized.Keys.OrderBy (n => n, StringComparer.Ordinal)) { if (!newNormalized.TryGetValue (javaName, out var newGroups)) { missing.Add (javaName); continue; @@ -1040,7 +1043,7 @@ void AssertTypeMapMatch (List legacy, List newEntrie var managedNameMismatches = new List (); var skipMismatches = new List (); - foreach (var javaName in allJavaNames.OrderBy (n => n)) { + foreach (var javaName in allJavaNames.OrderBy (n => n, StringComparer.Ordinal)) { var inLegacy = legacyMap.TryGetValue (javaName, out var legacyEntries); var inNew = newMap.TryGetValue (javaName, out var newEntriesForName); From c25d830f98fbccd19af0336974db74e8eb80a701 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 18 Feb 2026 16:52:09 -0600 Subject: [PATCH 3/8] Update build-tools/automation/yaml-templates/build-windows-steps.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- build-tools/automation/yaml-templates/build-windows-steps.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/build-tools/automation/yaml-templates/build-windows-steps.yaml b/build-tools/automation/yaml-templates/build-windows-steps.yaml index 76ea70b7f36..2227e7ae8fc 100644 --- a/build-tools/automation/yaml-templates/build-windows-steps.yaml +++ b/build-tools/automation/yaml-templates/build-windows-steps.yaml @@ -106,6 +106,7 @@ steps: testResultsFormat: VSTest testResultsFiles: "$(Agent.TempDirectory)/trimmable-typemap-integration-tests/*.trx" testRunTitle: Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests + continueOnError: true - task: BatchScript@1 displayName: Test dotnet-local.cmd - create template inputs: From 42d36adb4a8b267094738b3baad764729f2f79eb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Feb 2026 10:08:00 +0100 Subject: [PATCH 4/8] [TrimmableTypeMap] Expose scanner internals to integration tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Properties/AssemblyInfo.cs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Properties/AssemblyInfo.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Properties/AssemblyInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..284ce4f5671 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo ("Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] From 1592d1ff5fde3f44f6693203d20ac4c5f6d61d10 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Feb 2026 16:32:02 +0100 Subject: [PATCH 5/8] [TrimmableTypeMap][Tests] Simplify scanner comparison tests Remove test logging/noise, extract reusable helpers, and split long comparison flows into focused helper methods. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ScannerComparisonTests.cs | 426 +++++++----------- 1 file changed, 164 insertions(+), 262 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index 6133eaeafca..d113ec88c3b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -8,25 +8,30 @@ using Mono.Cecil; using Xamarin.Android.Tasks; using Xunit; -using Xunit.Abstractions; namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; public class ScannerComparisonTests { - readonly ITestOutputHelper output; - - public ScannerComparisonTests (ITestOutputHelper output) - { - this.output = output; - } - record TypeMapEntry (string JavaName, string ManagedName, bool SkipInJavaToManaged); record MethodEntry (string JniName, string JniSignature, string? Connector); record TypeMethodGroup (string ManagedName, List Methods); + record MarshalMethodComparisonResult ( + List MissingTypes, + List ExtraTypes, + List MissingMethods, + List ExtraMethods, + List ConnectorMismatches + ); + + record UserTypesMethodComparisonResult ( + List Missing, + List MethodMismatches + ); + static (List entries, Dictionary> methodsByJavaName) RunLegacyScanner (string assemblyPath) { var cache = new TypeDefinitionCache (); @@ -58,9 +63,6 @@ record TypeMethodGroup (string ManagedName, List Methods); .ThenBy (e => e.ManagedName, StringComparer.Ordinal) .ToList (); - // Extract method-level [Register] attributes from each TypeDefinition. - // Use the raw javaTypes list to get ALL types — multiple managed types - // can map to the same JNI name (aliases). var methodsByJavaName = new Dictionary> (); foreach (var typeDef in javaTypes) { var javaName = GetCecilJavaName (typeDef); @@ -68,8 +70,7 @@ record TypeMethodGroup (string ManagedName, List Methods); continue; } - // Cecil uses '/' for nested types, SRM uses '+' (CLR format) — normalize - var managedName = $"{typeDef.FullName.Replace ('/', '+')}, {typeDef.Module.Assembly.Name.Name}"; + var managedName = GetManagedName (typeDef); var methods = ExtractMethodRegistrations (typeDef); if (!methodsByJavaName.TryGetValue (javaName, out var groups)) { @@ -85,9 +86,6 @@ record TypeMethodGroup (string ManagedName, List Methods); )); } - // Some types appear in dataSets.JavaToManaged (the typemap) but not in - // javaTypes (the raw list). Include them with empty method lists so the - // comparison covers all types known to the legacy scanner. foreach (var entry in dataSets.JavaToManaged) { if (methodsByJavaName.ContainsKey (entry.JavaName)) { continue; @@ -124,59 +122,46 @@ static List ExtractMethodRegistrations (TypeDefinition typeDef) { var methods = new List (); - // Collect [Register] from methods directly foreach (var method in typeDef.Methods) { if (!method.HasCustomAttributes) { continue; } - foreach (var attr in method.CustomAttributes) { - if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { - continue; - } - - if (attr.ConstructorArguments.Count < 2) { - continue; - } - - var jniMethodName = (string) attr.ConstructorArguments [0].Value; - var jniSignature = (string) attr.ConstructorArguments [1].Value; - var connector = attr.ConstructorArguments.Count > 2 - ? (string) attr.ConstructorArguments [2].Value - : null; - - methods.Add (new MethodEntry (jniMethodName, jniSignature, connector)); - } + AddRegisterMethods (method.CustomAttributes, methods); } - // Collect [Register] from properties (attribute is on the property, not the getter/setter) if (typeDef.HasProperties) { foreach (var prop in typeDef.Properties) { if (!prop.HasCustomAttributes) { continue; } - foreach (var attr in prop.CustomAttributes) { - if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { - continue; - } + AddRegisterMethods (prop.CustomAttributes, methods); + } + } - if (attr.ConstructorArguments.Count < 2) { - continue; - } + return methods; + } - var jniMethodName = (string) attr.ConstructorArguments [0].Value; - var jniSignature = (string) attr.ConstructorArguments [1].Value; - var connector = attr.ConstructorArguments.Count > 2 - ? (string) attr.ConstructorArguments [2].Value - : null; + static string GetManagedName (TypeDefinition typeDef) + { + return $"{typeDef.FullName.Replace ('/', '+')}, {typeDef.Module.Assembly.Name.Name}"; + } - methods.Add (new MethodEntry (jniMethodName, jniSignature, connector)); - } + static void AddRegisterMethods (IEnumerable attributes, List methods) + { + foreach (var attr in attributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute" || attr.ConstructorArguments.Count < 2) { + continue; } - } - return methods; + var jniMethodName = (string) attr.ConstructorArguments [0].Value; + var jniSignature = (string) attr.ConstructorArguments [1].Value; + var connector = attr.ConstructorArguments.Count > 2 + ? (string) attr.ConstructorArguments [2].Value + : null; + methods.Add (new MethodEntry (jniMethodName, jniSignature, connector)); + } } static (List entries, Dictionary> methodsByJavaName) RunNewScanner (string[] assemblyPaths) @@ -223,33 +208,37 @@ public void ExactTypeMap_MonoAndroid () { var (legacy, _) = RunLegacyScanner (MonoAndroidAssemblyPath); var (newEntries, _) = RunNewScanner (AllAssemblyPaths); - output.WriteLine ($"Legacy: {legacy.Count} entries, New: {newEntries.Count} entries"); AssertTypeMapMatch (legacy, newEntries); } [Fact] public void ExactMarshalMethods_MonoAndroid () { - var assemblyPath = MonoAndroidAssemblyPath; - - var (_, legacyMethods) = RunLegacyScanner (assemblyPath); + var (_, legacyMethods) = RunLegacyScanner (MonoAndroidAssemblyPath); var (_, newMethods) = RunNewScanner (AllAssemblyPaths); + var result = CompareMarshalMethods (legacyMethods, newMethods); - var legacyTypeCount = legacyMethods.Values.Sum (g => g.Count); - var newTypeCount = newMethods.Values.Sum (g => g.Count); - var legacyMethodCount = legacyMethods.Values.Sum (g => g.Sum (t => t.Methods.Count)); - var newMethodCount = newMethods.Values.Sum (g => g.Sum (t => t.Methods.Count)); - output.WriteLine ($"Legacy: {legacyTypeCount} type groups across {legacyMethods.Count} JNI names, {legacyMethodCount} total methods"); - output.WriteLine ($"New: {newTypeCount} type groups across {newMethods.Count} JNI names, {newMethodCount} total methods"); + AssertNoDiffs ("MANAGED TYPES MISSING from new scanner", result.MissingTypes); + AssertNoDiffs ("MANAGED TYPES EXTRA in new scanner", result.ExtraTypes); + AssertNoDiffs ("METHODS MISSING from new scanner", result.MissingMethods); + AssertNoDiffs ("METHODS EXTRA in new scanner", result.ExtraMethods); + AssertNoDiffs ("CONNECTOR MISMATCHES", result.ConnectorMismatches); + } + static MarshalMethodComparisonResult CompareMarshalMethods ( + Dictionary> legacyMethods, + Dictionary> newMethods) + { var allJavaNames = new HashSet (legacyMethods.Keys); allJavaNames.UnionWith (newMethods.Keys); - var missingTypes = new List (); - var extraTypes = new List (); - var missingMethods = new List (); - var extraMethods = new List (); - var connectorMismatches = new List (); + var result = new MarshalMethodComparisonResult ( + new List (), + new List (), + new List (), + new List (), + new List () + ); foreach (var javaName in allJavaNames.OrderBy (n => n, StringComparer.Ordinal)) { var inLegacy = legacyMethods.TryGetValue (javaName, out var legacyGroups); @@ -257,108 +246,83 @@ public void ExactMarshalMethods_MonoAndroid () if (inLegacy && !inNew) { foreach (var g in legacyGroups!) { - missingTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)"); + result.MissingTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)"); } continue; } if (!inLegacy && inNew) { foreach (var g in newGroups!) { - extraTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)"); + result.ExtraTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)"); } continue; } - // Both scanners found this JNI name — compare managed types within it var legacyByManaged = legacyGroups!.ToDictionary (g => g.ManagedName, g => g.Methods); var newByManaged = newGroups!.ToDictionary (g => g.ManagedName, g => g.Methods); foreach (var managedName in legacyByManaged.Keys.Except (newByManaged.Keys)) { - missingTypes.Add ($"{javaName} → {managedName} ({legacyByManaged [managedName].Count} methods)"); + result.MissingTypes.Add ($"{javaName} → {managedName} ({legacyByManaged [managedName].Count} methods)"); } foreach (var managedName in newByManaged.Keys.Except (legacyByManaged.Keys)) { - extraTypes.Add ($"{javaName} → {managedName} ({newByManaged [managedName].Count} methods)"); + result.ExtraTypes.Add ($"{javaName} → {managedName} ({newByManaged [managedName].Count} methods)"); } - // For managed types present in both, compare their method sets foreach (var managedName in legacyByManaged.Keys.Intersect (newByManaged.Keys)) { - var legacyMethodList = legacyByManaged [managedName]; - var newMethodList = newByManaged [managedName]; - - var legacySet = new HashSet<(string name, string sig)> ( - legacyMethodList.Select (m => (m.JniName, m.JniSignature)) - ); - var newSet = new HashSet<(string name, string sig)> ( - newMethodList.Select (m => (m.JniName, m.JniSignature)) - ); - - foreach (var m in legacySet.Except (newSet)) { - missingMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}"); - } + CompareMethodGroups (javaName, managedName, legacyByManaged [managedName], newByManaged [managedName], result); + } + } - foreach (var m in newSet.Except (legacySet)) { - extraMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}"); - } + return result; + } - // For methods in both, compare connector strings - var legacyByKey = legacyMethodList - .GroupBy (m => (m.JniName, m.JniSignature)) - .ToDictionary (g => g.Key, g => g.First ()); - var newByKey = newMethodList - .GroupBy (m => (m.JniName, m.JniSignature)) - .ToDictionary (g => g.Key, g => g.First ()); - - foreach (var key in legacyByKey.Keys.Intersect (newByKey.Keys)) { - var lc = legacyByKey [key].Connector ?? ""; - var nc = newByKey [key].Connector ?? ""; - if (lc != nc) { - connectorMismatches.Add ($"{javaName} [{managedName}]: {key.JniName}{key.JniSignature} legacy='{lc}' new='{nc}'"); - } - } - } + static void CompareMethodGroups ( + string javaName, + string managedName, + List legacyMethodList, + List newMethodList, + MarshalMethodComparisonResult result) + { + var legacySet = new HashSet<(string name, string sig)> ( + legacyMethodList.Select (m => (m.JniName, m.JniSignature)) + ); + var newSet = new HashSet<(string name, string sig)> ( + newMethodList.Select (m => (m.JniName, m.JniSignature)) + ); + + foreach (var m in legacySet.Except (newSet)) { + result.MissingMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}"); + } + + foreach (var m in newSet.Except (legacySet)) { + result.ExtraMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}"); } - LogDiffs ("MANAGED TYPES MISSING from new scanner", missingTypes); - LogDiffs ("MANAGED TYPES EXTRA in new scanner", extraTypes); - LogDiffs ("METHODS MISSING from new scanner", missingMethods); - LogDiffs ("METHODS EXTRA in new scanner", extraMethods); - LogDiffs ("CONNECTOR MISMATCHES", connectorMismatches); - - Assert.Empty (missingTypes); - Assert.Empty (extraTypes); - Assert.Empty (missingMethods); - Assert.Empty (extraMethods); - Assert.Empty (connectorMismatches); + var legacyByKey = legacyMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + var newByKey = newMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + + foreach (var key in legacyByKey.Keys.Intersect (newByKey.Keys)) { + var lc = legacyByKey [key].Connector ?? ""; + var nc = newByKey [key].Connector ?? ""; + if (lc != nc) { + result.ConnectorMismatches.Add ($"{javaName} [{managedName}]: {key.JniName}{key.JniSignature} legacy='{lc}' new='{nc}'"); + } + } } [Fact] public void ScannerDiagnostics_MonoAndroid () { - var assemblyPath = MonoAndroidAssemblyPath; - using var scanner = new JavaPeerScanner (); - var peers = scanner.Scan (new [] { assemblyPath }); + var peers = scanner.Scan (new [] { MonoAndroidAssemblyPath }); var interfaces = peers.Count (p => p.IsInterface); - var abstracts = peers.Count (p => p.IsAbstract); - var generics = peers.Count (p => p.IsGenericDefinition); - var withMethods = peers.Count (p => p.MarshalMethods.Count > 0); var totalMethods = peers.Sum (p => p.MarshalMethods.Count); - var withConstructors = peers.Count (p => p.MarshalMethods.Any (m => m.IsConstructor)); - var withBase = peers.Count (p => p.BaseJavaName != null); - var withInterfaces = peers.Count (p => p.ImplementedInterfaceJavaNames.Count > 0); - - output.WriteLine ($"Total types: {peers.Count}"); - output.WriteLine ($"Interfaces: {interfaces}"); - output.WriteLine ($"Abstract classes: {abstracts}"); - output.WriteLine ($"Generic defs: {generics}"); - output.WriteLine ($"With marshal methods: {withMethods} ({totalMethods} total methods)"); - output.WriteLine ($"With constructors: {withConstructors}"); - output.WriteLine ($"With base Java: {withBase}"); - output.WriteLine ($"With interfaces: {withInterfaces}"); - - // Mono.Android.dll should have thousands of types Assert.True (peers.Count > 3000, $"Expected >3000 types, got {peers.Count}"); Assert.True (interfaces > 500, $"Expected >500 interfaces, got {interfaces}"); Assert.True (totalMethods > 10000, $"Expected >10000 marshal methods, got {totalMethods}"); @@ -376,14 +340,11 @@ public void ExactBaseJavaNames_MonoAndroid () allManagedNames.IntersectWith (newData.Keys); var mismatches = new List (); - int compared = 0; foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { var legacy = legacyData [managedName]; var newInfo = newData [managedName]; - compared++; - if (legacy.BaseJavaName != newInfo.BaseJavaName) { // Legacy ToJniName can't resolve bases for open generic types (returns null). // Our scanner resolves them correctly. Accept this known difference. @@ -411,11 +372,7 @@ public void ExactBaseJavaNames_MonoAndroid () } } - output.WriteLine ($"Compared BaseJavaName for {compared} types"); - - LogDiffs ("BASE JAVA NAME MISMATCHES", mismatches); - - Assert.Empty (mismatches); + AssertNoDiffs ("BASE JAVA NAME MISMATCHES", mismatches); } [Fact] @@ -431,14 +388,11 @@ public void ExactImplementedInterfaces_MonoAndroid () var missingInterfaces = new List (); var extraInterfaces = new List (); - int compared = 0; foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { var legacy = legacyData [managedName]; var newInfo = newData [managedName]; - compared++; - var legacySet = new HashSet (legacy.ImplementedInterfaces, StringComparer.Ordinal); var newSet = new HashSet (newInfo.ImplementedInterfaces, StringComparer.Ordinal); @@ -451,13 +405,8 @@ public void ExactImplementedInterfaces_MonoAndroid () } } - output.WriteLine ($"Compared ImplementedInterfaces for {compared} types"); - - LogDiffs ("INTERFACES MISSING from new scanner", missingInterfaces); - LogDiffs ("INTERFACES EXTRA in new scanner", extraInterfaces); - - Assert.Empty (missingInterfaces); - Assert.Empty (extraInterfaces); + AssertNoDiffs ("INTERFACES MISSING from new scanner", missingInterfaces); + AssertNoDiffs ("INTERFACES EXTRA in new scanner", extraInterfaces); } [Fact] @@ -474,15 +423,11 @@ public void ExactActivationCtors_MonoAndroid () var presenceMismatches = new List (); var declaringTypeMismatches = new List (); var styleMismatches = new List (); - int compared = 0; - int withActivationCtor = 0; foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { var legacy = legacyData [managedName]; var newInfo = newData [managedName]; - compared++; - if (legacy.HasActivationCtor != newInfo.HasActivationCtor) { presenceMismatches.Add ($"{managedName}: legacy.has={legacy.HasActivationCtor} new.has={newInfo.HasActivationCtor}"); continue; @@ -492,8 +437,6 @@ public void ExactActivationCtors_MonoAndroid () continue; } - withActivationCtor++; - if (legacy.ActivationCtorDeclaringType != newInfo.ActivationCtorDeclaringType) { declaringTypeMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorDeclaringType}' new='{newInfo.ActivationCtorDeclaringType}'"); } @@ -503,15 +446,9 @@ public void ExactActivationCtors_MonoAndroid () } } - output.WriteLine ($"Compared ActivationCtor for {compared} types ({withActivationCtor} have activation ctors)"); - - LogDiffs ("ACTIVATION CTOR PRESENCE MISMATCHES", presenceMismatches); - LogDiffs ("ACTIVATION CTOR DECLARING TYPE MISMATCHES", declaringTypeMismatches); - LogDiffs ("ACTIVATION CTOR STYLE MISMATCHES", styleMismatches); - - Assert.Empty (presenceMismatches); - Assert.Empty (declaringTypeMismatches); - Assert.Empty (styleMismatches); + AssertNoDiffs ("ACTIVATION CTOR PRESENCE MISMATCHES", presenceMismatches); + AssertNoDiffs ("ACTIVATION CTOR DECLARING TYPE MISMATCHES", declaringTypeMismatches); + AssertNoDiffs ("ACTIVATION CTOR STYLE MISMATCHES", styleMismatches); } [Fact] @@ -527,18 +464,13 @@ public void ExactJavaConstructors_MonoAndroid () var missingCtors = new List (); var extraCtors = new List (); - int compared = 0; - int totalCtors = 0; foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { var legacy = legacyData [managedName]; var newInfo = newData [managedName]; - compared++; - var legacySet = new HashSet (legacy.JavaConstructorSignatures, StringComparer.Ordinal); var newSet = new HashSet (newInfo.JavaConstructorSignatures, StringComparer.Ordinal); - totalCtors += newSet.Count; foreach (var sig in legacySet.Except (newSet)) { missingCtors.Add ($"{managedName}: missing '{sig}'"); @@ -549,13 +481,8 @@ public void ExactJavaConstructors_MonoAndroid () } } - output.WriteLine ($"Compared JavaConstructors for {compared} types ({totalCtors} total constructors)"); - - LogDiffs ("JAVA CONSTRUCTORS MISSING from new scanner", missingCtors); - LogDiffs ("JAVA CONSTRUCTORS EXTRA in new scanner", extraCtors); - - Assert.Empty (missingCtors); - Assert.Empty (extraCtors); + AssertNoDiffs ("JAVA CONSTRUCTORS MISSING from new scanner", missingCtors); + AssertNoDiffs ("JAVA CONSTRUCTORS EXTRA in new scanner", extraCtors); } [Fact] @@ -573,14 +500,11 @@ public void ExactTypeFlags_MonoAndroid () var abstractMismatches = new List (); var genericMismatches = new List (); var acwMismatches = new List (); - int compared = 0; foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { var legacy = legacyData [managedName]; var newInfo = newData [managedName]; - compared++; - if (legacy.IsInterface != newInfo.IsInterface) { interfaceMismatches.Add ($"{managedName}: legacy={legacy.IsInterface} new={newInfo.IsInterface}"); } @@ -598,17 +522,10 @@ public void ExactTypeFlags_MonoAndroid () } } - output.WriteLine ($"Compared type flags for {compared} types"); - - LogDiffs ("IsInterface MISMATCHES", interfaceMismatches); - LogDiffs ("IsAbstract MISMATCHES", abstractMismatches); - LogDiffs ("IsGenericDefinition MISMATCHES", genericMismatches); - LogDiffs ("DoNotGenerateAcw MISMATCHES", acwMismatches); - - Assert.Empty (interfaceMismatches); - Assert.Empty (abstractMismatches); - Assert.Empty (genericMismatches); - Assert.Empty (acwMismatches); + AssertNoDiffs ("IsInterface MISMATCHES", interfaceMismatches); + AssertNoDiffs ("IsAbstract MISMATCHES", abstractMismatches); + AssertNoDiffs ("IsGenericDefinition MISMATCHES", genericMismatches); + AssertNoDiffs ("DoNotGenerateAcw MISMATCHES", acwMismatches); } @@ -666,22 +583,17 @@ bool DoNotGenerateAcw continue; } - // Cecil uses '/' for nested types, SRM uses '+' — normalize - var managedName = $"{typeDef.FullName.Replace ('/', '+')}, {typeDef.Module.Assembly.Name.Name}"; + var managedName = GetManagedName (typeDef); - // Base Java name string? baseJavaName = null; var baseType = typeDef.GetBaseType (cache); if (baseType != null) { var baseJni = JavaNativeTypeManager.ToJniName (baseType, cache); - // Filter self-references: ToJniName can return the type's own JNI name - // (e.g., Java.Lang.Object → System.Object → "java/lang/Object"). if (baseJni != null && baseJni != javaName) { baseJavaName = baseJni; } } - // Implemented interfaces (only Java peer interfaces with [Register]) var implementedInterfaces = new List (); if (typeDef.HasInterfaces) { foreach (var ifaceImpl in typeDef.Interfaces) { @@ -698,13 +610,11 @@ bool DoNotGenerateAcw } implementedInterfaces.Sort (StringComparer.Ordinal); - // Activation constructor bool hasActivationCtor = false; string? activationCtorDeclaringType = null; string? activationCtorStyle = null; FindLegacyActivationCtor (typeDef, cache, out hasActivationCtor, out activationCtorDeclaringType, out activationCtorStyle); - // Java constructors: [Register("", sig, ...)] on .ctor methods var javaCtorSignatures = new List (); foreach (var method in typeDef.Methods) { if (!method.IsConstructor || method.IsStatic || !method.HasCustomAttributes) { @@ -724,7 +634,6 @@ bool DoNotGenerateAcw } javaCtorSignatures.Sort (StringComparer.Ordinal); - // Type flags var isInterface = typeDef.IsInterface; var isAbstract = typeDef.IsAbstract && !typeDef.IsInterface; var isGenericDefinition = typeDef.HasGenericParameters; @@ -756,7 +665,6 @@ static void FindLegacyActivationCtor (TypeDefinition typeDef, TypeDefinitionCach declaringType = null; style = null; - // Walk from current type up through base types TypeDefinition? current = typeDef; while (current != null) { foreach (var method in current.Methods) { @@ -769,7 +677,7 @@ static void FindLegacyActivationCtor (TypeDefinition typeDef, TypeDefinitionCach if (p0 == "System.IntPtr" && p1 == "Android.Runtime.JniHandleOwnership") { found = true; - declaringType = $"{current.FullName.Replace ('/', '+')}, {current.Module.Assembly.Name.Name}"; + declaringType = GetManagedName (current); style = "XamarinAndroid"; return; } @@ -777,7 +685,7 @@ static void FindLegacyActivationCtor (TypeDefinition typeDef, TypeDefinitionCach if ((p0 == "Java.Interop.JniObjectReference&" || p0 == "Java.Interop.JniObjectReference") && p1 == "Java.Interop.JniObjectReferenceOptions") { found = true; - declaringType = $"{current.FullName.Replace ('/', '+')}, {current.Module.Assembly.Name.Name}"; + declaringType = GetManagedName (current); style = "JavaInterop"; return; } @@ -804,7 +712,6 @@ static bool GetCecilDoNotGenerateAcw (TypeDefinition typeDef) } } } - // [Register] found but DoNotGenerateAcw not set — defaults to false return false; } @@ -820,14 +727,12 @@ static Dictionary BuildNewTypeData (string[] assembl var perType = new Dictionary (StringComparer.Ordinal); foreach (var peer in peers) { - // Only include types from the primary assembly if (peer.AssemblyName != primaryAssemblyName) { continue; } var managedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; - // Map ActivationCtor bool hasActivationCtor = peer.ActivationCtor != null; string? activationCtorDeclaringType = null; string? activationCtorStyle = null; @@ -836,14 +741,12 @@ static Dictionary BuildNewTypeData (string[] assembl activationCtorStyle = peer.ActivationCtor.Style.ToString (); } - // Java constructor signatures (sorted) — derived from constructor marshal methods var javaCtorSignatures = peer.MarshalMethods .Where (m => m.IsConstructor) .Select (m => m.JniSignature) .OrderBy (s => s, StringComparer.Ordinal) .ToList (); - // Implemented interfaces (sorted) var implementedInterfaces = peer.ImplementedInterfaceJavaNames .OrderBy (i => i, StringComparer.Ordinal) .ToList (); @@ -858,7 +761,7 @@ static Dictionary BuildNewTypeData (string[] assembl activationCtorStyle, javaCtorSignatures, peer.IsInterface, - peer.IsAbstract && !peer.IsInterface, // Match legacy: isAbstract excludes interfaces + peer.IsAbstract && !peer.IsInterface, peer.IsGenericDefinition, peer.DoNotGenerateAcw ); @@ -869,11 +772,8 @@ static Dictionary BuildNewTypeData (string[] assembl static string MonoAndroidAssemblyPath { get { - // Compile-time check: this ensures the Mono.Android reference is properly configured. - // It's never actually evaluated at runtime — it just validates the build setup. _ = nameof (Java.Lang.Object); - // At runtime, find the Mono.Android.dll copy in the test output directory. var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location) ?? throw new InvalidOperationException ("Could not determine test assembly directory."); var path = Path.Combine (testDir, "Mono.Android.dll"); @@ -954,10 +854,6 @@ public void ExactTypeMap_UserTypesFixture () var fixturePath = paths! [0]; var (legacy, _) = RunLegacyScanner (fixturePath); var (newEntries, _) = RunNewScanner (paths); - - output.WriteLine ($"UserTypesFixture: Legacy={legacy.Count} entries, New={newEntries.Count} entries"); - - // Normalize CRC64 hashes — the two scanners use different polynomials var legacyNormalized = legacy.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); var newNormalized = newEntries.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); @@ -974,15 +870,20 @@ public void ExactMarshalMethods_UserTypesFixture () var (_, legacyMethods) = RunLegacyScanner (fixturePath); var (_, newMethods) = RunNewScanner (paths); - // Normalize CRC64 hashes in method group keys var legacyNormalized = legacyMethods .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); var newNormalized = newMethods .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); - output.WriteLine ($"UserTypesFixture: Legacy={legacyNormalized.Count} types with methods, New={newNormalized.Count}"); + var result = CompareUserTypeMarshalMethods (legacyNormalized, newNormalized); + AssertNoDiffs ("MISSING from new scanner", result.Missing); + AssertNoDiffs ("METHOD MISMATCHES", result.MethodMismatches); + } - // Only compare types that the legacy scanner found (it skips user types without [Register]) + static UserTypesMethodComparisonResult CompareUserTypeMarshalMethods ( + Dictionary> legacyNormalized, + Dictionary> newNormalized) + { var missing = new List (); var methodMismatches = new List (); @@ -995,39 +896,42 @@ public void ExactMarshalMethods_UserTypesFixture () var legacyGroups = legacyNormalized [javaName]; foreach (var legacyGroup in legacyGroups) { - var newGroup = newGroups.FirstOrDefault (g => g.ManagedName == legacyGroup.ManagedName); - if (newGroup == null) { - missing.Add ($"{javaName} → {legacyGroup.ManagedName}"); - continue; - } + CompareUserTypeMethodGroup (javaName, legacyGroup, newGroups, missing, methodMismatches); + } + } - // Legacy test helper only extracts [Register] methods, not [Export] methods. - // When legacy has 0 methods (from the typemap fallback path) but new has some, - // the new scanner is correct — it handles [Export] too. Skip comparison. - if (legacyGroup.Methods.Count == 0) { - continue; - } + return new UserTypesMethodComparisonResult (missing, methodMismatches); + } - if (legacyGroup.Methods.Count != newGroup.Methods.Count) { - methodMismatches.Add ($"{javaName}/{legacyGroup.ManagedName}: legacy={legacyGroup.Methods.Count} methods, new={newGroup.Methods.Count}"); - continue; - } + static void CompareUserTypeMethodGroup ( + string javaName, + TypeMethodGroup legacyGroup, + List newGroups, + List missing, + List methodMismatches) + { + var newGroup = newGroups.FirstOrDefault (g => g.ManagedName == legacyGroup.ManagedName); + if (newGroup == null) { + missing.Add ($"{javaName} → {legacyGroup.ManagedName}"); + return; + } - for (int i = 0; i < legacyGroup.Methods.Count; i++) { - var lm = legacyGroup.Methods [i]; - var nm = newGroup.Methods [i]; - if (lm.JniName != nm.JniName || lm.JniSignature != nm.JniSignature) { - methodMismatches.Add ($"{javaName}: [{i}] legacy=({lm.JniName}, {lm.JniSignature}) new=({nm.JniName}, {nm.JniSignature})"); - } - } - } + if (legacyGroup.Methods.Count == 0) { + return; } - LogDiffs ("MISSING from new scanner", missing); - LogDiffs ("METHOD MISMATCHES", methodMismatches); + if (legacyGroup.Methods.Count != newGroup.Methods.Count) { + methodMismatches.Add ($"{javaName}/{legacyGroup.ManagedName}: legacy={legacyGroup.Methods.Count} methods, new={newGroup.Methods.Count}"); + return; + } - Assert.Empty (missing); - Assert.Empty (methodMismatches); + for (int i = 0; i < legacyGroup.Methods.Count; i++) { + var lm = legacyGroup.Methods [i]; + var nm = newGroup.Methods [i]; + if (lm.JniName != nm.JniName || lm.JniSignature != nm.JniSignature) { + methodMismatches.Add ($"{javaName}: [{i}] legacy=({lm.JniName}, {lm.JniSignature}) new=({nm.JniName}, {nm.JniSignature})"); + } + } } void AssertTypeMapMatch (List legacy, List newEntries) @@ -1069,21 +973,19 @@ void AssertTypeMapMatch (List legacy, List newEntrie skipMismatches.Add ($"{javaName}: legacy.skip={le.SkipInJavaToManaged} new.skip={ne.SkipInJavaToManaged}"); } - LogDiffs ("MISSING", missing); - LogDiffs ("EXTRA", extra); - LogDiffs ("MANAGED NAME MISMATCHES", managedNameMismatches); - LogDiffs ("SKIP FLAG MISMATCHES", skipMismatches); - - Assert.Empty (missing); - Assert.Empty (extra); - Assert.Empty (managedNameMismatches); - Assert.Empty (skipMismatches); + AssertNoDiffs ("MISSING", missing); + AssertNoDiffs ("EXTRA", extra); + AssertNoDiffs ("MANAGED NAME MISMATCHES", managedNameMismatches); + AssertNoDiffs ("SKIP FLAG MISMATCHES", skipMismatches); } - void LogDiffs (string label, List items) + static void AssertNoDiffs (string label, List items) { - if (items.Count == 0) return; - output.WriteLine ($"\n--- {label} ({items.Count}) ---"); - foreach (var item in items) output.WriteLine ($" {item}"); + if (items.Count == 0) { + return; + } + + var details = string.Join (Environment.NewLine, items.Take (20).Select (item => $" {item}")); + Assert.True (false, $"{label} ({items.Count}){Environment.NewLine}{details}"); } } From 29e2659dc3749aa854781cbd2f330980eb08c325 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Feb 2026 16:43:39 +0100 Subject: [PATCH 6/8] [TrimmableTypeMap][Tests] Move diffing out of facts Extract list-diff logic from [Fact] methods into ComparisonDiffHelper and remove section-divider comments from UserTypes fixture. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ScannerComparisonTests.cs | 294 ++++++++++-------- .../UserTypesFixture/UserTypes.cs | 18 -- 2 files changed, 164 insertions(+), 148 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index d113ec88c3b..742834f615f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -335,42 +335,7 @@ public void ExactBaseJavaNames_MonoAndroid () var (legacyData, _) = BuildLegacyTypeData (assemblyPath); var newData = BuildNewTypeData (AllAssemblyPaths); - - var allManagedNames = new HashSet (legacyData.Keys); - allManagedNames.IntersectWith (newData.Keys); - - var mismatches = new List (); - - foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { - var legacy = legacyData [managedName]; - var newInfo = newData [managedName]; - - if (legacy.BaseJavaName != newInfo.BaseJavaName) { - // Legacy ToJniName can't resolve bases for open generic types (returns null). - // Our scanner resolves them correctly. Accept this known difference. - if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && managedName.Contains ('`')) { - continue; - } - - // Invokers share JNI names with their base class. Legacy ToJniName - // self-reference filter discards the base (baseJni == javaName), but - // our scanner correctly resolves it. Accept legacy=null, new=valid - // for DoNotGenerateAcw types. - if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && newInfo.DoNotGenerateAcw) { - continue; - } - - // Legacy ToJniName(System.Object) returns "java/lang/Object" as a fallback, - // making Java.Lang.Object/Throwable appear to have themselves as base. - // Our scanner correctly returns null. Accept legacy=self, new=null. - if (legacy.BaseJavaName != null && newInfo.BaseJavaName == null && - legacy.BaseJavaName == legacy.JavaName) { - continue; - } - - mismatches.Add ($"{managedName}: legacy='{legacy.BaseJavaName ?? "(null)"}' new='{newInfo.BaseJavaName ?? "(null)"}'"); - } - } + var mismatches = ComparisonDiffHelper.CompareBaseJavaNames (legacyData, newData); AssertNoDiffs ("BASE JAVA NAME MISMATCHES", mismatches); } @@ -382,28 +347,7 @@ public void ExactImplementedInterfaces_MonoAndroid () var (legacyData, _) = BuildLegacyTypeData (assemblyPath); var newData = BuildNewTypeData (AllAssemblyPaths); - - var allManagedNames = new HashSet (legacyData.Keys); - allManagedNames.IntersectWith (newData.Keys); - - var missingInterfaces = new List (); - var extraInterfaces = new List (); - - foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { - var legacy = legacyData [managedName]; - var newInfo = newData [managedName]; - - var legacySet = new HashSet (legacy.ImplementedInterfaces, StringComparer.Ordinal); - var newSet = new HashSet (newInfo.ImplementedInterfaces, StringComparer.Ordinal); - - foreach (var iface in legacySet.Except (newSet)) { - missingInterfaces.Add ($"{managedName}: missing '{iface}'"); - } - - foreach (var iface in newSet.Except (legacySet)) { - extraInterfaces.Add ($"{managedName}: extra '{iface}'"); - } - } + var (missingInterfaces, extraInterfaces) = ComparisonDiffHelper.CompareImplementedInterfaces (legacyData, newData); AssertNoDiffs ("INTERFACES MISSING from new scanner", missingInterfaces); AssertNoDiffs ("INTERFACES EXTRA in new scanner", extraInterfaces); @@ -416,35 +360,7 @@ public void ExactActivationCtors_MonoAndroid () var (legacyData, _) = BuildLegacyTypeData (assemblyPath); var newData = BuildNewTypeData (AllAssemblyPaths); - - var allManagedNames = new HashSet (legacyData.Keys); - allManagedNames.IntersectWith (newData.Keys); - - var presenceMismatches = new List (); - var declaringTypeMismatches = new List (); - var styleMismatches = new List (); - - foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { - var legacy = legacyData [managedName]; - var newInfo = newData [managedName]; - - if (legacy.HasActivationCtor != newInfo.HasActivationCtor) { - presenceMismatches.Add ($"{managedName}: legacy.has={legacy.HasActivationCtor} new.has={newInfo.HasActivationCtor}"); - continue; - } - - if (!legacy.HasActivationCtor) { - continue; - } - - if (legacy.ActivationCtorDeclaringType != newInfo.ActivationCtorDeclaringType) { - declaringTypeMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorDeclaringType}' new='{newInfo.ActivationCtorDeclaringType}'"); - } - - if (legacy.ActivationCtorStyle != newInfo.ActivationCtorStyle) { - styleMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorStyle}' new='{newInfo.ActivationCtorStyle}'"); - } - } + var (presenceMismatches, declaringTypeMismatches, styleMismatches) = ComparisonDiffHelper.CompareActivationCtors (legacyData, newData); AssertNoDiffs ("ACTIVATION CTOR PRESENCE MISMATCHES", presenceMismatches); AssertNoDiffs ("ACTIVATION CTOR DECLARING TYPE MISMATCHES", declaringTypeMismatches); @@ -458,28 +374,7 @@ public void ExactJavaConstructors_MonoAndroid () var (legacyData, _) = BuildLegacyTypeData (assemblyPath); var newData = BuildNewTypeData (AllAssemblyPaths); - - var allManagedNames = new HashSet (legacyData.Keys); - allManagedNames.IntersectWith (newData.Keys); - - var missingCtors = new List (); - var extraCtors = new List (); - - foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { - var legacy = legacyData [managedName]; - var newInfo = newData [managedName]; - - var legacySet = new HashSet (legacy.JavaConstructorSignatures, StringComparer.Ordinal); - var newSet = new HashSet (newInfo.JavaConstructorSignatures, StringComparer.Ordinal); - - foreach (var sig in legacySet.Except (newSet)) { - missingCtors.Add ($"{managedName}: missing '{sig}'"); - } - - foreach (var sig in newSet.Except (legacySet)) { - extraCtors.Add ($"{managedName}: extra '{sig}'"); - } - } + var (missingCtors, extraCtors) = ComparisonDiffHelper.CompareJavaConstructors (legacyData, newData); AssertNoDiffs ("JAVA CONSTRUCTORS MISSING from new scanner", missingCtors); AssertNoDiffs ("JAVA CONSTRUCTORS EXTRA in new scanner", extraCtors); @@ -492,40 +387,179 @@ public void ExactTypeFlags_MonoAndroid () var (legacyData, _) = BuildLegacyTypeData (assemblyPath); var newData = BuildNewTypeData (AllAssemblyPaths); + var (interfaceMismatches, abstractMismatches, genericMismatches, acwMismatches) = ComparisonDiffHelper.CompareTypeFlags (legacyData, newData); + + AssertNoDiffs ("IsInterface MISMATCHES", interfaceMismatches); + AssertNoDiffs ("IsAbstract MISMATCHES", abstractMismatches); + AssertNoDiffs ("IsGenericDefinition MISMATCHES", genericMismatches); + AssertNoDiffs ("DoNotGenerateAcw MISMATCHES", acwMismatches); + } + + static class ComparisonDiffHelper + { + public static List CompareBaseJavaNames ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var mismatches = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; - var allManagedNames = new HashSet (legacyData.Keys); - allManagedNames.IntersectWith (newData.Keys); + if (legacy.BaseJavaName != newInfo.BaseJavaName) { + if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && managedName.Contains ('`')) { + continue; + } - var interfaceMismatches = new List (); - var abstractMismatches = new List (); - var genericMismatches = new List (); - var acwMismatches = new List (); + if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && newInfo.DoNotGenerateAcw) { + continue; + } - foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { - var legacy = legacyData [managedName]; - var newInfo = newData [managedName]; + if (legacy.BaseJavaName != null && newInfo.BaseJavaName == null && + legacy.BaseJavaName == legacy.JavaName) { + continue; + } - if (legacy.IsInterface != newInfo.IsInterface) { - interfaceMismatches.Add ($"{managedName}: legacy={legacy.IsInterface} new={newInfo.IsInterface}"); + mismatches.Add ($"{managedName}: legacy='{legacy.BaseJavaName ?? "(null)"}' new='{newInfo.BaseJavaName ?? "(null)"}'"); + } } - if (legacy.IsAbstract != newInfo.IsAbstract) { - abstractMismatches.Add ($"{managedName}: legacy={legacy.IsAbstract} new={newInfo.IsAbstract}"); + return mismatches; + } + + public static (List missingInterfaces, List extraInterfaces) CompareImplementedInterfaces ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var missingInterfaces = new List (); + var extraInterfaces = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + var legacySet = new HashSet (legacy.ImplementedInterfaces, StringComparer.Ordinal); + var newSet = new HashSet (newInfo.ImplementedInterfaces, StringComparer.Ordinal); + + foreach (var iface in legacySet.Except (newSet)) { + missingInterfaces.Add ($"{managedName}: missing '{iface}'"); + } + + foreach (var iface in newSet.Except (legacySet)) { + extraInterfaces.Add ($"{managedName}: extra '{iface}'"); + } } - if (legacy.IsGenericDefinition != newInfo.IsGenericDefinition) { - genericMismatches.Add ($"{managedName}: legacy={legacy.IsGenericDefinition} new={newInfo.IsGenericDefinition}"); + return (missingInterfaces, extraInterfaces); + } + + public static (List presenceMismatches, List declaringTypeMismatches, List styleMismatches) CompareActivationCtors ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var presenceMismatches = new List (); + var declaringTypeMismatches = new List (); + var styleMismatches = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + if (legacy.HasActivationCtor != newInfo.HasActivationCtor) { + presenceMismatches.Add ($"{managedName}: legacy.has={legacy.HasActivationCtor} new.has={newInfo.HasActivationCtor}"); + continue; + } + + if (!legacy.HasActivationCtor) { + continue; + } + + if (legacy.ActivationCtorDeclaringType != newInfo.ActivationCtorDeclaringType) { + declaringTypeMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorDeclaringType}' new='{newInfo.ActivationCtorDeclaringType}'"); + } + + if (legacy.ActivationCtorStyle != newInfo.ActivationCtorStyle) { + styleMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorStyle}' new='{newInfo.ActivationCtorStyle}'"); + } } - if (legacy.DoNotGenerateAcw != newInfo.DoNotGenerateAcw) { - acwMismatches.Add ($"{managedName}: legacy={legacy.DoNotGenerateAcw} new={newInfo.DoNotGenerateAcw}"); + return (presenceMismatches, declaringTypeMismatches, styleMismatches); + } + + public static (List missingCtors, List extraCtors) CompareJavaConstructors ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var missingCtors = new List (); + var extraCtors = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + var legacySet = new HashSet (legacy.JavaConstructorSignatures, StringComparer.Ordinal); + var newSet = new HashSet (newInfo.JavaConstructorSignatures, StringComparer.Ordinal); + + foreach (var sig in legacySet.Except (newSet)) { + missingCtors.Add ($"{managedName}: missing '{sig}'"); + } + + foreach (var sig in newSet.Except (legacySet)) { + extraCtors.Add ($"{managedName}: extra '{sig}'"); + } } + + return (missingCtors, extraCtors); } - AssertNoDiffs ("IsInterface MISMATCHES", interfaceMismatches); - AssertNoDiffs ("IsAbstract MISMATCHES", abstractMismatches); - AssertNoDiffs ("IsGenericDefinition MISMATCHES", genericMismatches); - AssertNoDiffs ("DoNotGenerateAcw MISMATCHES", acwMismatches); + public static (List interfaceMismatches, List abstractMismatches, List genericMismatches, List acwMismatches) CompareTypeFlags ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var interfaceMismatches = new List (); + var abstractMismatches = new List (); + var genericMismatches = new List (); + var acwMismatches = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + if (legacy.IsInterface != newInfo.IsInterface) { + interfaceMismatches.Add ($"{managedName}: legacy={legacy.IsInterface} new={newInfo.IsInterface}"); + } + + if (legacy.IsAbstract != newInfo.IsAbstract) { + abstractMismatches.Add ($"{managedName}: legacy={legacy.IsAbstract} new={newInfo.IsAbstract}"); + } + + if (legacy.IsGenericDefinition != newInfo.IsGenericDefinition) { + genericMismatches.Add ($"{managedName}: legacy={legacy.IsGenericDefinition} new={newInfo.IsGenericDefinition}"); + } + + if (legacy.DoNotGenerateAcw != newInfo.DoNotGenerateAcw) { + acwMismatches.Add ($"{managedName}: legacy={legacy.DoNotGenerateAcw} new={newInfo.DoNotGenerateAcw}"); + } + } + + return (interfaceMismatches, abstractMismatches, genericMismatches, acwMismatches); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs index 291137278cb..75586236a8e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs @@ -15,8 +15,6 @@ [assembly: SupportedOSPlatform ("android21.0")] -// --- User Activity with explicit Name --- - namespace UserApp { [Activity (Name = "com.example.userapp.MainActivity", MainLauncher = true, Label = "User App")] @@ -40,8 +38,6 @@ public class PlainActivity : Activity } } -// --- Services --- - namespace UserApp.Services { [Service (Name = "com.example.userapp.MyBackgroundService")] @@ -58,8 +54,6 @@ public class UnnamedService : Android.App.Service } } -// --- BroadcastReceiver --- - namespace UserApp.Receivers { [BroadcastReceiver (Name = "com.example.userapp.BootReceiver", Exported = false)] @@ -71,8 +65,6 @@ public override void OnReceive (Context? context, Intent? intent) } } -// --- Application with BackupAgent --- - namespace UserApp { public class MyBackupAgent : Android.App.Backup.BackupAgent @@ -100,8 +92,6 @@ public MyApp (IntPtr handle, JniHandleOwnership transfer) } } -// --- Nested types --- - namespace UserApp.Nested { [Register ("com/example/userapp/OuterClass")] @@ -122,8 +112,6 @@ public class DeepHelper : Java.Lang.Object } } -// --- Plain Java.Lang.Object subclasses (no attributes) --- - namespace UserApp.Models { // These should all get CRC64-based JNI names @@ -136,8 +124,6 @@ public class DataManager : Java.Lang.Object } } -// --- Explicit [Register] on user type --- - namespace UserApp { [Register ("com/example/userapp/CustomView")] @@ -150,8 +136,6 @@ protected CustomView (IntPtr handle, JniHandleOwnership transfer) } } -// --- Interface implementation --- - namespace UserApp.Listeners { public class MyClickListener : Java.Lang.Object, Android.Views.View.IOnClickListener @@ -162,8 +146,6 @@ public void OnClick (Android.Views.View? v) } } -// --- [Export] method --- - namespace UserApp { public class ExportedMethodHolder : Java.Lang.Object From 5d7b92803134f8a8ffd765aa74e696a9e2017829 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Feb 2026 17:38:05 +0100 Subject: [PATCH 7/8] [TrimmableTypeMap][Tests] Split test helpers into separate files - ScannerRunner.cs: TypeMapEntry/MethodEntry/TypeMethodGroup records + scanner logic - TypeDataBuilder.cs: TypeComparisonData record + Cecil/peer type data builders - MarshalMethodDiffHelper.cs: marshal method comparison logic - ComparisonDiffHelper.cs: per-field type comparison helpers - ScannerComparisonTests.Helpers.cs: partial class with assertion helpers + assembly path properties - ScannerComparisonTests.cs: now contains only [Fact] methods Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ComparisonDiffHelper.cs | 173 +++ .../MarshalMethodDiffHelper.cs | 165 +++ .../ScannerComparisonTests.Helpers.cs | 140 +++ .../ScannerComparisonTests.cs | 1108 ++--------------- .../ScannerRunner.cs | 190 +++ .../TypeDataBuilder.cs | 248 ++++ 6 files changed, 1025 insertions(+), 999 deletions(-) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ComparisonDiffHelper.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MarshalMethodDiffHelper.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ComparisonDiffHelper.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ComparisonDiffHelper.cs new file mode 100644 index 00000000000..d94e932b8ce --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ComparisonDiffHelper.cs @@ -0,0 +1,173 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; + +static class ComparisonDiffHelper +{ + public static List CompareBaseJavaNames ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var mismatches = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, System.StringComparer.Ordinal)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + if (legacy.BaseJavaName == newInfo.BaseJavaName) { + continue; + } + + if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && managedName.Contains ('`')) { + continue; + } + + if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && newInfo.DoNotGenerateAcw) { + continue; + } + + if (legacy.BaseJavaName != null && newInfo.BaseJavaName == null && + legacy.BaseJavaName == legacy.JavaName) { + continue; + } + + mismatches.Add ($"{managedName}: legacy='{legacy.BaseJavaName ?? "(null)"}' new='{newInfo.BaseJavaName ?? "(null)"}'"); + } + + return mismatches; + } + + public static (List missingInterfaces, List extraInterfaces) CompareImplementedInterfaces ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var missingInterfaces = new List (); + var extraInterfaces = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, System.StringComparer.Ordinal)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + var legacySet = new HashSet (legacy.ImplementedInterfaces, System.StringComparer.Ordinal); + var newSet = new HashSet (newInfo.ImplementedInterfaces, System.StringComparer.Ordinal); + + foreach (var iface in legacySet.Except (newSet)) { + missingInterfaces.Add ($"{managedName}: missing '{iface}'"); + } + + foreach (var iface in newSet.Except (legacySet)) { + extraInterfaces.Add ($"{managedName}: extra '{iface}'"); + } + } + + return (missingInterfaces, extraInterfaces); + } + + public static (List presenceMismatches, List declaringTypeMismatches, List styleMismatches) CompareActivationCtors ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var presenceMismatches = new List (); + var declaringTypeMismatches = new List (); + var styleMismatches = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, System.StringComparer.Ordinal)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + if (legacy.HasActivationCtor != newInfo.HasActivationCtor) { + presenceMismatches.Add ($"{managedName}: legacy.has={legacy.HasActivationCtor} new.has={newInfo.HasActivationCtor}"); + continue; + } + + if (!legacy.HasActivationCtor) { + continue; + } + + if (legacy.ActivationCtorDeclaringType != newInfo.ActivationCtorDeclaringType) { + declaringTypeMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorDeclaringType}' new='{newInfo.ActivationCtorDeclaringType}'"); + } + + if (legacy.ActivationCtorStyle != newInfo.ActivationCtorStyle) { + styleMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorStyle}' new='{newInfo.ActivationCtorStyle}'"); + } + } + + return (presenceMismatches, declaringTypeMismatches, styleMismatches); + } + + public static (List missingCtors, List extraCtors) CompareJavaConstructors ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var missingCtors = new List (); + var extraCtors = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, System.StringComparer.Ordinal)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + var legacySet = new HashSet (legacy.JavaConstructorSignatures, System.StringComparer.Ordinal); + var newSet = new HashSet (newInfo.JavaConstructorSignatures, System.StringComparer.Ordinal); + + foreach (var sig in legacySet.Except (newSet)) { + missingCtors.Add ($"{managedName}: missing '{sig}'"); + } + + foreach (var sig in newSet.Except (legacySet)) { + extraCtors.Add ($"{managedName}: extra '{sig}'"); + } + } + + return (missingCtors, extraCtors); + } + + public static (List interfaceMismatches, List abstractMismatches, List genericMismatches, List acwMismatches) CompareTypeFlags ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var interfaceMismatches = new List (); + var abstractMismatches = new List (); + var genericMismatches = new List (); + var acwMismatches = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, System.StringComparer.Ordinal)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + if (legacy.IsInterface != newInfo.IsInterface) { + interfaceMismatches.Add ($"{managedName}: legacy={legacy.IsInterface} new={newInfo.IsInterface}"); + } + + if (legacy.IsAbstract != newInfo.IsAbstract) { + abstractMismatches.Add ($"{managedName}: legacy={legacy.IsAbstract} new={newInfo.IsAbstract}"); + } + + if (legacy.IsGenericDefinition != newInfo.IsGenericDefinition) { + genericMismatches.Add ($"{managedName}: legacy={legacy.IsGenericDefinition} new={newInfo.IsGenericDefinition}"); + } + + if (legacy.DoNotGenerateAcw != newInfo.DoNotGenerateAcw) { + acwMismatches.Add ($"{managedName}: legacy={legacy.DoNotGenerateAcw} new={newInfo.DoNotGenerateAcw}"); + } + } + + return (interfaceMismatches, abstractMismatches, genericMismatches, acwMismatches); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MarshalMethodDiffHelper.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MarshalMethodDiffHelper.cs new file mode 100644 index 00000000000..cea308ee3bc --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MarshalMethodDiffHelper.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; + +record MarshalMethodComparisonResult ( + List MissingTypes, + List ExtraTypes, + List MissingMethods, + List ExtraMethods, + List ConnectorMismatches +); + +record UserTypesMethodComparisonResult ( + List Missing, + List MethodMismatches +); + +static class MarshalMethodDiffHelper +{ + public static MarshalMethodComparisonResult CompareMarshalMethods ( + Dictionary> legacyMethods, + Dictionary> newMethods) + { + var allJavaNames = new HashSet (legacyMethods.Keys); + allJavaNames.UnionWith (newMethods.Keys); + + var result = new MarshalMethodComparisonResult ( + new List (), + new List (), + new List (), + new List (), + new List () + ); + + foreach (var javaName in allJavaNames.OrderBy (n => n, StringComparer.Ordinal)) { + var inLegacy = legacyMethods.TryGetValue (javaName, out var legacyGroups); + var inNew = newMethods.TryGetValue (javaName, out var newGroups); + + if (inLegacy && !inNew) { + foreach (var g in legacyGroups!) { + result.MissingTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)"); + } + continue; + } + + if (!inLegacy && inNew) { + foreach (var g in newGroups!) { + result.ExtraTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)"); + } + continue; + } + + var legacyByManaged = legacyGroups!.ToDictionary (g => g.ManagedName, g => g.Methods); + var newByManaged = newGroups!.ToDictionary (g => g.ManagedName, g => g.Methods); + + foreach (var managedName in legacyByManaged.Keys.Except (newByManaged.Keys)) { + result.MissingTypes.Add ($"{javaName} → {managedName} ({legacyByManaged [managedName].Count} methods)"); + } + + foreach (var managedName in newByManaged.Keys.Except (legacyByManaged.Keys)) { + result.ExtraTypes.Add ($"{javaName} → {managedName} ({newByManaged [managedName].Count} methods)"); + } + + foreach (var managedName in legacyByManaged.Keys.Intersect (newByManaged.Keys)) { + CompareMethodGroups (javaName, managedName, legacyByManaged [managedName], newByManaged [managedName], result); + } + } + + return result; + } + + public static UserTypesMethodComparisonResult CompareUserTypeMarshalMethods ( + Dictionary> legacyNormalized, + Dictionary> newNormalized) + { + var missing = new List (); + var methodMismatches = new List (); + + foreach (var javaName in legacyNormalized.Keys.OrderBy (n => n, StringComparer.Ordinal)) { + if (!newNormalized.TryGetValue (javaName, out var newGroups)) { + missing.Add (javaName); + continue; + } + + var legacyGroups = legacyNormalized [javaName]; + + foreach (var legacyGroup in legacyGroups) { + CompareUserTypeMethodGroup (javaName, legacyGroup, newGroups, missing, methodMismatches); + } + } + + return new UserTypesMethodComparisonResult (missing, methodMismatches); + } + + static void CompareMethodGroups ( + string javaName, + string managedName, + List legacyMethodList, + List newMethodList, + MarshalMethodComparisonResult result) + { + var legacySet = new HashSet<(string name, string sig)> ( + legacyMethodList.Select (m => (m.JniName, m.JniSignature)) + ); + var newSet = new HashSet<(string name, string sig)> ( + newMethodList.Select (m => (m.JniName, m.JniSignature)) + ); + + foreach (var m in legacySet.Except (newSet)) { + result.MissingMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}"); + } + + foreach (var m in newSet.Except (legacySet)) { + result.ExtraMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}"); + } + + var legacyByKey = legacyMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + var newByKey = newMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + + foreach (var key in legacyByKey.Keys.Intersect (newByKey.Keys)) { + var lc = legacyByKey [key].Connector ?? ""; + var nc = newByKey [key].Connector ?? ""; + if (lc != nc) { + result.ConnectorMismatches.Add ($"{javaName} [{managedName}]: {key.JniName}{key.JniSignature} legacy='{lc}' new='{nc}'"); + } + } + } + + static void CompareUserTypeMethodGroup ( + string javaName, + TypeMethodGroup legacyGroup, + List newGroups, + List missing, + List methodMismatches) + { + var newGroup = newGroups.FirstOrDefault (g => g.ManagedName == legacyGroup.ManagedName); + if (newGroup == null) { + missing.Add ($"{javaName} → {legacyGroup.ManagedName}"); + return; + } + + if (legacyGroup.Methods.Count == 0) { + return; + } + + if (legacyGroup.Methods.Count != newGroup.Methods.Count) { + methodMismatches.Add ($"{javaName}/{legacyGroup.ManagedName}: legacy={legacyGroup.Methods.Count} methods, new={newGroup.Methods.Count}"); + return; + } + + for (int i = 0; i < legacyGroup.Methods.Count; i++) { + var lm = legacyGroup.Methods [i]; + var nm = newGroup.Methods [i]; + if (lm.JniName != nm.JniName || lm.JniSignature != nm.JniSignature) { + methodMismatches.Add ($"{javaName}: [{i}] legacy=({lm.JniName}, {lm.JniSignature}) new=({nm.JniName}, {nm.JniSignature})"); + } + } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs new file mode 100644 index 00000000000..c69bbe800fb --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; + +public partial class ScannerComparisonTests +{ + static string MonoAndroidAssemblyPath { + get { + _ = nameof (Java.Lang.Object); + + var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location) + ?? throw new InvalidOperationException ("Could not determine test assembly directory."); + var path = Path.Combine (testDir, "Mono.Android.dll"); + + if (!File.Exists (path)) { + throw new InvalidOperationException ( + $"Mono.Android.dll not found at '{path}'. " + + "Ensure Mono.Android is built (bin/Debug/lib/packs/Microsoft.Android.Ref.*)."); + } + + return path; + } + } + + static string[] AllAssemblyPaths { + get { + var monoAndroidPath = MonoAndroidAssemblyPath; + var dir = Path.GetDirectoryName (monoAndroidPath) + ?? throw new InvalidOperationException ("Could not determine Mono.Android directory."); + var javaInteropPath = Path.Combine (dir, "Java.Interop.dll"); + + if (!File.Exists (javaInteropPath)) { + return new [] { monoAndroidPath }; + } + + return new [] { monoAndroidPath, javaInteropPath }; + } + } + + static string? UserTypesFixturePath { + get { + var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location) + ?? throw new InvalidOperationException ("Could not determine test assembly directory."); + var path = Path.Combine (testDir, "UserTypesFixture.dll"); + return File.Exists (path) ? path : null; + } + } + + static string[]? AllUserTypesAssemblyPaths { + get { + var fixturePath = UserTypesFixturePath; + if (fixturePath == null) { + return null; + } + + var dir = Path.GetDirectoryName (fixturePath)!; + var monoAndroidPath = Path.Combine (dir, "Mono.Android.dll"); + var javaInteropPath = Path.Combine (dir, "Java.Interop.dll"); + + var paths = new List { fixturePath }; + if (File.Exists (monoAndroidPath)) { + paths.Add (monoAndroidPath); + } + if (File.Exists (javaInteropPath)) { + paths.Add (javaInteropPath); + } + return paths.ToArray (); + } + } + + static string NormalizeCrc64 (string javaName) + { + if (javaName.StartsWith ("crc64", StringComparison.Ordinal)) { + int slash = javaName.IndexOf ('/'); + if (slash > 0) { + return "crc64.../" + javaName.Substring (slash + 1); + } + } + return javaName; + } + + void AssertTypeMapMatch (List legacy, List newEntries) + { + var legacyMap = legacy.GroupBy (e => e.JavaName).ToDictionary (g => g.Key, g => g.ToList ()); + var newMap = newEntries.GroupBy (e => e.JavaName).ToDictionary (g => g.Key, g => g.ToList ()); + + var allJavaNames = new HashSet (legacyMap.Keys); + allJavaNames.UnionWith (newMap.Keys); + + var missing = new List (); + var extra = new List (); + var managedNameMismatches = new List (); + var skipMismatches = new List (); + + foreach (var javaName in allJavaNames.OrderBy (n => n, StringComparer.Ordinal)) { + var inLegacy = legacyMap.TryGetValue (javaName, out var legacyEntries); + var inNew = newMap.TryGetValue (javaName, out var newEntriesForName); + + if (inLegacy && !inNew) { + foreach (var e in legacyEntries!) + missing.Add ($"{e.JavaName} → {e.ManagedName} (skip={e.SkipInJavaToManaged})"); + continue; + } + + if (!inLegacy && inNew) { + foreach (var e in newEntriesForName!) + extra.Add ($"{e.JavaName} → {e.ManagedName} (skip={e.SkipInJavaToManaged})"); + continue; + } + + var le = legacyEntries!.OrderBy (e => e.ManagedName).First (); + var ne = newEntriesForName!.OrderBy (e => e.ManagedName).First (); + + if (le.ManagedName != ne.ManagedName) + managedNameMismatches.Add ($"{javaName}: legacy='{le.ManagedName}' new='{ne.ManagedName}'"); + + if (le.SkipInJavaToManaged != ne.SkipInJavaToManaged) + skipMismatches.Add ($"{javaName}: legacy.skip={le.SkipInJavaToManaged} new.skip={ne.SkipInJavaToManaged}"); + } + + AssertNoDiffs ("MISSING", missing); + AssertNoDiffs ("EXTRA", extra); + AssertNoDiffs ("MANAGED NAME MISMATCHES", managedNameMismatches); + AssertNoDiffs ("SKIP FLAG MISMATCHES", skipMismatches); + } + + static void AssertNoDiffs (string label, List items) + { + if (items.Count == 0) { + return; + } + + var details = string.Join (Environment.NewLine, items.Take (20).Select (item => $" {item}")); + Assert.True (false, $"{label} ({items.Count}){Environment.NewLine}{details}"); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index 742834f615f..3b0d79e43d8 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -1,1025 +1,135 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Linq; -using Java.Interop.Tools.Cecil; -using Java.Interop.Tools.TypeNameMappings; -using Microsoft.Build.Utilities; -using Mono.Cecil; using Xamarin.Android.Tasks; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; -public class ScannerComparisonTests +public partial class ScannerComparisonTests { - record TypeMapEntry (string JavaName, string ManagedName, bool SkipInJavaToManaged); - - record MethodEntry (string JniName, string JniSignature, string? Connector); - - record TypeMethodGroup (string ManagedName, List Methods); - - record MarshalMethodComparisonResult ( - List MissingTypes, - List ExtraTypes, - List MissingMethods, - List ExtraMethods, - List ConnectorMismatches - ); - - record UserTypesMethodComparisonResult ( - List Missing, - List MethodMismatches - ); - - static (List entries, Dictionary> methodsByJavaName) RunLegacyScanner (string assemblyPath) - { - var cache = new TypeDefinitionCache (); - var resolver = new DefaultAssemblyResolver (); - resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); - - var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location); - if (runtimeDir != null) { - resolver.AddSearchDirectory (runtimeDir); - } - - var readerParams = new ReaderParameters { AssemblyResolver = resolver }; - using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams); - - var scanner = new Xamarin.Android.Tasks.XAJavaTypeScanner ( - Xamarin.Android.Tools.AndroidTargetArch.Arm64, - new TaskLoggingHelper (new MockBuildEngine (), "test"), - cache - ); - - var javaTypes = scanner.GetJavaTypes (assembly); - var (dataSets, _) = Xamarin.Android.Tasks.TypeMapCecilAdapter.GetDebugNativeEntries ( - javaTypes, cache, needUniqueAssemblies: false - ); - - var entries = dataSets.JavaToManaged - .Select (e => new TypeMapEntry (e.JavaName, e.ManagedName, e.SkipInJavaToManaged)) - .OrderBy (e => e.JavaName, StringComparer.Ordinal) - .ThenBy (e => e.ManagedName, StringComparer.Ordinal) - .ToList (); - - var methodsByJavaName = new Dictionary> (); - foreach (var typeDef in javaTypes) { - var javaName = GetCecilJavaName (typeDef); - if (javaName == null) { - continue; - } - - var managedName = GetManagedName (typeDef); - var methods = ExtractMethodRegistrations (typeDef); - - if (!methodsByJavaName.TryGetValue (javaName, out var groups)) { - groups = new List (); - methodsByJavaName [javaName] = groups; - } - - groups.Add (new TypeMethodGroup ( - managedName, - methods.OrderBy (m => m.JniName, StringComparer.Ordinal) - .ThenBy (m => m.JniSignature, StringComparer.Ordinal) - .ToList () - )); - } - - foreach (var entry in dataSets.JavaToManaged) { - if (methodsByJavaName.ContainsKey (entry.JavaName)) { - continue; - } - - methodsByJavaName [entry.JavaName] = new List { - new TypeMethodGroup (entry.ManagedName, new List ()) - }; - } - - return (entries, methodsByJavaName); - } - - static string? GetCecilJavaName (TypeDefinition typeDef) - { - if (!typeDef.HasCustomAttributes) { - return null; - } - - foreach (var attr in typeDef.CustomAttributes) { - if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { - continue; - } - - if (attr.ConstructorArguments.Count > 0) { - return ((string) attr.ConstructorArguments [0].Value).Replace ('.', '/'); - } - } - - return null; - } - - static List ExtractMethodRegistrations (TypeDefinition typeDef) - { - var methods = new List (); - - foreach (var method in typeDef.Methods) { - if (!method.HasCustomAttributes) { - continue; - } - - AddRegisterMethods (method.CustomAttributes, methods); - } - - if (typeDef.HasProperties) { - foreach (var prop in typeDef.Properties) { - if (!prop.HasCustomAttributes) { - continue; - } - - AddRegisterMethods (prop.CustomAttributes, methods); - } - } - - return methods; - } - - static string GetManagedName (TypeDefinition typeDef) - { - return $"{typeDef.FullName.Replace ('/', '+')}, {typeDef.Module.Assembly.Name.Name}"; - } - - static void AddRegisterMethods (IEnumerable attributes, List methods) - { - foreach (var attr in attributes) { - if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute" || attr.ConstructorArguments.Count < 2) { - continue; - } - - var jniMethodName = (string) attr.ConstructorArguments [0].Value; - var jniSignature = (string) attr.ConstructorArguments [1].Value; - var connector = attr.ConstructorArguments.Count > 2 - ? (string) attr.ConstructorArguments [2].Value - : null; - methods.Add (new MethodEntry (jniMethodName, jniSignature, connector)); - } - } - - static (List entries, Dictionary> methodsByJavaName) RunNewScanner (string[] assemblyPaths) - { - var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]); - using var scanner = new JavaPeerScanner (); - var allPeers = scanner.Scan (assemblyPaths); - var peers = allPeers.Where (p => p.AssemblyName == primaryAssemblyName).ToList (); - - var entries = peers - .Select (p => new TypeMapEntry ( - p.JavaName, - $"{p.ManagedTypeName}, {p.AssemblyName}", - p.IsInterface || p.IsGenericDefinition - )) - .OrderBy (e => e.JavaName, StringComparer.Ordinal) - .ThenBy (e => e.ManagedName, StringComparer.Ordinal) - .ToList (); - - var methodsByJavaName = new Dictionary> (); - foreach (var peer in peers) { - var managedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; - - if (!methodsByJavaName.TryGetValue (peer.JavaName, out var groups)) { - groups = new List (); - methodsByJavaName [peer.JavaName] = groups; - } - - groups.Add (new TypeMethodGroup ( - managedName, - peer.MarshalMethods - .Select (m => new MethodEntry (m.JniName, m.JniSignature, m.Connector)) - .OrderBy (m => m.JniName, StringComparer.Ordinal) - .ThenBy (m => m.JniSignature, StringComparer.Ordinal) - .ToList () - )); - } - - return (entries, methodsByJavaName); - } - - [Fact] - public void ExactTypeMap_MonoAndroid () - { - var (legacy, _) = RunLegacyScanner (MonoAndroidAssemblyPath); - var (newEntries, _) = RunNewScanner (AllAssemblyPaths); - AssertTypeMapMatch (legacy, newEntries); - } - - [Fact] - public void ExactMarshalMethods_MonoAndroid () - { - var (_, legacyMethods) = RunLegacyScanner (MonoAndroidAssemblyPath); - var (_, newMethods) = RunNewScanner (AllAssemblyPaths); - var result = CompareMarshalMethods (legacyMethods, newMethods); - - AssertNoDiffs ("MANAGED TYPES MISSING from new scanner", result.MissingTypes); - AssertNoDiffs ("MANAGED TYPES EXTRA in new scanner", result.ExtraTypes); - AssertNoDiffs ("METHODS MISSING from new scanner", result.MissingMethods); - AssertNoDiffs ("METHODS EXTRA in new scanner", result.ExtraMethods); - AssertNoDiffs ("CONNECTOR MISMATCHES", result.ConnectorMismatches); - } - - static MarshalMethodComparisonResult CompareMarshalMethods ( - Dictionary> legacyMethods, - Dictionary> newMethods) - { - var allJavaNames = new HashSet (legacyMethods.Keys); - allJavaNames.UnionWith (newMethods.Keys); - - var result = new MarshalMethodComparisonResult ( - new List (), - new List (), - new List (), - new List (), - new List () - ); - - foreach (var javaName in allJavaNames.OrderBy (n => n, StringComparer.Ordinal)) { - var inLegacy = legacyMethods.TryGetValue (javaName, out var legacyGroups); - var inNew = newMethods.TryGetValue (javaName, out var newGroups); - - if (inLegacy && !inNew) { - foreach (var g in legacyGroups!) { - result.MissingTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)"); - } - continue; - } - - if (!inLegacy && inNew) { - foreach (var g in newGroups!) { - result.ExtraTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)"); - } - continue; - } - - var legacyByManaged = legacyGroups!.ToDictionary (g => g.ManagedName, g => g.Methods); - var newByManaged = newGroups!.ToDictionary (g => g.ManagedName, g => g.Methods); - - foreach (var managedName in legacyByManaged.Keys.Except (newByManaged.Keys)) { - result.MissingTypes.Add ($"{javaName} → {managedName} ({legacyByManaged [managedName].Count} methods)"); - } - - foreach (var managedName in newByManaged.Keys.Except (legacyByManaged.Keys)) { - result.ExtraTypes.Add ($"{javaName} → {managedName} ({newByManaged [managedName].Count} methods)"); - } - - foreach (var managedName in legacyByManaged.Keys.Intersect (newByManaged.Keys)) { - CompareMethodGroups (javaName, managedName, legacyByManaged [managedName], newByManaged [managedName], result); - } - } - - return result; - } - - static void CompareMethodGroups ( - string javaName, - string managedName, - List legacyMethodList, - List newMethodList, - MarshalMethodComparisonResult result) - { - var legacySet = new HashSet<(string name, string sig)> ( - legacyMethodList.Select (m => (m.JniName, m.JniSignature)) - ); - var newSet = new HashSet<(string name, string sig)> ( - newMethodList.Select (m => (m.JniName, m.JniSignature)) - ); - - foreach (var m in legacySet.Except (newSet)) { - result.MissingMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}"); - } - - foreach (var m in newSet.Except (legacySet)) { - result.ExtraMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}"); - } - - var legacyByKey = legacyMethodList - .GroupBy (m => (m.JniName, m.JniSignature)) - .ToDictionary (g => g.Key, g => g.First ()); - var newByKey = newMethodList - .GroupBy (m => (m.JniName, m.JniSignature)) - .ToDictionary (g => g.Key, g => g.First ()); - - foreach (var key in legacyByKey.Keys.Intersect (newByKey.Keys)) { - var lc = legacyByKey [key].Connector ?? ""; - var nc = newByKey [key].Connector ?? ""; - if (lc != nc) { - result.ConnectorMismatches.Add ($"{javaName} [{managedName}]: {key.JniName}{key.JniSignature} legacy='{lc}' new='{nc}'"); - } - } - } - - [Fact] - public void ScannerDiagnostics_MonoAndroid () - { - using var scanner = new JavaPeerScanner (); - var peers = scanner.Scan (new [] { MonoAndroidAssemblyPath }); - - var interfaces = peers.Count (p => p.IsInterface); - var totalMethods = peers.Sum (p => p.MarshalMethods.Count); - Assert.True (peers.Count > 3000, $"Expected >3000 types, got {peers.Count}"); - Assert.True (interfaces > 500, $"Expected >500 interfaces, got {interfaces}"); - Assert.True (totalMethods > 10000, $"Expected >10000 marshal methods, got {totalMethods}"); - } - - [Fact] - public void ExactBaseJavaNames_MonoAndroid () - { - var assemblyPath = MonoAndroidAssemblyPath; - - var (legacyData, _) = BuildLegacyTypeData (assemblyPath); - var newData = BuildNewTypeData (AllAssemblyPaths); - var mismatches = ComparisonDiffHelper.CompareBaseJavaNames (legacyData, newData); - - AssertNoDiffs ("BASE JAVA NAME MISMATCHES", mismatches); - } - - [Fact] - public void ExactImplementedInterfaces_MonoAndroid () - { - var assemblyPath = MonoAndroidAssemblyPath; - - var (legacyData, _) = BuildLegacyTypeData (assemblyPath); - var newData = BuildNewTypeData (AllAssemblyPaths); - var (missingInterfaces, extraInterfaces) = ComparisonDiffHelper.CompareImplementedInterfaces (legacyData, newData); - - AssertNoDiffs ("INTERFACES MISSING from new scanner", missingInterfaces); - AssertNoDiffs ("INTERFACES EXTRA in new scanner", extraInterfaces); - } - - [Fact] - public void ExactActivationCtors_MonoAndroid () - { - var assemblyPath = MonoAndroidAssemblyPath; - - var (legacyData, _) = BuildLegacyTypeData (assemblyPath); - var newData = BuildNewTypeData (AllAssemblyPaths); - var (presenceMismatches, declaringTypeMismatches, styleMismatches) = ComparisonDiffHelper.CompareActivationCtors (legacyData, newData); - - AssertNoDiffs ("ACTIVATION CTOR PRESENCE MISMATCHES", presenceMismatches); - AssertNoDiffs ("ACTIVATION CTOR DECLARING TYPE MISMATCHES", declaringTypeMismatches); - AssertNoDiffs ("ACTIVATION CTOR STYLE MISMATCHES", styleMismatches); - } - - [Fact] - public void ExactJavaConstructors_MonoAndroid () - { - var assemblyPath = MonoAndroidAssemblyPath; - - var (legacyData, _) = BuildLegacyTypeData (assemblyPath); - var newData = BuildNewTypeData (AllAssemblyPaths); - var (missingCtors, extraCtors) = ComparisonDiffHelper.CompareJavaConstructors (legacyData, newData); - - AssertNoDiffs ("JAVA CONSTRUCTORS MISSING from new scanner", missingCtors); - AssertNoDiffs ("JAVA CONSTRUCTORS EXTRA in new scanner", extraCtors); - } - - [Fact] - public void ExactTypeFlags_MonoAndroid () - { - var assemblyPath = MonoAndroidAssemblyPath; - - var (legacyData, _) = BuildLegacyTypeData (assemblyPath); - var newData = BuildNewTypeData (AllAssemblyPaths); - var (interfaceMismatches, abstractMismatches, genericMismatches, acwMismatches) = ComparisonDiffHelper.CompareTypeFlags (legacyData, newData); - - AssertNoDiffs ("IsInterface MISMATCHES", interfaceMismatches); - AssertNoDiffs ("IsAbstract MISMATCHES", abstractMismatches); - AssertNoDiffs ("IsGenericDefinition MISMATCHES", genericMismatches); - AssertNoDiffs ("DoNotGenerateAcw MISMATCHES", acwMismatches); - } - - static class ComparisonDiffHelper - { - public static List CompareBaseJavaNames ( - Dictionary legacyData, - Dictionary newData) - { - var allManagedNames = new HashSet (legacyData.Keys); - allManagedNames.IntersectWith (newData.Keys); - - var mismatches = new List (); - - foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { - var legacy = legacyData [managedName]; - var newInfo = newData [managedName]; - - if (legacy.BaseJavaName != newInfo.BaseJavaName) { - if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && managedName.Contains ('`')) { - continue; - } - - if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && newInfo.DoNotGenerateAcw) { - continue; - } - - if (legacy.BaseJavaName != null && newInfo.BaseJavaName == null && - legacy.BaseJavaName == legacy.JavaName) { - continue; - } - - mismatches.Add ($"{managedName}: legacy='{legacy.BaseJavaName ?? "(null)"}' new='{newInfo.BaseJavaName ?? "(null)"}'"); - } - } - - return mismatches; - } - - public static (List missingInterfaces, List extraInterfaces) CompareImplementedInterfaces ( - Dictionary legacyData, - Dictionary newData) - { - var allManagedNames = new HashSet (legacyData.Keys); - allManagedNames.IntersectWith (newData.Keys); - - var missingInterfaces = new List (); - var extraInterfaces = new List (); - - foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { - var legacy = legacyData [managedName]; - var newInfo = newData [managedName]; - - var legacySet = new HashSet (legacy.ImplementedInterfaces, StringComparer.Ordinal); - var newSet = new HashSet (newInfo.ImplementedInterfaces, StringComparer.Ordinal); - - foreach (var iface in legacySet.Except (newSet)) { - missingInterfaces.Add ($"{managedName}: missing '{iface}'"); - } - - foreach (var iface in newSet.Except (legacySet)) { - extraInterfaces.Add ($"{managedName}: extra '{iface}'"); - } - } - - return (missingInterfaces, extraInterfaces); - } - - public static (List presenceMismatches, List declaringTypeMismatches, List styleMismatches) CompareActivationCtors ( - Dictionary legacyData, - Dictionary newData) - { - var allManagedNames = new HashSet (legacyData.Keys); - allManagedNames.IntersectWith (newData.Keys); - - var presenceMismatches = new List (); - var declaringTypeMismatches = new List (); - var styleMismatches = new List (); - - foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { - var legacy = legacyData [managedName]; - var newInfo = newData [managedName]; - - if (legacy.HasActivationCtor != newInfo.HasActivationCtor) { - presenceMismatches.Add ($"{managedName}: legacy.has={legacy.HasActivationCtor} new.has={newInfo.HasActivationCtor}"); - continue; - } - - if (!legacy.HasActivationCtor) { - continue; - } - - if (legacy.ActivationCtorDeclaringType != newInfo.ActivationCtorDeclaringType) { - declaringTypeMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorDeclaringType}' new='{newInfo.ActivationCtorDeclaringType}'"); - } - - if (legacy.ActivationCtorStyle != newInfo.ActivationCtorStyle) { - styleMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorStyle}' new='{newInfo.ActivationCtorStyle}'"); - } - } - - return (presenceMismatches, declaringTypeMismatches, styleMismatches); - } - - public static (List missingCtors, List extraCtors) CompareJavaConstructors ( - Dictionary legacyData, - Dictionary newData) - { - var allManagedNames = new HashSet (legacyData.Keys); - allManagedNames.IntersectWith (newData.Keys); - - var missingCtors = new List (); - var extraCtors = new List (); - - foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { - var legacy = legacyData [managedName]; - var newInfo = newData [managedName]; - - var legacySet = new HashSet (legacy.JavaConstructorSignatures, StringComparer.Ordinal); - var newSet = new HashSet (newInfo.JavaConstructorSignatures, StringComparer.Ordinal); - - foreach (var sig in legacySet.Except (newSet)) { - missingCtors.Add ($"{managedName}: missing '{sig}'"); - } - - foreach (var sig in newSet.Except (legacySet)) { - extraCtors.Add ($"{managedName}: extra '{sig}'"); - } - } - - return (missingCtors, extraCtors); - } - - public static (List interfaceMismatches, List abstractMismatches, List genericMismatches, List acwMismatches) CompareTypeFlags ( - Dictionary legacyData, - Dictionary newData) - { - var allManagedNames = new HashSet (legacyData.Keys); - allManagedNames.IntersectWith (newData.Keys); - - var interfaceMismatches = new List (); - var abstractMismatches = new List (); - var genericMismatches = new List (); - var acwMismatches = new List (); - - foreach (var managedName in allManagedNames.OrderBy (n => n, StringComparer.Ordinal)) { - var legacy = legacyData [managedName]; - var newInfo = newData [managedName]; - - if (legacy.IsInterface != newInfo.IsInterface) { - interfaceMismatches.Add ($"{managedName}: legacy={legacy.IsInterface} new={newInfo.IsInterface}"); - } - - if (legacy.IsAbstract != newInfo.IsAbstract) { - abstractMismatches.Add ($"{managedName}: legacy={legacy.IsAbstract} new={newInfo.IsAbstract}"); - } - - if (legacy.IsGenericDefinition != newInfo.IsGenericDefinition) { - genericMismatches.Add ($"{managedName}: legacy={legacy.IsGenericDefinition} new={newInfo.IsGenericDefinition}"); - } - - if (legacy.DoNotGenerateAcw != newInfo.DoNotGenerateAcw) { - acwMismatches.Add ($"{managedName}: legacy={legacy.DoNotGenerateAcw} new={newInfo.DoNotGenerateAcw}"); - } - } - - return (interfaceMismatches, abstractMismatches, genericMismatches, acwMismatches); - } - } - - - record TypeComparisonData ( - string ManagedName, - string JavaName, - string? BaseJavaName, - IReadOnlyList ImplementedInterfaces, - bool HasActivationCtor, - string? ActivationCtorDeclaringType, - string? ActivationCtorStyle, - IReadOnlyList JavaConstructorSignatures, - bool IsInterface, - bool IsAbstract, - bool IsGenericDefinition, - bool DoNotGenerateAcw - ); - - static (Dictionary perType, List entries) BuildLegacyTypeData (string assemblyPath) - { - var cache = new TypeDefinitionCache (); - var resolver = new DefaultAssemblyResolver (); - resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); - - var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location); - if (runtimeDir != null) { - resolver.AddSearchDirectory (runtimeDir); - } - - var readerParams = new ReaderParameters { AssemblyResolver = resolver }; - using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams); - - var scanner = new Xamarin.Android.Tasks.XAJavaTypeScanner ( - Xamarin.Android.Tools.AndroidTargetArch.Arm64, - new TaskLoggingHelper (new MockBuildEngine (), "test"), - cache - ); - - var javaTypes = scanner.GetJavaTypes (assembly); - var (dataSets, _) = Xamarin.Android.Tasks.TypeMapCecilAdapter.GetDebugNativeEntries ( - javaTypes, cache, needUniqueAssemblies: false - ); - - var entries = dataSets.JavaToManaged - .Select (e => new TypeMapEntry (e.JavaName, e.ManagedName, e.SkipInJavaToManaged)) - .OrderBy (e => e.JavaName, StringComparer.Ordinal) - .ThenBy (e => e.ManagedName, StringComparer.Ordinal) - .ToList (); - - var perType = new Dictionary (StringComparer.Ordinal); - - foreach (var typeDef in javaTypes) { - var javaName = GetCecilJavaName (typeDef); - if (javaName == null) { - continue; - } - - var managedName = GetManagedName (typeDef); - - string? baseJavaName = null; - var baseType = typeDef.GetBaseType (cache); - if (baseType != null) { - var baseJni = JavaNativeTypeManager.ToJniName (baseType, cache); - if (baseJni != null && baseJni != javaName) { - baseJavaName = baseJni; - } - } - - var implementedInterfaces = new List (); - if (typeDef.HasInterfaces) { - foreach (var ifaceImpl in typeDef.Interfaces) { - var ifaceDef = cache.Resolve (ifaceImpl.InterfaceType); - if (ifaceDef == null) { - continue; - } - var ifaceRegs = CecilExtensions.GetTypeRegistrationAttributes (ifaceDef); - var ifaceReg = ifaceRegs.FirstOrDefault (); - if (ifaceReg != null) { - implementedInterfaces.Add (ifaceReg.Name.Replace ('.', '/')); - } - } - } - implementedInterfaces.Sort (StringComparer.Ordinal); - - bool hasActivationCtor = false; - string? activationCtorDeclaringType = null; - string? activationCtorStyle = null; - FindLegacyActivationCtor (typeDef, cache, out hasActivationCtor, out activationCtorDeclaringType, out activationCtorStyle); - - var javaCtorSignatures = new List (); - foreach (var method in typeDef.Methods) { - if (!method.IsConstructor || method.IsStatic || !method.HasCustomAttributes) { - continue; - } - foreach (var attr in method.CustomAttributes) { - if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { - continue; - } - if (attr.ConstructorArguments.Count >= 2) { - var regName = (string) attr.ConstructorArguments [0].Value; - if (regName == "" || regName == ".ctor") { - javaCtorSignatures.Add ((string) attr.ConstructorArguments [1].Value); - } - } - } - } - javaCtorSignatures.Sort (StringComparer.Ordinal); - - var isInterface = typeDef.IsInterface; - var isAbstract = typeDef.IsAbstract && !typeDef.IsInterface; - var isGenericDefinition = typeDef.HasGenericParameters; - var doNotGenerateAcw = GetCecilDoNotGenerateAcw (typeDef); - - perType [managedName] = new TypeComparisonData ( - managedName, - javaName, - baseJavaName, - implementedInterfaces, - hasActivationCtor, - activationCtorDeclaringType, - activationCtorStyle, - javaCtorSignatures, - isInterface, - isAbstract, - isGenericDefinition, - doNotGenerateAcw - ); - } - - return (perType, entries); - } - - static void FindLegacyActivationCtor (TypeDefinition typeDef, TypeDefinitionCache cache, - out bool found, out string? declaringType, out string? style) - { - found = false; - declaringType = null; - style = null; - - TypeDefinition? current = typeDef; - while (current != null) { - foreach (var method in current.Methods) { - if (!method.IsConstructor || method.IsStatic || method.Parameters.Count != 2) { - continue; - } - - var p0 = method.Parameters [0].ParameterType.FullName; - var p1 = method.Parameters [1].ParameterType.FullName; - - if (p0 == "System.IntPtr" && p1 == "Android.Runtime.JniHandleOwnership") { - found = true; - declaringType = GetManagedName (current); - style = "XamarinAndroid"; - return; - } - - if ((p0 == "Java.Interop.JniObjectReference&" || p0 == "Java.Interop.JniObjectReference") && - p1 == "Java.Interop.JniObjectReferenceOptions") { - found = true; - declaringType = GetManagedName (current); - style = "JavaInterop"; - return; - } - } - - current = current.GetBaseType (cache); - } - } - - static bool GetCecilDoNotGenerateAcw (TypeDefinition typeDef) - { - if (!typeDef.HasCustomAttributes) { - return false; - } - - foreach (var attr in typeDef.CustomAttributes) { - if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { - continue; - } - if (attr.HasProperties) { - foreach (var prop in attr.Properties.Where (p => p.Name == "DoNotGenerateAcw")) { - if (prop.Argument.Value is bool val) { - return val; - } - } - } - return false; - } - - return false; - } - - static Dictionary BuildNewTypeData (string[] assemblyPaths) - { - var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]); - using var scanner = new JavaPeerScanner (); - var peers = scanner.Scan (assemblyPaths); - - var perType = new Dictionary (StringComparer.Ordinal); - - foreach (var peer in peers) { - if (peer.AssemblyName != primaryAssemblyName) { - continue; - } - - var managedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; - - bool hasActivationCtor = peer.ActivationCtor != null; - string? activationCtorDeclaringType = null; - string? activationCtorStyle = null; - if (peer.ActivationCtor != null) { - activationCtorDeclaringType = $"{peer.ActivationCtor.DeclaringTypeName}, {peer.ActivationCtor.DeclaringAssemblyName}"; - activationCtorStyle = peer.ActivationCtor.Style.ToString (); - } - - var javaCtorSignatures = peer.MarshalMethods - .Where (m => m.IsConstructor) - .Select (m => m.JniSignature) - .OrderBy (s => s, StringComparer.Ordinal) - .ToList (); - - var implementedInterfaces = peer.ImplementedInterfaceJavaNames - .OrderBy (i => i, StringComparer.Ordinal) - .ToList (); - - perType [managedName] = new TypeComparisonData ( - managedName, - peer.JavaName, - peer.BaseJavaName, - implementedInterfaces, - hasActivationCtor, - activationCtorDeclaringType, - activationCtorStyle, - javaCtorSignatures, - peer.IsInterface, - peer.IsAbstract && !peer.IsInterface, - peer.IsGenericDefinition, - peer.DoNotGenerateAcw - ); - } - - return perType; - } - - static string MonoAndroidAssemblyPath { - get { - _ = nameof (Java.Lang.Object); - - var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location) - ?? throw new InvalidOperationException ("Could not determine test assembly directory."); - var path = Path.Combine (testDir, "Mono.Android.dll"); - - if (!File.Exists (path)) { - throw new InvalidOperationException ( - $"Mono.Android.dll not found at '{path}'. " + - "Ensure Mono.Android is built (bin/Debug/lib/packs/Microsoft.Android.Ref.*)."); - } - - return path; - } - } - - static string[] AllAssemblyPaths { - get { - var monoAndroidPath = MonoAndroidAssemblyPath; - var dir = Path.GetDirectoryName (monoAndroidPath) - ?? throw new InvalidOperationException ("Could not determine Mono.Android directory."); - var javaInteropPath = Path.Combine (dir, "Java.Interop.dll"); - - if (!File.Exists (javaInteropPath)) { - return new [] { monoAndroidPath }; - } - - return new [] { monoAndroidPath, javaInteropPath }; - } - } - - static string NormalizeCrc64 (string javaName) - { - if (javaName.StartsWith ("crc64", StringComparison.Ordinal)) { - int slash = javaName.IndexOf ('/'); - if (slash > 0) { - return "crc64.../" + javaName.Substring (slash + 1); - } - } - return javaName; - } - - static string? UserTypesFixturePath { - get { - var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location) - ?? throw new InvalidOperationException ("Could not determine test assembly directory."); - var path = Path.Combine (testDir, "UserTypesFixture.dll"); - return File.Exists (path) ? path : null; - } - } - - static string[]? AllUserTypesAssemblyPaths { - get { - var fixturePath = UserTypesFixturePath; - if (fixturePath == null) { - return null; - } - - var dir = Path.GetDirectoryName (fixturePath)!; - var monoAndroidPath = Path.Combine (dir, "Mono.Android.dll"); - var javaInteropPath = Path.Combine (dir, "Java.Interop.dll"); - - var paths = new List { fixturePath }; - if (File.Exists (monoAndroidPath)) { - paths.Add (monoAndroidPath); - } - if (File.Exists (javaInteropPath)) { - paths.Add (javaInteropPath); - } - return paths.ToArray (); - } - } - - [Fact] - public void ExactTypeMap_UserTypesFixture () - { - var paths = AllUserTypesAssemblyPaths; - Assert.NotNull (paths); - - var fixturePath = paths! [0]; - var (legacy, _) = RunLegacyScanner (fixturePath); - var (newEntries, _) = RunNewScanner (paths); - var legacyNormalized = legacy.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); - var newNormalized = newEntries.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); - - AssertTypeMapMatch (legacyNormalized, newNormalized); - } - - [Fact] - public void ExactMarshalMethods_UserTypesFixture () - { - var paths = AllUserTypesAssemblyPaths; - Assert.NotNull (paths); - - var fixturePath = paths! [0]; - var (_, legacyMethods) = RunLegacyScanner (fixturePath); - var (_, newMethods) = RunNewScanner (paths); - - var legacyNormalized = legacyMethods - .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); - var newNormalized = newMethods - .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); - - var result = CompareUserTypeMarshalMethods (legacyNormalized, newNormalized); - AssertNoDiffs ("MISSING from new scanner", result.Missing); - AssertNoDiffs ("METHOD MISMATCHES", result.MethodMismatches); - } - - static UserTypesMethodComparisonResult CompareUserTypeMarshalMethods ( - Dictionary> legacyNormalized, - Dictionary> newNormalized) - { - var missing = new List (); - var methodMismatches = new List (); - - foreach (var javaName in legacyNormalized.Keys.OrderBy (n => n, StringComparer.Ordinal)) { - if (!newNormalized.TryGetValue (javaName, out var newGroups)) { - missing.Add (javaName); - continue; - } - - var legacyGroups = legacyNormalized [javaName]; +[Fact] +public void ExactTypeMap_MonoAndroid () +{ +var (legacy, _) = ScannerRunner.RunLegacy (MonoAndroidAssemblyPath); +var (newEntries, _) = ScannerRunner.RunNew (AllAssemblyPaths); +AssertTypeMapMatch (legacy, newEntries); +} - foreach (var legacyGroup in legacyGroups) { - CompareUserTypeMethodGroup (javaName, legacyGroup, newGroups, missing, methodMismatches); - } - } +[Fact] +public void ExactMarshalMethods_MonoAndroid () +{ +var (_, legacyMethods) = ScannerRunner.RunLegacy (MonoAndroidAssemblyPath); +var (_, newMethods) = ScannerRunner.RunNew (AllAssemblyPaths); +var result = MarshalMethodDiffHelper.CompareMarshalMethods (legacyMethods, newMethods); + +AssertNoDiffs ("MANAGED TYPES MISSING from new scanner", result.MissingTypes); +AssertNoDiffs ("MANAGED TYPES EXTRA in new scanner", result.ExtraTypes); +AssertNoDiffs ("METHODS MISSING from new scanner", result.MissingMethods); +AssertNoDiffs ("METHODS EXTRA in new scanner", result.ExtraMethods); +AssertNoDiffs ("CONNECTOR MISMATCHES", result.ConnectorMismatches); +} - return new UserTypesMethodComparisonResult (missing, methodMismatches); - } +[Fact] +public void ScannerDiagnostics_MonoAndroid () +{ +using var scanner = new JavaPeerScanner (); +var peers = scanner.Scan (new [] { MonoAndroidAssemblyPath }); + +var interfaces = peers.Count (p => p.IsInterface); +var totalMethods = peers.Sum (p => p.MarshalMethods.Count); +Assert.True (peers.Count > 3000, $"Expected >3000 types, got {peers.Count}"); +Assert.True (interfaces > 500, $"Expected >500 interfaces, got {interfaces}"); +Assert.True (totalMethods > 10000, $"Expected >10000 marshal methods, got {totalMethods}"); +} - static void CompareUserTypeMethodGroup ( - string javaName, - TypeMethodGroup legacyGroup, - List newGroups, - List missing, - List methodMismatches) - { - var newGroup = newGroups.FirstOrDefault (g => g.ManagedName == legacyGroup.ManagedName); - if (newGroup == null) { - missing.Add ($"{javaName} → {legacyGroup.ManagedName}"); - return; - } +[Fact] +public void ExactBaseJavaNames_MonoAndroid () +{ +var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); +var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); +var mismatches = ComparisonDiffHelper.CompareBaseJavaNames (legacyData, newData); - if (legacyGroup.Methods.Count == 0) { - return; - } +AssertNoDiffs ("BASE JAVA NAME MISMATCHES", mismatches); +} - if (legacyGroup.Methods.Count != newGroup.Methods.Count) { - methodMismatches.Add ($"{javaName}/{legacyGroup.ManagedName}: legacy={legacyGroup.Methods.Count} methods, new={newGroup.Methods.Count}"); - return; - } +[Fact] +public void ExactImplementedInterfaces_MonoAndroid () +{ +var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); +var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); +var (missingInterfaces, extraInterfaces) = ComparisonDiffHelper.CompareImplementedInterfaces (legacyData, newData); - for (int i = 0; i < legacyGroup.Methods.Count; i++) { - var lm = legacyGroup.Methods [i]; - var nm = newGroup.Methods [i]; - if (lm.JniName != nm.JniName || lm.JniSignature != nm.JniSignature) { - methodMismatches.Add ($"{javaName}: [{i}] legacy=({lm.JniName}, {lm.JniSignature}) new=({nm.JniName}, {nm.JniSignature})"); - } - } - } +AssertNoDiffs ("INTERFACES MISSING from new scanner", missingInterfaces); +AssertNoDiffs ("INTERFACES EXTRA in new scanner", extraInterfaces); +} - void AssertTypeMapMatch (List legacy, List newEntries) - { - var legacyMap = legacy.GroupBy (e => e.JavaName).ToDictionary (g => g.Key, g => g.ToList ()); - var newMap = newEntries.GroupBy (e => e.JavaName).ToDictionary (g => g.Key, g => g.ToList ()); +[Fact] +public void ExactActivationCtors_MonoAndroid () +{ +var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); +var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); +var (presenceMismatches, declaringTypeMismatches, styleMismatches) = ComparisonDiffHelper.CompareActivationCtors (legacyData, newData); - var allJavaNames = new HashSet (legacyMap.Keys); - allJavaNames.UnionWith (newMap.Keys); +AssertNoDiffs ("ACTIVATION CTOR PRESENCE MISMATCHES", presenceMismatches); +AssertNoDiffs ("ACTIVATION CTOR DECLARING TYPE MISMATCHES", declaringTypeMismatches); +AssertNoDiffs ("ACTIVATION CTOR STYLE MISMATCHES", styleMismatches); +} - var missing = new List (); - var extra = new List (); - var managedNameMismatches = new List (); - var skipMismatches = new List (); +[Fact] +public void ExactJavaConstructors_MonoAndroid () +{ +var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); +var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); +var (missingCtors, extraCtors) = ComparisonDiffHelper.CompareJavaConstructors (legacyData, newData); - foreach (var javaName in allJavaNames.OrderBy (n => n, StringComparer.Ordinal)) { - var inLegacy = legacyMap.TryGetValue (javaName, out var legacyEntries); - var inNew = newMap.TryGetValue (javaName, out var newEntriesForName); +AssertNoDiffs ("JAVA CONSTRUCTORS MISSING from new scanner", missingCtors); +AssertNoDiffs ("JAVA CONSTRUCTORS EXTRA in new scanner", extraCtors); +} - if (inLegacy && !inNew) { - foreach (var e in legacyEntries!) - missing.Add ($"{e.JavaName} → {e.ManagedName} (skip={e.SkipInJavaToManaged})"); - continue; - } +[Fact] +public void ExactTypeFlags_MonoAndroid () +{ +var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); +var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); +var (interfaceMismatches, abstractMismatches, genericMismatches, acwMismatches) = ComparisonDiffHelper.CompareTypeFlags (legacyData, newData); + +AssertNoDiffs ("IsInterface MISMATCHES", interfaceMismatches); +AssertNoDiffs ("IsAbstract MISMATCHES", abstractMismatches); +AssertNoDiffs ("IsGenericDefinition MISMATCHES", genericMismatches); +AssertNoDiffs ("DoNotGenerateAcw MISMATCHES", acwMismatches); +} - if (!inLegacy && inNew) { - foreach (var e in newEntriesForName!) - extra.Add ($"{e.JavaName} → {e.ManagedName} (skip={e.SkipInJavaToManaged})"); - continue; - } +[Fact] +public void ExactTypeMap_UserTypesFixture () +{ +var paths = AllUserTypesAssemblyPaths; +Assert.NotNull (paths); - var le = legacyEntries!.OrderBy (e => e.ManagedName).First (); - var ne = newEntriesForName!.OrderBy (e => e.ManagedName).First (); +var fixturePath = paths! [0]; +var (legacy, _) = ScannerRunner.RunLegacy (fixturePath); +var (newEntries, _) = ScannerRunner.RunNew (paths); +var legacyNormalized = legacy.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); +var newNormalized = newEntries.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); - if (le.ManagedName != ne.ManagedName) - managedNameMismatches.Add ($"{javaName}: legacy='{le.ManagedName}' new='{ne.ManagedName}'"); +AssertTypeMapMatch (legacyNormalized, newNormalized); +} - if (le.SkipInJavaToManaged != ne.SkipInJavaToManaged) - skipMismatches.Add ($"{javaName}: legacy.skip={le.SkipInJavaToManaged} new.skip={ne.SkipInJavaToManaged}"); - } +[Fact] +public void ExactMarshalMethods_UserTypesFixture () +{ +var paths = AllUserTypesAssemblyPaths; +Assert.NotNull (paths); - AssertNoDiffs ("MISSING", missing); - AssertNoDiffs ("EXTRA", extra); - AssertNoDiffs ("MANAGED NAME MISMATCHES", managedNameMismatches); - AssertNoDiffs ("SKIP FLAG MISMATCHES", skipMismatches); - } +var fixturePath = paths! [0]; +var (_, legacyMethods) = ScannerRunner.RunLegacy (fixturePath); +var (_, newMethods) = ScannerRunner.RunNew (paths); - static void AssertNoDiffs (string label, List items) - { - if (items.Count == 0) { - return; - } +var legacyNormalized = legacyMethods +.ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); +var newNormalized = newMethods +.ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); - var details = string.Join (Environment.NewLine, items.Take (20).Select (item => $" {item}")); - Assert.True (false, $"{label} ({items.Count}){Environment.NewLine}{details}"); - } +var result = MarshalMethodDiffHelper.CompareUserTypeMarshalMethods (legacyNormalized, newNormalized); +AssertNoDiffs ("MISSING from new scanner", result.Missing); +AssertNoDiffs ("METHOD MISMATCHES", result.MethodMismatches); +} } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs new file mode 100644 index 00000000000..008c67163d0 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Java.Interop.Tools.Cecil; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using Xamarin.Android.Tasks; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; + +record TypeMapEntry (string JavaName, string ManagedName, bool SkipInJavaToManaged); + +record MethodEntry (string JniName, string JniSignature, string? Connector); + +record TypeMethodGroup (string ManagedName, List Methods); + +static class ScannerRunner +{ + public static (List entries, Dictionary> methodsByJavaName) RunLegacy (string assemblyPath) + { + var cache = new TypeDefinitionCache (); + var resolver = new DefaultAssemblyResolver (); + resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); + + var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location); + if (runtimeDir != null) { + resolver.AddSearchDirectory (runtimeDir); + } + + var readerParams = new ReaderParameters { AssemblyResolver = resolver }; + using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams); + + var scanner = new XAJavaTypeScanner ( + Xamarin.Android.Tools.AndroidTargetArch.Arm64, + new TaskLoggingHelper (new MockBuildEngine (), "test"), + cache + ); + + var javaTypes = scanner.GetJavaTypes (assembly); + var (dataSets, _) = TypeMapCecilAdapter.GetDebugNativeEntries ( + javaTypes, cache, needUniqueAssemblies: false + ); + + var entries = dataSets.JavaToManaged + .Select (e => new TypeMapEntry (e.JavaName, e.ManagedName, e.SkipInJavaToManaged)) + .OrderBy (e => e.JavaName, StringComparer.Ordinal) + .ThenBy (e => e.ManagedName, StringComparer.Ordinal) + .ToList (); + + var methodsByJavaName = new Dictionary> (); + foreach (var typeDef in javaTypes) { + var javaName = GetCecilJavaName (typeDef); + if (javaName == null) { + continue; + } + + var managedName = GetManagedName (typeDef); + var methods = ExtractMethodRegistrations (typeDef); + + if (!methodsByJavaName.TryGetValue (javaName, out var groups)) { + groups = new List (); + methodsByJavaName [javaName] = groups; + } + + groups.Add (new TypeMethodGroup ( + managedName, + methods.OrderBy (m => m.JniName, StringComparer.Ordinal) + .ThenBy (m => m.JniSignature, StringComparer.Ordinal) + .ToList () + )); + } + + foreach (var entry in dataSets.JavaToManaged) { + if (methodsByJavaName.ContainsKey (entry.JavaName)) { + continue; + } + + methodsByJavaName [entry.JavaName] = new List { + new TypeMethodGroup (entry.ManagedName, new List ()) + }; + } + + return (entries, methodsByJavaName); + } + + public static (List entries, Dictionary> methodsByJavaName) RunNew (string[] assemblyPaths) + { + var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]); + using var scanner = new JavaPeerScanner (); + var allPeers = scanner.Scan (assemblyPaths); + var peers = allPeers.Where (p => p.AssemblyName == primaryAssemblyName).ToList (); + + var entries = peers + .Select (p => new TypeMapEntry ( + p.JavaName, + $"{p.ManagedTypeName}, {p.AssemblyName}", + p.IsInterface || p.IsGenericDefinition + )) + .OrderBy (e => e.JavaName, StringComparer.Ordinal) + .ThenBy (e => e.ManagedName, StringComparer.Ordinal) + .ToList (); + + var methodsByJavaName = new Dictionary> (); + foreach (var peer in peers) { + var managedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + + if (!methodsByJavaName.TryGetValue (peer.JavaName, out var groups)) { + groups = new List (); + methodsByJavaName [peer.JavaName] = groups; + } + + groups.Add (new TypeMethodGroup ( + managedName, + peer.MarshalMethods + .Select (m => new MethodEntry (m.JniName, m.JniSignature, m.Connector)) + .OrderBy (m => m.JniName, StringComparer.Ordinal) + .ThenBy (m => m.JniSignature, StringComparer.Ordinal) + .ToList () + )); + } + + return (entries, methodsByJavaName); + } + + public static string? GetCecilJavaName (TypeDefinition typeDef) + { + if (!typeDef.HasCustomAttributes) { + return null; + } + + foreach (var attr in typeDef.CustomAttributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { + continue; + } + + if (attr.ConstructorArguments.Count > 0) { + return ((string) attr.ConstructorArguments [0].Value).Replace ('.', '/'); + } + } + + return null; + } + + public static string GetManagedName (TypeDefinition typeDef) + { + return $"{typeDef.FullName.Replace ('/', '+')}, {typeDef.Module.Assembly.Name.Name}"; + } + + static List ExtractMethodRegistrations (TypeDefinition typeDef) + { + var methods = new List (); + + foreach (var method in typeDef.Methods) { + if (!method.HasCustomAttributes) { + continue; + } + + AddRegisterMethods (method.CustomAttributes, methods); + } + + if (typeDef.HasProperties) { + foreach (var prop in typeDef.Properties) { + if (!prop.HasCustomAttributes) { + continue; + } + + AddRegisterMethods (prop.CustomAttributes, methods); + } + } + + return methods; + } + + static void AddRegisterMethods (IEnumerable attributes, List methods) + { + foreach (var attr in attributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute" || attr.ConstructorArguments.Count < 2) { + continue; + } + + var jniMethodName = (string) attr.ConstructorArguments [0].Value; + var jniSignature = (string) attr.ConstructorArguments [1].Value; + var connector = attr.ConstructorArguments.Count > 2 + ? (string) attr.ConstructorArguments [2].Value + : null; + methods.Add (new MethodEntry (jniMethodName, jniSignature, connector)); + } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs new file mode 100644 index 00000000000..3c82cb03506 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Java.Interop.Tools.Cecil; +using Java.Interop.Tools.TypeNameMappings; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using Xamarin.Android.Tasks; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; + +record TypeComparisonData ( + string ManagedName, + string JavaName, + string? BaseJavaName, + IReadOnlyList ImplementedInterfaces, + bool HasActivationCtor, + string? ActivationCtorDeclaringType, + string? ActivationCtorStyle, + IReadOnlyList JavaConstructorSignatures, + bool IsInterface, + bool IsAbstract, + bool IsGenericDefinition, + bool DoNotGenerateAcw +); + +static class TypeDataBuilder +{ + public static (Dictionary perType, List entries) BuildLegacy (string assemblyPath) + { + var cache = new TypeDefinitionCache (); + var resolver = new DefaultAssemblyResolver (); + resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); + + var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location); + if (runtimeDir != null) { + resolver.AddSearchDirectory (runtimeDir); + } + + var readerParams = new ReaderParameters { AssemblyResolver = resolver }; + using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams); + + var scanner = new XAJavaTypeScanner ( + Xamarin.Android.Tools.AndroidTargetArch.Arm64, + new TaskLoggingHelper (new MockBuildEngine (), "test"), + cache + ); + + var javaTypes = scanner.GetJavaTypes (assembly); + var (dataSets, _) = TypeMapCecilAdapter.GetDebugNativeEntries ( + javaTypes, cache, needUniqueAssemblies: false + ); + + var entries = dataSets.JavaToManaged + .Select (e => new TypeMapEntry (e.JavaName, e.ManagedName, e.SkipInJavaToManaged)) + .OrderBy (e => e.JavaName, StringComparer.Ordinal) + .ThenBy (e => e.ManagedName, StringComparer.Ordinal) + .ToList (); + + var perType = new Dictionary (StringComparer.Ordinal); + + foreach (var typeDef in javaTypes) { + var javaName = ScannerRunner.GetCecilJavaName (typeDef); + if (javaName == null) { + continue; + } + + var managedName = ScannerRunner.GetManagedName (typeDef); + + string? baseJavaName = null; + var baseType = typeDef.GetBaseType (cache); + if (baseType != null) { + var baseJni = JavaNativeTypeManager.ToJniName (baseType, cache); + if (baseJni != null && baseJni != javaName) { + baseJavaName = baseJni; + } + } + + var implementedInterfaces = new List (); + if (typeDef.HasInterfaces) { + foreach (var ifaceImpl in typeDef.Interfaces) { + var ifaceDef = cache.Resolve (ifaceImpl.InterfaceType); + if (ifaceDef == null) { + continue; + } + var ifaceRegs = CecilExtensions.GetTypeRegistrationAttributes (ifaceDef); + var ifaceReg = ifaceRegs.FirstOrDefault (); + if (ifaceReg != null) { + implementedInterfaces.Add (ifaceReg.Name.Replace ('.', '/')); + } + } + } + implementedInterfaces.Sort (StringComparer.Ordinal); + + FindLegacyActivationCtor (typeDef, cache, + out bool hasActivationCtor, out string? activationCtorDeclaringType, out string? activationCtorStyle); + + var javaCtorSignatures = new List (); + foreach (var method in typeDef.Methods) { + if (!method.IsConstructor || method.IsStatic || !method.HasCustomAttributes) { + continue; + } + foreach (var attr in method.CustomAttributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { + continue; + } + if (attr.ConstructorArguments.Count >= 2) { + var regName = (string) attr.ConstructorArguments [0].Value; + if (regName == "" || regName == ".ctor") { + javaCtorSignatures.Add ((string) attr.ConstructorArguments [1].Value); + } + } + } + } + javaCtorSignatures.Sort (StringComparer.Ordinal); + + perType [managedName] = new TypeComparisonData ( + managedName, + javaName, + baseJavaName, + implementedInterfaces, + hasActivationCtor, + activationCtorDeclaringType, + activationCtorStyle, + javaCtorSignatures, + typeDef.IsInterface, + typeDef.IsAbstract && !typeDef.IsInterface, + typeDef.HasGenericParameters, + GetCecilDoNotGenerateAcw (typeDef) + ); + } + + return (perType, entries); + } + + public static Dictionary BuildNew (string[] assemblyPaths) + { + var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]); + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (assemblyPaths); + + var perType = new Dictionary (StringComparer.Ordinal); + + foreach (var peer in peers) { + if (peer.AssemblyName != primaryAssemblyName) { + continue; + } + + var managedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + + bool hasActivationCtor = peer.ActivationCtor != null; + string? activationCtorDeclaringType = null; + string? activationCtorStyle = null; + if (peer.ActivationCtor != null) { + activationCtorDeclaringType = $"{peer.ActivationCtor.DeclaringTypeName}, {peer.ActivationCtor.DeclaringAssemblyName}"; + activationCtorStyle = peer.ActivationCtor.Style.ToString (); + } + + var javaCtorSignatures = peer.MarshalMethods + .Where (m => m.IsConstructor) + .Select (m => m.JniSignature) + .OrderBy (s => s, StringComparer.Ordinal) + .ToList (); + + var implementedInterfaces = peer.ImplementedInterfaceJavaNames + .OrderBy (i => i, StringComparer.Ordinal) + .ToList (); + + perType [managedName] = new TypeComparisonData ( + managedName, + peer.JavaName, + peer.BaseJavaName, + implementedInterfaces, + hasActivationCtor, + activationCtorDeclaringType, + activationCtorStyle, + javaCtorSignatures, + peer.IsInterface, + peer.IsAbstract && !peer.IsInterface, + peer.IsGenericDefinition, + peer.DoNotGenerateAcw + ); + } + + return perType; + } + + static void FindLegacyActivationCtor (TypeDefinition typeDef, TypeDefinitionCache cache, + out bool found, out string? declaringType, out string? style) + { + found = false; + declaringType = null; + style = null; + + TypeDefinition? current = typeDef; + while (current != null) { + foreach (var method in current.Methods) { + if (!method.IsConstructor || method.IsStatic || method.Parameters.Count != 2) { + continue; + } + + var p0 = method.Parameters [0].ParameterType.FullName; + var p1 = method.Parameters [1].ParameterType.FullName; + + if (p0 == "System.IntPtr" && p1 == "Android.Runtime.JniHandleOwnership") { + found = true; + declaringType = ScannerRunner.GetManagedName (current); + style = "XamarinAndroid"; + return; + } + + if ((p0 == "Java.Interop.JniObjectReference&" || p0 == "Java.Interop.JniObjectReference") && + p1 == "Java.Interop.JniObjectReferenceOptions") { + found = true; + declaringType = ScannerRunner.GetManagedName (current); + style = "JavaInterop"; + return; + } + } + + current = current.GetBaseType (cache); + } + } + + static bool GetCecilDoNotGenerateAcw (TypeDefinition typeDef) + { + if (!typeDef.HasCustomAttributes) { + return false; + } + + foreach (var attr in typeDef.CustomAttributes) { + if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") { + continue; + } + if (attr.HasProperties) { + foreach (var prop in attr.Properties.Where (p => p.Name == "DoNotGenerateAcw")) { + if (prop.Argument.Value is bool val) { + return val; + } + } + } + return false; + } + + return false; + } +} From d446864e2a83fafc340125dd8e37ed181961f144 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 4 Mar 2026 13:28:07 -0600 Subject: [PATCH 8/8] Fix xUnit2020 build error: use Assert.Fail instead of Assert.True(false, ...) Replace Assert.True(false, message) with Assert.Fail(message) to satisfy the xUnit2020 analyzer rule. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ScannerComparisonTests.Helpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs index c69bbe800fb..5014fe28a4c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs @@ -135,6 +135,6 @@ static void AssertNoDiffs (string label, List items) } var details = string.Join (Environment.NewLine, items.Take (20).Select (item => $" {item}")); - Assert.True (false, $"{label} ({items.Count}){Environment.NewLine}{details}"); + Assert.Fail ($"{label} ({items.Count}){Environment.NewLine}{details}"); } }