diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt index 0c09ea383a..307a67e31e 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt @@ -81,7 +81,7 @@ class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) { // We automatically handle 'toString', 'equals' and 'hashCode' to simplify the task of the caller // and provide consistency. "toString" -> "Godot Callable Proxy for $interfaceName" - "equals" -> proxy == args[0] + "equals" -> proxy === args[0] "hashCode" -> godotCallable.hashCode() // Invocation for the interface single abstract method falls here and is dispatched to the @@ -106,8 +106,8 @@ class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) { // We automatically handle 'toString', 'equals' and 'hashCode' to simplify the task of the caller // and provide consistency. "toString" -> "Godot Object Proxy for ${interfaces.joinToString(",")}" - "equals" -> proxy == args[0] - "hashCode" -> godotObjectID + "equals" -> proxy === args[0] + "hashCode" -> godotObjectID.hashCode() // Invocation for the remaining interface(s) methods falls here and is dispatched to the // Godot Object. diff --git a/platform/android/java/lib/src/test/java/org/godotengine/godot/plugin/AndroidRuntimePluginTest.kt b/platform/android/java/lib/src/test/java/org/godotengine/godot/plugin/AndroidRuntimePluginTest.kt new file mode 100644 index 0000000000..ac459e617c --- /dev/null +++ b/platform/android/java/lib/src/test/java/org/godotengine/godot/plugin/AndroidRuntimePluginTest.kt @@ -0,0 +1,131 @@ +/**************************************************************************/ +/* AndroidRuntimePluginTest.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot.plugin + +import org.godotengine.godot.variant.Callable +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class AndroidRuntimePluginTest { + + private val createProxyFromGodotCallable = AndroidRuntimePlugin::class.java + .getDeclaredMethod("createProxyFromGodotCallable", String::class.java, Callable::class.java) + .apply { isAccessible = true } + + private val createProxyFromGodotObjectID = AndroidRuntimePlugin::class.java + .getDeclaredMethod("createProxyFromGodotObjectID", Long::class.javaPrimitiveType, Array::class.java) + .apply { isAccessible = true } + + private fun newCallable(): Callable { + val ctor = Callable::class.java.getDeclaredConstructor(Long::class.javaPrimitiveType) + ctor.isAccessible = true + return ctor.newInstance(0L) + } + + private fun callableProxy(): Any { + val proxy = createProxyFromGodotCallable.invoke(null, Runnable::class.java.name, newCallable()) + assertNotNull(proxy) + return proxy!! + } + + private fun objectIDProxy(godotObjectID: Long = 0L): Any { + val proxy = createProxyFromGodotObjectID.invoke(null, godotObjectID, arrayOf(Runnable::class.java.name)) + assertNotNull(proxy) + return proxy!! + } + + // Regression tests for the infinite recursion in the proxy equals handler. + // Kotlin's `proxy == args[0]` compiles to `proxy.equals(args[0])`, which the + // JDK proxy dispatches back through the InvocationHandler — the previous + // implementation hit the same branch and recursed into StackOverflowError. + + @Test + fun `Given Callable proxy, When equals is invoked with itself, Then returns true without recursion`() { + val proxy = callableProxy() + assertTrue(proxy.equals(proxy)) + } + + @Test + fun `Given Callable proxy, When equals is invoked with another instance, Then returns false without recursion`() { + val proxy = callableProxy() + val otherProxy = callableProxy() + assertFalse(proxy.equals(otherProxy)) + assertFalse(proxy.equals(Any())) + assertFalse(proxy.equals(null)) + } + + @Test + fun `Given ObjectID proxy, When equals is invoked with itself, Then returns true without recursion`() { + val proxy = objectIDProxy() + assertTrue(proxy.equals(proxy)) + } + + @Test + fun `Given ObjectID proxy, When equals is invoked with another instance, Then returns false without recursion`() { + val proxy = objectIDProxy() + val otherProxy = objectIDProxy() + assertFalse(proxy.equals(otherProxy)) + assertFalse(proxy.equals(Any())) + assertFalse(proxy.equals(null)) + } + + // Sanity checks that the proxy's toString / hashCode handlers still operate + // as documented and that they do not get caught up in the equals fix. + + @Test + fun `Given Callable proxy, When toString is invoked, Then returns the documented label`() { + val proxy = callableProxy() + assertEquals("Godot Callable Proxy for ${Runnable::class.java.name}", proxy.toString()) + } + + @Test + fun `Given ObjectID proxy, When toString is invoked, Then returns the documented label`() { + val proxy = objectIDProxy() + assertEquals("Godot Object Proxy for ${Runnable::class.java.name}", proxy.toString()) + } + + @Test + fun `Given Callable proxy, When hashCode is invoked, Then returns the Callable hashCode`() { + val callable = newCallable() + val proxy = createProxyFromGodotCallable.invoke(null, Runnable::class.java.name, callable)!! + assertEquals(callable.hashCode(), proxy.hashCode()) + } + + @Test + fun `Given ObjectID proxy, When hashCode is invoked, Then returns the ObjectID hashCode`() { + val godotObjectID = 0x1_0000_002AL + val proxy = objectIDProxy(godotObjectID) + assertEquals(godotObjectID.hashCode(), proxy.hashCode()) + } +}