Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/skills/tests/references/test-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Xamarin.Android-Tests.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<Project Path="samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/JniReferenceLeakTests/JniReferenceLeakTests.csproj" />
<Project Path="tests/StartupHook/StartupHook.csproj" />
</Folder>
<Folder Name="/tests/Mono.Android-Tests/" />
Expand Down
10 changes: 10 additions & 0 deletions build-tools/automation/azure-pipelines-nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions build-tools/automation/yaml-templates/stage-package-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions eng/Version.Details.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@
<Uri>https://github.com/dotnet/dotnet</Uri>
<Sha>b4b350a66ea5dcf13420747036d8b263cdf6cbef</Sha>
</Dependency>
<Dependency Name="MSTest" Version="4.3.0-preview.26355.11">
<Dependency Name="MSTest" Version="4.3.0">
<Uri>https://github.com/microsoft/testfx</Uri>
<Sha>8609d3bf430db360f420fbe9af9b223aa372ec2b</Sha>
<Sha>79f26ff23fac8769c84783fe2fa0cfc20134bcf8</Sha>
</Dependency>
</ToolsetDependencies>
</Dependencies>
2 changes: 1 addition & 1 deletion eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<MicrosoftNETWorkloadEmscriptenPackageVersion>$(MicrosoftNETWorkloadEmscriptenCurrentManifest110100preview4PackageVersion)</MicrosoftNETWorkloadEmscriptenPackageVersion>
<MicrosoftTemplateEngineAuthoringTasksPackageVersion>11.0.100-preview.7.26355.102</MicrosoftTemplateEngineAuthoringTasksPackageVersion>
<MicrosoftDotNetCecilPackageVersion>0.11.5-preview.26355.102</MicrosoftDotNetCecilPackageVersion>
<MSTestPackageVersion>4.3.0-preview.26355.11</MSTestPackageVersion>
<MSTestPackageVersion>4.3.0</MSTestPackageVersion>
<SystemIOHashingPackageVersion>10.0.9</SystemIOHashingPackageVersion>
<SystemReflectionMetadataPackageVersion>11.0.0-preview.1.26104.118</SystemReflectionMetadataPackageVersion>
<!-- Previous .NET Android version -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Java.InteropTests
{
public abstract class JavaArrayContract<T> : ListContract<T>
{
#if !__ANDROID__
int lrefStartCount;

[OneTimeSetUp]
Expand All @@ -24,6 +25,7 @@ public void EndArrayTests ()
int lref = JniEnvironment.LocalReferenceCount;
Assert.AreEqual (lrefStartCount, lref, "JNI local references");
}
#endif // !__ANDROID__

[Test]
public void ToArray ()
Expand All @@ -41,4 +43,3 @@ public void ToArray ()
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public class JavaObjectArray_object_ContractTest : JavaObjectArrayContractTest<o
protected override object CreateValueA () {return a;}
protected override object CreateValueB () {return 42;}

#if !__ANDROID__
int grefStartCount;

[OneTimeSetUp]
Expand All @@ -176,6 +177,7 @@ public void EndCheckGlobalRefCount ()
string.Format ("JNI global references: grefStartCount={0}; gref={1}", grefStartCount, gref));
JniEnvironment.Runtime.ValueManager.CollectPeers ();
}
#endif // !__ANDROID__

[Test]
public void ObjectArrayType ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public void JavaReferencedInstanceSurvivesCollection ()
}
#endif // !NO_GC_BRIDGE_SUPPORT

#if !__ANDROID__
[Test]
public void UnregisterFromRuntime ()
{
Expand All @@ -62,6 +63,7 @@ public void UnregisterFromRuntime ()
JniObjectReference.Dispose (ref l);
Assert.Throws<ObjectDisposedException> (() => o.UnregisterFromRuntime ());
}
#endif // !__ANDROID__

[Test]
public void RegisterWithVM_PermitsAliases ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public unsafe void Handles_NewReturnToJniRef ()
}
}

#if !__ANDROID__
[Test]
public void References_CreatedReference_InvalidRef ()
{
Expand Down Expand Up @@ -142,6 +143,6 @@ public void References_CreatedReference_GlobalRef ()
GetDeleteRefFunc ("java_interop_jnienv_delete_global_ref")?.Invoke (JniEnvironment.EnvironmentPointer, o.Handle);
}
}
#endif // !__ANDROID__
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -61,75 +61,51 @@ 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);
}

[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 ()
Expand Down Expand Up @@ -169,6 +145,7 @@ public void ConstructPeer_CalledMultipleTimes_ShouldNotLeakGlobalRefs ()
}
}
}
#endif // !__ANDROID__


[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConstructorTest>();
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));
}
}

Expand Down Expand Up @@ -391,4 +387,3 @@ public class B : Java.Lang.Object {
}
}
}

6 changes: 6 additions & 0 deletions tests/JniReferenceLeakTests/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:label="JniReferenceLeakTests">
</application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
3 changes: 3 additions & 0 deletions tests/JniReferenceLeakTests/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;

[assembly: DoNotParallelize]
84 changes: 84 additions & 0 deletions tests/JniReferenceLeakTests/GlobalReferenceTests.cs
Original file line number Diff line number Diff line change
@@ -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<object> (1);
array [0] = value;
}
}

[Register (JniName)]
public class ActivationProbe : Java.Lang.Object
{
internal const string JniName = "net/dot/jni/referenceleaktests/ActivationProbe";
const string FactoryJniName = "net/dot/jni/referenceleaktests/ActivationProbeFactory";

public bool DefaultConstructorInvoked { get; }

public ActivationProbe ()
{
DefaultConstructorInvoked = true;
}

public static unsafe ActivationProbe CreateFromJava ()
{
using var type = new JniType (JniName);
using var factory = new JniType (FactoryJniName);
var create = factory.GetStaticMethod ("create", "(Ljava/lang/Class;)Ljava/lang/Object;");
JniArgumentValue* arguments = stackalloc JniArgumentValue [1];
arguments [0] = new JniArgumentValue (type.PeerReference);
var reference = JniEnvironment.StaticMethods.CallStaticObjectMethod (factory.PeerReference, create, arguments);
try {
if (JniEnvironment.Runtime.ValueManager.PeekPeer (reference) is not ActivationProbe instance) {
throw new InvalidOperationException ("Java-side activation did not create an ActivationProbe peer.");
}

return instance;
} finally {
JniObjectReference.Dispose (ref reference);
}
}
}
Loading
Loading