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..2227e7ae8fc 100644 --- a/build-tools/automation/yaml-templates/build-windows-steps.yaml +++ b/build-tools/automation/yaml-templates/build-windows-steps.yaml @@ -92,6 +92,21 @@ 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 + continueOnError: true - task: BatchScript@1 displayName: Test dotnet-local.cmd - create template inputs: 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")] 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/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/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.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 new file mode 100644 index 00000000000..3b0d79e43d8 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -0,0 +1,135 @@ +using System.Linq; +using Xamarin.Android.Tasks; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; + +public partial class ScannerComparisonTests +{ +[Fact] +public void ExactTypeMap_MonoAndroid () +{ +var (legacy, _) = ScannerRunner.RunLegacy (MonoAndroidAssemblyPath); +var (newEntries, _) = ScannerRunner.RunNew (AllAssemblyPaths); +AssertTypeMapMatch (legacy, newEntries); +} + +[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); +} + +[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 (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); +var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); +var mismatches = ComparisonDiffHelper.CompareBaseJavaNames (legacyData, newData); + +AssertNoDiffs ("BASE JAVA NAME MISMATCHES", mismatches); +} + +[Fact] +public void ExactImplementedInterfaces_MonoAndroid () +{ +var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); +var newData = TypeDataBuilder.BuildNew (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 (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); +var newData = TypeDataBuilder.BuildNew (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 (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); +var newData = TypeDataBuilder.BuildNew (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 (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); +} + +[Fact] +public void ExactTypeMap_UserTypesFixture () +{ +var paths = AllUserTypesAssemblyPaths; +Assert.NotNull (paths); + +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 (); + +AssertTypeMapMatch (legacyNormalized, newNormalized); +} + +[Fact] +public void ExactMarshalMethods_UserTypesFixture () +{ +var paths = AllUserTypesAssemblyPaths; +Assert.NotNull (paths); + +var fixturePath = paths! [0]; +var (_, legacyMethods) = ScannerRunner.RunLegacy (fixturePath); +var (_, newMethods) = ScannerRunner.RunNew (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 = 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; + } +} 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..75586236a8e --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs @@ -0,0 +1,158 @@ +// 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")] + +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 + { + } +} + +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; + } +} + +namespace UserApp.Receivers +{ + [BroadcastReceiver (Name = "com.example.userapp.BootReceiver", Exported = false)] + public class BootReceiver : BroadcastReceiver + { + public override void OnReceive (Context? context, Intent? intent) + { + } + } +} + +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) + { + } + } +} + +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 + { + } + } + } +} + +namespace UserApp.Models +{ + // These should all get CRC64-based JNI names + public class UserModel : Java.Lang.Object + { + } + + public class DataManager : Java.Lang.Object + { + } +} + +namespace UserApp +{ + [Register ("com/example/userapp/CustomView")] + public class CustomView : Android.Views.View + { + protected CustomView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace UserApp.Listeners +{ + public class MyClickListener : Java.Lang.Object, Android.Views.View.IOnClickListener + { + public void OnClick (Android.Views.View? v) + { + } + } +} + +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) + + + + +