[tests] Isolate JNI reference leak tests#12038
Conversation
Move JNI reference and peer leak checks into a dedicated serial MSTest/MTP app and run it under CoreCLR and Mono in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7be7d34c-aff3-4338-b15b-6061bc7a3ba9
There was a problem hiding this comment.
Pull request overview
This PR isolates JNI reference/peer leak checks into a dedicated on-device MSTest/Microsoft Testing Platform (MTP) test app (tests/JniReferenceLeakTests) to avoid flaky process-wide assertions caused by unrelated background activity in shared device test processes.
Changes:
- Added a dedicated
JniReferenceLeakTestsAndroid test app with serialized MSTest execution ([assembly: DoNotParallelize]) and focused leak/peer assertions. - Migrated/removed flaky or process-global JNI reference/peer-count assertions from shared device test suites; narrowed remaining assertions to object-specific checks or restricted them to non-Android runs.
- Wired the new test app into solution/test documentation and into CI (package tests for CoreCLR + Mono; nightly emulator coverage).
Show a summary per file
| File | Description |
|---|---|
| Xamarin.Android-Tests.slnx | Adds the new JniReferenceLeakTests project to the tests solution structure. |
| tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs | Removes the weak-peer surfaced-object drift test from the shared device suite. |
| tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/CustomWidgetTests.cs | Removes the previously ignored/flaky custom-widget GREF leak test from the shared suite. |
| tests/JniReferenceLeakTests/JniReferenceLeakTests.csproj | New dedicated Android MSTest/MTP app project for leak/peer accounting tests. |
| tests/JniReferenceLeakTests/TestInstrumentation.cs | Implements an MTP-based Android Instrumentation runner for MSTest with TRX reporting. |
| tests/JniReferenceLeakTests/AssemblyInfo.cs | Disables MSTest parallelism at the assembly level to stabilize process-wide accounting. |
| tests/JniReferenceLeakTests/ReferenceTestHelpers.cs | Adds helpers to amplify leak detection and synchronize GC/peer collection. |
| tests/JniReferenceLeakTests/GlobalReferenceTests.cs | Adds amplified GREF leak checks (TryFindClass, activation, object arrays). |
| tests/JniReferenceLeakTests/LocalReferenceTests.cs | Adds thread-local LREF accounting checks in an isolated process. |
| tests/JniReferenceLeakTests/PeerReferenceTests.cs | Adds object-specific surfaced-peer assertions + weak-peer collection check in isolation. |
| tests/JniReferenceLeakTests/WidgetReferenceTests.cs | Reintroduces the custom-view inflate leak check in the isolated harness. |
| tests/JniReferenceLeakTests/Resources/layout/leak_test_widget.axml | Layout used to inflate a custom managed widget for the leak test. |
| tests/JniReferenceLeakTests/java/net/dot/jni/referenceleaktests/ActivationProbeFactory.java | Java-side factory to exercise Java-side activation paths. |
| tests/JniReferenceLeakTests/AndroidManifest.xml | Manifest for the new test app. |
| tests/JniReferenceLeakTests/global.json | Forces Microsoft Testing Platform runner behavior when invoking dotnet test. |
| tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs | Removes process-global GREF equality assertion from Java-side activation test. |
| external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs | Restricts process-global GREF leak assertions to non-Android runs. |
| external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs | Changes peer assertions from total-count based to peer-specific checks; restricts GREF leak test to non-Android runs. |
| external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniEnvironmentTests.cs | Restricts process-global reference-count assertions to non-Android runs. |
| external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs | Restricts runtime-global surfaced-peer count assertion to non-Android runs. |
| external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs | Restricts fixture-wide process-global GREF baseline checks to non-Android runs. |
| external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaArrayContract.cs | Restricts fixture-wide LREF baseline checks to non-Android runs (covered in new isolated app). |
| build-tools/automation/yaml-templates/stage-package-tests.yaml | Adds CI legs to run the new app under CoreCLR and Mono in package tests. |
| build-tools/automation/azure-pipelines-nightly.yaml | Adds nightly emulator coverage for the new isolated leak test app. |
| .github/skills/tests/references/test-catalog.md | Documents how to build/run the new dedicated leak test app locally. |
Copilot's findings
- Files reviewed: 25/25 changed files
- Comments generated: 0
Keep the dedicated on-device JNI reference leak checks on CoreCLR only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7be7d34c-aff3-4338-b15b-6061bc7a3ba9
Treat warnings as errors in the leak-test project locally so unavailable package versions fail before CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7be7d34c-aff3-4338-b15b-6061bc7a3ba9
Track the stable TestFX release centrally and keep the leak-test project on MSTestPackageVersion with local warnings treated as errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7be7d34c-aff3-4338-b15b-6061bc7a3ba9
✅ New
|
…icated-jni-reference-leak-tests # Conflicts: # eng/Version.Details.xml # eng/Versions.props
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
⚠️ Review — Isolate JNI reference leak tests
Solid, well-motivated PR. Moving process-wide GREF/surfaced-peer accounting out of the shared multi-fixture test apps into a dedicated [assembly: DoNotParallelize] MSTest/MTP app is the right fix for the flakiness described in #12031: the refactored contract tests now count only the specific peer under test (GetSurfacedPeerCount(peer)) instead of process-global totals, and the warmup+measurement amplification in AssertNoGlobalReferenceLeak is a sound way to turn per-op leaks into detectable deltas. Guarding the process-wide assertions in the Java.Interop suite with #if !__ANDROID__ cleanly leaves JVM-only accounting where it belongs.
Verdict:
Findings
- 💡 Unreachable
return;afterAssert.InconclusiveinPeerReferenceTests. - 💡 Surfaced-peer tolerance/batch duplicated inline instead of reusing the
ReferenceTestHelpersamplification helper. - 💡 MTP run exceptions in
TestInstrumentationare only surfaced via the resultBundle; also log toadb logcat. ⚠️ Nightly + package lanes build this app asDebugonly, so full trimming (Release-only) leaks aren't exercised — please confirm intent.
Notes
- CI status is still pending on the head commit — the Azure DevOps
dotnet-androidpipeline hadn't reported at review time. This isn't LGTM-able until those checks are green; please re-check the pipeline (and the newly-addedJniReferenceLeakTestslanes) before merge. - Nice touches: per-thread exact LREF accounting retained,
[MethodImpl(NoInlining)]on the weak-reference/array creators to keep locals from being kept alive, and the migrated custom-widget test (#11201) now running in isolation.
Positive callout: the diff meaningfully reduces ambient-noise coupling rather than just papering over flakes with wider tolerances. 👍
Generated by Android PR Reviewer for #12038 · 116.1 AIC · ⌖ 13.3 AIC · ⊞ 6.8K
Comment /review to run again
Addresses the Android PR Reviewer feedback on #12038: - Remove the unreachable `return;` after `Assert.Inconclusive` in PeerReferenceTests (Assert.Inconclusive always throws). - Log MTP run exceptions to logcat in TestInstrumentation so failures remain observable via `adb logcat` even if the result Bundle is missed or truncated. - Add a Release JniReferenceLeakTests lane to the nightly pipeline so full trimming (TrimMode=full, Release-only) reference leaks are exercised. The PR package-tests lane stays Debug to keep PR CI fast. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ad688158-19a2-49f4-a9d3-09a9457d0dfd
JniReferenceLeakTests is the first project to restore the Maestro-managed MSTest package ($(MSTestPackageVersion)) in the public dnceng-public pipeline. That version (currently 4.4.0-preview.26360.5, flowed from microsoft/testfx) is published to the public `dotnet-tools` feed, which was missing from NuGet.config. Restore therefore failed with: error NU1102: Unable to find package MSTest with version (>= 4.4.0-preview.26360.5) [nearest on dotnet-public: 4.3.2] Add the `dotnet-tools` feed so the darc-managed MSTest preview resolves in the public pipeline, fixing the JniReferenceLeakTests build lanes (and any other $(MSTestPackageVersion) consumer) without pinning away from the centrally-managed version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ad688158-19a2-49f4-a9d3-09a9457d0dfd
|
@copilot build fails with: look for best ways to resolve this. |
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
... Fixed in e254bf4 by adding an explicit |
Switch the MSTest preview feed from dotnet-tools to test-tools to match the feed convention already used in the repo for microsoft/testfx previews. Both feeds host the package; test-tools is the established one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 72dc2338-ad30-4719-8d9e-24893af0683f
Reuse the shared Xamarin.Android.UnitTests.TestInstrumentation runner (tests/TestRunner.Core) instead of a bespoke MSTest/MTP instrumentation, matching the sibling on-device NUnit apps (e.g. JcwGen-Tests). The runner executes synchronously (NumberOfTestWorkers=0), so serial execution is preserved without [assembly: DoNotParallelize], and process isolation is unchanged (still a dedicated app run via apk-instrumentation.yaml). This lets us drop the MSTest preview PackageReference (and the pinned Microsoft.Testing.Extensions.CodeCoverage), which in turn removes the need for the extra NuGet feed entirely - NuGet.config now matches main. - TestInstrumentation.cs: 15-line subclass overriding GetTestAssemblies. - csproj: NUnit + ProjectReference to TestRunner.Core; IsTestingPlatform- Application/GenerateTestingPlatformEntryPoint/AndroidInstrumentation to match the shared MTP-on-device wiring; global using NUnit.Framework. - Tests: [TestClass]/[TestMethod] -> [TestFixture]/[Test]; AssertFailedException -> AssertionException (classic Assert.* unchanged). - Remove AssemblyInfo.cs (MSTest DoNotParallelize) and the test-tools feed. - Update test-catalog.md description (MSTest/MTP -> NUnit/MTP). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 72dc2338-ad30-4719-8d9e-24893af0683f
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac393634-8640-4ed6-8094-0331551307e4
Summary
TestRunner.Coreinstrumentation with synchronous NUnit executionCreatedReferenceand Java array collection-contract coverage in the isolated appJNIEnv.DeleteRef()so ownership flags combined withDoNotRegisterstill release local/global referencesWhy
GREF and surfaced-peer counts are process-wide. The existing Android test apps load many unrelated fixtures into one process, and runtime or test-infrastructure work on background threads can change those totals while a test is taking before/after snapshots. Serializing workers in those shared apps did not remove the ambient process activity.
The new app contains only reference-accounting tests and runs NUnit synchronously through the shared instrumentation. Process-wide checks execute equal 100-operation warmup and measurement batches, allowing only a small fixed amount of ambient drift, so a per-operation leak is strongly amplified. Thread-local LREF checks retain exact accounting, while registry tests count only the specific peer under test.
The isolated array coverage exercises the inherited collection surface across every primitive and object array fixture, including
IndexOf,Insert,RemoveAt,CopyTo, and indexer boundary paths. Running that coverage with the trimmable type map exposed a latent, path-specific leak. Normal CoreCLR uses the reflection-backed value manager and releases these references through a different path. The trimmable value manager converts boxed array elements throughJavaConvert, which creates a temporary wrapper withTransferLocalRef | DoNotRegister. That combined value did not match any exact-valueJNIEnv.DeleteRef()switch case, leaving one local reference behind for every object-array getter.Typical short JNI calls masked the problem because the VM releases remaining locals when the native frame returns. The instrumentation runner keeps one frame open for the test run, so repeated operations made the leak deterministic: the
JavaObjectArray<int>contract pass accumulated 11 local references. Masking the registration-only flag before selecting the ownership action restores the expected reference cleanup.Functional coverage remains in the original suites, and JVM-only accounting tests continue to run there. The custom-widget test previously quarantined under #11201 now passes reliably in the isolated process.
Validation
git diff --checkFixes #12031.
Supersedes #12037.