From 145de9cd08457c4894e43017fba3190a82f92568 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 19 Mar 2026 18:53:35 +0100 Subject: [PATCH 1/2] Bump emulator to 36.4.10 and add diagnostics for GetObjectArray #10973 Bump emulator from 32.1.9 to 36.4.10 to reproduce the CoreCLR JnienvArrayMarshaling.GetObjectArray failure reported in #10973. Add comprehensive diagnostic output to the test: - Log JNI class names, array lengths, and raw element handles - Collect all diagnostic info before asserting so the full picture is visible in CI test results even when assertions fail - Include diagnostics in assertion messages (NUnit XML output) - Use TestContext.Out.WriteLine for NUnit output capture - Improve AssertArrays to show element index, types, and values Local testing on arm64 macOS (emulator 36.4.10) passes on both API 29 and API 36 in Debug and Release. The CI failure may be x86_64-specific. Fixes https://github.com/dotnet/android/issues/10973 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Configuration.props | 4 +- .../Android.Runtime/JnienvArrayMarshaling.cs | 92 +++++++++++++++++-- 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/Configuration.props b/Configuration.props index 4239731a034..6196c65c032 100644 --- a/Configuration.props +++ b/Configuration.props @@ -166,8 +166,8 @@ 13114758_latest $(AndroidSdkFullPath)\cmdline-tools\$(CommandLineToolsFolder)\bin - 9364964 - 32.1.9 + 15004761 + 36.4.10 $(AndroidSdkFullPath)\emulator emulator emulator.exe diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Runtime/JnienvArrayMarshaling.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Runtime/JnienvArrayMarshaling.cs index 6683807633e..fdfa92d142a 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Runtime/JnienvArrayMarshaling.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Runtime/JnienvArrayMarshaling.cs @@ -267,22 +267,96 @@ public void SetArrayItem_JavaLangString () public void GetObjectArray () { using (var byteArray = new Java.Lang.Object (JNIEnv.NewArray (new byte[]{1,2,3}), JniHandleOwnership.TransferLocalRef)) { + string jniClassName = JNIEnv.GetClassNameFromInstance (byteArray.Handle); + int jniArrayLength = JNIEnv.GetArrayLength (byteArray.Handle); + Log ($"GetObjectArray byte[]: JNI class='{jniClassName}', length={jniArrayLength}"); + object[] data = JNIEnv.GetObjectArray (byteArray.Handle, new[]{typeof (byte), typeof (byte), typeof (byte)}); + Log ($"GetObjectArray byte[]: data.Length={data?.Length}"); + if (data != null) { + for (int i = 0; i < data.Length; i++) + Log ($"GetObjectArray byte[]: data[{i}] = {data [i]} (type: {data [i]?.GetType ()})"); + } AssertArrays ("GetObjectArray", data, (object) 1, (object) 2, (object) 3); } + + // Collect all diagnostics before asserting so we can see the full picture in CI + var diagnostics = new System.Text.StringBuilder (); + object[]? values = null; + Exception? marshalingException = null; + using (var objectArray = new Java.Lang.Object ( JNIEnv.NewArray ( new Java.Lang.Object[]{Application.Context, 42L, "string"}, typeof (Java.Lang.Object)), JniHandleOwnership.TransferLocalRef)) { - object[] values = JNIEnv.GetObjectArray (objectArray.Handle, new[]{typeof(Context), typeof (int)}); - Assert.AreEqual (3, values.Length); - Assert.IsTrue (object.ReferenceEquals (values [0], Application.Context)); - Assert.IsTrue (values [1] is int); - Assert.AreEqual (42, (int)values [1]); - Assert.AreEqual ("string", values [2].ToString ()); + string jniClassName = JNIEnv.GetClassNameFromInstance (objectArray.Handle); + int jniArrayLength = JNIEnv.GetArrayLength (objectArray.Handle); + diagnostics.AppendLine ($"mixed[]: JNI class='{jniClassName}', length={jniArrayLength}"); + + for (int i = 0; i < jniArrayLength; i++) { + IntPtr elemHandle = JNIEnv.GetObjectArrayElement (objectArray.Handle, i); + string elemClass = elemHandle != IntPtr.Zero ? JNIEnv.GetClassNameFromInstance (elemHandle) : "(null)"; + diagnostics.AppendLine ($" raw[{i}] JNI class='{elemClass}', handle=0x{elemHandle:x}"); + JNIEnv.DeleteLocalRef (elemHandle); + } + + try { + values = JNIEnv.GetObjectArray (objectArray.Handle, new[]{typeof(Context), typeof (int)}); + } catch (Exception ex) { + marshalingException = ex; + } } + + if (marshalingException != null) { + diagnostics.AppendLine ($"GetObjectArray THREW: {marshalingException}"); + Log (diagnostics.ToString ()); + Assert.Fail ($"GetObjectArray threw: {marshalingException.Message}\n{diagnostics}"); + return; + } + + diagnostics.AppendLine ($"values.Length={values?.Length}"); + if (values != null) { + for (int i = 0; i < values.Length; i++) { + var v = values [i]; + diagnostics.AppendLine ($" values[{i}] = {v} (type: {v?.GetType ()}, IJavaPeerable: {v is Java.Interop.IJavaPeerable})"); + } + + if (values.Length >= 1) { + bool refEqual = object.ReferenceEquals (values [0], Application.Context); + diagnostics.AppendLine ($" ReferenceEquals(values[0], Context) = {refEqual}"); + diagnostics.AppendLine ($" values[0] type = {values [0]?.GetType ()}, hash = {values [0]?.GetHashCode ()}"); + diagnostics.AppendLine ($" Context type = {Application.Context?.GetType ()}, hash = {Application.Context?.GetHashCode ()}"); + } + if (values.Length >= 2) { + diagnostics.AppendLine ($" values[1] is int = {values [1] is int}"); + if (values [1] is Java.Interop.IJavaPeerable jp) + diagnostics.AppendLine ($" values[1] peer JNI type = {JNIEnv.GetClassNameFromInstance (jp.PeerReference.Handle)}"); + } + if (values.Length >= 3) { + diagnostics.AppendLine ($" values[2].ToString() = '{values [2]}'"); + } + } + + string diag = diagnostics.ToString (); + Log (diag); + + // Now assert with full diagnostic context + Assert.AreEqual (3, values!.Length, $"Expected 3 elements\n{diag}"); + Assert.IsTrue (object.ReferenceEquals (values [0], Application.Context), + $"values[0] should be ReferenceEquals to Application.Context\n{diag}"); + Assert.IsTrue (values [1] is int, + $"values[1] should be int\n{diag}"); + Assert.AreEqual (42, (int)values [1], + $"values[1] should be 42\n{diag}"); + Assert.AreEqual ("string", values [2].ToString (), + $"values[2] should be 'string'\n{diag}"); + } + + static void Log (string message) + { + Console.WriteLine (message); } [Test] @@ -354,9 +428,11 @@ public void BoundArrayPropertiesHaveSetters () static void AssertArrays (string message, IList actual, params T[] expected) { - Assert.AreEqual (expected.Length, actual.Count, message); + Assert.AreEqual (expected.Length, actual.Count, + $"{message}: expected length {expected.Length}, got {actual.Count}"); for (int i = 0; i < expected.Length; ++i) - Assert.AreEqual (expected [i], actual [i], message); + Assert.AreEqual (expected [i], actual [i], + $"{message}[{i}]: expected '{expected [i]}' ({expected [i]?.GetType ()}), got '{actual [i]}' ({actual [i]?.GetType ()})"); } } } From e59caea09b77ec9829fcc60f400e0f760f938131 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Mar 2026 09:20:15 +0100 Subject: [PATCH 2/2] Add CI troubleshooting hint to copilot-instructions.md Mention the azdo-build-investigator skill and the az CLI for investigating Azure DevOps pipeline failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index dc057a1e03e..6e462210d7f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -188,3 +188,4 @@ This pattern ensures proper encoding, timestamps, and file attributes are handle - **MSBuild:** Test in isolation, validate inputs - **Device:** Use update directories for rapid Debug iteration - **Performance:** See `../Documentation/guides/profiling.md` and `../Documentation/guides/tracing.md` +- **CI failures:** Use the `azdo-build-investigator` skill to investigate Azure DevOps pipeline failures. The `az` CLI tool is available and authenticated — use it to fetch build timelines, download logs, and analyze `.binlog` artifacts from failing CI runs. This applies to both the public (`dev.azure.com/dnceng-public`) and internal (`devdiv.visualstudio.com`) pipelines.