From f54a7bc5174f6e64bc61db69538a69c6deb66fa4 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 11 Dec 2019 20:15:58 -0500 Subject: [PATCH] [Java.Interop-Tests] Fix JniTypeTest.Name() missing argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: https://github.com/xamarin/xamarin-android/pull/3393 When running the `JniTypeTest.Name()` unit test within Xamarin.Android, sometimes the unit test runner process would crash. The crash is *more* likely in Release runtimes: F o.Android_Test: java_vm_ext.cc:570] JNI DETECTED ERROR IN APPLICATION: use of invalid jobject 0xc58fa798 F o.Android_Test: java_vm_ext.cc:570] from void crc64f295cabbf85394f5.TestSuiteInstrumentation.n_onStart() F o.Android_Test: runtime.cc:630] Runtime aborting... What was particularly odd about this crash was that after enabling both GREF and LREF logging, there was no handle 0xc58fa798! Where was this invalid handle coming from? Turns Outâ„¢, 0xc58fa798 isn't a handle. Instead, it's a parameter mismatch! Commit 9f380ebb had a bug. `JniTypeTest.Name()` was *attempting* to do: // Java java.lang.reflect.Method Object_hashCode = Object.class.getMethod("hashCode", new Class[0]); String Object_hashCode_returnType = Object_hashCode.getReturnType(); However, via JNI, it was *actually* doing: java.lang.reflect.Method Object_hashCode = Object.class.getMethod("hashCode", GARBAGE_VALUE); String Object_hashCode_returnType = Object_hashCode.getReturnType(); because it was only passing *one* value to `Class.getMethod()`, not two values, so the second parameter was entirely undefined and unknowable. Through sheer luck (or a terrible bug) this "worked" on a Desktop JVM and (frequently) in Debug configuration builds of the `Mono.Android_Tests-*.apk` test app in Xamarin.Android. On Android in a Release configuration, GARBAGE_VALUE was treated as a garbage value, and brought down the process because there *was*, in fact, a JNI error in the application. Fix `JniTypeTest.Name()`, and pass the correct number of parameters to `Class.getMethod(String, Class[])`. Additionally, make two (mostly unrelated) changes: * Add a `JniType.ToString()` override. * Change the exception ordering in `JavaExceptionTests.StackTrace(). While debugging the above crash, I attempted to use `JniType.ToString()` in some debug messages. These were useless, because there was no `JniType.ToString()` override. Add one. The conceptual problem `JavaExceptionTests.StackTrace()`, meanwhile, is that *eventually* `Java.Lang.Throwable` will be updated to inherit from `Java.Interop.JavaException`. If `JavaException` is caught first, then the `__ANDROID__`-specific behavior around `Java.Lang.Throwable` will no longer be tested, *and* the test will continue to pass (while testing subtly different behavior). Reorder the `catch` blocks so that when the `Java.Lang.Throwable` base class changes, the semantics of the test will be unaltered. --- src/Java.Interop/Java.Interop/JniType.cs | 5 +++++ .../Java.Interop/JavaExceptionTests.cs | 18 ++++++++++-------- .../Java.Interop/JniTypeTest.cs | 9 ++++++--- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JniType.cs b/src/Java.Interop/Java.Interop/JniType.cs index 0ebcf7a76..83837d10f 100644 --- a/src/Java.Interop/Java.Interop/JniType.cs +++ b/src/Java.Interop/Java.Interop/JniType.cs @@ -57,6 +57,11 @@ public string Name { } } + public override string ToString () + { + return $"JniType(Name='{Name}' PeerReference={PeerReference})"; + } + #if XA_INTEGRATION internal #else // !XA_INTEGRATION diff --git a/tests/Java.Interop-Tests/Java.Interop/JavaExceptionTests.cs b/tests/Java.Interop-Tests/Java.Interop/JavaExceptionTests.cs index 19bc51a64..e663fd516 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JavaExceptionTests.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JavaExceptionTests.cs @@ -14,28 +14,30 @@ public void StackTrace () { try { new JniType ("this/type/had/better/not/exist"); - } catch (JavaException e) { + } +#if __ANDROID__ + catch (Java.Lang.Throwable e) { Assert.IsTrue ( string.Equals ("this/type/had/better/not/exist", e.Message, StringComparison.OrdinalIgnoreCase) || e.Message.StartsWith ("Didn't find class \"this.type.had.better.not.exist\" on path: DexPathList")); Assert.IsTrue ( // ART - e.JavaStackTrace.StartsWith ("java.lang.ClassNotFoundException: ", StringComparison.Ordinal) || + e.StackTrace.Contains ("java.lang.ClassNotFoundException: ", StringComparison.Ordinal) || // Dalvik, JVM - e.JavaStackTrace.StartsWith ("java.lang.NoClassDefFoundError: this/type/had/better/not/exist", StringComparison.Ordinal)); + e.StackTrace.Contains ("java.lang.NoClassDefFoundError: this/type/had/better/not/exist", StringComparison.Ordinal)); e.Dispose (); -#if __ANDROID__ - } catch (Java.Lang.Throwable e) { + } +#endif // __ANDROID__ + catch (JavaException e) { Assert.IsTrue ( string.Equals ("this/type/had/better/not/exist", e.Message, StringComparison.OrdinalIgnoreCase) || e.Message.StartsWith ("Didn't find class \"this.type.had.better.not.exist\" on path: DexPathList")); Assert.IsTrue ( // ART - e.StackTrace.Contains ("java.lang.ClassNotFoundException: ", StringComparison.Ordinal) || + e.JavaStackTrace.StartsWith ("java.lang.ClassNotFoundException: ", StringComparison.Ordinal) || // Dalvik, JVM - e.StackTrace.Contains ("java.lang.NoClassDefFoundError: this/type/had/better/not/exist", StringComparison.Ordinal)); + e.JavaStackTrace.StartsWith ("java.lang.NoClassDefFoundError: this/type/had/better/not/exist", StringComparison.Ordinal)); e.Dispose (); -#endif // __ANDROID__ } } diff --git a/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs b/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs index 0fb59d7cf..9d3d8c294 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs @@ -141,9 +141,11 @@ public unsafe void Name () var Class_getMethod = Class_class.GetInstanceMethod ("getMethod", "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;"); var Method_getReturnType = Method_class.GetInstanceMethod ("getReturnType", "()Ljava/lang/Class;"); var hashCode_str = JniEnvironment.Strings.NewString ("hashCode"); - var hashCode_args = stackalloc JniArgumentValue [1]; - hashCode_args [0] = new JniArgumentValue (hashCode_str); - var Object_hashCode = JniEnvironment.InstanceMethods.CallObjectMethod (Object_class.PeerReference, Class_getMethod, hashCode_args); + var emptyArray = JniEnvironment.Arrays.NewObjectArray (0, Class_class.PeerReference, new JniObjectReference ()); + var getHashcodeMethodArgs = stackalloc JniArgumentValue [2]; + getHashcodeMethodArgs [0] = new JniArgumentValue (hashCode_str); + getHashcodeMethodArgs [1] = new JniArgumentValue (emptyArray); + var Object_hashCode = JniEnvironment.InstanceMethods.CallObjectMethod (Object_class.PeerReference, Class_getMethod, getHashcodeMethodArgs); var Object_hashCode_rt = JniEnvironment.InstanceMethods.CallObjectMethod (Object_hashCode, Method_getReturnType); try { Assert.AreEqual ("java/lang/Object", Object_class.Name); @@ -154,6 +156,7 @@ public unsafe void Name () JniObjectReference.Dispose (ref hashCode_str); JniObjectReference.Dispose (ref Object_hashCode); JniObjectReference.Dispose (ref Object_hashCode_rt); + JniObjectReference.Dispose (ref emptyArray); } } }