From 9402670711a7dcf4948ecbdd942a178c8bbf0d90 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 28 Aug 2026 13:03:16 +0200 Subject: [PATCH 1/2] [tests] Cover virtual callbacks during construction Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../VirtualCallbackConstructorTests.cs | 241 ++++++++++++++++++ .../Mono.Android.NET-Tests.csproj | 1 + .../test/VirtualCallbackConstructorBase.java | 10 + 3 files changed, 252 insertions(+) create mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/VirtualCallbackConstructorTests.cs create mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/VirtualCallbackConstructorBase.java diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/VirtualCallbackConstructorTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/VirtualCallbackConstructorTests.cs new file mode 100644 index 00000000000..b617d795c01 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/VirtualCallbackConstructorTests.cs @@ -0,0 +1,241 @@ +#nullable enable annotations + +using System; +using System.Threading; + +using Android.Runtime; + +using Java.Interop; + +using NUnit.Framework; + +namespace Java.InteropTests +{ + [TestFixture] + [NonParallelizable] + [Category ("VirtualCallbackConstructorParity")] + public class VirtualCallbackConstructorTests + { + [Test] + public void ManagedFirstConstructionUsesFinalPeerThroughoutCallback () + { + VirtualCallbackConstructorDerived.Reset (); + + using var javaClass = VirtualCallbackConstructorDerived.GetJavaClass (); + using (var instance = new VirtualCallbackConstructorDerived (42)) { + Assert.AreEqual (1, VirtualCallbackConstructorDerived.ConstructorInvocations); + Assert.AreEqual (0, VirtualCallbackConstructorDerived.ActivationConstructorInvocations); + Assert.AreEqual (1, VirtualCallbackConstructorDerived.CallbackInvocations); + Assert.AreEqual (42, instance.CallbackValue); + Assert.AreSame (instance, VirtualCallbackConstructorDerived.CallbackPeer); + Assert.AreSame (instance, VirtualCallbackConstructorDerived.ReentrantPeer); + Assert.AreSame (instance, VirtualCallbackConstructorDerived.ConstructorPeer); + AssertRegisteredSame (instance); + } + + VirtualCallbackConstructorDerived.ClearReferences (); + } + + [Test] + public void JavaFirstConstructionPromotesOneProvisionalPeer () + { + VirtualCallbackConstructorDerived.Reset (); + VirtualCallbackConstructorDerived.RunConcurrentLookup = true; + + using (var instance = CreateFromJava (42)) { + Assert.AreEqual (1, VirtualCallbackConstructorDerived.ConstructorInvocations); + Assert.AreEqual (1, VirtualCallbackConstructorDerived.ActivationConstructorInvocations); + Assert.AreEqual (1, VirtualCallbackConstructorDerived.CallbackInvocations); + Assert.AreEqual (42, instance.CallbackValue); + Assert.AreSame (instance, VirtualCallbackConstructorDerived.ActivationPeer); + Assert.AreSame (instance, VirtualCallbackConstructorDerived.CallbackPeer); + Assert.AreSame (instance, VirtualCallbackConstructorDerived.ReentrantPeer); + Assert.AreSame (instance, VirtualCallbackConstructorDerived.ConstructorPeer); + Assert.IsNull (VirtualCallbackConstructorDerived.ConcurrentLookupException); + Assert.IsNotNull (VirtualCallbackConstructorDerived.ConcurrentPeers); + foreach (var peer in VirtualCallbackConstructorDerived.ConcurrentPeers) { + Assert.AreSame (instance, peer); + } + AssertRegisteredSame (instance); + } + + VirtualCallbackConstructorDerived.ClearReferences (); + } + + [Test] + public void JavaFirstVirtualCallbackExceptionAbortsConstruction () + { + VirtualCallbackConstructorDerived.Reset (); + VirtualCallbackConstructorDerived.ThrowFromCallback = true; + + try { + var exception = Assert.Catch (() => CreateFromJavaExpectingException (42)); + + Assert.IsNotNull (exception); + Assert.IsTrue (exception.ToString ().Contains (VirtualCallbackConstructorDerived.CallbackExceptionMessage)); + Assert.AreEqual (0, VirtualCallbackConstructorDerived.ConstructorInvocations); + Assert.AreEqual (1, VirtualCallbackConstructorDerived.ActivationConstructorInvocations); + Assert.AreEqual (1, VirtualCallbackConstructorDerived.CallbackInvocations); + Assert.AreSame (VirtualCallbackConstructorDerived.ActivationPeer, VirtualCallbackConstructorDerived.CallbackPeer); + Assert.AreSame (VirtualCallbackConstructorDerived.CallbackPeer, VirtualCallbackConstructorDerived.ReentrantPeer); + } finally { + VirtualCallbackConstructorDerived.CallbackPeer?.Dispose (); + VirtualCallbackConstructorDerived.ClearReferences (); + } + } + + static VirtualCallbackConstructorDerived CreateFromJava (int value) + { + using var javaClass = VirtualCallbackConstructorDerived.GetJavaClass (); + var constructor = JNIEnv.GetMethodID (javaClass.Handle, "", "(I)V"); + var instance = JNIEnv.StartCreateInstance (javaClass.Handle, constructor, new JValue (value)); + JNIEnv.FinishCreateInstance (instance, javaClass.Handle, constructor, new JValue (value)); + var result = Java.Lang.Object.GetObject ( + instance, + JniHandleOwnership.TransferLocalRef); + Assert.IsNotNull (result); + return result; + } + + static void CreateFromJavaExpectingException (int value) + { + IntPtr instance = IntPtr.Zero; + try { + using var javaClass = VirtualCallbackConstructorDerived.GetJavaClass (); + var constructor = JNIEnv.GetMethodID (javaClass.Handle, "", "(I)V"); + instance = JNIEnv.StartCreateInstance (javaClass.Handle, constructor, new JValue (value)); + JNIEnv.FinishCreateInstance (instance, javaClass.Handle, constructor, new JValue (value)); + } finally { + if (instance != IntPtr.Zero) { + JNIEnv.DeleteLocalRef (instance); + } + } + } + + static void AssertRegisteredSame (VirtualCallbackConstructorDerived instance) + { + var registered = Java.Lang.Object.GetObject ( + instance.Handle, + JniHandleOwnership.DoNotTransfer); + Assert.AreSame (instance, registered); + } + } + + [Register ("net/dot/android/test/VirtualCallbackConstructorDerived")] + public class VirtualCallbackConstructorDerived : global::Net.Dot.Android.Test.VirtualCallbackConstructorBase + { + public const string CallbackExceptionMessage = "virtual constructor callback throw"; + const string JavaClassName = "net.dot.android.test.VirtualCallbackConstructorDerived"; + + public static int ConstructorInvocations; + public static int ActivationConstructorInvocations; + public static int CallbackInvocations; + public static bool RunConcurrentLookup; + public static bool ThrowFromCallback; + public static VirtualCallbackConstructorDerived? ActivationPeer; + public static VirtualCallbackConstructorDerived? CallbackPeer; + public static VirtualCallbackConstructorDerived? ReentrantPeer; + public static VirtualCallbackConstructorDerived? ConstructorPeer; + public static VirtualCallbackConstructorDerived? []? ConcurrentPeers; + public static Exception? ConcurrentLookupException; + + public int CallbackValue; + + [Register (".ctor", "(I)V", "")] + public VirtualCallbackConstructorDerived (int value) + : base (value) + { + ConstructorInvocations++; + ConstructorPeer = this; + } + + public VirtualCallbackConstructorDerived (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + RecordActivation (this); + } + + public override void OnConstructed (int value) + { + CallbackInvocations++; + CallbackValue = value; + CallbackPeer = this; + ReentrantPeer = Java.Lang.Object.GetObject ( + Handle, + JniHandleOwnership.DoNotTransfer); + + if (RunConcurrentLookup) { + RunConcurrentLookups (); + } + + if (ThrowFromCallback) { + throw new InvalidOperationException (CallbackExceptionMessage); + } + } + + internal static void RecordActivation (VirtualCallbackConstructorDerived peer) + { + ActivationConstructorInvocations++; + ActivationPeer = peer; + } + + internal static Java.Lang.Class GetJavaClass () + => Java.Lang.Class.ForName ( + JavaClassName, + initialize: true, + Android.App.Application.Context.ClassLoader); + + static void RunConcurrentLookups () + { + const int threadCount = 2; + var peers = new VirtualCallbackConstructorDerived? [threadCount]; + using var ready = new CountdownEvent (threadCount); + using var start = new ManualResetEventSlim (); + var threads = new Thread [threadCount]; + + for (int i = 0; i < threads.Length; i++) { + int index = i; + threads [i] = new Thread (() => { + ready.Signal (); + start.Wait (); + try { + peers [index] = Java.Lang.Object.GetObject ( + CallbackPeer?.Handle ?? IntPtr.Zero, + JniHandleOwnership.DoNotTransfer); + } catch (Exception e) { + ConcurrentLookupException = e; + } + }); + threads [i].Start (); + } + + ready.Wait (); + start.Set (); + foreach (var thread in threads) { + thread.Join (); + } + + ConcurrentPeers = peers; + } + + public static void Reset () + { + ConstructorInvocations = 0; + ActivationConstructorInvocations = 0; + CallbackInvocations = 0; + RunConcurrentLookup = false; + ThrowFromCallback = false; + ClearReferences (); + } + + public static void ClearReferences () + { + ActivationPeer = null; + CallbackPeer = null; + ReentrantPeer = null; + ConstructorPeer = null; + ConcurrentPeers = null; + ConcurrentLookupException = null; + } + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index 6405287d880..7456fa0d33e 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -144,6 +144,7 @@ + diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/VirtualCallbackConstructorBase.java b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/VirtualCallbackConstructorBase.java new file mode 100644 index 00000000000..77fe17259d0 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/VirtualCallbackConstructorBase.java @@ -0,0 +1,10 @@ +package net.dot.android.test; + +public class VirtualCallbackConstructorBase { + public VirtualCallbackConstructorBase (int value) { + onConstructed (value); + } + + public void onConstructed (int value) { + } +} From ee1c3b6399a462c29a8abd31bfe5f19a7d0a578f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 28 Aug 2026 13:10:23 +0200 Subject: [PATCH 2/2] [tests] Bound constructor concurrency waits Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../VirtualCallbackConstructorTests.cs | 55 ++++++++++++++----- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/VirtualCallbackConstructorTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/VirtualCallbackConstructorTests.cs index b617d795c01..c1d694c1319 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/VirtualCallbackConstructorTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/VirtualCallbackConstructorTests.cs @@ -20,6 +20,7 @@ public class VirtualCallbackConstructorTests public void ManagedFirstConstructionUsesFinalPeerThroughoutCallback () { VirtualCallbackConstructorDerived.Reset (); + VirtualCallbackConstructorDerived.RunConcurrentLookup = true; using var javaClass = VirtualCallbackConstructorDerived.GetJavaClass (); using (var instance = new VirtualCallbackConstructorDerived (42)) { @@ -30,6 +31,7 @@ public void ManagedFirstConstructionUsesFinalPeerThroughoutCallback () Assert.AreSame (instance, VirtualCallbackConstructorDerived.CallbackPeer); Assert.AreSame (instance, VirtualCallbackConstructorDerived.ReentrantPeer); Assert.AreSame (instance, VirtualCallbackConstructorDerived.ConstructorPeer); + AssertConcurrentPeers (instance); AssertRegisteredSame (instance); } @@ -51,11 +53,7 @@ public void JavaFirstConstructionPromotesOneProvisionalPeer () Assert.AreSame (instance, VirtualCallbackConstructorDerived.CallbackPeer); Assert.AreSame (instance, VirtualCallbackConstructorDerived.ReentrantPeer); Assert.AreSame (instance, VirtualCallbackConstructorDerived.ConstructorPeer); - Assert.IsNull (VirtualCallbackConstructorDerived.ConcurrentLookupException); - Assert.IsNotNull (VirtualCallbackConstructorDerived.ConcurrentPeers); - foreach (var peer in VirtualCallbackConstructorDerived.ConcurrentPeers) { - Assert.AreSame (instance, peer); - } + AssertConcurrentPeers (instance); AssertRegisteredSame (instance); } @@ -119,6 +117,15 @@ static void AssertRegisteredSame (VirtualCallbackConstructorDerived instance) JniHandleOwnership.DoNotTransfer); Assert.AreSame (instance, registered); } + + static void AssertConcurrentPeers (VirtualCallbackConstructorDerived instance) + { + Assert.IsNull (VirtualCallbackConstructorDerived.ConcurrentLookupException); + Assert.IsNotNull (VirtualCallbackConstructorDerived.ConcurrentPeers); + foreach (var peer in VirtualCallbackConstructorDerived.ConcurrentPeers) { + Assert.AreSame (instance, peer); + } + } } [Register ("net/dot/android/test/VirtualCallbackConstructorDerived")] @@ -188,34 +195,56 @@ internal static Java.Lang.Class GetJavaClass () static void RunConcurrentLookups () { const int threadCount = 2; + var timeout = TimeSpan.FromSeconds (10); var peers = new VirtualCallbackConstructorDerived? [threadCount]; - using var ready = new CountdownEvent (threadCount); - using var start = new ManualResetEventSlim (); + var exceptions = new Exception? [threadCount]; + var ready = new CountdownEvent (threadCount); + var start = new ManualResetEventSlim (); var threads = new Thread [threadCount]; for (int i = 0; i < threads.Length; i++) { int index = i; threads [i] = new Thread (() => { - ready.Signal (); - start.Wait (); try { + ready.Signal (); + start.Wait (); peers [index] = Java.Lang.Object.GetObject ( CallbackPeer?.Handle ?? IntPtr.Zero, JniHandleOwnership.DoNotTransfer); } catch (Exception e) { - ConcurrentLookupException = e; + exceptions [index] = e; } - }); + }) { + IsBackground = true, + }; threads [i].Start (); } - ready.Wait (); + bool allReady = ready.Wait (timeout); start.Set (); + bool allJoined = true; foreach (var thread in threads) { - thread.Join (); + allJoined &= thread.Join (timeout); + } + + if (allJoined) { + ready.Dispose (); + start.Dispose (); + } + if (!allReady) { + Assert.Fail ($"Concurrent constructor lookup workers did not become ready within {timeout}."); + } + if (!allJoined) { + Assert.Fail ($"Concurrent constructor lookup workers did not complete within {timeout}."); } ConcurrentPeers = peers; + foreach (var exception in exceptions) { + if (exception != null) { + ConcurrentLookupException = exception; + break; + } + } } public static void Reset ()