Repository navigation
[tests] Migrate Android runtime and SourceWriter tests to MSTest/MTP - #13434
Draft
simonrozsival wants to merge 2 commits into
Draft
simonrozsival wants to merge 2 commits into
simonrozsival wants to merge 2 commits into
Conversation
This was referenced Nov 11, 2026
Context: https://learn.microsoft.com/dotnet/core/testing/unit-testing-with-dotnet-test Context: https://github.com/microsoft/testfx/blob/88a31ec394abe9f5c5dcf596f0a150ff998d3a0e/docs/source-generator/design.md Context: #13432 Context: #13433 The on-device runtime tests (`Mono.Android.NET-Tests`, its `Java.Interop-Tests` fixtures, and `Xamarin.Android.JcwGen-Tests`) run NUnit through `TestRunner.Core`. NUnit discovers and invokes tests via reflection, which fights trimming and NativeAOT. To keep it working, the test app roots entire assemblies (`<TrimmerRootAssembly RootMode="All"/>` for `Mono.Android.NET-Tests` and `Java.Interop-Tests`), sets `IlcGenerateCompleteTypeMetadata=true`, and ships a `NativeAOT.rd.xml` whose only purpose is to keep NUnit's own reflection alive: * `EquatablesComparer.GetEquatableImplementations()` calls `GetGenericArguments()[0]` on closed `IEquatable<T>` instantiations whose metadata ILC otherwise trims, and * `TaskAwaitAdapter+GenericAdapter<VoidTaskResult>` is built via `Type.MakeGenericType()` for every `async Task` test. That is, we were testing a trimmed app whose trimming had been substantially turned off for the benefit of the test framework. Migrate these suites to MSTest running natively on Microsoft.Testing.Platform (MTP), with `MSTest.SourceGeneration` emitting the test registry and invocation delegates at compile time. ### Android runtime tests * Add `tests/TestRunner.MSTest.Android`, an Android `Instrumentation` that hosts MTP in-process, registers the fixture assemblies via `AddMSTest`, applies `IncludeCategories`/`ExcludeCategories` from `runtimeconfig.json` (and `-e include`, `-e exclude`, `-e noexclusions true` extras), and streams results back through the existing `dotnet test` Android protocol. Test failures return native exit code `2`; infrastructure failures cancel instrumentation instead of reporting an empty successful run. * Convert test attributes/assertions to MSTest, preserving each case's canonical name, data rows, categories and multiplicity. Exception assertions keep their original exactness; where NUnit's `Assert.Throws` semantics differed, the test body was restructured rather than weakened. * Remove `NativeAOT.rd.xml`, `IlcGenerateCompleteTypeMetadata`, and the whole-assembly roots for `Mono.Android.NET-Tests` and `Java.Interop-Tests`. * Keep roots that protect product/JNI behavior rather than test discovery: `Mono.Android-Test.Library.NET`, `StartupHook`, converter/reflection roots, the JCW whole-app root, and new narrow `TrimmerRoots.xml` entries for `RegisterMeOnNewNativeThreadOne` (loaded only by Java name from native code; without it `RegisterTypeOnNewNativeThread` returns `-104`) and the three `ExportField` initializers on `ExportStaticField`/`ExportInstanceField` (called only from Java field initialization; without them the generated Java `ANSWER` field disappears). The existing `apk-instrumentation.yaml` CI path is unchanged: it still runs `dotnet test` from the app directory and passes `IncludeCategories=JniReferenceLeak` for the dedicated JNI lanes. ### SourceWriter host tests `Xamarin.SourceWriter-Tests` moves to MSTest/MTP as the host-side pilot. A new opt-in `runNativeMtpHostTests` pipeline parameter (default `false`) adds an independent Linux stage that installs `$(DotNetSdkVersion)` and runs: DOTNET_TEST_RUNNER=Microsoft.Testing.Platform dotnet test \ --project tests/Xamarin.SourceWriter-Tests/Xamarin.SourceWriter-Tests.csproj \ -c Debug --report-trx --results-directory $(Agent.TempDirectory)/TestResults The TRX is published with `PublishTestResults@2` and `failTaskOnMissingResultsFile: true`. MTP selection is scoped to that one step via `DOTNET_TEST_RUNNER`, so every other NUnit suite keeps using VSTest and all existing stages are unchanged; `skipTests` still skips the new stage. An earlier revision drove this stage through a custom Python adapter (`run-native-mtp-tests.py`) that did UID-based slicing, retried failed tests and rewrote TRX files. For an 11-test suite that was all cost and no benefit, so it was removed in favor of the plain `dotnet test` invocation above: MTP's exit code already fails the step, and the standard Azure task publishes the results. ### Package versions All migrated suites take their versions from `eng/Versions.props`: the Maestro-flowed `$(MSTestPackageVersion)` (`4.6.0-preview.26531.5`) plus a new testfx dependency in `eng/Version.Details.xml` for `$(MicrosoftTestingExtensionsTrxReportPackageVersion)`, so TrxReport flows in lockstep with MSTest. `Microsoft.Testing.Platform.MSBuild` comes transitively from `MSTest.TestAdapter`. The repo-wide `$(MicrosoftTestingPlatformVersion)` used by `Microsoft.Android.Run` is not touched. ### Documentation * `tests/Xamarin.SourceWriter-Tests/README.md` describes how to build, run and filter the MTP suite. * `Documentation/workflow/UnitTests.md` and the `tests` skill catalog describe the MSTest suites and their `dotnet test` commands. ### Results Release, full trim, API-35 arm64 emulator, local SDK built from this branch (`make prepare leeroy CONFIGURATION=Release`), MSTest `4.6.0-preview.26531.5`. NUnit baseline is a fresh run of the original suites on the same `main` (`598780a6`): | Suite | Cases | Passed | Skipped | NUnit baseline | | -------------- | ----: | -----: | ------: | -------------- | | Mono CoreCLR | 1046 | 1038 | 8 | identical | | Mono NativeAOT | 983 | 975 | 8 | identical | | JCW CoreCLR | 47 | 46 | 1 | identical | Each was run from its test project directory after `./dotnet-local.sh build -t:Install -c Release [-p:PublishAot=true] <project>`, via `../../../dotnet-local.sh test <project> --no-build -c Release [-p:PublishAot=true] --report-trx`. Identities, categories and multiplicities were also compared case by case earlier in development, along with the `JniReferenceLeak` (Mono 6, JCW 1) and NTLM opt-in lanes; those category lanes were not re-run on this head. `Xamarin.SourceWriter-Tests`: 11/11 passed with SDK `11.0.100-rc.3.26522.148` (the pipeline's `$(DotNetSdkVersion)`) and SDK 12; a filter matching nothing exits `8`, so failures propagate. On-device execution time was measured but is noisy: median 16.9s (MSTest) vs. 19.2s (NUnit), while repeated runs of the *unchanged* NUnit app ranged 16.7s–43.4s. This PR does not claim a CI speedup. ### Known issues * JCW under NativeAOT aborts with `SIGABRT` (`RhFailFast`) once `JavaAbstractMethodTest` starts, under **both** the original NUnit and the migrated MSTest app. This is pre-existing and not addressed here. * `MSTest.SourceGeneration` still reports incomplete descriptors for some fixtures (generic helpers, inherited members) and retains a bounded reflection fallback; this is not "reflection-free" test execution. * Only API-35 arm64 with CoreCLR and NativeAOT was exercised; the Azure stages have not been run against this revision. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CI builds test projects with TreatWarningsAsErrors. * Make BaseRocksFixture helpers protected so the MSTest source generator does not treat them as test candidates (AOTSG0004). * Rename JniTypeManagerTests.GetType, which hid object.GetType (MSTEST0036). * Use AreNotEqual for JniObjectReference struct comparisons; AreNotSame on boxed structs always passed (MSTEST0038). * Drop always-true IsNotNull checks on TimeSpan/DateTime; the surrounding DoesNotThrow is the actual assertion (MSTEST0032). * Keep IL2026/IL2062/IL2111/IL3050 non-fatal in Mono.Android.NET-Tests: the generated test registry calls tests that intentionally exercise trim/AOT-unsafe APIs, matching the existing ILLinkTreatWarningsAsErrors=false policy. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
simonrozsival
force-pushed
the
simonrozsival-mstest-delivery-host-ci
branch
from
November 11, 2026 12:35
7ea8e50 to
63e8c17
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context: https://learn.microsoft.com/dotnet/core/testing/unit-testing-with-dotnet-test
Context: https://github.com/microsoft/testfx/blob/88a31ec394abe9f5c5dcf596f0a150ff998d3a0e/docs/source-generator/design.md
Context: #13432
Context: #13433
The on-device runtime tests (
Mono.Android.NET-Tests, itsJava.Interop-Testsfixtures, andXamarin.Android.JcwGen-Tests) run NUnit throughTestRunner.Core. NUnit discovers and invokes tests via reflection, which fights trimming and NativeAOT. To keep it working, the test app roots entire assemblies (<TrimmerRootAssembly RootMode="All"/>forMono.Android.NET-TestsandJava.Interop-Tests), setsIlcGenerateCompleteTypeMetadata=true, and ships aNativeAOT.rd.xmlwhose only purpose is to keep NUnit's own reflection alive:EquatablesComparer.GetEquatableImplementations()callsGetGenericArguments()[0]on closedIEquatable<T>instantiations whose metadata ILC otherwise trims, andTaskAwaitAdapter+GenericAdapter<VoidTaskResult>is built viaType.MakeGenericType()for everyasync Tasktest.That is, we were testing a trimmed app whose trimming had been substantially turned off for the benefit of the test framework.
Migrate these suites to MSTest running natively on Microsoft.Testing.Platform (MTP), with
MSTest.SourceGenerationemitting the test registry and invocation delegates at compile time.Android runtime tests
tests/TestRunner.MSTest.Android, an AndroidInstrumentationthat hosts MTP in-process, registers the fixture assemblies viaAddMSTest, appliesIncludeCategories/ExcludeCategoriesfromruntimeconfig.json(and-e include,-e exclude,-e noexclusions trueextras), and streams results back through the existingdotnet testAndroid protocol. Test failures return native exit code2; infrastructure failures cancel instrumentation instead of reporting an empty successful run.Assert.Throwssemantics differed, the test body was restructured rather than weakened.NativeAOT.rd.xml,IlcGenerateCompleteTypeMetadata, and the whole-assembly roots forMono.Android.NET-TestsandJava.Interop-Tests.Mono.Android-Test.Library.NET,StartupHook, converter/reflection roots, the JCW whole-app root, and new narrowTrimmerRoots.xmlentries forRegisterMeOnNewNativeThreadOne(loaded only by Java name from native code; without itRegisterTypeOnNewNativeThreadreturns-104) and the threeExportFieldinitializers onExportStaticField/ExportInstanceField(called only from Java field initialization; without them the generated JavaANSWERfield disappears).Mono.Android.NET-Testskeeps the resultingIL2026/IL2062/IL2111/IL3050analyzer diagnostics as warnings, matching its existingILLinkTreatWarningsAsErrors=falsepolicy. All other CI warnings-as-errors diagnostics were fixed in the tests themselves.The existing
apk-instrumentation.yamlCI path is unchanged: it still runsdotnet testfrom the app directory and passesIncludeCategories=JniReferenceLeakfor the dedicated JNI lanes.SourceWriter host tests
Xamarin.SourceWriter-Testsmoves to MSTest/MTP as the host-side pilot. A new opt-inrunNativeMtpHostTestspipeline parameter (defaultfalse) adds an independent Linux stage that installs$(DotNetSdkVersion)and runs:The TRX is published with
PublishTestResults@2andfailTaskOnMissingResultsFile: true. MTP selection is scoped to that one step viaDOTNET_TEST_RUNNER, so every other NUnit suite keeps using VSTest and all existing stages are unchanged;skipTestsstill skips the new stage.An earlier revision drove this stage through a custom Python adapter (
run-native-mtp-tests.py) that did UID-based slicing, retried failed tests and rewrote TRX files. For an 11-test suite that was all cost and no benefit, so it was removed in favor of the plaindotnet testinvocation above: MTP's exit code already fails the step, and the standard Azure task publishes the results.Package versions
All migrated suites take their versions from
eng/Versions.props: the Maestro-flowed$(MSTestPackageVersion)(4.6.0-preview.26531.5) plus a new testfx dependency ineng/Version.Details.xmlfor$(MicrosoftTestingExtensionsTrxReportPackageVersion), so TrxReport flows in lockstep with MSTest.Microsoft.Testing.Platform.MSBuildcomes transitively fromMSTest.TestAdapter. The repo-wide$(MicrosoftTestingPlatformVersion)used byMicrosoft.Android.Runis not touched.Documentation
tests/Xamarin.SourceWriter-Tests/README.mddescribes how to build, run and filter the MTP suite.Documentation/workflow/UnitTests.mdand thetestsskill catalog describe the MSTest suites and theirdotnet testcommands.Results
Release, full trim, API-35 arm64 emulator, local SDK built from this branch (rebased on
mainaa91d328), built with-p:_AndroidTreatWarningsAsErrors=truelike CI, MSTest4.6.0-preview.26531.5:Before rebasing (on
598780a6), the same suites matched a fresh run of the original NUnit suites exactly (1046/1038/8, 983/975/8, 47/46/1); the 17 additional cases come from tests added onmainsince.Each was run from its test project directory after
./dotnet-local.sh build -t:Install -c Release [-p:PublishAot=true] <project>, via../../../dotnet-local.sh test <project> --no-build -c Release [-p:PublishAot=true] --report-trx.Identities, categories and multiplicities were also compared case by case earlier in development, along with the
JniReferenceLeak(Mono 6, JCW 1) and NTLM opt-in lanes; those category lanes were not re-run on this head.Xamarin.SourceWriter-Tests: 11/11 passed with SDK11.0.100-rc.3.26522.148(the pipeline's$(DotNetSdkVersion)) and SDK 12; a filter matching nothing exits8, so failures propagate.On-device execution time was measured but is noisy: median 16.9s (MSTest) vs. 19.2s (NUnit), while repeated runs of the unchanged NUnit app ranged 16.7s–43.4s. This PR does not claim a CI speedup.
Known issues
SIGABRT(RhFailFast) onceJavaAbstractMethodTeststarts, under both the original NUnit and the migrated MSTest app. This is pre-existing and not addressed here.MSTest.SourceGenerationstill reports incomplete descriptors for some fixtures (generic helpers, inherited members) and retains a bounded reflection fallback; this is not "reflection-free" test execution.JavaMarshalGCBridgeTests.ManagedPeerCycleSurvivesJavaRootAndIsCollectedAfterReleaseis intermittent under NativeAOT (missing global-to-weak bridge events): 1 of 3 local runs on this head failed it, the others passed. It was also seen intermittently earlier in development.