diff --git a/.github/skills/tests/references/test-catalog.md b/.github/skills/tests/references/test-catalog.md
index ca48bdadc0d..441d74dc117 100644
--- a/.github/skills/tests/references/test-catalog.md
+++ b/.github/skills/tests/references/test-catalog.md
@@ -123,6 +123,7 @@ Device: **Yes**
| Test Area | Project | Notes |
|-----------|---------|-------|
| **runtime** (all) | `tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj` | Core runtime tests |
+| **JNI reference leaks** | `tests/JniReferenceLeakTests/JniReferenceLeakTests.csproj` | Dedicated serial MSTest/MTP app for JNI reference and peer leak checks |
| **networking** | Same project — tests in `Xamarin.Android.Net/` and `System.Net/` | `AndroidMessageHandlerTests`, `AndroidMessageHandlerIntegrationTests` |
| **java interop (on-device)** | Same project — tests in `Java.Interop/` | `JnienvTest`, `JavaListTest` |
| **android app** | Same project — tests in `Android.App/` | `Application`, `Activity` tests |
@@ -154,6 +155,15 @@ Command:
Results are `.trx` files under `bin/TestDebug/TestResults/` and are published as VSTest results in CI. Always pass the same configuration and MSBuild properties to the install and `dotnet test --no-build` commands (for example, `-c Release -p:UseMonoRuntime=false`).
+For the dedicated JNI reference leak tests:
+```bash
+./dotnet-local.sh build -t:Install -c Debug tests/JniReferenceLeakTests/JniReferenceLeakTests.csproj
+(
+ cd tests/JniReferenceLeakTests
+ ../../dotnet-local.sh test JniReferenceLeakTests.csproj --no-build -c Debug --report-trx --results-directory ../../bin/TestDebug/TestResults
+)
+```
+
For `Xamarin.Android.JcwGen-Tests`, use the same pattern from `tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/`:
```bash
./dotnet-local.sh build -t:Install -c Debug tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj
diff --git a/Xamarin.Android-Tests.slnx b/Xamarin.Android-Tests.slnx
index 14fb985a7a5..6d830e68144 100644
--- a/Xamarin.Android-Tests.slnx
+++ b/Xamarin.Android-Tests.slnx
@@ -27,6 +27,7 @@
+
diff --git a/build-tools/automation/azure-pipelines-nightly.yaml b/build-tools/automation/azure-pipelines-nightly.yaml
index 12733b86759..a4bb08d0281 100644
--- a/build-tools/automation/azure-pipelines-nightly.yaml
+++ b/build-tools/automation/azure-pipelines-nightly.yaml
@@ -145,6 +145,16 @@ stages:
artifactSource: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)-android/Mono.Android.NET_Tests-Signed.aab
artifactFolder: Mono.Android-$(XA.Build.Configuration)-$(avdApiLevel)
+ - template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml
+ parameters:
+ buildConfiguration: $(XA.Build.Configuration)
+ configuration: Debug
+ testName: JniReferenceLeakTests-$(avdApiLevel)
+ project: tests/JniReferenceLeakTests/JniReferenceLeakTests.csproj
+ extraBuildArgs: -p:UseMonoRuntime=false -p:TestAvdApiLevel=$(avdApiLevel) -p:TestAvdAbi=$(avdAbi) -p:TestAvdType=$(avdType)
+ artifactSource: bin/TestDebug/$(DotNetTargetFramework)-android/JniReferenceLeakTests-Signed.apk
+ artifactFolder: JniReferenceLeakTests-$(avdApiLevel)
+
- template: /build-tools/automation/yaml-templates/upload-results.yaml
parameters:
configuration: $(XA.Build.Configuration)
diff --git a/build-tools/automation/yaml-templates/stage-package-tests.yaml b/build-tools/automation/yaml-templates/stage-package-tests.yaml
index d4a684b183c..dc233aaff3b 100644
--- a/build-tools/automation/yaml-templates/stage-package-tests.yaml
+++ b/build-tools/automation/yaml-templates/stage-package-tests.yaml
@@ -140,6 +140,16 @@ stages:
- template: /build-tools/automation/yaml-templates/start-stop-emulator.yaml
+ - template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml
+ parameters:
+ buildConfiguration: $(XA.Build.Configuration)
+ configuration: Debug
+ testName: JniReferenceLeakTests-CoreCLR
+ project: tests/JniReferenceLeakTests/JniReferenceLeakTests.csproj
+ extraBuildArgs: -p:UseMonoRuntime=false
+ artifactSource: bin/TestDebug/$(DotNetTargetFramework)-android/JniReferenceLeakTests-Signed.apk
+ artifactFolder: $(DotNetTargetFramework)-JniReferenceLeakTests-CoreCLR
+
- template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml
parameters:
configuration: $(XA.Build.Configuration)
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 15fa92708b6..bba8767a19e 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -48,9 +48,9 @@
https://github.com/dotnet/dotnet
b4b350a66ea5dcf13420747036d8b263cdf6cbef
-
+
https://github.com/microsoft/testfx
- 8609d3bf430db360f420fbe9af9b223aa372ec2b
+ 79f26ff23fac8769c84783fe2fa0cfc20134bcf8
diff --git a/eng/Versions.props b/eng/Versions.props
index 7323919cabd..d92ef0b8f14 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -15,7 +15,7 @@
$(MicrosoftNETWorkloadEmscriptenCurrentManifest110100preview4PackageVersion)
11.0.100-preview.7.26355.102
0.11.5-preview.26355.102
- 4.3.0-preview.26355.11
+ 4.3.0
10.0.9
11.0.0-preview.1.26104.118
diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaArrayContract.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaArrayContract.cs
index 1279e28426c..5dd341166fa 100644
--- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaArrayContract.cs
+++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaArrayContract.cs
@@ -10,6 +10,7 @@ namespace Java.InteropTests
{
public abstract class JavaArrayContract : ListContract
{
+#if !__ANDROID__
int lrefStartCount;
[OneTimeSetUp]
@@ -24,6 +25,7 @@ public void EndArrayTests ()
int lref = JniEnvironment.LocalReferenceCount;
Assert.AreEqual (lrefStartCount, lref, "JNI local references");
}
+#endif // !__ANDROID__
[Test]
public void ToArray ()
@@ -41,4 +43,3 @@ public void ToArray ()
}
}
}
-
diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs
index 3e5b9c9fc56..30bdbc21868 100644
--- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs
+++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs
@@ -157,6 +157,7 @@ public class JavaObjectArray_object_ContractTest : JavaObjectArrayContractTest (() => o.UnregisterFromRuntime ());
}
+#endif // !__ANDROID__
[Test]
public void RegisterWithVM_PermitsAliases ()
diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniEnvironmentTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniEnvironmentTests.cs
index fd91beb3d72..e71cf856db4 100644
--- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniEnvironmentTests.cs
+++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniEnvironmentTests.cs
@@ -55,6 +55,7 @@ public unsafe void Handles_NewReturnToJniRef ()
}
}
+#if !__ANDROID__
[Test]
public void References_CreatedReference_InvalidRef ()
{
@@ -142,6 +143,6 @@ public void References_CreatedReference_GlobalRef ()
GetDeleteRefFunc ("java_interop_jnienv_delete_global_ref")?.Invoke (JniEnvironment.EnvironmentPointer, o.Handle);
}
}
+#endif // !__ANDROID__
}
}
-
diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs
index dc41461b685..48a73295035 100644
--- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs
+++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs
@@ -61,45 +61,33 @@ public void AddPeer ()
{
}
- int GetSurfacedPeersCount ()
+ int GetSurfacedPeerCount (IJavaPeerable peer)
{
- return valueManager.GetSurfacedPeers ().Count;
+ return valueManager.GetSurfacedPeers ().Count (candidate => {
+ return candidate.SurfacedPeer.TryGetTarget (out var target) && object.ReferenceEquals (target, peer);
+ });
}
[Test]
public void AddPeer_NoDuplicates ()
{
- int startPeerCount = GetSurfacedPeersCount ();
using (var v = new MyDisposableObject ()) {
// MyDisposableObject ctor implicitly calls AddPeer();
- Assert.AreEqual (startPeerCount + 1, GetSurfacedPeersCount (), DumpPeers ());
+ Assert.AreEqual (1, GetSurfacedPeerCount (v), DumpPeers ());
valueManager.AddPeer (v);
- Assert.AreEqual (startPeerCount + 1, GetSurfacedPeersCount (), DumpPeers ());
+ Assert.AreEqual (1, GetSurfacedPeerCount (v), DumpPeers ());
}
}
[Test]
public void ConstructPeer_ImplicitViaBindingConstructor_PeerIsInSurfacedPeers ()
{
- int startPeerCount = GetSurfacedPeersCount ();
-
var g = new GetThis ();
- var surfaced = valueManager.GetSurfacedPeers ();
- Assert.AreEqual (startPeerCount + 1, surfaced.Count);
-
- var found = false;
- foreach (var pr in surfaced) {
- if (!pr.SurfacedPeer.TryGetTarget (out var p))
- continue;
- if (object.ReferenceEquals (g, p)) {
- found = true;
- }
- }
- Assert.IsTrue (found);
+ Assert.AreEqual (1, GetSurfacedPeerCount (g), DumpPeers ());
var localRef = g.PeerReference.NewLocalRef ();
g.Dispose ();
- Assert.AreEqual (startPeerCount, GetSurfacedPeersCount ());
+ Assert.AreEqual (0, GetSurfacedPeerCount (g));
Assert.IsNull (valueManager.PeekPeer (localRef));
JniObjectReference.Dispose (ref localRef);
}
@@ -107,29 +95,17 @@ public void ConstructPeer_ImplicitViaBindingConstructor_PeerIsInSurfacedPeers ()
[Test]
public void ConstructPeer_ImplicitViaBindingMethod_PeerIsInSurfacedPeers ()
{
- int startPeerCount = GetSurfacedPeersCount ();
-
var g = new GetThis ();
- var surfaced = valueManager.GetSurfacedPeers ();
- Assert.AreEqual (startPeerCount + 1, surfaced.Count);
-
- var found = false;
- foreach (var pr in surfaced) {
- if (!pr.SurfacedPeer.TryGetTarget (out var p))
- continue;
- if (object.ReferenceEquals (g, p)) {
- found = true;
- }
- }
- Assert.IsTrue (found);
+ Assert.AreEqual (1, GetSurfacedPeerCount (g), DumpPeers ());
var localRef = g.PeerReference.NewLocalRef ();
g.Dispose ();
- Assert.AreEqual (startPeerCount, GetSurfacedPeersCount ());
+ Assert.AreEqual (0, GetSurfacedPeerCount (g));
Assert.IsNull (valueManager.PeekPeer (localRef));
JniObjectReference.Dispose (ref localRef);
}
+#if !__ANDROID__
// https://github.com/dotnet/android/issues/11101
[Test]
public void ConstructPeer_CalledMultipleTimes_ShouldNotLeakGlobalRefs ()
@@ -169,6 +145,7 @@ public void ConstructPeer_CalledMultipleTimes_ShouldNotLeakGlobalRefs ()
}
}
}
+#endif // !__ANDROID__
[Test]
diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs
index feba304c0a5..753acf11fef 100644
--- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs
+++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs
@@ -90,8 +90,8 @@ public void TryFindClass_Utf8 ()
Assert.IsFalse (notFound.IsValid);
}
+#if !__ANDROID__
[Test]
- [Ignore ("Frequently failing: https://github.com/dotnet/android/issues/12031")]
public void TryFindClass_Utf8_DoesNotLeakGlobalRefs ()
{
int grefsBefore = JniEnvironment.Runtime.GlobalReferenceCount;
@@ -110,6 +110,7 @@ public void TryFindClass_String_DoesNotLeakGlobalRefs ()
Assert.AreEqual (grefsBefore, grefsAfter,
"TryFindClass for non-existent classes should not leak global references");
}
+#endif // !__ANDROID__
[Test]
public void GetMethodID_Utf8_MatchesStringOverload ()
diff --git a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs
index 33132c9479e..c4054b74286 100644
--- a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs
+++ b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs
@@ -101,16 +101,12 @@ public void JavaSideActivation ()
// To ensure that CallMethodFromCtor.class_ref is initialized
}
using (var c = Java.Lang.Class.FromType (typeof (ConstructorTest))) {
- int initGref = Java.Interop.Runtime.GlobalReferenceCount;
using (var j = Com.Xamarin.Android.CallMethodFromCtor.NewInstance (c)) {
var instance = j.JavaCast();
Assert.AreSame (j, instance);
Assert.IsTrue (instance.DefaultConstructorInvoked);
Assert.IsTrue (instance.ActivationConstructorInvoked);
}
- int finiGref = Java.Interop.Runtime.GlobalReferenceCount;
- Assert.AreEqual (initGref, finiGref,
- string.Format ("Initial grefc={0}; final gref={1}; No GREFs should be lost!", initGref, finiGref));
}
}
@@ -391,4 +387,3 @@ public class B : Java.Lang.Object {
}
}
}
-
diff --git a/tests/JniReferenceLeakTests/AndroidManifest.xml b/tests/JniReferenceLeakTests/AndroidManifest.xml
new file mode 100644
index 00000000000..960b6866d49
--- /dev/null
+++ b/tests/JniReferenceLeakTests/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/tests/JniReferenceLeakTests/AssemblyInfo.cs b/tests/JniReferenceLeakTests/AssemblyInfo.cs
new file mode 100644
index 00000000000..3119b2d65c5
--- /dev/null
+++ b/tests/JniReferenceLeakTests/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[assembly: DoNotParallelize]
diff --git a/tests/JniReferenceLeakTests/GlobalReferenceTests.cs b/tests/JniReferenceLeakTests/GlobalReferenceTests.cs
new file mode 100644
index 00000000000..48dc05f52d4
--- /dev/null
+++ b/tests/JniReferenceLeakTests/GlobalReferenceTests.cs
@@ -0,0 +1,84 @@
+using System.Runtime.CompilerServices;
+
+using Android.Runtime;
+using Java.Interop;
+
+namespace JniReferenceLeakTests;
+
+[TestClass]
+public sealed class GlobalReferenceTests
+{
+ [TestMethod]
+ public void TryFindClassUtf8DoesNotLeakGlobalReferences ()
+ {
+ ReferenceTestHelpers.AssertNoGlobalReferenceLeak (() => {
+ Assert.IsFalse (JniEnvironment.Types.TryFindClass ("does/not/Exist"u8, out var notFound));
+ Assert.IsFalse (notFound.IsValid);
+ });
+ }
+
+ [TestMethod]
+ public void TryFindClassStringDoesNotLeakGlobalReferences ()
+ {
+ ReferenceTestHelpers.AssertNoGlobalReferenceLeak (() => {
+ Assert.IsFalse (JniEnvironment.Types.TryFindClass ("does/not/Exist", out var notFound));
+ Assert.IsFalse (notFound.IsValid);
+ });
+ }
+
+ [TestMethod]
+ public void JavaObjectArrayOperationsDoNotLeakGlobalReferences ()
+ {
+ ReferenceTestHelpers.AssertNoGlobalReferenceLeak (CreateAndDisposeObjectArray);
+ }
+
+ [TestMethod]
+ public void JavaSideActivationDoesNotLeakGlobalReferences ()
+ {
+ ReferenceTestHelpers.AssertNoGlobalReferenceLeak (() => {
+ using var instance = ActivationProbe.CreateFromJava ();
+ Assert.IsTrue (instance.DefaultConstructorInvoked);
+ });
+ }
+
+ [MethodImpl (MethodImplOptions.NoInlining)]
+ static void CreateAndDisposeObjectArray ()
+ {
+ var value = new object ();
+ using var array = new JavaObjectArray