From f38988150dfe0dd4d47294c91106c5d9c171ceef Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 1 Sep 2026 07:07:46 +0200 Subject: [PATCH 1/9] [tests] Cover interface-valued Java collections Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../InterfaceCollectionMarshallingTests.cs | 268 ++++++++++++++++++ .../Mono.Android.NET-Tests.csproj | 1 + .../test/InterfaceCollectionHolder.java | 86 ++++++ .../android/test/InterfaceCollectionPeer.java | 21 ++ 4 files changed, 376 insertions(+) create mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs create mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionHolder.java create mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionPeer.java diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs new file mode 100644 index 00000000000..b1c690013a2 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Android.Runtime; + +using Java.Interop; + +using NUnit.Framework; + +namespace Java.InteropTests +{ + [TestFixture] + [Category ("InterfaceCollections")] + public class InterfaceCollectionMarshallingTests + { + [Test] + public void JavaList_InterfaceElementsPreserveIdentityAndRoundTrip () + { + using var holder = new global::Net.Dot.Android.Test.InterfaceCollectionHolder (); + var list = holder.CreateList (); + try { + Assert.AreEqual (typeof (JavaList), list.GetType ()); + Assert.AreEqual (4, list.Count); + + var first = list [0]; + var duplicate = list [1]; + var second = list [2]; + + AssertInterfacePeer (first, 11); + AssertInterfacePeer (second, 22); + Assert.AreSame (first, duplicate); + Assert.AreSame (first, list [0]); + AssertSameJavaObject (first, duplicate); + AssertDistinctJavaObjects (first, second); + Assert.IsNull (list [3]); + Assert.IsTrue (list.Contains (first)); + Assert.IsTrue (list.Contains (null)); + + list.Add (second); + Assert.AreEqual (5, list.Count); + Assert.AreSame (second, list [4]); + CollectionAssert.AreEqual (new [] { 11, 11, 22, 22 }, list.Where (value => value != null).Select (value => value.Value)); + + var roundTrip = holder.RoundTripList (list); + try { + Assert.AreEqual (typeof (JavaList), roundTrip.GetType ()); + AssertSameJavaObject (list, roundTrip); + Assert.AreSame (first, roundTrip [0]); + } finally { + DisposeIfDistinct (list, roundTrip); + } + + Assert.IsTrue (list.Remove (first)); + Assert.IsTrue (list.Contains (first)); + Assert.IsTrue (list.Remove (first)); + Assert.IsFalse (list.Contains (first)); + } finally { + DisposeJavaObject (list); + } + } + + [Test] + public void JavaList_InheritedInterfaceUsesExplicitInvoker () + { + using var holder = new global::Net.Dot.Android.Test.InterfaceCollectionHolder (); + var list = holder.CreateInheritedList (); + try { + Assert.AreEqual (typeof (JavaList), list.GetType ()); + Assert.AreEqual (2, list.Count); + AssertInterfacePeer (list [0], 11); + AssertInterfacePeer (list [1], 22); + Assert.AreEqual (111, list [0].OtherValue); + Assert.AreEqual (222, list [1].OtherValue); + StringAssert.EndsWith ("IExtendedValueProviderInvoker", list [0].GetType ().FullName); + } finally { + DisposeJavaObject (list); + } + } + + [Test] + public void JavaCollection_InterfaceElementsSupportOperationsAndRoundTrip () + { + using var holder = new global::Net.Dot.Android.Test.InterfaceCollectionHolder (); + var collection = holder.CreateCollection (); + try { + Assert.AreEqual (typeof (JavaCollection), collection.GetType ()); + Assert.AreEqual (3, collection.Count); + + var values = collection.ToArray (); + var first = values [0]; + var second = values [1]; + AssertInterfacePeer (first, 11); + AssertInterfacePeer (second, 22); + AssertDistinctJavaObjects (first, second); + Assert.IsNull (values [2]); + + collection.Add (first); + Assert.AreEqual (4, collection.Count); + Assert.IsTrue (collection.Contains (first)); + Assert.IsTrue (collection.Contains (null)); + Assert.AreSame (first, collection.ToArray () [3]); + + var roundTrip = holder.RoundTripCollection (collection); + try { + Assert.AreEqual (typeof (JavaCollection), roundTrip.GetType ()); + AssertSameJavaObject (collection, roundTrip); + Assert.AreSame (first, roundTrip.First ()); + } finally { + DisposeIfDistinct (collection, roundTrip); + } + + collection.Clear (); + Assert.AreEqual (0, collection.Count); + } finally { + DisposeJavaObject (collection); + } + } + + [Test] + public void JavaDictionary_InterfaceKeysSupportOperationsAndRoundTrip () + { + using var holder = new global::Net.Dot.Android.Test.InterfaceCollectionHolder (); + var dictionary = holder.CreateKeyDictionary (); + try { + Assert.AreEqual ( + typeof (JavaDictionary), + dictionary.GetType ()); + Assert.AreEqual (3, dictionary.Count); + + var first = dictionary.Keys.Single (key => key != null && key.Value == 11); + var second = dictionary.Keys.Single (key => key != null && key.Value == 22); + AssertInterfacePeer (first, 11); + AssertInterfacePeer (second, 22); + AssertDistinctJavaObjects (first, second); + Assert.IsTrue (dictionary.ContainsKey (first)); + Assert.IsTrue (dictionary.ContainsKey (null)); + Assert.AreEqual ("first", dictionary [first]); + Assert.AreEqual ("null", dictionary [null]); + Assert.AreSame (first, dictionary.Keys.Single (key => key != null && key.Value == 11)); + + var roundTrip = holder.RoundTripKeyDictionary (dictionary); + try { + AssertSameJavaObject (dictionary, roundTrip); + Assert.AreEqual ("second", roundTrip [second]); + } finally { + DisposeIfDistinct (dictionary, roundTrip); + } + + Assert.IsTrue (dictionary.Remove (first)); + Assert.IsFalse (dictionary.ContainsKey (first)); + } finally { + DisposeJavaObject (dictionary); + } + } + + [Test] + public void JavaDictionary_InterfaceValuesPreserveDuplicatesAndRoundTrip () + { + using var holder = new global::Net.Dot.Android.Test.InterfaceCollectionHolder (); + var dictionary = holder.CreateValueDictionary (); + try { + Assert.AreEqual ( + typeof (JavaDictionary), + dictionary.GetType ()); + Assert.AreEqual (4, dictionary.Count); + + var first = dictionary ["first"]; + var duplicate = dictionary ["duplicate"]; + var second = dictionary ["second"]; + AssertInterfacePeer (first, 11); + AssertInterfacePeer (second, 22); + Assert.AreSame (first, duplicate); + Assert.AreSame (first, dictionary ["first"]); + AssertDistinctJavaObjects (first, second); + Assert.IsNull (dictionary ["null"]); + CollectionAssert.AreEquivalent (new [] { 11, 11, 22 }, dictionary.Values.Where (value => value != null).Select (value => value.Value)); + + dictionary.Add ("added", second); + Assert.AreSame (second, dictionary ["added"]); + + var roundTrip = holder.RoundTripValueDictionary (dictionary); + try { + AssertSameJavaObject (dictionary, roundTrip); + Assert.AreSame (first, roundTrip ["duplicate"]); + } finally { + DisposeIfDistinct (dictionary, roundTrip); + } + + Assert.IsTrue (dictionary.Remove ("first")); + Assert.IsFalse (dictionary.ContainsKey ("first")); + } finally { + DisposeJavaObject (dictionary); + } + } + + [Test] + public void JavaDictionary_InterfaceKeysAndValuesPreserveIdentityAndRoundTrip () + { + using var holder = new global::Net.Dot.Android.Test.InterfaceCollectionHolder (); + var dictionary = holder.CreateInterfaceDictionary (); + try { + Assert.AreEqual ( + typeof (JavaDictionary< + global::Net.Dot.Android.Test.IValueProvider, + global::Net.Dot.Android.Test.IValueProvider>), + dictionary.GetType ()); + Assert.AreEqual (3, dictionary.Count); + + var first = dictionary.Keys.Single (key => key != null && key.Value == 11); + var second = dictionary.Keys.Single (key => key != null && key.Value == 22); + Assert.AreSame (second, dictionary [first]); + Assert.AreSame (first, dictionary [second]); + Assert.IsNull (dictionary [null]); + Assert.IsTrue (dictionary.ContainsKey (first)); + Assert.IsTrue (dictionary.Any (pair => ReferenceEquals (pair.Key, first) && ReferenceEquals (pair.Value, second))); + + var roundTrip = holder.RoundTripInterfaceDictionary (dictionary); + try { + AssertSameJavaObject (dictionary, roundTrip); + Assert.AreSame (second, roundTrip [first]); + } finally { + DisposeIfDistinct (dictionary, roundTrip); + } + + Assert.IsTrue (dictionary.Remove (second)); + Assert.IsFalse (dictionary.ContainsKey (second)); + } finally { + DisposeJavaObject (dictionary); + } + } + + static void AssertInterfacePeer (global::Net.Dot.Android.Test.IValueProvider peer, int expectedValue) + { + Assert.IsNotNull (peer); + Assert.AreEqual (expectedValue, peer.Value); + StringAssert.EndsWith ("Invoker", peer.GetType ().Name); + } + + static void AssertSameJavaObject (object expected, object actual) + { + var expectedPeer = (IJavaObject) expected; + var actualPeer = (IJavaObject) actual; + Assert.IsTrue (JNIEnv.IsSameObject (expectedPeer.Handle, actualPeer.Handle)); + } + + static void AssertDistinctJavaObjects (object first, object second) + { + var firstPeer = (IJavaObject) first; + var secondPeer = (IJavaObject) second; + Assert.IsFalse (JNIEnv.IsSameObject (firstPeer.Handle, secondPeer.Handle)); + } + + static void DisposeIfDistinct (object owner, object value) + { + if (!ReferenceEquals (owner, value)) { + DisposeJavaObject (value); + } + } + + static void DisposeJavaObject (object value) + { + if (value is IDisposable disposable) { + disposable.Dispose (); + } + } + } +} 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..dbf0fb83010 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 @@ -138,6 +138,7 @@ + diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionHolder.java b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionHolder.java new file mode 100644 index 00000000000..75ef6179c5a --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionHolder.java @@ -0,0 +1,86 @@ +package net.dot.android.test; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class InterfaceCollectionHolder { + private final InterfaceCollectionPeer first; + private final InterfaceCollectionPeer second; + + public InterfaceCollectionHolder() { + first = new InterfaceCollectionPeer(11, 111); + second = new InterfaceCollectionPeer(22, 222); + } + + public List createList() { + List result = new ArrayList<>(); + result.add(first); + result.add(first); + result.add(second); + result.add(null); + return result; + } + + public List createInheritedList() { + List result = new ArrayList<>(); + result.add(first); + result.add(second); + return result; + } + + public Collection createCollection() { + Collection result = new ArrayList<>(); + result.add(first); + result.add(second); + result.add(null); + return result; + } + + public Map createKeyDictionary() { + Map result = new LinkedHashMap<>(); + result.put(first, "first"); + result.put(second, "second"); + result.put(null, "null"); + return result; + } + + public Map createValueDictionary() { + Map result = new LinkedHashMap<>(); + result.put("first", first); + result.put("duplicate", first); + result.put("second", second); + result.put("null", null); + return result; + } + + public Map createInterfaceDictionary() { + Map result = new LinkedHashMap<>(); + result.put(first, second); + result.put(second, first); + result.put(null, null); + return result; + } + + public List roundTripList(List value) { + return value; + } + + public Collection roundTripCollection(Collection value) { + return value; + } + + public Map roundTripKeyDictionary(Map value) { + return value; + } + + public Map roundTripValueDictionary(Map value) { + return value; + } + + public Map roundTripInterfaceDictionary(Map value) { + return value; + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionPeer.java b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionPeer.java new file mode 100644 index 00000000000..7af72afd67c --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionPeer.java @@ -0,0 +1,21 @@ +package net.dot.android.test; + +final class InterfaceCollectionPeer implements ExtendedValueProvider { + private final int value; + private final int otherValue; + + public InterfaceCollectionPeer(int value, int otherValue) { + this.value = value; + this.otherValue = otherValue; + } + + @Override + public int getValue() { + return value; + } + + @Override + public int getOtherValue() { + return otherValue; + } +} From c181c0ab3a96c89a6c8b333ce8961be4456bc507 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 1 Sep 2026 07:26:06 +0200 Subject: [PATCH 2/9] [tests] Avoid rooting interface collection wrappers Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../InterfaceCollectionMarshallingTests.cs | 257 ++++++++++++++---- .../test/InterfaceCollectionBasePeer.java | 14 + .../test/InterfaceCollectionHolder.java | 18 +- .../Mono.Android-Tests/proguard.cfg | 3 + 4 files changed, 237 insertions(+), 55 deletions(-) create mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionBasePeer.java diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs index b1c690013a2..f968aa4f54c 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Android.Runtime; @@ -14,21 +15,29 @@ namespace Java.InteropTests [Category ("InterfaceCollections")] public class InterfaceCollectionMarshallingTests { + const string CollectionSignature = "()Ljava/util/Collection;"; + const string DictionarySignature = "()Ljava/util/Map;"; + const string ListSignature = "()Ljava/util/List;"; + const string RoundTripCollectionSignature = "(Ljava/util/Collection;)Ljava/util/Collection;"; + const string RoundTripDictionarySignature = "(Ljava/util/Map;)Ljava/util/Map;"; + const string RoundTripListSignature = "(Ljava/util/List;)Ljava/util/List;"; + [Test] public void JavaList_InterfaceElementsPreserveIdentityAndRoundTrip () { - using var holder = new global::Net.Dot.Android.Test.InterfaceCollectionHolder (); - var list = holder.CreateList (); + using var holder = new RawInterfaceCollectionHolder (); + var targetType = typeof (IList); + var list = holder.Create> ("createList", ListSignature, targetType); try { - Assert.AreEqual (typeof (JavaList), list.GetType ()); + AssertWrapperType (list, typeof (JavaList<>), typeof (global::Net.Dot.Android.Test.IValueProvider)); Assert.AreEqual (4, list.Count); var first = list [0]; var duplicate = list [1]; var second = list [2]; - AssertInterfacePeer (first, 11); - AssertInterfacePeer (second, 22); + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); Assert.AreSame (first, duplicate); Assert.AreSame (first, list [0]); AssertSameJavaObject (first, duplicate); @@ -42,9 +51,13 @@ public void JavaList_InterfaceElementsPreserveIdentityAndRoundTrip () Assert.AreSame (second, list [4]); CollectionAssert.AreEqual (new [] { 11, 11, 22, 22 }, list.Where (value => value != null).Select (value => value.Value)); - var roundTrip = holder.RoundTripList (list); + var roundTrip = holder.RoundTrip> ( + "roundTripList", + RoundTripListSignature, + list, + targetType); try { - Assert.AreEqual (typeof (JavaList), roundTrip.GetType ()); + AssertWrapperType (roundTrip, typeof (JavaList<>), typeof (global::Net.Dot.Android.Test.IValueProvider)); AssertSameJavaObject (list, roundTrip); Assert.AreSame (first, roundTrip [0]); } finally { @@ -63,16 +76,17 @@ public void JavaList_InterfaceElementsPreserveIdentityAndRoundTrip () [Test] public void JavaList_InheritedInterfaceUsesExplicitInvoker () { - using var holder = new global::Net.Dot.Android.Test.InterfaceCollectionHolder (); - var list = holder.CreateInheritedList (); + using var holder = new RawInterfaceCollectionHolder (); + var targetType = typeof (IList); + var list = holder.Create> ( + "createInheritedList", + ListSignature, + targetType); try { - Assert.AreEqual (typeof (JavaList), list.GetType ()); + AssertWrapperType (list, typeof (JavaList<>), typeof (global::Net.Dot.Android.Test.IExtendedValueProvider)); Assert.AreEqual (2, list.Count); - AssertInterfacePeer (list [0], 11); - AssertInterfacePeer (list [1], 22); - Assert.AreEqual (111, list [0].OtherValue); - Assert.AreEqual (222, list [1].OtherValue); - StringAssert.EndsWith ("IExtendedValueProviderInvoker", list [0].GetType ().FullName); + AssertExtendedInterfacePeer (list [0], 33, 333); + AssertExtendedInterfacePeer (list [1], 44, 444); } finally { DisposeJavaObject (list); } @@ -81,17 +95,21 @@ public void JavaList_InheritedInterfaceUsesExplicitInvoker () [Test] public void JavaCollection_InterfaceElementsSupportOperationsAndRoundTrip () { - using var holder = new global::Net.Dot.Android.Test.InterfaceCollectionHolder (); - var collection = holder.CreateCollection (); + using var holder = new RawInterfaceCollectionHolder (); + var targetType = typeof (ICollection); + var collection = holder.Create> ( + "createCollection", + CollectionSignature, + targetType); try { - Assert.AreEqual (typeof (JavaCollection), collection.GetType ()); + AssertWrapperType (collection, typeof (JavaCollection<>), typeof (global::Net.Dot.Android.Test.IValueProvider)); Assert.AreEqual (3, collection.Count); var values = collection.ToArray (); var first = values [0]; var second = values [1]; - AssertInterfacePeer (first, 11); - AssertInterfacePeer (second, 22); + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); AssertDistinctJavaObjects (first, second); Assert.IsNull (values [2]); @@ -101,9 +119,13 @@ public void JavaCollection_InterfaceElementsSupportOperationsAndRoundTrip () Assert.IsTrue (collection.Contains (null)); Assert.AreSame (first, collection.ToArray () [3]); - var roundTrip = holder.RoundTripCollection (collection); + var roundTrip = holder.RoundTrip> ( + "roundTripCollection", + RoundTripCollectionSignature, + collection, + targetType); try { - Assert.AreEqual (typeof (JavaCollection), roundTrip.GetType ()); + AssertWrapperType (roundTrip, typeof (JavaCollection<>), typeof (global::Net.Dot.Android.Test.IValueProvider)); AssertSameJavaObject (collection, roundTrip); Assert.AreSame (first, roundTrip.First ()); } finally { @@ -120,18 +142,24 @@ public void JavaCollection_InterfaceElementsSupportOperationsAndRoundTrip () [Test] public void JavaDictionary_InterfaceKeysSupportOperationsAndRoundTrip () { - using var holder = new global::Net.Dot.Android.Test.InterfaceCollectionHolder (); - var dictionary = holder.CreateKeyDictionary (); + using var holder = new RawInterfaceCollectionHolder (); + var targetType = typeof (IDictionary); + var dictionary = holder.Create> ( + "createKeyDictionary", + DictionarySignature, + targetType); try { - Assert.AreEqual ( - typeof (JavaDictionary), - dictionary.GetType ()); + AssertWrapperType ( + dictionary, + typeof (JavaDictionary<,>), + typeof (global::Net.Dot.Android.Test.IValueProvider), + typeof (string)); Assert.AreEqual (3, dictionary.Count); var first = dictionary.Keys.Single (key => key != null && key.Value == 11); var second = dictionary.Keys.Single (key => key != null && key.Value == 22); - AssertInterfacePeer (first, 11); - AssertInterfacePeer (second, 22); + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); AssertDistinctJavaObjects (first, second); Assert.IsTrue (dictionary.ContainsKey (first)); Assert.IsTrue (dictionary.ContainsKey (null)); @@ -139,8 +167,17 @@ public void JavaDictionary_InterfaceKeysSupportOperationsAndRoundTrip () Assert.AreEqual ("null", dictionary [null]); Assert.AreSame (first, dictionary.Keys.Single (key => key != null && key.Value == 11)); - var roundTrip = holder.RoundTripKeyDictionary (dictionary); + var roundTrip = holder.RoundTrip> ( + "roundTripKeyDictionary", + RoundTripDictionarySignature, + dictionary, + targetType); try { + AssertWrapperType ( + roundTrip, + typeof (JavaDictionary<,>), + typeof (global::Net.Dot.Android.Test.IValueProvider), + typeof (string)); AssertSameJavaObject (dictionary, roundTrip); Assert.AreEqual ("second", roundTrip [second]); } finally { @@ -157,19 +194,25 @@ public void JavaDictionary_InterfaceKeysSupportOperationsAndRoundTrip () [Test] public void JavaDictionary_InterfaceValuesPreserveDuplicatesAndRoundTrip () { - using var holder = new global::Net.Dot.Android.Test.InterfaceCollectionHolder (); - var dictionary = holder.CreateValueDictionary (); + using var holder = new RawInterfaceCollectionHolder (); + var targetType = typeof (IDictionary); + var dictionary = holder.Create> ( + "createValueDictionary", + DictionarySignature, + targetType); try { - Assert.AreEqual ( - typeof (JavaDictionary), - dictionary.GetType ()); + AssertWrapperType ( + dictionary, + typeof (JavaDictionary<,>), + typeof (string), + typeof (global::Net.Dot.Android.Test.IValueProvider)); Assert.AreEqual (4, dictionary.Count); var first = dictionary ["first"]; var duplicate = dictionary ["duplicate"]; var second = dictionary ["second"]; - AssertInterfacePeer (first, 11); - AssertInterfacePeer (second, 22); + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); Assert.AreSame (first, duplicate); Assert.AreSame (first, dictionary ["first"]); AssertDistinctJavaObjects (first, second); @@ -179,8 +222,17 @@ public void JavaDictionary_InterfaceValuesPreserveDuplicatesAndRoundTrip () dictionary.Add ("added", second); Assert.AreSame (second, dictionary ["added"]); - var roundTrip = holder.RoundTripValueDictionary (dictionary); + var roundTrip = holder.RoundTrip> ( + "roundTripValueDictionary", + RoundTripDictionarySignature, + dictionary, + targetType); try { + AssertWrapperType ( + roundTrip, + typeof (JavaDictionary<,>), + typeof (string), + typeof (global::Net.Dot.Android.Test.IValueProvider)); AssertSameJavaObject (dictionary, roundTrip); Assert.AreSame (first, roundTrip ["duplicate"]); } finally { @@ -197,26 +249,47 @@ public void JavaDictionary_InterfaceValuesPreserveDuplicatesAndRoundTrip () [Test] public void JavaDictionary_InterfaceKeysAndValuesPreserveIdentityAndRoundTrip () { - using var holder = new global::Net.Dot.Android.Test.InterfaceCollectionHolder (); - var dictionary = holder.CreateInterfaceDictionary (); + using var holder = new RawInterfaceCollectionHolder (); + var targetType = typeof (IDictionary< + global::Net.Dot.Android.Test.IValueProvider, + global::Net.Dot.Android.Test.IValueProvider>); + var dictionary = holder.Create> ( + "createInterfaceDictionary", + DictionarySignature, + targetType); try { - Assert.AreEqual ( - typeof (JavaDictionary< - global::Net.Dot.Android.Test.IValueProvider, - global::Net.Dot.Android.Test.IValueProvider>), - dictionary.GetType ()); + AssertWrapperType ( + dictionary, + typeof (JavaDictionary<,>), + typeof (global::Net.Dot.Android.Test.IValueProvider), + typeof (global::Net.Dot.Android.Test.IValueProvider)); Assert.AreEqual (3, dictionary.Count); var first = dictionary.Keys.Single (key => key != null && key.Value == 11); var second = dictionary.Keys.Single (key => key != null && key.Value == 22); + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); Assert.AreSame (second, dictionary [first]); Assert.AreSame (first, dictionary [second]); Assert.IsNull (dictionary [null]); Assert.IsTrue (dictionary.ContainsKey (first)); Assert.IsTrue (dictionary.Any (pair => ReferenceEquals (pair.Key, first) && ReferenceEquals (pair.Value, second))); - var roundTrip = holder.RoundTripInterfaceDictionary (dictionary); + var roundTrip = holder.RoundTrip> ( + "roundTripInterfaceDictionary", + RoundTripDictionarySignature, + dictionary, + targetType); try { + AssertWrapperType ( + roundTrip, + typeof (JavaDictionary<,>), + typeof (global::Net.Dot.Android.Test.IValueProvider), + typeof (global::Net.Dot.Android.Test.IValueProvider)); AssertSameJavaObject (dictionary, roundTrip); Assert.AreSame (second, roundTrip [first]); } finally { @@ -230,11 +303,49 @@ public void JavaDictionary_InterfaceKeysAndValuesPreserveIdentityAndRoundTrip () } } - static void AssertInterfacePeer (global::Net.Dot.Android.Test.IValueProvider peer, int expectedValue) + static void AssertBaseInterfacePeer (global::Net.Dot.Android.Test.IValueProvider peer, int expectedValue) + { + Assert.IsNotNull (peer); + Assert.AreEqual (expectedValue, peer.Value); + Assert.AreEqual (typeof (global::Net.Dot.Android.Test.IValueProviderInvoker), peer.GetType ()); + Assert.IsFalse (peer is global::Net.Dot.Android.Test.IExtendedValueProvider); + } + + static void AssertExtendedInterfacePeer ( + global::Net.Dot.Android.Test.IExtendedValueProvider peer, + int expectedValue, + int expectedOtherValue) { Assert.IsNotNull (peer); Assert.AreEqual (expectedValue, peer.Value); - StringAssert.EndsWith ("Invoker", peer.GetType ().Name); + Assert.AreEqual (expectedOtherValue, peer.OtherValue); + Assert.AreEqual (typeof (global::Net.Dot.Android.Test.IExtendedValueProviderInvoker), peer.GetType ()); + } + + static void AssertWrapperType (object wrapper, Type expectedGenericDefinition, params Type [] expectedArguments) + { + var wrapperType = wrapper.GetType (); + Assert.IsTrue (wrapperType.IsGenericType); + Assert.AreEqual (expectedGenericDefinition, wrapperType.GetGenericTypeDefinition ()); + CollectionAssert.AreEqual (expectedArguments, wrapperType.GenericTypeArguments); + } + + static object InvokeJavaConvertFromJniHandle (Type targetType, IntPtr handle) + { + var javaConvert = typeof (Java.Lang.Object).Assembly.GetType ("Java.Interop.JavaConvert"); + Assert.IsNotNull (javaConvert); + + var method = javaConvert.GetMethod ( + "FromJniHandle", + BindingFlags.Public | BindingFlags.Static, + binder: null, + types: new [] { typeof (IntPtr), typeof (JniHandleOwnership), typeof (Type) }, + modifiers: null); + Assert.IsNotNull (method); + + var value = method.Invoke (null, new object [] { handle, JniHandleOwnership.TransferLocalRef, targetType }); + Assert.IsNotNull (value); + return value; } static void AssertSameJavaObject (object expected, object actual) @@ -264,5 +375,55 @@ static void DisposeJavaObject (object value) disposable.Dispose (); } } + + sealed class RawInterfaceCollectionHolder : IDisposable + { + const string JniName = "net/dot/android/test/InterfaceCollectionHolder"; + + readonly Java.Lang.Object holder; + + public RawInterfaceCollectionHolder () + { + var holderClass = JniEnvironment.Types.FindClass (JniName); + try { + var constructor = JNIEnv.GetMethodID (holderClass.Handle, "", "()V"); + var handle = JNIEnv.NewObject (holderClass.Handle, constructor); + holder = new Java.Lang.Object (handle, JniHandleOwnership.TransferLocalRef); + } finally { + JniObjectReference.Dispose (ref holderClass); + } + } + + public T Create (string methodName, string signature, Type targetType) + { + var method = GetMethod (methodName, signature); + var handle = JNIEnv.CallObjectMethod (holder.Handle, method); + return (T) InvokeJavaConvertFromJniHandle (targetType, handle); + } + + public T RoundTrip (string methodName, string signature, object value, Type targetType) + { + var method = GetMethod (methodName, signature); + var peer = (IJavaObject) value; + var handle = JNIEnv.CallObjectMethod (holder.Handle, method, new JValue (peer.Handle)); + GC.KeepAlive (value); + return (T) InvokeJavaConvertFromJniHandle (targetType, handle); + } + + public void Dispose () + { + holder.Dispose (); + } + + IntPtr GetMethod (string methodName, string signature) + { + var holderClass = JNIEnv.GetObjectClass (holder.Handle); + try { + return JNIEnv.GetMethodID (holderClass, methodName, signature); + } finally { + JNIEnv.DeleteLocalRef (holderClass); + } + } + } } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionBasePeer.java b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionBasePeer.java new file mode 100644 index 00000000000..c2748a391f2 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionBasePeer.java @@ -0,0 +1,14 @@ +package net.dot.android.test; + +final class InterfaceCollectionBasePeer implements ValueProvider { + private final int value; + + public InterfaceCollectionBasePeer(int value) { + this.value = value; + } + + @Override + public int getValue() { + return value; + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionHolder.java b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionHolder.java index 75ef6179c5a..9d954701ae4 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionHolder.java +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionHolder.java @@ -6,13 +6,17 @@ import java.util.List; import java.util.Map; -public final class InterfaceCollectionHolder { - private final InterfaceCollectionPeer first; - private final InterfaceCollectionPeer second; +final class InterfaceCollectionHolder { + private final InterfaceCollectionBasePeer first; + private final InterfaceCollectionBasePeer second; + private final InterfaceCollectionPeer inheritedFirst; + private final InterfaceCollectionPeer inheritedSecond; public InterfaceCollectionHolder() { - first = new InterfaceCollectionPeer(11, 111); - second = new InterfaceCollectionPeer(22, 222); + first = new InterfaceCollectionBasePeer(11); + second = new InterfaceCollectionBasePeer(22); + inheritedFirst = new InterfaceCollectionPeer(33, 333); + inheritedSecond = new InterfaceCollectionPeer(44, 444); } public List createList() { @@ -26,8 +30,8 @@ public List createList() { public List createInheritedList() { List result = new ArrayList<>(); - result.add(first); - result.add(second); + result.add(inheritedFirst); + result.add(inheritedSecond); return result; } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/proguard.cfg b/tests/Mono.Android-Tests/Mono.Android-Tests/proguard.cfg index 64005b977cb..676c17eac25 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/proguard.cfg +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/proguard.cfg @@ -1,3 +1,6 @@ # Need to preserve the contents of Mono.Android-Test-classes.jar -keep class net.dot.jni.test.** { *; (); } +-keep class net.dot.android.test.InterfaceCollectionBasePeer { *; } +-keep class net.dot.android.test.InterfaceCollectionHolder { *; } +-keep class net.dot.android.test.InterfaceCollectionPeer { *; } From 28ee46a22e895f1b801bc8f20e6d7262e2c9fddd Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 1 Sep 2026 10:08:55 +0200 Subject: [PATCH 3/9] [tests] Isolate interface collection rooting Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ExtendedValueProvider.java | 5 + .../InterfaceCollectionFixture.java} | 41 +- .../InterfaceCollectionApp/MainActivity.cs | 541 ++++++++++++++++++ .../InterfaceCollectionApp/ValueProvider.java | 5 + .../InterfaceCollectionApp/proguard.cfg | 3 + .../Tests/InterfaceCollectionTests.cs | 295 ++++++++++ .../InterfaceCollectionMarshallingTests.cs | 429 -------------- .../Mono.Android.NET-Tests.csproj | 1 - .../test/InterfaceCollectionBasePeer.java | 14 - .../android/test/InterfaceCollectionPeer.java | 21 - .../Mono.Android-Tests/proguard.cfg | 3 - 11 files changed, 886 insertions(+), 472 deletions(-) create mode 100644 tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/ExtendedValueProvider.java rename tests/{Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionHolder.java => MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/InterfaceCollectionFixture.java} (72%) create mode 100644 tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs create mode 100644 tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/ValueProvider.java create mode 100644 tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/proguard.cfg create mode 100644 tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs delete mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs delete mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionBasePeer.java delete mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionPeer.java diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/ExtendedValueProvider.java b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/ExtendedValueProvider.java new file mode 100644 index 00000000000..98a4579ec12 --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/ExtendedValueProvider.java @@ -0,0 +1,5 @@ +package net.dot.android.test; + +public interface ExtendedValueProvider extends ValueProvider { + int getOtherValue(); +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionHolder.java b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/InterfaceCollectionFixture.java similarity index 72% rename from tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionHolder.java rename to tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/InterfaceCollectionFixture.java index 9d954701ae4..3edffb49d05 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionHolder.java +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/InterfaceCollectionFixture.java @@ -6,17 +6,50 @@ import java.util.List; import java.util.Map; +final class InterfaceCollectionBasePeer implements ValueProvider { + private final int value; + + public InterfaceCollectionBasePeer(int value) { + this.value = value; + } + + @Override + public int getValue() { + return value; + } +} + +final class InterfaceCollectionExtendedPeer implements ExtendedValueProvider { + private final int value; + private final int otherValue; + + public InterfaceCollectionExtendedPeer(int value, int otherValue) { + this.value = value; + this.otherValue = otherValue; + } + + @Override + public int getValue() { + return value; + } + + @Override + public int getOtherValue() { + return otherValue; + } +} + final class InterfaceCollectionHolder { private final InterfaceCollectionBasePeer first; private final InterfaceCollectionBasePeer second; - private final InterfaceCollectionPeer inheritedFirst; - private final InterfaceCollectionPeer inheritedSecond; + private final InterfaceCollectionExtendedPeer inheritedFirst; + private final InterfaceCollectionExtendedPeer inheritedSecond; public InterfaceCollectionHolder() { first = new InterfaceCollectionBasePeer(11); second = new InterfaceCollectionBasePeer(22); - inheritedFirst = new InterfaceCollectionPeer(33, 333); - inheritedSecond = new InterfaceCollectionPeer(44, 444); + inheritedFirst = new InterfaceCollectionExtendedPeer(33, 333); + inheritedSecond = new InterfaceCollectionExtendedPeer(44, 444); } public List createList() { diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs new file mode 100644 index 00000000000..e405e365283 --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs @@ -0,0 +1,541 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +using Android.App; +using Android.OS; +using Android.Runtime; +using Android.Util; + +using Java.Interop; + +using Net.Dot.Android.Test; + +namespace ${ROOT_NAMESPACE} +{ + [Register ("${JAVA_PACKAGENAME}.MainActivity"), Activity (Label = "${PROJECT_NAME}", MainLauncher = true)] + public class MainActivity : Activity + { + const string ResultPrefix = "INTERFACE_COLLECTION_RESULT"; + const string Tag = "InterfaceCollections"; + + protected override void OnCreate (Bundle savedInstanceState) + { + base.OnCreate (savedInstanceState); + + int passed = 0; + try { + JavaList_InterfaceElementsPreserveIdentityAndRoundTrip (); + passed++; + JavaList_InheritedInterfaceUsesExplicitInvoker (); + passed++; + JavaCollection_InterfaceElementsSupportOperationsAndRoundTrip (); + passed++; + JavaDictionary_InterfaceKeysSupportOperationsAndRoundTrip (); + passed++; + JavaDictionary_InterfaceValuesPreserveDuplicatesAndRoundTrip (); + passed++; + JavaDictionary_InterfaceKeysAndValuesPreserveIdentityAndRoundTrip (); + passed++; + Log.Info (Tag, $"{ResultPrefix} PASS {passed}/6"); + } catch (Exception e) { + Log.Error (Tag, $"{ResultPrefix} FAIL {passed}/6: {e}"); + } finally { + Finish (); + } + } + + static void JavaList_InterfaceElementsPreserveIdentityAndRoundTrip () + { + using var holder = new RawInterfaceCollectionHolder (); + var list = holder.CreateList (); + try { + AssertWrapperType (list, typeof (JavaList<>), typeof (IValueProvider)); + AssertEqual (4, list.Count, "list count"); + + var first = list [0]; + var duplicate = list [1]; + var second = list [2]; + + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); + AssertSame (first, duplicate, "duplicate list reference"); + AssertSame (first, list [0], "repeated list lookup"); + AssertSameJavaObject (first, duplicate); + AssertDistinctJavaObjects (first, second); + AssertNull (list [3], "null list element"); + AssertTrue (list.Contains (first), "list contains first"); + AssertTrue (list.Contains (null), "list contains null"); + + list.Add (second); + AssertEqual (5, list.Count, "list count after add"); + AssertSame (second, list [4], "added list reference"); + AssertSequence ([11, 11, 22, 22], GetValues (list), "list enumeration"); + + var roundTrip = holder.RoundTripList (list); + try { + AssertWrapperType (roundTrip, typeof (JavaList<>), typeof (IValueProvider)); + AssertSameJavaObject (list, roundTrip); + AssertSame (first, roundTrip [0], "round-tripped list element"); + } finally { + DisposeIfDistinct (list, roundTrip); + } + + AssertTrue (list.Remove (first), "remove first list reference"); + AssertTrue (list.Contains (first), "list retains duplicate"); + AssertTrue (list.Remove (first), "remove duplicate list reference"); + AssertFalse (list.Contains (first), "list no longer contains first"); + } finally { + DisposeJavaObject (list); + } + } + + static void JavaList_InheritedInterfaceUsesExplicitInvoker () + { + using var holder = new RawInterfaceCollectionHolder (); + var list = holder.CreateInheritedList (); + try { + AssertWrapperType (list, typeof (JavaList<>), typeof (IExtendedValueProvider)); + AssertEqual (2, list.Count, "inherited list count"); + AssertExtendedInterfacePeer (list [0], 33, 333); + AssertExtendedInterfacePeer (list [1], 44, 444); + } finally { + DisposeJavaObject (list); + } + } + + static void JavaCollection_InterfaceElementsSupportOperationsAndRoundTrip () + { + using var holder = new RawInterfaceCollectionHolder (); + var collection = holder.CreateCollection (); + try { + AssertWrapperType (collection, typeof (JavaCollection<>), typeof (IValueProvider)); + AssertEqual (3, collection.Count, "collection count"); + + var values = new IValueProvider [3]; + collection.CopyTo (values, 0); + var first = values [0]; + var second = values [1]; + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); + AssertDistinctJavaObjects (first, second); + AssertNull (values [2], "null collection element"); + + collection.Add (first); + AssertEqual (4, collection.Count, "collection count after add"); + AssertTrue (collection.Contains (first), "collection contains first"); + AssertTrue (collection.Contains (null), "collection contains null"); + AssertSame (first, GetElement (collection, 3), "collection enumeration"); + + var roundTrip = holder.RoundTripCollection (collection); + try { + AssertWrapperType (roundTrip, typeof (JavaCollection<>), typeof (IValueProvider)); + AssertSameJavaObject (collection, roundTrip); + AssertSame (first, GetElement (roundTrip, 0), "round-tripped collection element"); + } finally { + DisposeIfDistinct (collection, roundTrip); + } + + collection.Clear (); + AssertEqual (0, collection.Count, "collection count after clear"); + } finally { + DisposeJavaObject (collection); + } + } + + static void JavaDictionary_InterfaceKeysSupportOperationsAndRoundTrip () + { + using var holder = new RawInterfaceCollectionHolder (); + var dictionary = holder.CreateKeyDictionary (); + try { + AssertWrapperType (dictionary, typeof (JavaDictionary<,>), typeof (IValueProvider), typeof (string)); + AssertEqual (3, dictionary.Count, "key dictionary count"); + + var first = FindPeer (dictionary.Keys, 11); + var second = FindPeer (dictionary.Keys, 22); + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); + AssertDistinctJavaObjects (first, second); + AssertTrue (dictionary.ContainsKey (first), "dictionary contains first key"); + AssertTrue (dictionary.ContainsKey (null), "dictionary contains null key"); + AssertEqual ("first", dictionary [first], "first key value"); + AssertEqual ("null", dictionary [null], "null key value"); + AssertSame (first, FindPeer (dictionary.Keys, 11), "repeated key lookup"); + + var roundTrip = holder.RoundTripKeyDictionary (dictionary); + try { + AssertWrapperType (roundTrip, typeof (JavaDictionary<,>), typeof (IValueProvider), typeof (string)); + AssertSameJavaObject (dictionary, roundTrip); + AssertEqual ("second", roundTrip [second], "round-tripped key value"); + } finally { + DisposeIfDistinct (dictionary, roundTrip); + } + + AssertTrue (dictionary.Remove (first), "remove interface key"); + AssertFalse (dictionary.ContainsKey (first), "removed interface key"); + } finally { + DisposeJavaObject (dictionary); + } + } + + static void JavaDictionary_InterfaceValuesPreserveDuplicatesAndRoundTrip () + { + using var holder = new RawInterfaceCollectionHolder (); + var dictionary = holder.CreateValueDictionary (); + try { + AssertWrapperType (dictionary, typeof (JavaDictionary<,>), typeof (string), typeof (IValueProvider)); + AssertEqual (4, dictionary.Count, "value dictionary count"); + + var first = dictionary ["first"]; + var duplicate = dictionary ["duplicate"]; + var second = dictionary ["second"]; + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); + AssertSame (first, duplicate, "duplicate dictionary value"); + AssertSame (first, dictionary ["first"], "repeated value lookup"); + AssertDistinctJavaObjects (first, second); + AssertNull (dictionary ["null"], "null dictionary value"); + AssertSequence ([11, 11, 22], GetValues (dictionary.Values), "dictionary value enumeration"); + + dictionary.Add ("added", second); + AssertSame (second, dictionary ["added"], "added dictionary value"); + + var roundTrip = holder.RoundTripValueDictionary (dictionary); + try { + AssertWrapperType (roundTrip, typeof (JavaDictionary<,>), typeof (string), typeof (IValueProvider)); + AssertSameJavaObject (dictionary, roundTrip); + AssertSame (first, roundTrip ["duplicate"], "round-tripped dictionary value"); + } finally { + DisposeIfDistinct (dictionary, roundTrip); + } + + AssertTrue (dictionary.Remove ("first"), "remove string key"); + AssertFalse (dictionary.ContainsKey ("first"), "removed string key"); + } finally { + DisposeJavaObject (dictionary); + } + } + + static void JavaDictionary_InterfaceKeysAndValuesPreserveIdentityAndRoundTrip () + { + using var holder = new RawInterfaceCollectionHolder (); + var dictionary = holder.CreateInterfaceDictionary (); + try { + AssertWrapperType (dictionary, typeof (JavaDictionary<,>), typeof (IValueProvider), typeof (IValueProvider)); + AssertEqual (3, dictionary.Count, "interface dictionary count"); + + var first = FindPeer (dictionary.Keys, 11); + var second = FindPeer (dictionary.Keys, 22); + AssertBaseInterfacePeer (first, 11); + AssertBaseInterfacePeer (second, 22); + AssertSame (second, dictionary [first], "interface dictionary first value"); + AssertSame (first, dictionary [second], "interface dictionary second value"); + AssertNull (dictionary [null], "interface dictionary null value"); + AssertTrue (dictionary.ContainsKey (first), "interface dictionary contains first"); + AssertTrue (ContainsPair (dictionary, first, second), "interface dictionary enumeration"); + + var roundTrip = holder.RoundTripInterfaceDictionary (dictionary); + try { + AssertWrapperType (roundTrip, typeof (JavaDictionary<,>), typeof (IValueProvider), typeof (IValueProvider)); + AssertSameJavaObject (dictionary, roundTrip); + AssertSame (second, roundTrip [first], "round-tripped interface dictionary value"); + } finally { + DisposeIfDistinct (dictionary, roundTrip); + } + + AssertTrue (dictionary.Remove (second), "remove interface dictionary key"); + AssertFalse (dictionary.ContainsKey (second), "removed interface dictionary key"); + } finally { + DisposeJavaObject (dictionary); + } + } + + static void AssertBaseInterfacePeer (IValueProvider peer, int expectedValue) + { + AssertNotNull (peer, "base interface peer"); + AssertEqual (expectedValue, peer.Value, "base interface value"); + AssertEqual (typeof (IValueProviderInvoker), peer.GetType (), "base interface invoker"); + AssertFalse (peer is IExtendedValueProvider, "base peer must not implement the derived interface"); + } + + static void AssertExtendedInterfacePeer (IExtendedValueProvider peer, int expectedValue, int expectedOtherValue) + { + AssertNotNull (peer, "extended interface peer"); + AssertEqual (expectedValue, peer.Value, "extended interface value"); + AssertEqual (expectedOtherValue, peer.OtherValue, "extended interface other value"); + AssertEqual (typeof (IExtendedValueProviderInvoker), peer.GetType (), "extended interface invoker"); + } + + static void AssertWrapperType (object wrapper, Type expectedGenericDefinition, params Type [] expectedArguments) + { + var wrapperType = wrapper.GetType (); + AssertTrue (wrapperType.IsGenericType, "wrapper must be generic"); + AssertEqual (expectedGenericDefinition, wrapperType.GetGenericTypeDefinition (), "wrapper generic definition"); + AssertEqual (expectedArguments.Length, wrapperType.GenericTypeArguments.Length, "wrapper generic argument count"); + for (int i = 0; i < expectedArguments.Length; i++) { + AssertEqual (expectedArguments [i], wrapperType.GenericTypeArguments [i], $"wrapper generic argument {i}"); + } + } + + static T ConvertCollection (IntPtr handle) + { + return (T) InvokeJavaConvertFromJniHandle (typeof (T), handle); + } + + [DynamicDependency ("FromJniHandle", "Java.Interop.JavaConvert", "Mono.Android")] + static object InvokeJavaConvertFromJniHandle (Type targetType, IntPtr handle) + { + var javaConvert = typeof (Java.Lang.Object).Assembly.GetType ("Java.Interop.JavaConvert"); + if (javaConvert == null) { + throw new InvalidOperationException ("JavaConvert type was not found."); + } + + var method = javaConvert.GetMethod ( + "FromJniHandle", + BindingFlags.Public | BindingFlags.Static, + binder: null, + types: [typeof (IntPtr), typeof (JniHandleOwnership), typeof (Type)], + modifiers: null); + if (method == null) { + throw new InvalidOperationException ("JavaConvert.FromJniHandle method was not found."); + } + + var value = method.Invoke (null, [handle, JniHandleOwnership.TransferLocalRef, targetType]); + if (value == null) { + throw new InvalidOperationException ($"JavaConvert returned null for target type '{targetType}'."); + } + return value; + } + + static IValueProvider FindPeer (ICollection peers, int value) + { + foreach (var peer in peers) { + if (peer != null && peer.Value == value) { + return peer; + } + } + throw new InvalidOperationException ($"Peer with value {value} was not found."); + } + + static IValueProvider GetElement (ICollection values, int index) + { + int current = 0; + foreach (var value in values) { + if (current == index) { + return value; + } + current++; + } + throw new InvalidOperationException ($"Collection element {index} was not found."); + } + + static int [] GetValues (IEnumerable peers) + { + var values = new List (); + foreach (var peer in peers) { + if (peer != null) { + values.Add (peer.Value); + } + } + return values.ToArray (); + } + + static bool ContainsPair ( + IEnumerable> pairs, + IValueProvider key, + IValueProvider value) + { + foreach (var pair in pairs) { + if (ReferenceEquals (pair.Key, key) && ReferenceEquals (pair.Value, value)) { + return true; + } + } + return false; + } + + static void AssertSequence (int [] expected, int [] actual, string message) + { + AssertEqual (expected.Length, actual.Length, $"{message} length"); + for (int i = 0; i < expected.Length; i++) { + AssertEqual (expected [i], actual [i], $"{message} element {i}"); + } + } + + static void AssertSameJavaObject (object expected, object actual) + { + var expectedPeer = (IJavaObject) expected; + var actualPeer = (IJavaObject) actual; + AssertTrue (JNIEnv.IsSameObject (expectedPeer.Handle, actualPeer.Handle), "expected identical Java peers"); + } + + static void AssertDistinctJavaObjects (object first, object second) + { + var firstPeer = (IJavaObject) first; + var secondPeer = (IJavaObject) second; + AssertFalse (JNIEnv.IsSameObject (firstPeer.Handle, secondPeer.Handle), "expected distinct Java peers"); + } + + static void AssertTrue (bool value, string message) + { + if (!value) { + throw new InvalidOperationException ($"Assertion failed: {message}."); + } + } + + static void AssertFalse (bool value, string message) + { + AssertTrue (!value, message); + } + + static void AssertNull (object value, string message) + { + if (value != null) { + throw new InvalidOperationException ($"Assertion failed: {message}; expected null, found '{value}'."); + } + } + + static void AssertNotNull (object value, string message) + { + if (value == null) { + throw new InvalidOperationException ($"Assertion failed: {message}; value was null."); + } + } + + static void AssertSame (object expected, object actual, string message) + { + if (!ReferenceEquals (expected, actual)) { + throw new InvalidOperationException ($"Assertion failed: {message}; managed references differ."); + } + } + + static void AssertEqual (T expected, T actual, string message) + { + if (!EqualityComparer.Default.Equals (expected, actual)) { + throw new InvalidOperationException ($"Assertion failed: {message}; expected '{expected}', found '{actual}'."); + } + } + + static void DisposeIfDistinct (object owner, object value) + { + if (!ReferenceEquals (owner, value)) { + DisposeJavaObject (value); + } + } + + static void DisposeJavaObject (object value) + { + if (value is IDisposable disposable) { + disposable.Dispose (); + } + } + + sealed class RawInterfaceCollectionHolder : IDisposable + { + const string CollectionSignature = "()Ljava/util/Collection;"; + const string DictionarySignature = "()Ljava/util/Map;"; + const string JniName = "net/dot/android/test/InterfaceCollectionHolder"; + const string ListSignature = "()Ljava/util/List;"; + const string RoundTripCollectionSignature = "(Ljava/util/Collection;)Ljava/util/Collection;"; + const string RoundTripDictionarySignature = "(Ljava/util/Map;)Ljava/util/Map;"; + const string RoundTripListSignature = "(Ljava/util/List;)Ljava/util/List;"; + + readonly Java.Lang.Object holder; + + public RawInterfaceCollectionHolder () + { + var holderClass = JniEnvironment.Types.FindClass (JniName); + try { + var constructor = JNIEnv.GetMethodID (holderClass.Handle, "", "()V"); + var handle = JNIEnv.NewObject (holderClass.Handle, constructor); + holder = new Java.Lang.Object (handle, JniHandleOwnership.TransferLocalRef); + } finally { + JniObjectReference.Dispose (ref holderClass); + } + } + + public IList CreateList () + { + return ConvertCollection> (Call ("createList", ListSignature)); + } + + public IList CreateInheritedList () + { + return ConvertCollection> (Call ("createInheritedList", ListSignature)); + } + + public ICollection CreateCollection () + { + return ConvertCollection> (Call ("createCollection", CollectionSignature)); + } + + public IDictionary CreateKeyDictionary () + { + return ConvertCollection> (Call ("createKeyDictionary", DictionarySignature)); + } + + public IDictionary CreateValueDictionary () + { + return ConvertCollection> (Call ("createValueDictionary", DictionarySignature)); + } + + public IDictionary CreateInterfaceDictionary () + { + return ConvertCollection> (Call ("createInterfaceDictionary", DictionarySignature)); + } + + public IList RoundTripList (IList value) + { + return ConvertCollection> (Call ("roundTripList", RoundTripListSignature, value)); + } + + public ICollection RoundTripCollection (ICollection value) + { + return ConvertCollection> (Call ("roundTripCollection", RoundTripCollectionSignature, value)); + } + + public IDictionary RoundTripKeyDictionary (IDictionary value) + { + return ConvertCollection> ( + Call ("roundTripKeyDictionary", RoundTripDictionarySignature, value)); + } + + public IDictionary RoundTripValueDictionary (IDictionary value) + { + return ConvertCollection> ( + Call ("roundTripValueDictionary", RoundTripDictionarySignature, value)); + } + + public IDictionary RoundTripInterfaceDictionary ( + IDictionary value) + { + return ConvertCollection> ( + Call ("roundTripInterfaceDictionary", RoundTripDictionarySignature, value)); + } + + public void Dispose () + { + holder.Dispose (); + } + + IntPtr Call (string methodName, string signature, object value = null) + { + var holderClass = JniEnvironment.Types.GetObjectClass (holder.PeerReference); + try { + var method = JNIEnv.GetMethodID (holderClass.Handle, methodName, signature); + IntPtr handle; + if (value == null) { + handle = JNIEnv.CallObjectMethod (holder.Handle, method); + } else { + var peer = (IJavaObject) value; + handle = JNIEnv.CallObjectMethod (holder.Handle, method, new JValue (peer.Handle)); + GC.KeepAlive (value); + } + return handle; + } finally { + JniObjectReference.Dispose (ref holderClass); + } + } + } + } +} diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/ValueProvider.java b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/ValueProvider.java new file mode 100644 index 00000000000..a272243c4ec --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/ValueProvider.java @@ -0,0 +1,5 @@ +package net.dot.android.test; + +public interface ValueProvider { + int getValue(); +} diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/proguard.cfg b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/proguard.cfg new file mode 100644 index 00000000000..7ad86bae874 --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/proguard.cfg @@ -0,0 +1,3 @@ +-keep class net.dot.android.test.InterfaceCollectionBasePeer { *; } +-keep class net.dot.android.test.InterfaceCollectionExtendedPeer { *; } +-keep class net.dot.android.test.InterfaceCollectionHolder { *; } diff --git a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs new file mode 100644 index 00000000000..b74461206aa --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using NUnit.Framework; + +using Xamarin.Android.Tasks; +using Xamarin.Android.Tools; +using Xamarin.ProjectTools; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + [Category ("UsesDevice")] + public class InterfaceCollectionTests : DeviceTest + { + const string ResultPrefix = "INTERFACE_COLLECTION_RESULT"; + + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR)] + [TestCase ("trimmable", AndroidRuntime.CoreCLR)] + [TestCase ("trimmable", AndroidRuntime.NativeAOT)] + public void InterfaceValuedJavaCollections (string typemapImplementation, AndroidRuntime runtime) + { + var suffix = $"interfacecollections{typemapImplementation.Replace ("-", "")}{runtime}".ToLowerInvariant (); + var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime, suffix)) { + IsRelease = true, + }; + proj.SetRuntime (runtime); + proj.SetRuntimeIdentifiers ([DeviceAbi]); + proj.SetProperty ("AndroidTypeMapImplementation", typemapImplementation); + proj.SetProperty ("AndroidSdkDirectory", AndroidSdkResolver.GetAndroidSdkPath ()); + var javaSdkDirectory = AndroidSdkResolver.GetJavaSdkPath (); + proj.SetProperty ("JavaSdkDirectory", javaSdkDirectory); + proj.SetProperty ("JavaCPath", Path.Combine (javaSdkDirectory, "bin", "javac")); + proj.SetProperty ("JarPath", Path.Combine (javaSdkDirectory, "bin", "jar")); + proj.SetDefaultTargetDevice (); + proj.MainActivity = proj.ProcessSourceTemplate (ReadFixture ("MainActivity.cs")); + proj.AndroidJavaSources.Add (CreateJavaSource ("ValueProvider.java", bind: true)); + proj.AndroidJavaSources.Add (CreateJavaSource ("ExtendedValueProvider.java", bind: true)); + proj.AndroidJavaSources.Add (CreateJavaSource ("InterfaceCollectionFixture.java", bind: false)); + proj.OtherBuildItems.Add (new AndroidItem.ProguardConfiguration ("proguard.cfg") { + TextContent = () => ReadFixture ("proguard.cfg"), + }); + + var testDirectory = Path.Combine ("temp", $"{nameof (InterfaceValuedJavaCollections)}-{typemapImplementation}-{runtime}"); + using var builder = CreateApkBuilder (testDirectory); + try { + Assert.IsTrue (builder.Install (proj), "The focused interface-collection app should install."); + AssertGeneratedBindingsAreIsolated (builder, proj); + + ClearAdbLogcat (); + string resultLine = ""; + var logcatPath = Path.Combine (Root, builder.ProjectDirectory, "interface-collections-logcat.log"); + Assert.IsTrue ( + MonitorAdbLogcat ( + line => { + if (!line.Contains (ResultPrefix, StringComparison.Ordinal)) { + return false; + } + resultLine = line; + return true; + }, + logcatPath, + timeout: 60, + onMonitoringStarted: () => StartActivityAndAssert (proj)), + $"The focused app did not report a result. See '{logcatPath}'."); + StringAssert.Contains ($"{ResultPrefix} PASS 6/6", resultLine); + + if (runtime == AndroidRuntime.NativeAOT) { + var projectDirectory = Path.Combine (Root, builder.ProjectDirectory); + var dgmlFiles = Directory.GetFiles (projectDirectory, $"{proj.ProjectName}.scan.dgml.xml", SearchOption.AllDirectories); + Assert.AreEqual (1, dgmlFiles.Length, "The focused NativeAOT app should produce one scan dependency graph."); + AssertCanonicalWrapperRooting (dgmlFiles [0]); + TestContext.Out.WriteLine ($"Focused NativeAOT dependency graph: {dgmlFiles [0]}"); + } + } finally { + RunAdbCommand ($"uninstall {proj.PackageName}"); + } + } + + static AndroidItem.AndroidJavaSource CreateJavaSource (string fileName, bool bind) + { + return new AndroidItem.AndroidJavaSource (Path.Combine ("java", "net", "dot", "android", "test", fileName)) { + Encoding = Encoding.ASCII, + TextContent = () => ReadFixture (fileName), + Metadata = { + { "Bind", bind.ToString () }, + }, + }; + } + + void AssertGeneratedBindingsAreIsolated (ProjectBuilder builder, XamarinAndroidApplicationProject proj) + { + var projectDirectory = Path.Combine (Root, builder.ProjectDirectory); + var generatedSourceDirectory = Path.Combine (projectDirectory, proj.IntermediateOutputPath, "generated", "src"); + FileAssert.Exists (Path.Combine (generatedSourceDirectory, "Net.Dot.Android.Test.IValueProvider.cs")); + FileAssert.Exists (Path.Combine (generatedSourceDirectory, "Net.Dot.Android.Test.IExtendedValueProvider.cs")); + Assert.IsEmpty ( + Directory.GetFiles (generatedSourceDirectory, "*InterfaceCollection*.cs", SearchOption.TopDirectoryOnly), + "The raw JNI holder and concrete peers must not produce managed bindings that can root closed collection wrappers."); + } + + static void AssertCanonicalWrapperRooting (string dgmlFile) + { + var chains = new [] { + new RootingChain ( + "JavaList", + "SafeJavaCollectionFactory__CreateReferenceListFromJniHandle, Type metadata: [Java.Interop]Java.Interop.IJavaPeerable)", + "Android_Runtime_JavaList_1<Java_Interop_Java_Interop_IJavaPeerable> constructed\"", + "Label=\"__GenericDict_Mono_Android_Android_Runtime_JavaList_1<Java_Interop_Java_Interop_IJavaPeerable>\"", + "(__GenericDict_Mono_Android_Android_Runtime_JavaList_1<Java_Interop_Java_Interop_IJavaPeerable>, " + + "Mono_Android_Android_Runtime_JavaList_1<System___Canon>___ctor_0)", + "JavaList`1<Java.Interop.IJavaPeerable>..ctor(native int,JniHandleOwnership)"), + new RootingChain ( + "JavaCollection", + "SafeJavaCollectionFactory__CreateReferenceCollectionFromJniHandle, Type metadata: [Java.Interop]Java.Interop.IJavaPeerable)", + "Android_Runtime_JavaCollection_1<Java_Interop_Java_Interop_IJavaPeerable> constructed\"", + "Label=\"__GenericDict_Mono_Android_Android_Runtime_JavaCollection_1<Java_Interop_Java_Interop_IJavaPeerable>\"", + "(__GenericDict_Mono_Android_Android_Runtime_JavaCollection_1<Java_Interop_Java_Interop_IJavaPeerable>, " + + "Mono_Android_Android_Runtime_JavaCollection_1<System___Canon>___ctor)", + "JavaCollection`1<Java.Interop.IJavaPeerable>..ctor(native int,JniHandleOwnership)"), + new RootingChain ( + "JavaDictionary", + "Label=\"Mono_Android_Java_Interop_SafeJavaCollectionFactory__CreateReferenceDictionaryFromJniHandle\"", + "Android_Runtime_JavaDictionary_2<Java_Interop_Java_Interop_IJavaPeerable__Java_Interop_Java_Interop_IJavaPeerable> constructed\"", + "Label=\"__GenericDict_Mono_Android_Android_Runtime_JavaDictionary_2<Java_Interop_Java_Interop_IJavaPeerable__Java_Interop_Java_Interop_IJavaPeerable>\"", + "(__GenericDict_Mono_Android_Android_Runtime_JavaDictionary_2<Java_Interop_Java_Interop_IJavaPeerable__" + + "Java_Interop_Java_Interop_IJavaPeerable>, " + + "Mono_Android_Android_Runtime_JavaDictionary_2<System___Canon__System___Canon>___ctor_0)", + "JavaDictionary`2<Java.Interop.IJavaPeerable,Java.Interop.IJavaPeerable>..ctor(native int,JniHandleOwnership)"), + }; + var unexpectedCanonicalRoots = new List (); + + foreach (var line in File.ReadLines (dgmlFile)) { + if (line.Contains (" 0 && + target.Length > 0 && + line.Contains ($"Source=\"{source}\"", StringComparison.Ordinal) && + line.Contains ($"Target=\"{target}\"", StringComparison.Ordinal) && + line.Contains ($"Reason=\"{reason}\"", StringComparison.Ordinal); + } + + static string GetAttribute (string line, string name) + { + var prefix = $"{name}=\""; + int start = line.IndexOf (prefix, StringComparison.Ordinal); + if (start < 0) { + return ""; + } + start += prefix.Length; + int end = line.IndexOf ('"', start); + return end < 0 ? "" : line.Substring (start, end - start); + } + } + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs deleted file mode 100644 index f968aa4f54c..00000000000 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InterfaceCollectionMarshallingTests.cs +++ /dev/null @@ -1,429 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -using Android.Runtime; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests -{ - [TestFixture] - [Category ("InterfaceCollections")] - public class InterfaceCollectionMarshallingTests - { - const string CollectionSignature = "()Ljava/util/Collection;"; - const string DictionarySignature = "()Ljava/util/Map;"; - const string ListSignature = "()Ljava/util/List;"; - const string RoundTripCollectionSignature = "(Ljava/util/Collection;)Ljava/util/Collection;"; - const string RoundTripDictionarySignature = "(Ljava/util/Map;)Ljava/util/Map;"; - const string RoundTripListSignature = "(Ljava/util/List;)Ljava/util/List;"; - - [Test] - public void JavaList_InterfaceElementsPreserveIdentityAndRoundTrip () - { - using var holder = new RawInterfaceCollectionHolder (); - var targetType = typeof (IList); - var list = holder.Create> ("createList", ListSignature, targetType); - try { - AssertWrapperType (list, typeof (JavaList<>), typeof (global::Net.Dot.Android.Test.IValueProvider)); - Assert.AreEqual (4, list.Count); - - var first = list [0]; - var duplicate = list [1]; - var second = list [2]; - - AssertBaseInterfacePeer (first, 11); - AssertBaseInterfacePeer (second, 22); - Assert.AreSame (first, duplicate); - Assert.AreSame (first, list [0]); - AssertSameJavaObject (first, duplicate); - AssertDistinctJavaObjects (first, second); - Assert.IsNull (list [3]); - Assert.IsTrue (list.Contains (first)); - Assert.IsTrue (list.Contains (null)); - - list.Add (second); - Assert.AreEqual (5, list.Count); - Assert.AreSame (second, list [4]); - CollectionAssert.AreEqual (new [] { 11, 11, 22, 22 }, list.Where (value => value != null).Select (value => value.Value)); - - var roundTrip = holder.RoundTrip> ( - "roundTripList", - RoundTripListSignature, - list, - targetType); - try { - AssertWrapperType (roundTrip, typeof (JavaList<>), typeof (global::Net.Dot.Android.Test.IValueProvider)); - AssertSameJavaObject (list, roundTrip); - Assert.AreSame (first, roundTrip [0]); - } finally { - DisposeIfDistinct (list, roundTrip); - } - - Assert.IsTrue (list.Remove (first)); - Assert.IsTrue (list.Contains (first)); - Assert.IsTrue (list.Remove (first)); - Assert.IsFalse (list.Contains (first)); - } finally { - DisposeJavaObject (list); - } - } - - [Test] - public void JavaList_InheritedInterfaceUsesExplicitInvoker () - { - using var holder = new RawInterfaceCollectionHolder (); - var targetType = typeof (IList); - var list = holder.Create> ( - "createInheritedList", - ListSignature, - targetType); - try { - AssertWrapperType (list, typeof (JavaList<>), typeof (global::Net.Dot.Android.Test.IExtendedValueProvider)); - Assert.AreEqual (2, list.Count); - AssertExtendedInterfacePeer (list [0], 33, 333); - AssertExtendedInterfacePeer (list [1], 44, 444); - } finally { - DisposeJavaObject (list); - } - } - - [Test] - public void JavaCollection_InterfaceElementsSupportOperationsAndRoundTrip () - { - using var holder = new RawInterfaceCollectionHolder (); - var targetType = typeof (ICollection); - var collection = holder.Create> ( - "createCollection", - CollectionSignature, - targetType); - try { - AssertWrapperType (collection, typeof (JavaCollection<>), typeof (global::Net.Dot.Android.Test.IValueProvider)); - Assert.AreEqual (3, collection.Count); - - var values = collection.ToArray (); - var first = values [0]; - var second = values [1]; - AssertBaseInterfacePeer (first, 11); - AssertBaseInterfacePeer (second, 22); - AssertDistinctJavaObjects (first, second); - Assert.IsNull (values [2]); - - collection.Add (first); - Assert.AreEqual (4, collection.Count); - Assert.IsTrue (collection.Contains (first)); - Assert.IsTrue (collection.Contains (null)); - Assert.AreSame (first, collection.ToArray () [3]); - - var roundTrip = holder.RoundTrip> ( - "roundTripCollection", - RoundTripCollectionSignature, - collection, - targetType); - try { - AssertWrapperType (roundTrip, typeof (JavaCollection<>), typeof (global::Net.Dot.Android.Test.IValueProvider)); - AssertSameJavaObject (collection, roundTrip); - Assert.AreSame (first, roundTrip.First ()); - } finally { - DisposeIfDistinct (collection, roundTrip); - } - - collection.Clear (); - Assert.AreEqual (0, collection.Count); - } finally { - DisposeJavaObject (collection); - } - } - - [Test] - public void JavaDictionary_InterfaceKeysSupportOperationsAndRoundTrip () - { - using var holder = new RawInterfaceCollectionHolder (); - var targetType = typeof (IDictionary); - var dictionary = holder.Create> ( - "createKeyDictionary", - DictionarySignature, - targetType); - try { - AssertWrapperType ( - dictionary, - typeof (JavaDictionary<,>), - typeof (global::Net.Dot.Android.Test.IValueProvider), - typeof (string)); - Assert.AreEqual (3, dictionary.Count); - - var first = dictionary.Keys.Single (key => key != null && key.Value == 11); - var second = dictionary.Keys.Single (key => key != null && key.Value == 22); - AssertBaseInterfacePeer (first, 11); - AssertBaseInterfacePeer (second, 22); - AssertDistinctJavaObjects (first, second); - Assert.IsTrue (dictionary.ContainsKey (first)); - Assert.IsTrue (dictionary.ContainsKey (null)); - Assert.AreEqual ("first", dictionary [first]); - Assert.AreEqual ("null", dictionary [null]); - Assert.AreSame (first, dictionary.Keys.Single (key => key != null && key.Value == 11)); - - var roundTrip = holder.RoundTrip> ( - "roundTripKeyDictionary", - RoundTripDictionarySignature, - dictionary, - targetType); - try { - AssertWrapperType ( - roundTrip, - typeof (JavaDictionary<,>), - typeof (global::Net.Dot.Android.Test.IValueProvider), - typeof (string)); - AssertSameJavaObject (dictionary, roundTrip); - Assert.AreEqual ("second", roundTrip [second]); - } finally { - DisposeIfDistinct (dictionary, roundTrip); - } - - Assert.IsTrue (dictionary.Remove (first)); - Assert.IsFalse (dictionary.ContainsKey (first)); - } finally { - DisposeJavaObject (dictionary); - } - } - - [Test] - public void JavaDictionary_InterfaceValuesPreserveDuplicatesAndRoundTrip () - { - using var holder = new RawInterfaceCollectionHolder (); - var targetType = typeof (IDictionary); - var dictionary = holder.Create> ( - "createValueDictionary", - DictionarySignature, - targetType); - try { - AssertWrapperType ( - dictionary, - typeof (JavaDictionary<,>), - typeof (string), - typeof (global::Net.Dot.Android.Test.IValueProvider)); - Assert.AreEqual (4, dictionary.Count); - - var first = dictionary ["first"]; - var duplicate = dictionary ["duplicate"]; - var second = dictionary ["second"]; - AssertBaseInterfacePeer (first, 11); - AssertBaseInterfacePeer (second, 22); - Assert.AreSame (first, duplicate); - Assert.AreSame (first, dictionary ["first"]); - AssertDistinctJavaObjects (first, second); - Assert.IsNull (dictionary ["null"]); - CollectionAssert.AreEquivalent (new [] { 11, 11, 22 }, dictionary.Values.Where (value => value != null).Select (value => value.Value)); - - dictionary.Add ("added", second); - Assert.AreSame (second, dictionary ["added"]); - - var roundTrip = holder.RoundTrip> ( - "roundTripValueDictionary", - RoundTripDictionarySignature, - dictionary, - targetType); - try { - AssertWrapperType ( - roundTrip, - typeof (JavaDictionary<,>), - typeof (string), - typeof (global::Net.Dot.Android.Test.IValueProvider)); - AssertSameJavaObject (dictionary, roundTrip); - Assert.AreSame (first, roundTrip ["duplicate"]); - } finally { - DisposeIfDistinct (dictionary, roundTrip); - } - - Assert.IsTrue (dictionary.Remove ("first")); - Assert.IsFalse (dictionary.ContainsKey ("first")); - } finally { - DisposeJavaObject (dictionary); - } - } - - [Test] - public void JavaDictionary_InterfaceKeysAndValuesPreserveIdentityAndRoundTrip () - { - using var holder = new RawInterfaceCollectionHolder (); - var targetType = typeof (IDictionary< - global::Net.Dot.Android.Test.IValueProvider, - global::Net.Dot.Android.Test.IValueProvider>); - var dictionary = holder.Create> ( - "createInterfaceDictionary", - DictionarySignature, - targetType); - try { - AssertWrapperType ( - dictionary, - typeof (JavaDictionary<,>), - typeof (global::Net.Dot.Android.Test.IValueProvider), - typeof (global::Net.Dot.Android.Test.IValueProvider)); - Assert.AreEqual (3, dictionary.Count); - - var first = dictionary.Keys.Single (key => key != null && key.Value == 11); - var second = dictionary.Keys.Single (key => key != null && key.Value == 22); - AssertBaseInterfacePeer (first, 11); - AssertBaseInterfacePeer (second, 22); - Assert.AreSame (second, dictionary [first]); - Assert.AreSame (first, dictionary [second]); - Assert.IsNull (dictionary [null]); - Assert.IsTrue (dictionary.ContainsKey (first)); - Assert.IsTrue (dictionary.Any (pair => ReferenceEquals (pair.Key, first) && ReferenceEquals (pair.Value, second))); - - var roundTrip = holder.RoundTrip> ( - "roundTripInterfaceDictionary", - RoundTripDictionarySignature, - dictionary, - targetType); - try { - AssertWrapperType ( - roundTrip, - typeof (JavaDictionary<,>), - typeof (global::Net.Dot.Android.Test.IValueProvider), - typeof (global::Net.Dot.Android.Test.IValueProvider)); - AssertSameJavaObject (dictionary, roundTrip); - Assert.AreSame (second, roundTrip [first]); - } finally { - DisposeIfDistinct (dictionary, roundTrip); - } - - Assert.IsTrue (dictionary.Remove (second)); - Assert.IsFalse (dictionary.ContainsKey (second)); - } finally { - DisposeJavaObject (dictionary); - } - } - - static void AssertBaseInterfacePeer (global::Net.Dot.Android.Test.IValueProvider peer, int expectedValue) - { - Assert.IsNotNull (peer); - Assert.AreEqual (expectedValue, peer.Value); - Assert.AreEqual (typeof (global::Net.Dot.Android.Test.IValueProviderInvoker), peer.GetType ()); - Assert.IsFalse (peer is global::Net.Dot.Android.Test.IExtendedValueProvider); - } - - static void AssertExtendedInterfacePeer ( - global::Net.Dot.Android.Test.IExtendedValueProvider peer, - int expectedValue, - int expectedOtherValue) - { - Assert.IsNotNull (peer); - Assert.AreEqual (expectedValue, peer.Value); - Assert.AreEqual (expectedOtherValue, peer.OtherValue); - Assert.AreEqual (typeof (global::Net.Dot.Android.Test.IExtendedValueProviderInvoker), peer.GetType ()); - } - - static void AssertWrapperType (object wrapper, Type expectedGenericDefinition, params Type [] expectedArguments) - { - var wrapperType = wrapper.GetType (); - Assert.IsTrue (wrapperType.IsGenericType); - Assert.AreEqual (expectedGenericDefinition, wrapperType.GetGenericTypeDefinition ()); - CollectionAssert.AreEqual (expectedArguments, wrapperType.GenericTypeArguments); - } - - static object InvokeJavaConvertFromJniHandle (Type targetType, IntPtr handle) - { - var javaConvert = typeof (Java.Lang.Object).Assembly.GetType ("Java.Interop.JavaConvert"); - Assert.IsNotNull (javaConvert); - - var method = javaConvert.GetMethod ( - "FromJniHandle", - BindingFlags.Public | BindingFlags.Static, - binder: null, - types: new [] { typeof (IntPtr), typeof (JniHandleOwnership), typeof (Type) }, - modifiers: null); - Assert.IsNotNull (method); - - var value = method.Invoke (null, new object [] { handle, JniHandleOwnership.TransferLocalRef, targetType }); - Assert.IsNotNull (value); - return value; - } - - static void AssertSameJavaObject (object expected, object actual) - { - var expectedPeer = (IJavaObject) expected; - var actualPeer = (IJavaObject) actual; - Assert.IsTrue (JNIEnv.IsSameObject (expectedPeer.Handle, actualPeer.Handle)); - } - - static void AssertDistinctJavaObjects (object first, object second) - { - var firstPeer = (IJavaObject) first; - var secondPeer = (IJavaObject) second; - Assert.IsFalse (JNIEnv.IsSameObject (firstPeer.Handle, secondPeer.Handle)); - } - - static void DisposeIfDistinct (object owner, object value) - { - if (!ReferenceEquals (owner, value)) { - DisposeJavaObject (value); - } - } - - static void DisposeJavaObject (object value) - { - if (value is IDisposable disposable) { - disposable.Dispose (); - } - } - - sealed class RawInterfaceCollectionHolder : IDisposable - { - const string JniName = "net/dot/android/test/InterfaceCollectionHolder"; - - readonly Java.Lang.Object holder; - - public RawInterfaceCollectionHolder () - { - var holderClass = JniEnvironment.Types.FindClass (JniName); - try { - var constructor = JNIEnv.GetMethodID (holderClass.Handle, "", "()V"); - var handle = JNIEnv.NewObject (holderClass.Handle, constructor); - holder = new Java.Lang.Object (handle, JniHandleOwnership.TransferLocalRef); - } finally { - JniObjectReference.Dispose (ref holderClass); - } - } - - public T Create (string methodName, string signature, Type targetType) - { - var method = GetMethod (methodName, signature); - var handle = JNIEnv.CallObjectMethod (holder.Handle, method); - return (T) InvokeJavaConvertFromJniHandle (targetType, handle); - } - - public T RoundTrip (string methodName, string signature, object value, Type targetType) - { - var method = GetMethod (methodName, signature); - var peer = (IJavaObject) value; - var handle = JNIEnv.CallObjectMethod (holder.Handle, method, new JValue (peer.Handle)); - GC.KeepAlive (value); - return (T) InvokeJavaConvertFromJniHandle (targetType, handle); - } - - public void Dispose () - { - holder.Dispose (); - } - - IntPtr GetMethod (string methodName, string signature) - { - var holderClass = JNIEnv.GetObjectClass (holder.Handle); - try { - return JNIEnv.GetMethodID (holderClass, methodName, signature); - } finally { - JNIEnv.DeleteLocalRef (holderClass); - } - } - } - } -} 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 dbf0fb83010..6405287d880 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 @@ -138,7 +138,6 @@ - diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionBasePeer.java b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionBasePeer.java deleted file mode 100644 index c2748a391f2..00000000000 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionBasePeer.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.dot.android.test; - -final class InterfaceCollectionBasePeer implements ValueProvider { - private final int value; - - public InterfaceCollectionBasePeer(int value) { - this.value = value; - } - - @Override - public int getValue() { - return value; - } -} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionPeer.java b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionPeer.java deleted file mode 100644 index 7af72afd67c..00000000000 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/InterfaceCollectionPeer.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.dot.android.test; - -final class InterfaceCollectionPeer implements ExtendedValueProvider { - private final int value; - private final int otherValue; - - public InterfaceCollectionPeer(int value, int otherValue) { - this.value = value; - this.otherValue = otherValue; - } - - @Override - public int getValue() { - return value; - } - - @Override - public int getOtherValue() { - return otherValue; - } -} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/proguard.cfg b/tests/Mono.Android-Tests/Mono.Android-Tests/proguard.cfg index 676c17eac25..64005b977cb 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/proguard.cfg +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/proguard.cfg @@ -1,6 +1,3 @@ # Need to preserve the contents of Mono.Android-Test-classes.jar -keep class net.dot.jni.test.** { *; (); } --keep class net.dot.android.test.InterfaceCollectionBasePeer { *; } --keep class net.dot.android.test.InterfaceCollectionHolder { *; } --keep class net.dot.android.test.InterfaceCollectionPeer { *; } From 2326716b2952060c360e48a6b099bdd0bffe7f51 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 1 Sep 2026 10:43:59 +0200 Subject: [PATCH 4/9] [tests] Enforce interface wrapper root isolation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../InterfaceCollectionFixture.java | 8 + .../InterfaceCollectionApp/MainActivity.cs | 189 ++++++++++++++---- .../Tests/InterfaceCollectionTests.cs | 78 ++++++-- 3 files changed, 225 insertions(+), 50 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/InterfaceCollectionFixture.java b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/InterfaceCollectionFixture.java index 3edffb49d05..b54665e61eb 100644 --- a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/InterfaceCollectionFixture.java +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/InterfaceCollectionFixture.java @@ -76,6 +76,14 @@ public Collection createCollection() { return result; } + public ValueProvider getFirst() { + return first; + } + + public ValueProvider getSecond() { + return second; + } + public Map createKeyDictionary() { Map result = new LinkedHashMap<>(); result.put(first, "first"); diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs index e405e365283..e3dbf34ce80 100644 --- a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs @@ -152,8 +152,8 @@ static void JavaDictionary_InterfaceKeysSupportOperationsAndRoundTrip () AssertWrapperType (dictionary, typeof (JavaDictionary<,>), typeof (IValueProvider), typeof (string)); AssertEqual (3, dictionary.Count, "key dictionary count"); - var first = FindPeer (dictionary.Keys, 11); - var second = FindPeer (dictionary.Keys, 22); + var first = holder.GetFirst (); + var second = holder.GetSecond (); AssertBaseInterfacePeer (first, 11); AssertBaseInterfacePeer (second, 22); AssertDistinctJavaObjects (first, second); @@ -161,7 +161,8 @@ static void JavaDictionary_InterfaceKeysSupportOperationsAndRoundTrip () AssertTrue (dictionary.ContainsKey (null), "dictionary contains null key"); AssertEqual ("first", dictionary [first], "first key value"); AssertEqual ("null", dictionary [null], "null key value"); - AssertSame (first, FindPeer (dictionary.Keys, 11), "repeated key lookup"); + AssertSame (first, holder.GetFirst (), "repeated key lookup"); + AssertKeyDictionaryEnumeration (dictionary, first, second); var roundTrip = holder.RoundTripKeyDictionary (dictionary); try { @@ -196,7 +197,7 @@ static void JavaDictionary_InterfaceValuesPreserveDuplicatesAndRoundTrip () AssertSame (first, dictionary ["first"], "repeated value lookup"); AssertDistinctJavaObjects (first, second); AssertNull (dictionary ["null"], "null dictionary value"); - AssertSequence ([11, 11, 22], GetValues (dictionary.Values), "dictionary value enumeration"); + AssertSequence ([11, 11, 22], GetRawDictionaryValues (dictionary), "dictionary value enumeration"); dictionary.Add ("added", second); AssertSame (second, dictionary ["added"], "added dictionary value"); @@ -225,15 +226,15 @@ static void JavaDictionary_InterfaceKeysAndValuesPreserveIdentityAndRoundTrip () AssertWrapperType (dictionary, typeof (JavaDictionary<,>), typeof (IValueProvider), typeof (IValueProvider)); AssertEqual (3, dictionary.Count, "interface dictionary count"); - var first = FindPeer (dictionary.Keys, 11); - var second = FindPeer (dictionary.Keys, 22); + var first = holder.GetFirst (); + var second = holder.GetSecond (); AssertBaseInterfacePeer (first, 11); AssertBaseInterfacePeer (second, 22); AssertSame (second, dictionary [first], "interface dictionary first value"); AssertSame (first, dictionary [second], "interface dictionary second value"); AssertNull (dictionary [null], "interface dictionary null value"); AssertTrue (dictionary.ContainsKey (first), "interface dictionary contains first"); - AssertTrue (ContainsPair (dictionary, first, second), "interface dictionary enumeration"); + AssertTrue (ContainsRawPair (dictionary, first, second), "interface dictionary enumeration"); var roundTrip = holder.RoundTripInterfaceDictionary (dictionary); try { @@ -278,13 +279,13 @@ static void AssertWrapperType (object wrapper, Type expectedGenericDefinition, p } } - static T ConvertCollection (IntPtr handle) + static T ConvertJavaValue (IntPtr handle, JniHandleOwnership transfer = JniHandleOwnership.TransferLocalRef) { - return (T) InvokeJavaConvertFromJniHandle (typeof (T), handle); + return (T) InvokeJavaConvertFromJniHandle (typeof (T), handle, transfer); } [DynamicDependency ("FromJniHandle", "Java.Interop.JavaConvert", "Mono.Android")] - static object InvokeJavaConvertFromJniHandle (Type targetType, IntPtr handle) + static object InvokeJavaConvertFromJniHandle (Type targetType, IntPtr handle, JniHandleOwnership transfer) { var javaConvert = typeof (Java.Lang.Object).Assembly.GetType ("Java.Interop.JavaConvert"); if (javaConvert == null) { @@ -301,21 +302,47 @@ static object InvokeJavaConvertFromJniHandle (Type targetType, IntPtr handle) throw new InvalidOperationException ("JavaConvert.FromJniHandle method was not found."); } - var value = method.Invoke (null, [handle, JniHandleOwnership.TransferLocalRef, targetType]); + var value = method.Invoke (null, [handle, transfer, targetType]); if (value == null) { throw new InvalidOperationException ($"JavaConvert returned null for target type '{targetType}'."); } return value; } - static IValueProvider FindPeer (ICollection peers, int value) + static void AssertKeyDictionaryEnumeration ( + IDictionary dictionary, + IValueProvider first, + IValueProvider second) { - foreach (var peer in peers) { - if (peer != null && peer.Value == value) { - return peer; + int count = 0; + bool foundFirst = false; + bool foundSecond = false; + bool foundNull = false; + VisitDictionaryEntries (dictionary, (keyHandle, valueHandle) => { + var key = keyHandle == IntPtr.Zero + ? null + : ConvertJavaValue (keyHandle, JniHandleOwnership.DoNotTransfer); + var value = valueHandle == IntPtr.Zero + ? null + : JNIEnv.GetString (valueHandle, JniHandleOwnership.DoNotTransfer); + count++; + if (key == null) { + AssertEqual ("null", value, "raw null key value"); + foundNull = true; + } else if (ReferenceEquals (key, first)) { + AssertEqual ("first", value, "raw first key value"); + foundFirst = true; + } else if (ReferenceEquals (key, second)) { + AssertEqual ("second", value, "raw second key value"); + foundSecond = true; + } else { + throw new InvalidOperationException ($"Unexpected raw dictionary key value '{key.Value}'."); } - } - throw new InvalidOperationException ($"Peer with value {value} was not found."); + }); + AssertEqual (3, count, "raw key dictionary entry count"); + AssertTrue (foundFirst, "raw first key entry"); + AssertTrue (foundSecond, "raw second key entry"); + AssertTrue (foundNull, "raw null key entry"); } static IValueProvider GetElement (ICollection values, int index) @@ -341,17 +368,97 @@ static int [] GetValues (IEnumerable peers) return values.ToArray (); } - static bool ContainsPair ( - IEnumerable> pairs, - IValueProvider key, - IValueProvider value) + static int [] GetRawDictionaryValues (IDictionary dictionary) { - foreach (var pair in pairs) { - if (ReferenceEquals (pair.Key, key) && ReferenceEquals (pair.Value, value)) { - return true; + var values = new List (); + bool foundNull = false; + VisitDictionaryEntries (dictionary, (keyHandle, valueHandle) => { + var value = valueHandle == IntPtr.Zero + ? null + : ConvertJavaValue (valueHandle, JniHandleOwnership.DoNotTransfer); + if (value == null) { + foundNull = true; + } else { + values.Add (value.Value); } + }); + AssertTrue (foundNull, "raw null dictionary value"); + return values.ToArray (); + } + + static bool ContainsRawPair ( + IDictionary dictionary, + IValueProvider expectedKey, + IValueProvider expectedValue) + { + int count = 0; + bool found = false; + VisitDictionaryEntries (dictionary, (keyHandle, valueHandle) => { + var key = keyHandle == IntPtr.Zero + ? null + : ConvertJavaValue (keyHandle, JniHandleOwnership.DoNotTransfer); + var value = valueHandle == IntPtr.Zero + ? null + : ConvertJavaValue (valueHandle, JniHandleOwnership.DoNotTransfer); + count++; + if (ReferenceEquals (key, expectedKey) && ReferenceEquals (value, expectedValue)) { + found = true; + } + }); + AssertEqual (3, count, "raw interface dictionary entry count"); + return found; + } + + static void VisitDictionaryEntries (object dictionary, Action visitor) + { + var mapClass = JniEnvironment.Types.FindClass ("java/util/Map"); + var setClass = JniEnvironment.Types.FindClass ("java/util/Set"); + var iteratorClass = JniEnvironment.Types.FindClass ("java/util/Iterator"); + var entryClass = JniEnvironment.Types.FindClass ("java/util/Map$Entry"); + IntPtr entrySet = IntPtr.Zero; + IntPtr iterator = IntPtr.Zero; + try { + var entrySetMethod = JNIEnv.GetMethodID (mapClass.Handle, "entrySet", "()Ljava/util/Set;"); + var iteratorMethod = JNIEnv.GetMethodID (setClass.Handle, "iterator", "()Ljava/util/Iterator;"); + var hasNextMethod = JNIEnv.GetMethodID (iteratorClass.Handle, "hasNext", "()Z"); + var nextMethod = JNIEnv.GetMethodID (iteratorClass.Handle, "next", "()Ljava/lang/Object;"); + var getKeyMethod = JNIEnv.GetMethodID (entryClass.Handle, "getKey", "()Ljava/lang/Object;"); + var getValueMethod = JNIEnv.GetMethodID (entryClass.Handle, "getValue", "()Ljava/lang/Object;"); + var dictionaryPeer = (IJavaObject) dictionary; + + entrySet = JNIEnv.CallObjectMethod (dictionaryPeer.Handle, entrySetMethod); + iterator = JNIEnv.CallObjectMethod (entrySet, iteratorMethod); + while (JNIEnv.CallBooleanMethod (iterator, hasNextMethod)) { + var entry = JNIEnv.CallObjectMethod (iterator, nextMethod); + var key = IntPtr.Zero; + var value = IntPtr.Zero; + try { + key = JNIEnv.CallObjectMethod (entry, getKeyMethod); + value = JNIEnv.CallObjectMethod (entry, getValueMethod); + visitor (key, value); + } finally { + DeleteLocalRef (ref value); + DeleteLocalRef (ref key); + DeleteLocalRef (ref entry); + } + } + GC.KeepAlive (dictionary); + } finally { + DeleteLocalRef (ref iterator); + DeleteLocalRef (ref entrySet); + JniObjectReference.Dispose (ref entryClass); + JniObjectReference.Dispose (ref iteratorClass); + JniObjectReference.Dispose (ref setClass); + JniObjectReference.Dispose (ref mapClass); + } + } + + static void DeleteLocalRef (ref IntPtr handle) + { + if (handle != IntPtr.Zero) { + JNIEnv.DeleteLocalRef (handle); + handle = IntPtr.Zero; } - return false; } static void AssertSequence (int [] expected, int [] actual, string message) @@ -456,60 +563,70 @@ public RawInterfaceCollectionHolder () public IList CreateList () { - return ConvertCollection> (Call ("createList", ListSignature)); + return ConvertJavaValue> (Call ("createList", ListSignature)); } public IList CreateInheritedList () { - return ConvertCollection> (Call ("createInheritedList", ListSignature)); + return ConvertJavaValue> (Call ("createInheritedList", ListSignature)); } public ICollection CreateCollection () { - return ConvertCollection> (Call ("createCollection", CollectionSignature)); + return ConvertJavaValue> (Call ("createCollection", CollectionSignature)); + } + + public IValueProvider GetFirst () + { + return ConvertJavaValue (Call ("getFirst", "()Lnet/dot/android/test/ValueProvider;")); + } + + public IValueProvider GetSecond () + { + return ConvertJavaValue (Call ("getSecond", "()Lnet/dot/android/test/ValueProvider;")); } public IDictionary CreateKeyDictionary () { - return ConvertCollection> (Call ("createKeyDictionary", DictionarySignature)); + return ConvertJavaValue> (Call ("createKeyDictionary", DictionarySignature)); } public IDictionary CreateValueDictionary () { - return ConvertCollection> (Call ("createValueDictionary", DictionarySignature)); + return ConvertJavaValue> (Call ("createValueDictionary", DictionarySignature)); } public IDictionary CreateInterfaceDictionary () { - return ConvertCollection> (Call ("createInterfaceDictionary", DictionarySignature)); + return ConvertJavaValue> (Call ("createInterfaceDictionary", DictionarySignature)); } public IList RoundTripList (IList value) { - return ConvertCollection> (Call ("roundTripList", RoundTripListSignature, value)); + return ConvertJavaValue> (Call ("roundTripList", RoundTripListSignature, value)); } public ICollection RoundTripCollection (ICollection value) { - return ConvertCollection> (Call ("roundTripCollection", RoundTripCollectionSignature, value)); + return ConvertJavaValue> (Call ("roundTripCollection", RoundTripCollectionSignature, value)); } public IDictionary RoundTripKeyDictionary (IDictionary value) { - return ConvertCollection> ( + return ConvertJavaValue> ( Call ("roundTripKeyDictionary", RoundTripDictionarySignature, value)); } public IDictionary RoundTripValueDictionary (IDictionary value) { - return ConvertCollection> ( + return ConvertJavaValue> ( Call ("roundTripValueDictionary", RoundTripDictionarySignature, value)); } public IDictionary RoundTripInterfaceDictionary ( IDictionary value) { - return ConvertCollection> ( + return ConvertJavaValue> ( Call ("roundTripInterfaceDictionary", RoundTripDictionarySignature, value)); } diff --git a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs index b74461206aa..2adeb70421c 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs @@ -50,21 +50,17 @@ public void InterfaceValuedJavaCollections (string typemapImplementation, Androi AssertGeneratedBindingsAreIsolated (builder, proj); ClearAdbLogcat (); - string resultLine = ""; var logcatPath = Path.Combine (Root, builder.ProjectDirectory, "interface-collections-logcat.log"); - Assert.IsTrue ( - MonitorAdbLogcat ( - line => { - if (!line.Contains (ResultPrefix, StringComparison.Ordinal)) { - return false; - } - resultLine = line; - return true; - }, - logcatPath, - timeout: 60, - onMonitoringStarted: () => StartActivityAndAssert (proj)), - $"The focused app did not report a result. See '{logcatPath}'."); + StartActivityAndAssert (proj); + string logcatOutput = ""; + string resultLine = ""; + WaitFor (TimeSpan.FromSeconds (60), () => { + logcatOutput = RunAdbCommand ("logcat -d"); + resultLine = FindResultLine (logcatOutput); + return resultLine.Length > 0; + }, intervalInMS: 250); + File.WriteAllText (logcatPath, logcatOutput); + Assert.IsNotEmpty (resultLine, $"The focused app did not report a result. See '{logcatPath}'."); StringAssert.Contains ($"{ResultPrefix} PASS 6/6", resultLine); if (runtime == AndroidRuntime.NativeAOT) { @@ -79,6 +75,16 @@ public void InterfaceValuedJavaCollections (string typemapImplementation, Androi } } + static string FindResultLine (string logcatOutput) + { + foreach (var line in logcatOutput.Split (['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)) { + if (line.Contains (ResultPrefix, StringComparison.Ordinal)) { + return line; + } + } + return ""; + } + static AndroidItem.AndroidJavaSource CreateJavaSource (string fileName, bool bind) { return new AndroidItem.AndroidJavaSource (Path.Combine ("java", "net", "dot", "android", "test", fileName)) { @@ -111,6 +117,7 @@ static void AssertCanonicalWrapperRooting (string dgmlFile) "Label=\"__GenericDict_Mono_Android_Android_Runtime_JavaList_1<Java_Interop_Java_Interop_IJavaPeerable>\"", "(__GenericDict_Mono_Android_Android_Runtime_JavaList_1<Java_Interop_Java_Interop_IJavaPeerable>, " + "Mono_Android_Android_Runtime_JavaList_1<System___Canon>___ctor_0)", + "Label=\"Mono_Android_Android_Runtime_JavaList_1<System___Canon>___ctor_0\"", "JavaList`1<Java.Interop.IJavaPeerable>..ctor(native int,JniHandleOwnership)"), new RootingChain ( "JavaCollection", @@ -119,6 +126,7 @@ static void AssertCanonicalWrapperRooting (string dgmlFile) "Label=\"__GenericDict_Mono_Android_Android_Runtime_JavaCollection_1<Java_Interop_Java_Interop_IJavaPeerable>\"", "(__GenericDict_Mono_Android_Android_Runtime_JavaCollection_1<Java_Interop_Java_Interop_IJavaPeerable>, " + "Mono_Android_Android_Runtime_JavaCollection_1<System___Canon>___ctor)", + "Label=\"Mono_Android_Android_Runtime_JavaCollection_1<System___Canon>___ctor\"", "JavaCollection`1<Java.Interop.IJavaPeerable>..ctor(native int,JniHandleOwnership)"), new RootingChain ( "JavaDictionary", @@ -128,6 +136,7 @@ static void AssertCanonicalWrapperRooting (string dgmlFile) "(__GenericDict_Mono_Android_Android_Runtime_JavaDictionary_2<Java_Interop_Java_Interop_IJavaPeerable__" + "Java_Interop_Java_Interop_IJavaPeerable>, " + "Mono_Android_Android_Runtime_JavaDictionary_2<System___Canon__System___Canon>___ctor_0)", + "Label=\"Mono_Android_Android_Runtime_JavaDictionary_2<System___Canon__System___Canon>___ctor_0\"", "JavaDictionary`2<Java.Interop.IJavaPeerable,Java.Interop.IJavaPeerable>..ctor(native int,JniHandleOwnership)"), }; var unexpectedCanonicalRoots = new List (); @@ -197,16 +206,20 @@ static string ReadFixture (string fileName) sealed class RootingChain { readonly string constructorPattern; + readonly string canonicalConstructorPattern; readonly string constructedTypePattern; readonly string genericDictionaryPattern; readonly string genericDictionaryDependencyPattern; readonly string sourcePattern; + readonly List unexpectedIncomingLinks = new (); + string canonicalConstructorId = ""; string constructedTypeId = ""; string constructorId = ""; string genericDictionaryId = ""; string genericDictionaryDependencyId = ""; string sourceId = ""; + bool canonicalConstructorToDependency; bool constructedTypeToGenericDictionary; bool genericDictionaryToDependency; bool genericDictionaryToConstructor; @@ -218,6 +231,7 @@ public RootingChain ( string constructedTypePattern, string genericDictionaryPattern, string genericDictionaryDependencyPattern, + string canonicalConstructorPattern, string constructorPattern) { Name = name; @@ -225,6 +239,7 @@ public RootingChain ( this.constructedTypePattern = constructedTypePattern; this.genericDictionaryPattern = genericDictionaryPattern; this.genericDictionaryDependencyPattern = genericDictionaryDependencyPattern; + this.canonicalConstructorPattern = canonicalConstructorPattern; this.constructorPattern = constructorPattern; } @@ -240,6 +255,8 @@ public void ObserveNode (string line) genericDictionaryId = GetAttribute (line, "Id"); } else if (line.Contains (genericDictionaryDependencyPattern, StringComparison.Ordinal)) { genericDictionaryDependencyId = GetAttribute (line, "Id"); + } else if (line.Contains (canonicalConstructorPattern, StringComparison.Ordinal)) { + canonicalConstructorId = GetAttribute (line, "Id"); } else if (line.Contains (constructorPattern, StringComparison.Ordinal)) { constructorId = GetAttribute (line, "Id"); } @@ -250,11 +267,29 @@ public void ObserveLink (string line) sourceToConstructedType |= IsLink (line, sourceId, constructedTypeId, "newobj"); constructedTypeToGenericDictionary |= IsLink (line, constructedTypeId, genericDictionaryId, "reloc"); genericDictionaryToDependency |= IsLink (line, genericDictionaryId, genericDictionaryDependencyId, "Primary"); + canonicalConstructorToDependency |= IsLink ( + line, + canonicalConstructorId, + genericDictionaryDependencyId, + "Secondary"); genericDictionaryToConstructor |= IsLink ( line, genericDictionaryDependencyId, constructorId, "Generic dictionary dependency"); + + RejectUnexpectedIncoming (line, constructedTypeId, sourceId, "newobj"); + RejectUnexpectedIncoming (line, genericDictionaryId, constructedTypeId, "reloc"); + if (IsIncomingLink (line, genericDictionaryDependencyId) && + !IsLink (line, genericDictionaryId, genericDictionaryDependencyId, "Primary") && + !IsLink (line, canonicalConstructorId, genericDictionaryDependencyId, "Secondary")) { + unexpectedIncomingLinks.Add (line.Trim ()); + } + RejectUnexpectedIncoming ( + line, + constructorId, + genericDictionaryDependencyId, + "Generic dictionary dependency"); } public void AssertComplete () @@ -263,11 +298,26 @@ public void AssertComplete () Assert.IsNotEmpty (constructedTypeId, $"{Name} IJavaPeerable constructed-type node was not found."); Assert.IsNotEmpty (genericDictionaryId, $"{Name} IJavaPeerable generic dictionary node was not found."); Assert.IsNotEmpty (genericDictionaryDependencyId, $"{Name} IJavaPeerable constructor dictionary dependency was not found."); + Assert.IsNotEmpty (canonicalConstructorId, $"{Name} canonical compiled constructor node was not found."); Assert.IsNotEmpty (constructorId, $"{Name} IJavaPeerable activation constructor node was not found."); Assert.IsTrue (sourceToConstructedType, $"{Name} SafeJavaCollectionFactory newobj dependency was not found."); Assert.IsTrue (constructedTypeToGenericDictionary, $"{Name} constructed-type relocation dependency was not found."); Assert.IsTrue (genericDictionaryToDependency, $"{Name} generic dictionary primary dependency was not found."); + Assert.IsTrue (canonicalConstructorToDependency, $"{Name} canonical constructor secondary dependency was not found."); Assert.IsTrue (genericDictionaryToConstructor, $"{Name} generic dictionary constructor dependency was not found."); + Assert.IsEmpty (unexpectedIncomingLinks, $"{Name} canonical constructor path had an unexpected incoming dependency."); + } + + void RejectUnexpectedIncoming (string line, string target, string expectedSource, string expectedReason) + { + if (IsIncomingLink (line, target) && !IsLink (line, expectedSource, target, expectedReason)) { + unexpectedIncomingLinks.Add (line.Trim ()); + } + } + + static bool IsIncomingLink (string line, string target) + { + return target.Length > 0 && line.Contains ($"Target=\"{target}\"", StringComparison.Ordinal); } static bool IsLink (string line, string source, string target, string reason) From efdb449be4a03961ef1b1e4e97e3603d952ac9ae Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 1 Sep 2026 12:35:53 +0200 Subject: [PATCH 5/9] [tests] Fix dictionary rooting graph selector Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs index 2adeb70421c..243119b874b 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs @@ -130,7 +130,7 @@ static void AssertCanonicalWrapperRooting (string dgmlFile) "JavaCollection`1<Java.Interop.IJavaPeerable>..ctor(native int,JniHandleOwnership)"), new RootingChain ( "JavaDictionary", - "Label=\"Mono_Android_Java_Interop_SafeJavaCollectionFactory__CreateReferenceDictionaryFromJniHandle\"", + "SafeJavaCollectionFactory__CreateReferenceDictionaryFromJniHandle, Type metadata: [Java.Interop]Java.Interop.IJavaPeerable)", "Android_Runtime_JavaDictionary_2<Java_Interop_Java_Interop_IJavaPeerable__Java_Interop_Java_Interop_IJavaPeerable> constructed\"", "Label=\"__GenericDict_Mono_Android_Android_Runtime_JavaDictionary_2<Java_Interop_Java_Interop_IJavaPeerable__Java_Interop_Java_Interop_IJavaPeerable>\"", "(__GenericDict_Mono_Android_Android_Runtime_JavaDictionary_2<Java_Interop_Java_Interop_IJavaPeerable__" + From cabe4e9529e3c6167c3d1855fbcf1c6315b869a5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 1 Sep 2026 12:52:43 +0200 Subject: [PATCH 6/9] [tests] Improve interface collection diagnostics Dispose JNI class references if a later lookup fails, and include peer types and handles in identity assertion failures. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../InterfaceCollectionApp/MainActivity.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs index e3dbf34ce80..8fd7585a70b 100644 --- a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs @@ -411,13 +411,17 @@ static bool ContainsRawPair ( static void VisitDictionaryEntries (object dictionary, Action visitor) { - var mapClass = JniEnvironment.Types.FindClass ("java/util/Map"); - var setClass = JniEnvironment.Types.FindClass ("java/util/Set"); - var iteratorClass = JniEnvironment.Types.FindClass ("java/util/Iterator"); - var entryClass = JniEnvironment.Types.FindClass ("java/util/Map$Entry"); + JniObjectReference mapClass = default; + JniObjectReference setClass = default; + JniObjectReference iteratorClass = default; + JniObjectReference entryClass = default; IntPtr entrySet = IntPtr.Zero; IntPtr iterator = IntPtr.Zero; try { + mapClass = JniEnvironment.Types.FindClass ("java/util/Map"); + setClass = JniEnvironment.Types.FindClass ("java/util/Set"); + iteratorClass = JniEnvironment.Types.FindClass ("java/util/Iterator"); + entryClass = JniEnvironment.Types.FindClass ("java/util/Map$Entry"); var entrySetMethod = JNIEnv.GetMethodID (mapClass.Handle, "entrySet", "()Ljava/util/Set;"); var iteratorMethod = JNIEnv.GetMethodID (setClass.Handle, "iterator", "()Ljava/util/Iterator;"); var hasNextMethod = JNIEnv.GetMethodID (iteratorClass.Handle, "hasNext", "()Z"); @@ -473,14 +477,20 @@ static void AssertSameJavaObject (object expected, object actual) { var expectedPeer = (IJavaObject) expected; var actualPeer = (IJavaObject) actual; - AssertTrue (JNIEnv.IsSameObject (expectedPeer.Handle, actualPeer.Handle), "expected identical Java peers"); + AssertTrue ( + JNIEnv.IsSameObject (expectedPeer.Handle, actualPeer.Handle), + $"expected identical Java peers; expected '{expected.GetType ()}' at '{expectedPeer.Handle}', " + + $"actual '{actual.GetType ()}' at '{actualPeer.Handle}'"); } static void AssertDistinctJavaObjects (object first, object second) { var firstPeer = (IJavaObject) first; var secondPeer = (IJavaObject) second; - AssertFalse (JNIEnv.IsSameObject (firstPeer.Handle, secondPeer.Handle), "expected distinct Java peers"); + AssertFalse ( + JNIEnv.IsSameObject (firstPeer.Handle, secondPeer.Handle), + $"expected distinct Java peers; first '{first.GetType ()}' at '{firstPeer.Handle}', " + + $"second '{second.GetType ()}' at '{secondPeer.Handle}'"); } static void AssertTrue (bool value, string message) From 9dce4491c037af8c201913f7a003dcfd6f60b974 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 1 Sep 2026 13:20:41 +0200 Subject: [PATCH 7/9] [tests] Harden NativeAOT rooting graph checks Parse DGML semantically in two passes so node/link order and XML formatting do not affect the result. Require every selected rooting node to be unique while preserving the complete canonical dependency and incoming-edge assertions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tests/InterfaceCollectionTests.cs | 252 +++++++++++------- 1 file changed, 163 insertions(+), 89 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs index 243119b874b..20f3ffbac09 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Xml; using NUnit.Framework; @@ -112,46 +113,66 @@ static void AssertCanonicalWrapperRooting (string dgmlFile) var chains = new [] { new RootingChain ( "JavaList", - "SafeJavaCollectionFactory__CreateReferenceListFromJniHandle, Type metadata: [Java.Interop]Java.Interop.IJavaPeerable)", - "Android_Runtime_JavaList_1<Java_Interop_Java_Interop_IJavaPeerable> constructed\"", - "Label=\"__GenericDict_Mono_Android_Android_Runtime_JavaList_1<Java_Interop_Java_Interop_IJavaPeerable>\"", - "(__GenericDict_Mono_Android_Android_Runtime_JavaList_1<Java_Interop_Java_Interop_IJavaPeerable>, " + - "Mono_Android_Android_Runtime_JavaList_1<System___Canon>___ctor_0)", - "Label=\"Mono_Android_Android_Runtime_JavaList_1<System___Canon>___ctor_0\"", - "JavaList`1<Java.Interop.IJavaPeerable>..ctor(native int,JniHandleOwnership)"), + "SafeJavaCollectionFactory__CreateReferenceListFromJniHandle, " + + "Type metadata: [Java.Interop]Java.Interop.IJavaPeerable)", + "Mono_Android_Android_Runtime_JavaList_1 constructed", + "__GenericDict_Mono_Android_Android_Runtime_JavaList_1", + "(__GenericDict_Mono_Android_Android_Runtime_JavaList_1, " + + "Mono_Android_Android_Runtime_JavaList_1___ctor_0)", + "Mono_Android_Android_Runtime_JavaList_1___ctor_0", + "JavaList`1..ctor(native int,JniHandleOwnership)"), new RootingChain ( "JavaCollection", - "SafeJavaCollectionFactory__CreateReferenceCollectionFromJniHandle, Type metadata: [Java.Interop]Java.Interop.IJavaPeerable)", - "Android_Runtime_JavaCollection_1<Java_Interop_Java_Interop_IJavaPeerable> constructed\"", - "Label=\"__GenericDict_Mono_Android_Android_Runtime_JavaCollection_1<Java_Interop_Java_Interop_IJavaPeerable>\"", - "(__GenericDict_Mono_Android_Android_Runtime_JavaCollection_1<Java_Interop_Java_Interop_IJavaPeerable>, " + - "Mono_Android_Android_Runtime_JavaCollection_1<System___Canon>___ctor)", - "Label=\"Mono_Android_Android_Runtime_JavaCollection_1<System___Canon>___ctor\"", - "JavaCollection`1<Java.Interop.IJavaPeerable>..ctor(native int,JniHandleOwnership)"), + "SafeJavaCollectionFactory__CreateReferenceCollectionFromJniHandle, " + + "Type metadata: [Java.Interop]Java.Interop.IJavaPeerable)", + "Mono_Android_Android_Runtime_JavaCollection_1 constructed", + "__GenericDict_Mono_Android_Android_Runtime_JavaCollection_1", + "(__GenericDict_Mono_Android_Android_Runtime_JavaCollection_1, " + + "Mono_Android_Android_Runtime_JavaCollection_1___ctor)", + "Mono_Android_Android_Runtime_JavaCollection_1___ctor", + "JavaCollection`1..ctor(native int,JniHandleOwnership)"), new RootingChain ( "JavaDictionary", - "SafeJavaCollectionFactory__CreateReferenceDictionaryFromJniHandle, Type metadata: [Java.Interop]Java.Interop.IJavaPeerable)", - "Android_Runtime_JavaDictionary_2<Java_Interop_Java_Interop_IJavaPeerable__Java_Interop_Java_Interop_IJavaPeerable> constructed\"", - "Label=\"__GenericDict_Mono_Android_Android_Runtime_JavaDictionary_2<Java_Interop_Java_Interop_IJavaPeerable__Java_Interop_Java_Interop_IJavaPeerable>\"", - "(__GenericDict_Mono_Android_Android_Runtime_JavaDictionary_2<Java_Interop_Java_Interop_IJavaPeerable__" + - "Java_Interop_Java_Interop_IJavaPeerable>, " + - "Mono_Android_Android_Runtime_JavaDictionary_2<System___Canon__System___Canon>___ctor_0)", - "Label=\"Mono_Android_Android_Runtime_JavaDictionary_2<System___Canon__System___Canon>___ctor_0\"", - "JavaDictionary`2<Java.Interop.IJavaPeerable,Java.Interop.IJavaPeerable>..ctor(native int,JniHandleOwnership)"), + "SafeJavaCollectionFactory__CreateReferenceDictionaryFromJniHandle, " + + "Type metadata: [Java.Interop]Java.Interop.IJavaPeerable)", + "Mono_Android_Android_Runtime_JavaDictionary_2 constructed", + "__GenericDict_Mono_Android_Android_Runtime_JavaDictionary_2", + "(__GenericDict_Mono_Android_Android_Runtime_JavaDictionary_2, " + + "Mono_Android_Android_Runtime_JavaDictionary_2___ctor_0)", + "Mono_Android_Android_Runtime_JavaDictionary_2___ctor_0", + "JavaDictionary`2..ctor(native int,JniHandleOwnership)"), }; var unexpectedCanonicalRoots = new List (); - foreach (var line in File.ReadLines (dgmlFile)) { - if (line.Contains ("___ctor_0", StringComparison.Ordinal) || + label.Contains ("JavaCollection_1___ctor", StringComparison.Ordinal) || + label.Contains ("JavaDictionary_2___ctor_0", StringComparison.Ordinal); if (!usesReferenceCanonicalCode) { return false; } bool isExpectedRoot = - line.Contains ("JavaList`1<Java.Interop.IJavaPeerable>..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal) || - line.Contains ("JavaList`1<System.__Canon>..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal) || - line.Contains ("JavaCollection`1<Java.Interop.IJavaPeerable>..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal) || - line.Contains ("JavaCollection`1<System.__Canon>..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal) || - line.Contains ( - "JavaDictionary`2<Java.Interop.IJavaPeerable,Java.Interop.IJavaPeerable>..ctor(native int,JniHandleOwnership)", + label.Contains ("JavaList`1..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal) || + label.Contains ("JavaList`1..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal) || + label.Contains ("JavaCollection`1..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal) || + label.Contains ("JavaCollection`1..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal) || + label.Contains ( + "JavaDictionary`2..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal) || - line.Contains ( - "JavaDictionary`2<System.__Canon,System.__Canon>..ctor(native int,JniHandleOwnership)", + label.Contains ( + "JavaDictionary`2..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal); return !isExpectedRoot; } @@ -211,6 +240,7 @@ sealed class RootingChain readonly string genericDictionaryPattern; readonly string genericDictionaryDependencyPattern; readonly string sourcePattern; + readonly List ambiguousNodeMatches = new (); readonly List unexpectedIncomingLinks = new (); string canonicalConstructorId = ""; @@ -245,48 +275,73 @@ public RootingChain ( public string Name { get; } - public void ObserveNode (string line) + public void ObserveNode (string id, string label) { - if (line.Contains (sourcePattern, StringComparison.Ordinal)) { - sourceId = GetAttribute (line, "Id"); - } else if (line.Contains (constructedTypePattern, StringComparison.Ordinal)) { - constructedTypeId = GetAttribute (line, "Id"); - } else if (line.Contains (genericDictionaryPattern, StringComparison.Ordinal)) { - genericDictionaryId = GetAttribute (line, "Id"); - } else if (line.Contains (genericDictionaryDependencyPattern, StringComparison.Ordinal)) { - genericDictionaryDependencyId = GetAttribute (line, "Id"); - } else if (line.Contains (canonicalConstructorPattern, StringComparison.Ordinal)) { - canonicalConstructorId = GetAttribute (line, "Id"); - } else if (line.Contains (constructorPattern, StringComparison.Ordinal)) { - constructorId = GetAttribute (line, "Id"); - } + ObserveNode ( + label.EndsWith (sourcePattern, StringComparison.Ordinal), + id, + label, + "SafeJavaCollectionFactory source", + ref sourceId); + ObserveNode ( + label.EndsWith (constructedTypePattern, StringComparison.Ordinal), + id, + label, + "IJavaPeerable constructed type", + ref constructedTypeId); + ObserveNode (label == genericDictionaryPattern, id, label, "IJavaPeerable generic dictionary", ref genericDictionaryId); + ObserveNode ( + label == genericDictionaryDependencyPattern, + id, + label, + "IJavaPeerable constructor dictionary dependency", + ref genericDictionaryDependencyId); + ObserveNode (label == canonicalConstructorPattern, id, label, "canonical compiled constructor", ref canonicalConstructorId); + ObserveNode ( + label.Contains (constructorPattern, StringComparison.Ordinal), + id, + label, + "IJavaPeerable activation constructor", + ref constructorId); } - public void ObserveLink (string line) + public void ObserveLink (string source, string target, string reason) { - sourceToConstructedType |= IsLink (line, sourceId, constructedTypeId, "newobj"); - constructedTypeToGenericDictionary |= IsLink (line, constructedTypeId, genericDictionaryId, "reloc"); - genericDictionaryToDependency |= IsLink (line, genericDictionaryId, genericDictionaryDependencyId, "Primary"); + sourceToConstructedType |= IsLink (source, target, reason, sourceId, constructedTypeId, "newobj"); + constructedTypeToGenericDictionary |= IsLink (source, target, reason, constructedTypeId, genericDictionaryId, "reloc"); + genericDictionaryToDependency |= IsLink ( + source, + target, + reason, + genericDictionaryId, + genericDictionaryDependencyId, + "Primary"); canonicalConstructorToDependency |= IsLink ( - line, + source, + target, + reason, canonicalConstructorId, genericDictionaryDependencyId, "Secondary"); genericDictionaryToConstructor |= IsLink ( - line, + source, + target, + reason, genericDictionaryDependencyId, constructorId, "Generic dictionary dependency"); - RejectUnexpectedIncoming (line, constructedTypeId, sourceId, "newobj"); - RejectUnexpectedIncoming (line, genericDictionaryId, constructedTypeId, "reloc"); - if (IsIncomingLink (line, genericDictionaryDependencyId) && - !IsLink (line, genericDictionaryId, genericDictionaryDependencyId, "Primary") && - !IsLink (line, canonicalConstructorId, genericDictionaryDependencyId, "Secondary")) { - unexpectedIncomingLinks.Add (line.Trim ()); + RejectUnexpectedIncoming (source, target, reason, constructedTypeId, sourceId, "newobj"); + RejectUnexpectedIncoming (source, target, reason, genericDictionaryId, constructedTypeId, "reloc"); + if (IsIncomingLink (target, genericDictionaryDependencyId) && + !IsLink (source, target, reason, genericDictionaryId, genericDictionaryDependencyId, "Primary") && + !IsLink (source, target, reason, canonicalConstructorId, genericDictionaryDependencyId, "Secondary")) { + unexpectedIncomingLinks.Add (FormatLink (source, target, reason)); } RejectUnexpectedIncoming ( - line, + source, + target, + reason, constructorId, genericDictionaryDependencyId, "Generic dictionary dependency"); @@ -294,6 +349,7 @@ public void ObserveLink (string line) public void AssertComplete () { + Assert.IsEmpty (ambiguousNodeMatches, $"{Name} canonical constructor path had ambiguous node matches."); Assert.IsNotEmpty (sourceId, $"{Name} SafeJavaCollectionFactory source node was not found."); Assert.IsNotEmpty (constructedTypeId, $"{Name} IJavaPeerable constructed-type node was not found."); Assert.IsNotEmpty (genericDictionaryId, $"{Name} IJavaPeerable generic dictionary node was not found."); @@ -308,37 +364,55 @@ public void AssertComplete () Assert.IsEmpty (unexpectedIncomingLinks, $"{Name} canonical constructor path had an unexpected incoming dependency."); } - void RejectUnexpectedIncoming (string line, string target, string expectedSource, string expectedReason) + void ObserveNode (bool matches, string id, string label, string role, ref string observedId) + { + if (!matches) { + return; + } + if (observedId.Length > 0) { + ambiguousNodeMatches.Add ($"{role}: Id=\"{id}\" Label=\"{label}\""); + return; + } + observedId = id; + } + + void RejectUnexpectedIncoming ( + string source, + string target, + string reason, + string expectedTarget, + string expectedSource, + string expectedReason) { - if (IsIncomingLink (line, target) && !IsLink (line, expectedSource, target, expectedReason)) { - unexpectedIncomingLinks.Add (line.Trim ()); + if (IsIncomingLink (target, expectedTarget) && + !IsLink (source, target, reason, expectedSource, expectedTarget, expectedReason)) { + unexpectedIncomingLinks.Add (FormatLink (source, target, reason)); } } - static bool IsIncomingLink (string line, string target) + static bool IsIncomingLink (string actualTarget, string expectedTarget) { - return target.Length > 0 && line.Contains ($"Target=\"{target}\"", StringComparison.Ordinal); + return expectedTarget.Length > 0 && actualTarget == expectedTarget; } - static bool IsLink (string line, string source, string target, string reason) + static bool IsLink ( + string actualSource, + string actualTarget, + string actualReason, + string expectedSource, + string expectedTarget, + string expectedReason) { - return source.Length > 0 && - target.Length > 0 && - line.Contains ($"Source=\"{source}\"", StringComparison.Ordinal) && - line.Contains ($"Target=\"{target}\"", StringComparison.Ordinal) && - line.Contains ($"Reason=\"{reason}\"", StringComparison.Ordinal); + return expectedSource.Length > 0 && + expectedTarget.Length > 0 && + actualSource == expectedSource && + actualTarget == expectedTarget && + actualReason == expectedReason; } - static string GetAttribute (string line, string name) + static string FormatLink (string source, string target, string reason) { - var prefix = $"{name}=\""; - int start = line.IndexOf (prefix, StringComparison.Ordinal); - if (start < 0) { - return ""; - } - start += prefix.Length; - int end = line.IndexOf ('"', start); - return end < 0 ? "" : line.Substring (start, end - start); + return $"Source=\"{source}\" Target=\"{target}\" Reason=\"{reason}\""; } } } From 6833c8244a4f3113381702c7fe022d0238591bbc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 1 Sep 2026 13:34:09 +0200 Subject: [PATCH 8/9] [tests] Reject ambiguous NativeAOT graph input Validate DGML namespaces, node identities, and exact compiler symbol shapes so malformed or decorated nodes cannot satisfy the rooting chain. Add a per-run result token so stale logcat output cannot pass a retried fixture when log clearing fails. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../InterfaceCollectionApp/MainActivity.cs | 5 +- .../Tests/InterfaceCollectionTests.cs | 123 +++++++++++++----- 2 files changed, 93 insertions(+), 35 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs index 8fd7585a70b..db1dce0915b 100644 --- a/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs +++ b/tests/MSBuildDeviceIntegration/Resources/InterfaceCollectionApp/MainActivity.cs @@ -18,6 +18,7 @@ namespace ${ROOT_NAMESPACE} public class MainActivity : Activity { const string ResultPrefix = "INTERFACE_COLLECTION_RESULT"; + const string ResultToken = "${RESULT_TOKEN}"; const string Tag = "InterfaceCollections"; protected override void OnCreate (Bundle savedInstanceState) @@ -38,9 +39,9 @@ protected override void OnCreate (Bundle savedInstanceState) passed++; JavaDictionary_InterfaceKeysAndValuesPreserveIdentityAndRoundTrip (); passed++; - Log.Info (Tag, $"{ResultPrefix} PASS {passed}/6"); + Log.Info (Tag, $"{ResultPrefix} PASS {passed}/6 {ResultToken}"); } catch (Exception e) { - Log.Error (Tag, $"{ResultPrefix} FAIL {passed}/6: {e}"); + Log.Error (Tag, $"{ResultPrefix} FAIL {passed}/6 {ResultToken}: {e}"); } finally { Finish (); } diff --git a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs index 20f3ffbac09..ca57b97bf8a 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs @@ -16,6 +16,7 @@ namespace Xamarin.Android.Build.Tests [Category ("UsesDevice")] public class InterfaceCollectionTests : DeviceTest { + const string DgmlNamespace = "http://schemas.microsoft.com/vs/2009/dgml"; const string ResultPrefix = "INTERFACE_COLLECTION_RESULT"; [TestCase ("llvm-ir", AndroidRuntime.CoreCLR)] @@ -36,7 +37,9 @@ public void InterfaceValuedJavaCollections (string typemapImplementation, Androi proj.SetProperty ("JavaCPath", Path.Combine (javaSdkDirectory, "bin", "javac")); proj.SetProperty ("JarPath", Path.Combine (javaSdkDirectory, "bin", "jar")); proj.SetDefaultTargetDevice (); - proj.MainActivity = proj.ProcessSourceTemplate (ReadFixture ("MainActivity.cs")); + var resultToken = Guid.NewGuid ().ToString ("N"); + proj.MainActivity = proj.ProcessSourceTemplate ( + ReadFixture ("MainActivity.cs").Replace ("${RESULT_TOKEN}", resultToken, StringComparison.Ordinal)); proj.AndroidJavaSources.Add (CreateJavaSource ("ValueProvider.java", bind: true)); proj.AndroidJavaSources.Add (CreateJavaSource ("ExtendedValueProvider.java", bind: true)); proj.AndroidJavaSources.Add (CreateJavaSource ("InterfaceCollectionFixture.java", bind: false)); @@ -57,7 +60,7 @@ public void InterfaceValuedJavaCollections (string typemapImplementation, Androi string resultLine = ""; WaitFor (TimeSpan.FromSeconds (60), () => { logcatOutput = RunAdbCommand ("logcat -d"); - resultLine = FindResultLine (logcatOutput); + resultLine = FindResultLine (logcatOutput, resultToken); return resultLine.Length > 0; }, intervalInMS: 250); File.WriteAllText (logcatPath, logcatOutput); @@ -76,10 +79,11 @@ public void InterfaceValuedJavaCollections (string typemapImplementation, Androi } } - static string FindResultLine (string logcatOutput) + static string FindResultLine (string logcatOutput, string resultToken) { foreach (var line in logcatOutput.Split (['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)) { - if (line.Contains (ResultPrefix, StringComparison.Ordinal)) { + if (line.Contains (ResultPrefix, StringComparison.Ordinal) && + line.Contains (resultToken, StringComparison.Ordinal)) { return line; } } @@ -145,15 +149,25 @@ static void AssertCanonicalWrapperRooting (string dgmlFile) "Mono_Android_Android_Runtime_JavaDictionary_2___ctor_0", "JavaDictionary`2..ctor(native int,JniHandleOwnership)"), }; + var duplicateNodeIds = new List (); + var missingNodeIds = new List (); + var nodeIds = new HashSet (StringComparer.Ordinal); var unexpectedCanonicalRoots = new List (); using (var reader = CreateDgmlReader (dgmlFile)) { while (reader.Read ()) { - if (reader.NodeType != XmlNodeType.Element || reader.LocalName != "Node") { + if (reader.NodeType != XmlNodeType.Element || + reader.LocalName != "Node" || + reader.NamespaceURI != DgmlNamespace) { continue; } var id = reader.GetAttribute ("Id") ?? ""; var label = reader.GetAttribute ("Label") ?? ""; + if (id.Length == 0) { + missingNodeIds.Add (label); + } else if (!nodeIds.Add (id)) { + duplicateNodeIds.Add ($"Id=\"{id}\" Label=\"{label}\""); + } foreach (var chain in chains) { chain.ObserveNode (id, label); } @@ -165,7 +179,9 @@ static void AssertCanonicalWrapperRooting (string dgmlFile) using (var reader = CreateDgmlReader (dgmlFile)) { while (reader.Read ()) { - if (reader.NodeType != XmlNodeType.Element || reader.LocalName != "Link") { + if (reader.NodeType != XmlNodeType.Element || + reader.LocalName != "Link" || + reader.NamespaceURI != DgmlNamespace) { continue; } var source = reader.GetAttribute ("Source") ?? ""; @@ -177,6 +193,8 @@ static void AssertCanonicalWrapperRooting (string dgmlFile) } } + Assert.IsEmpty (missingNodeIds, "The NativeAOT dependency graph contained nodes without IDs."); + Assert.IsEmpty (duplicateNodeIds, "The NativeAOT dependency graph contained duplicate node IDs."); Assert.IsEmpty ( unexpectedCanonicalRoots, "Only SafeJavaCollectionFactory's IJavaPeerable instantiations should root the reference-wrapper canonical constructors."); @@ -207,16 +225,20 @@ static bool IsUnexpectedCanonicalReferenceConstructor (string label) return false; } bool isExpectedRoot = - label.Contains ("JavaList`1..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal) || - label.Contains ("JavaList`1..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal) || - label.Contains ("JavaCollection`1..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal) || - label.Contains ("JavaCollection`1..ctor(native int,JniHandleOwnership)", StringComparison.Ordinal) || - label.Contains ( - "JavaDictionary`2..ctor(native int,JniHandleOwnership)", - StringComparison.Ordinal) || - label.Contains ( - "JavaDictionary`2..ctor(native int,JniHandleOwnership)", - StringComparison.Ordinal); + label == "[Mono.Android]Android.Runtime.JavaList`1..ctor(native int,JniHandleOwnership) " + + "backed by Mono_Android_Android_Runtime_JavaList_1___ctor_0" || + label == "[Mono.Android]Android.Runtime.JavaList`1..ctor(native int,JniHandleOwnership) " + + "backed by Mono_Android_Android_Runtime_JavaList_1___ctor_0" || + label == "[Mono.Android]Android.Runtime.JavaCollection`1..ctor(native int,JniHandleOwnership) " + + "backed by Mono_Android_Android_Runtime_JavaCollection_1___ctor" || + label == "[Mono.Android]Android.Runtime.JavaCollection`1..ctor(native int,JniHandleOwnership) " + + "backed by Mono_Android_Android_Runtime_JavaCollection_1___ctor" || + label == "[Mono.Android]Android.Runtime.JavaDictionary`2" + + "..ctor(native int,JniHandleOwnership) backed by " + + "Mono_Android_Android_Runtime_JavaDictionary_2___ctor_0" || + label == "[Mono.Android]Android.Runtime.JavaDictionary`2" + + "..ctor(native int,JniHandleOwnership) backed by " + + "Mono_Android_Android_Runtime_JavaDictionary_2___ctor_0"; return !isExpectedRoot; } @@ -241,6 +263,7 @@ sealed class RootingChain readonly string genericDictionaryDependencyPattern; readonly string sourcePattern; readonly List ambiguousNodeMatches = new (); + readonly HashSet observedNodeRoles = new (StringComparer.Ordinal); readonly List unexpectedIncomingLinks = new (); string canonicalConstructorId = ""; @@ -277,32 +300,46 @@ public RootingChain ( public void ObserveNode (string id, string label) { - ObserveNode ( - label.EndsWith (sourcePattern, StringComparison.Ordinal), + int matchedRoles = 0; + matchedRoles += ObserveNode ( + label == $"(Mono_Android_Java_Interop_{sourcePattern}", id, label, "SafeJavaCollectionFactory source", - ref sourceId); - ObserveNode ( - label.EndsWith (constructedTypePattern, StringComparison.Ordinal), + ref sourceId) ? 1 : 0; + matchedRoles += ObserveNode ( + IsConstructedTypeLabel (label, constructedTypePattern), id, label, "IJavaPeerable constructed type", - ref constructedTypeId); - ObserveNode (label == genericDictionaryPattern, id, label, "IJavaPeerable generic dictionary", ref genericDictionaryId); - ObserveNode ( + ref constructedTypeId) ? 1 : 0; + matchedRoles += ObserveNode ( + label == genericDictionaryPattern, + id, + label, + "IJavaPeerable generic dictionary", + ref genericDictionaryId) ? 1 : 0; + matchedRoles += ObserveNode ( label == genericDictionaryDependencyPattern, id, label, "IJavaPeerable constructor dictionary dependency", - ref genericDictionaryDependencyId); - ObserveNode (label == canonicalConstructorPattern, id, label, "canonical compiled constructor", ref canonicalConstructorId); - ObserveNode ( - label.Contains (constructorPattern, StringComparison.Ordinal), + ref genericDictionaryDependencyId) ? 1 : 0; + matchedRoles += ObserveNode ( + label == canonicalConstructorPattern, + id, + label, + "canonical compiled constructor", + ref canonicalConstructorId) ? 1 : 0; + matchedRoles += ObserveNode ( + label == $"[Mono.Android]Android.Runtime.{constructorPattern} backed by {canonicalConstructorPattern}", id, label, "IJavaPeerable activation constructor", - ref constructorId); + ref constructorId) ? 1 : 0; + if (matchedRoles > 1) { + ambiguousNodeMatches.Add ($"multiple roles: Id=\"{id}\" Label=\"{label}\""); + } } public void ObserveLink (string source, string target, string reason) @@ -364,16 +401,17 @@ public void AssertComplete () Assert.IsEmpty (unexpectedIncomingLinks, $"{Name} canonical constructor path had an unexpected incoming dependency."); } - void ObserveNode (bool matches, string id, string label, string role, ref string observedId) + bool ObserveNode (bool matches, string id, string label, string role, ref string observedId) { if (!matches) { - return; + return false; } - if (observedId.Length > 0) { + if (!observedNodeRoles.Add (role)) { ambiguousNodeMatches.Add ($"{role}: Id=\"{id}\" Label=\"{label}\""); - return; + return true; } observedId = id; + return true; } void RejectUnexpectedIncoming ( @@ -414,6 +452,25 @@ static string FormatLink (string source, string target, string reason) { return $"Source=\"{source}\" Target=\"{target}\" Reason=\"{reason}\""; } + + static bool IsConstructedTypeLabel (string label, string constructedTypePattern) + { + if (!label.EndsWith (constructedTypePattern, StringComparison.Ordinal)) { + return false; + } + + int prefixLength = label.Length - constructedTypePattern.Length; + if (prefixLength <= "_ZTV".Length || + !label.StartsWith ("_ZTV", StringComparison.Ordinal)) { + return false; + } + for (int i = "_ZTV".Length; i < prefixLength; i++) { + if (label [i] < '0' || label [i] > '9') { + return false; + } + } + return true; + } } } } From 73dc25308aa98d3d6e94f274cd037ad30f26787b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 1 Sep 2026 14:21:49 +0200 Subject: [PATCH 9/9] [tests] Use resilient device result timeout Use DeviceTest.ActivityStartTimeoutInSeconds while waiting for the app result so slow CI emulator launches do not fail the focused NativeAOT case prematurely. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs index ca57b97bf8a..b9e679909c8 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InterfaceCollectionTests.cs @@ -58,7 +58,7 @@ public void InterfaceValuedJavaCollections (string typemapImplementation, Androi StartActivityAndAssert (proj); string logcatOutput = ""; string resultLine = ""; - WaitFor (TimeSpan.FromSeconds (60), () => { + WaitFor (TimeSpan.FromSeconds (ActivityStartTimeoutInSeconds), () => { logcatOutput = RunAdbCommand ("logcat -d"); resultLine = FindResultLine (logcatOutput, resultToken); return resultLine.Length > 0;