diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle
index 5d90a495254..d75a63f101a 100644
--- a/platform/android/java/app/build.gradle
+++ b/platform/android/java/app/build.gradle
@@ -34,6 +34,11 @@ configurations {
}
dependencies {
+ // Android instrumented test dependencies
+ androidTestImplementation "androidx.test.ext:junit:1.3.0"
+ androidTestImplementation "androidx.test.espresso:espresso-core:3.7.0"
+ androidTestImplementation "org.jetbrains.kotlin:kotlin-test:1.3.11"
+
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
@@ -114,6 +119,8 @@ android {
targetSdkVersion getExportTargetSdkVersion()
missingDimensionStrategy 'products', 'template'
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
@@ -214,10 +221,19 @@ android {
flavorDimensions 'edition'
productFlavors {
+ // Product flavor for the standard (no .net support) builds.
standard {
getIsDefault().set(true)
}
+
+ // Product flavor for the Mono (.net) builds.
mono {}
+
+ // Product flavor used for running instrumented tests.
+ instrumented {
+ applicationIdSuffix ".instrumented"
+ versionNameSuffix "-instrumented"
+ }
}
sourceSets {
diff --git a/platform/android/java/app/src/androidTestInstrumented/java/com/godot/game/GodotAppTest.kt b/platform/android/java/app/src/androidTestInstrumented/java/com/godot/game/GodotAppTest.kt
new file mode 100644
index 00000000000..f55954fe59b
--- /dev/null
+++ b/platform/android/java/app/src/androidTestInstrumented/java/com/godot/game/GodotAppTest.kt
@@ -0,0 +1,76 @@
+/**************************************************************************/
+/* GodotAppTest.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 com.godot.game
+
+import android.util.Log
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.godot.game.test.GodotAppInstrumentedTestPlugin
+import org.godotengine.godot.plugin.GodotPluginRegistry
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+/**
+ * This instrumented test will launch the `instrumented` version of GodotApp and run a set of tests against it.
+ */
+@RunWith(AndroidJUnit4::class)
+class GodotAppTest {
+
+ companion object {
+ private val TAG = GodotAppTest::class.java.simpleName
+ }
+
+ @get:Rule
+ val godotAppRule = ActivityScenarioRule(GodotApp::class.java)
+
+ /**
+ * Runs the JavaClassWrapper tests via the GodotAppInstrumentedTestPlugin.
+ */
+ @Test
+ fun runJavaClassWrapperTests() {
+ val testPlugin = GodotPluginRegistry.getPluginRegistry()
+ .getPlugin("GodotAppInstrumentedTestPlugin") as GodotAppInstrumentedTestPlugin?
+ assertNotNull(testPlugin)
+
+ Log.d(TAG, "Waiting for the Godot main loop to start...")
+ testPlugin.waitForGodotMainLoopStarted()
+
+ Log.d(TAG, "Running JavaClassWrapper tests...")
+ val result = testPlugin.runJavaClassWrapperTests()
+ assertNotNull(result)
+ result.exceptionOrNull()?.let { throw it }
+ assertTrue(result.isSuccess)
+ Log.d(TAG, "Passed ${result.getOrNull()} tests")
+ }
+}
diff --git a/platform/android/java/app/src/instrumented/AndroidManifest.xml b/platform/android/java/app/src/instrumented/AndroidManifest.xml
new file mode 100644
index 00000000000..863ec4c09fa
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/AndroidManifest.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/platform/android/java/app/src/instrumented/assets/.gitattributes b/platform/android/java/app/src/instrumented/assets/.gitattributes
new file mode 100644
index 00000000000..8ad74f78d9c
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/.gitattributes
@@ -0,0 +1,2 @@
+# Normalize EOL for all files that Git considers text files.
+* text=auto eol=lf
diff --git a/platform/android/java/app/src/instrumented/assets/.gitignore b/platform/android/java/app/src/instrumented/assets/.gitignore
new file mode 100644
index 00000000000..a9e5f7dcd00
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/.gitignore
@@ -0,0 +1,3 @@
+# Godot 4+ specific ignores
+/android/
+/.godot/editor
diff --git a/platform/android/java/app/src/instrumented/assets/.godot/.gdignore b/platform/android/java/app/src/instrumented/assets/.godot/.gdignore
new file mode 100644
index 00000000000..8b137891791
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/.godot/.gdignore
@@ -0,0 +1 @@
+
diff --git a/platform/android/java/app/src/instrumented/assets/.godot/global_script_class_cache.cfg b/platform/android/java/app/src/instrumented/assets/.godot/global_script_class_cache.cfg
new file mode 100644
index 00000000000..07389a50ae3
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/.godot/global_script_class_cache.cfg
@@ -0,0 +1,17 @@
+list=[{
+"base": &"RefCounted",
+"class": &"BaseTest",
+"icon": "",
+"is_abstract": true,
+"is_tool": false,
+"language": &"GDScript",
+"path": "res://test/base_test.gd"
+}, {
+"base": &"BaseTest",
+"class": &"JavaClassWrapperTests",
+"icon": "",
+"is_abstract": false,
+"is_tool": false,
+"language": &"GDScript",
+"path": "res://test/javaclasswrapper/java_class_wrapper_tests.gd"
+}]
diff --git a/platform/android/java/app/src/instrumented/assets/.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex b/platform/android/java/app/src/instrumented/assets/.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex
new file mode 100644
index 00000000000..4650606ff35
Binary files /dev/null and b/platform/android/java/app/src/instrumented/assets/.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex differ
diff --git a/platform/android/java/app/src/instrumented/assets/.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.md5 b/platform/android/java/app/src/instrumented/assets/.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.md5
new file mode 100644
index 00000000000..9346d82667c
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.md5
@@ -0,0 +1,2 @@
+source_md5="4cdc64b13a9af63279c486903c9b54cc"
+dest_md5="ddbdfc47e6405ad8d8e9e6a88a32824e"
diff --git a/platform/android/java/app/src/instrumented/assets/.godot/scene_groups_cache.cfg b/platform/android/java/app/src/instrumented/assets/.godot/scene_groups_cache.cfg
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/platform/android/java/app/src/instrumented/assets/.godot/uid_cache.bin b/platform/android/java/app/src/instrumented/assets/.godot/uid_cache.bin
new file mode 100644
index 00000000000..08174116434
Binary files /dev/null and b/platform/android/java/app/src/instrumented/assets/.godot/uid_cache.bin differ
diff --git a/platform/android/java/app/src/instrumented/assets/icon.svg b/platform/android/java/app/src/instrumented/assets/icon.svg
new file mode 100644
index 00000000000..1640be71db9
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/icon.svg
@@ -0,0 +1 @@
+
diff --git a/platform/android/java/app/src/instrumented/assets/icon.svg.import b/platform/android/java/app/src/instrumented/assets/icon.svg.import
new file mode 100644
index 00000000000..2b8ba2993c8
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://srnrli5m8won"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/platform/android/java/app/src/instrumented/assets/main.gd b/platform/android/java/app/src/instrumented/assets/main.gd
new file mode 100644
index 00000000000..a18811ba100
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/main.gd
@@ -0,0 +1,60 @@
+extends Node2D
+
+var _plugin_name = "GodotAppInstrumentedTestPlugin"
+var _android_plugin
+
+func _ready():
+ if Engine.has_singleton(_plugin_name):
+ _android_plugin = Engine.get_singleton(_plugin_name)
+ _android_plugin.connect("launch_tests", _launch_tests)
+ else:
+ printerr("Couldn't find plugin " + _plugin_name)
+ get_tree().quit()
+
+func _launch_tests(test_label: String) -> void:
+ var test_instance: BaseTest = null
+ match test_label:
+ "javaclasswrapper_tests":
+ test_instance = JavaClassWrapperTests.new()
+
+ if test_instance:
+ test_instance.__reset_tests()
+ test_instance.run_tests()
+ var incomplete_tests = test_instance._test_started - test_instance._test_completed
+ _android_plugin.onTestsCompleted(test_label, test_instance._test_completed, test_instance._test_assert_failures + incomplete_tests)
+ else:
+ _android_plugin.onTestsFailed(test_label, "Unable to launch tests")
+
+
+func _on_plugin_toast_button_pressed() -> void:
+ if _android_plugin:
+ _android_plugin.helloWorld()
+
+func _on_vibration_button_pressed() -> void:
+ var android_runtime = Engine.get_singleton("AndroidRuntime")
+ if android_runtime:
+ print("Checking if the device supports vibration")
+ var vibrator_service = android_runtime.getApplicationContext().getSystemService("vibrator")
+ if vibrator_service:
+ if vibrator_service.hasVibrator():
+ print("Vibration is supported on device! Vibrating now...")
+ var VibrationEffect = JavaClassWrapper.wrap("android.os.VibrationEffect")
+ var effect = VibrationEffect.createOneShot(500, VibrationEffect.DEFAULT_AMPLITUDE)
+ vibrator_service.vibrate(effect)
+ else:
+ printerr("Vibration is not supported on device")
+ else:
+ printerr("Unable to retrieve the vibrator service")
+ else:
+ printerr("Couldn't find AndroidRuntime singleton")
+
+func _on_gd_script_toast_button_pressed() -> void:
+ var android_runtime = Engine.get_singleton("AndroidRuntime")
+ if android_runtime:
+ var activity = android_runtime.getActivity()
+
+ var toastCallable = func ():
+ var ToastClass = JavaClassWrapper.wrap("android.widget.Toast")
+ ToastClass.makeText(activity, "Toast from GDScript", ToastClass.LENGTH_LONG).show()
+
+ activity.runOnUiThread(android_runtime.createRunnableFromGodotCallable(toastCallable))
diff --git a/platform/android/java/app/src/instrumented/assets/main.gd.uid b/platform/android/java/app/src/instrumented/assets/main.gd.uid
new file mode 100644
index 00000000000..396335dbdce
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/main.gd.uid
@@ -0,0 +1 @@
+uid://bv6y7in6otgcm
diff --git a/platform/android/java/app/src/instrumented/assets/main.tscn b/platform/android/java/app/src/instrumented/assets/main.tscn
new file mode 100644
index 00000000000..848845bab62
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/main.tscn
@@ -0,0 +1,34 @@
+[gd_scene load_steps=2 format=3 uid="uid://cg3hylang5fxn"]
+
+[ext_resource type="Script" uid="uid://bv6y7in6otgcm" path="res://main.gd" id="1_j0gfq"]
+
+[node name="Main" type="Node2D"]
+script = ExtResource("1_j0gfq")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+offset_left = 68.0
+offset_top = 102.0
+offset_right = 506.0
+offset_bottom = 408.0
+theme_override_constants/separation = 25
+
+[node name="PluginToastButton" type="Button" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 50)
+layout_mode = 2
+text = "Plugin Toast
+"
+
+[node name="VibrationButton" type="Button" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 50)
+layout_mode = 2
+text = "Vibration"
+
+[node name="GDScriptToastButton" type="Button" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 50)
+layout_mode = 2
+text = "GDScript Toast
+"
+
+[connection signal="pressed" from="VBoxContainer/PluginToastButton" to="." method="_on_plugin_toast_button_pressed"]
+[connection signal="pressed" from="VBoxContainer/VibrationButton" to="." method="_on_vibration_button_pressed"]
+[connection signal="pressed" from="VBoxContainer/GDScriptToastButton" to="." method="_on_gd_script_toast_button_pressed"]
diff --git a/platform/android/java/app/src/instrumented/assets/project.godot b/platform/android/java/app/src/instrumented/assets/project.godot
new file mode 100644
index 00000000000..f4ec2997925
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/project.godot
@@ -0,0 +1,26 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="Godot App Instrumentation Tests"
+run/main_scene="res://main.tscn"
+config/features=PackedStringArray("4.5", "GL Compatibility")
+config/icon="res://icon.svg"
+
+[debug]
+
+settings/stdout/verbose_stdout=true
+
+[rendering]
+
+renderer/rendering_method="gl_compatibility"
+renderer/rendering_method.mobile="gl_compatibility"
+textures/vram_compression/import_etc2_astc=true
diff --git a/platform/android/java/app/src/instrumented/assets/test/base_test.gd b/platform/android/java/app/src/instrumented/assets/test/base_test.gd
new file mode 100644
index 00000000000..17a253ec974
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/test/base_test.gd
@@ -0,0 +1,44 @@
+@abstract class_name BaseTest
+
+var _test_started := 0
+var _test_completed := 0
+var _test_assert_passes := 0
+var _test_assert_failures := 0
+
+@abstract func run_tests()
+
+func __exec_test(test_func: Callable):
+ _test_started += 1
+ test_func.call()
+ _test_completed += 1
+
+func __reset_tests():
+ _test_started = 0
+ _test_completed = 0
+ _test_assert_passes = 0
+ _test_assert_failures = 0
+
+func __get_stack_frame():
+ for s in get_stack():
+ if not s.function.begins_with('__') and s.function != "assert_equal":
+ return s
+ return null
+
+func __assert_pass():
+ _test_assert_passes += 1
+ pass
+
+func __assert_fail():
+ _test_assert_failures += 1
+ var s = __get_stack_frame()
+ if s != null:
+ print_rich ("[color=red] == FAILURE: In function %s() from '%s' on line %s[/color]" % [s.function, s.source, s.line])
+ else:
+ print_rich ("[color=red] == FAILURE (run with --debug to get more information!) ==[/color]")
+
+func assert_equal(actual, expected):
+ if actual == expected:
+ __assert_pass()
+ else:
+ __assert_fail()
+ print (" |-> Expected '%s' but got '%s'" % [expected, actual])
diff --git a/platform/android/java/app/src/instrumented/assets/test/base_test.gd.uid b/platform/android/java/app/src/instrumented/assets/test/base_test.gd.uid
new file mode 100644
index 00000000000..e073901130e
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/test/base_test.gd.uid
@@ -0,0 +1 @@
+uid://mofa8j0d801f
diff --git a/platform/android/java/app/src/instrumented/assets/test/javaclasswrapper/java_class_wrapper_tests.gd b/platform/android/java/app/src/instrumented/assets/test/javaclasswrapper/java_class_wrapper_tests.gd
new file mode 100644
index 00000000000..e43cda069a9
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/test/javaclasswrapper/java_class_wrapper_tests.gd
@@ -0,0 +1,136 @@
+class_name JavaClassWrapperTests
+extends BaseTest
+
+func run_tests():
+ print("JavaClassWrapper tests starting..")
+
+ __exec_test(test_exceptions)
+
+ __exec_test(test_multiple_signatures)
+ __exec_test(test_array_arguments)
+ __exec_test(test_array_return)
+
+ __exec_test(test_dictionary)
+
+ __exec_test(test_object_overload)
+
+ __exec_test(test_variant_conversion_safe_from_stack_overflow)
+
+ print("JavaClassWrapper tests finished.")
+ print("Tests started: " + str(_test_started))
+ print("Tests completed: " + str(_test_completed))
+
+
+func test_exceptions() -> void:
+ var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
+ #print(TestClass.get_java_method_list())
+
+ assert_equal(JavaClassWrapper.get_exception(), null)
+
+ assert_equal(TestClass.testExc(27), 0)
+ assert_equal(str(JavaClassWrapper.get_exception()), '')
+
+ assert_equal(JavaClassWrapper.get_exception(), null)
+
+func test_multiple_signatures() -> void:
+ var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
+
+ var ai := [1, 2]
+ assert_equal(TestClass.testMethod(1, ai), "IntArray: [1, 2]")
+
+ var astr := ["abc"]
+ assert_equal(TestClass.testMethod(2, astr), "IntArray: [0]")
+
+ var atstr: Array[String] = ["abc"]
+ assert_equal(TestClass.testMethod(3, atstr), "StringArray: [abc]")
+
+ var TestClass2: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass2')
+ var aobjl: Array[Object] = [
+ TestClass2.TestClass2(27),
+ TestClass2.TestClass2(135),
+ ]
+ assert_equal(TestClass.testMethod(3, aobjl), "testObjects: 27 135")
+
+func test_array_arguments() -> void:
+ var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
+
+ assert_equal(TestClass.testArgBoolArray([true, false, true]), "[true, false, true]")
+ assert_equal(TestClass.testArgByteArray(PackedByteArray([1, 2, 3])), "[1, 2, 3]")
+ assert_equal(TestClass.testArgCharArray("abc".to_utf16_buffer()), "abc");
+ assert_equal(TestClass.testArgShortArray(PackedInt32Array([27, 28, 29])), "[27, 28, 29]")
+ assert_equal(TestClass.testArgShortArray([27, 28, 29]), "[27, 28, 29]")
+ assert_equal(TestClass.testArgIntArray(PackedInt32Array([7, 8, 9])), "[7, 8, 9]")
+ assert_equal(TestClass.testArgIntArray([7, 8, 9]), "[7, 8, 9]")
+ assert_equal(TestClass.testArgLongArray(PackedInt64Array([17, 18, 19])), "[17, 18, 19]")
+ assert_equal(TestClass.testArgLongArray([17, 18, 19]), "[17, 18, 19]")
+ assert_equal(TestClass.testArgFloatArray(PackedFloat32Array([17.1, 18.2, 19.3])), "[17.1, 18.2, 19.3]")
+ assert_equal(TestClass.testArgFloatArray([17.1, 18.2, 19.3]), "[17.1, 18.2, 19.3]")
+ assert_equal(TestClass.testArgDoubleArray(PackedFloat64Array([37.1, 38.2, 39.3])), "[37.1, 38.2, 39.3]")
+ assert_equal(TestClass.testArgDoubleArray([37.1, 38.2, 39.3]), "[37.1, 38.2, 39.3]")
+
+func test_array_return() -> void:
+ var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
+ #print(TestClass.get_java_method_list())
+
+ assert_equal(TestClass.testRetBoolArray(), [true, false, true])
+ assert_equal(TestClass.testRetWrappedBoolArray(), [true, false, true])
+
+ assert_equal(TestClass.testRetByteArray(), PackedByteArray([1, 2, 3]))
+ assert_equal(TestClass.testRetWrappedByteArray(), PackedByteArray([1, 2, 3]))
+
+ assert_equal(TestClass.testRetCharArray().get_string_from_utf16(), "abc")
+ assert_equal(TestClass.testRetWrappedCharArray().get_string_from_utf16(), "abc")
+
+ assert_equal(TestClass.testRetShortArray(), PackedInt32Array([11, 12, 13]))
+ assert_equal(TestClass.testRetWrappedShortArray(), PackedInt32Array([11, 12, 13]))
+
+ assert_equal(TestClass.testRetIntArray(), PackedInt32Array([21, 22, 23]))
+ assert_equal(TestClass.testRetWrappedIntArray(), PackedInt32Array([21, 22, 23]))
+
+ assert_equal(TestClass.testRetLongArray(), PackedInt64Array([41, 42, 43]))
+ assert_equal(TestClass.testRetWrappedLongArray(), PackedInt64Array([41, 42, 43]))
+
+ assert_equal(TestClass.testRetFloatArray(), PackedFloat32Array([31.1, 32.2, 33.3]))
+ assert_equal(TestClass.testRetWrappedFloatArray(), PackedFloat32Array([31.1, 32.2, 33.3]))
+
+ assert_equal(TestClass.testRetDoubleArray(), PackedFloat64Array([41.1, 42.2, 43.3]))
+ assert_equal(TestClass.testRetWrappedDoubleArray(), PackedFloat64Array([41.1, 42.2, 43.3]))
+
+ var obj_array = TestClass.testRetObjectArray()
+ assert_equal(str(obj_array[0]), '')
+ assert_equal(str(obj_array[1]), '')
+
+ assert_equal(TestClass.testRetStringArray(), PackedStringArray(["I", "am", "String"]))
+ assert_equal(TestClass.testRetCharSequenceArray(), PackedStringArray(["I", "am", "CharSequence"]))
+
+func test_dictionary():
+ var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
+ assert_equal(TestClass.testDictionary({a = 1, b = 2}), "{a=1, b=2}")
+ assert_equal(TestClass.testRetDictionary(), {a = 1, b = 2})
+ assert_equal(TestClass.testRetDictionaryArray(), [{a = 1, b = 2}])
+ assert_equal(TestClass.testDictionaryNested({a = 1, b = [2, 3], c = 4}), "{a: 1, b: [2, 3], c: 4}")
+
+func test_object_overload():
+ var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
+ var TestClass2: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass2')
+ var TestClass3: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass3')
+
+ var t2 = TestClass2.TestClass2(33)
+ var t3 = TestClass3.TestClass3("thirty three")
+
+ assert_equal(TestClass.testObjectOverload(t2), "TestClass2: 33")
+ assert_equal(TestClass.testObjectOverload(t3), "TestClass3: thirty three")
+
+ var arr_of_t2 = [t2, TestClass2.TestClass2(34)]
+ var arr_of_t3 = [t3, TestClass3.TestClass3("thirty four")]
+
+ assert_equal(TestClass.testObjectOverloadArray(arr_of_t2), "TestClass2: [33, 34]")
+ assert_equal(TestClass.testObjectOverloadArray(arr_of_t3), "TestClass3: [thirty three, thirty four]")
+
+func test_variant_conversion_safe_from_stack_overflow():
+ var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
+ var arr: Array = [42]
+ var dict: Dictionary = {"arr": arr}
+ arr.append(dict)
+ # The following line will crash with stack overflow if not handled property:
+ TestClass.testDictionary(dict)
diff --git a/platform/android/java/app/src/instrumented/assets/test/javaclasswrapper/java_class_wrapper_tests.gd.uid b/platform/android/java/app/src/instrumented/assets/test/javaclasswrapper/java_class_wrapper_tests.gd.uid
new file mode 100644
index 00000000000..93a2abe0e43
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/assets/test/javaclasswrapper/java_class_wrapper_tests.gd.uid
@@ -0,0 +1 @@
+uid://3ql82ggk41xc
diff --git a/platform/android/java/app/src/instrumented/java/com/godot/game/test/GodotAppInstrumentedTestPlugin.kt b/platform/android/java/app/src/instrumented/java/com/godot/game/test/GodotAppInstrumentedTestPlugin.kt
new file mode 100644
index 00000000000..2fc9eecfe65
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/java/com/godot/game/test/GodotAppInstrumentedTestPlugin.kt
@@ -0,0 +1,144 @@
+/**************************************************************************/
+/* GodotAppInstrumentedTestPlugin.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 com.godot.game.test
+
+import android.util.Log
+import android.widget.Toast
+import org.godotengine.godot.Godot
+import org.godotengine.godot.plugin.GodotPlugin
+import org.godotengine.godot.plugin.UsedByGodot
+import org.godotengine.godot.plugin.SignalInfo
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.CountDownLatch
+
+/**
+ * [GodotPlugin] used to drive instrumented tests.
+ */
+class GodotAppInstrumentedTestPlugin(godot: Godot) : GodotPlugin(godot) {
+
+ companion object {
+ private val TAG = GodotAppInstrumentedTestPlugin::class.java.simpleName
+ private const val MAIN_LOOP_STARTED_LATCH_KEY = "main_loop_started_latch"
+
+ private const val JAVACLASSWRAPPER_TESTS = "javaclasswrapper_tests"
+
+ private val LAUNCH_TESTS_SIGNAL = SignalInfo("launch_tests", String::class.java)
+
+ private val SIGNALS = setOf(
+ LAUNCH_TESTS_SIGNAL
+ )
+ }
+
+ private val testResults = ConcurrentHashMap>()
+ private val latches = ConcurrentHashMap()
+
+ init {
+ // Add a countdown latch that is triggered when `onGodotMainLoopStarted` is fired.
+ // This will be used by tests to wait until the engine is ready.
+ latches[MAIN_LOOP_STARTED_LATCH_KEY] = CountDownLatch(1)
+ }
+
+ override fun getPluginName() = "GodotAppInstrumentedTestPlugin"
+
+ override fun getPluginSignals() = SIGNALS
+
+ override fun onGodotMainLoopStarted() {
+ super.onGodotMainLoopStarted()
+ latches.remove(MAIN_LOOP_STARTED_LATCH_KEY)?.countDown()
+ }
+
+ /**
+ * Used by the instrumented test to wait until the Godot main loop is up and running.
+ */
+ internal fun waitForGodotMainLoopStarted() {
+ // Wait on the CountDownLatch for `onGodotMainLoopStarted`
+ try {
+ latches[MAIN_LOOP_STARTED_LATCH_KEY]?.await()
+ } catch (e: InterruptedException) {
+ Log.e(TAG, "Unable to wait for Godot main loop started event.", e)
+ }
+ }
+
+ /**
+ * This launches the JavaClassWrapper tests, and wait until the tests are complete before returning.
+ */
+ internal fun runJavaClassWrapperTests(): Result? {
+ return launchTests(JAVACLASSWRAPPER_TESTS)
+ }
+
+ private fun launchTests(testLabel: String): Result? {
+ val latch = latches.getOrPut(testLabel) { CountDownLatch(1) }
+ emitSignal(LAUNCH_TESTS_SIGNAL.name, testLabel)
+ return try {
+ latch.await()
+ val result = testResults.remove(testLabel)
+ result
+ } catch (e: InterruptedException) {
+ Log.e(TAG, "Unable to wait for completion for $testLabel", e)
+ null
+ }
+ }
+
+ /**
+ * Callback invoked from gdscript when the tests are completed.
+ */
+ @UsedByGodot
+ fun onTestsCompleted(testLabel: String, passes: Int, failures: Int) {
+ Log.d(TAG, "$testLabel tests completed")
+ val result = if (failures == 0) {
+ Result.success(passes)
+ } else {
+ Result.failure(AssertionError("$failures tests failed!"))
+ }
+
+ completeTest(testLabel, result)
+ }
+
+ @UsedByGodot
+ fun onTestsFailed(testLabel: String, failureMessage: String) {
+ Log.d(TAG, "$testLabel tests failed")
+ val result: Result = Result.failure(AssertionError(failureMessage))
+ completeTest(testLabel, result)
+ }
+
+ private fun completeTest(testKey: String, result: Result) {
+ testResults[testKey] = result
+ latches.remove(testKey)?.countDown()
+ }
+
+ @UsedByGodot
+ fun helloWorld() {
+ runOnHostThread {
+ Toast.makeText(activity, "Toast from Android plugin", Toast.LENGTH_LONG).show()
+ Log.v(pluginName, "Hello World")
+ }
+ }
+}
diff --git a/platform/android/java/app/src/instrumented/java/com/godot/game/test/javaclasswrapper/TestClass.kt b/platform/android/java/app/src/instrumented/java/com/godot/game/test/javaclasswrapper/TestClass.kt
new file mode 100644
index 00000000000..8bb677939a9
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/java/com/godot/game/test/javaclasswrapper/TestClass.kt
@@ -0,0 +1,261 @@
+/**************************************************************************/
+/* TestClass.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 com.godot.game.test.javaclasswrapper
+
+import org.godotengine.godot.Dictionary
+import kotlin.collections.contentToString
+import kotlin.collections.joinToString
+
+class TestClass {
+ companion object {
+ @JvmStatic
+ fun stringify(value: Any?): String {
+ return when (value) {
+ null -> "null"
+ is Map<*, *> -> {
+ val entries = value.entries.joinToString(", ") { (k, v) -> "${stringify(k)}: ${stringify(v)}" }
+ "{$entries}"
+ }
+
+ is List<*> -> value.joinToString(prefix = "[", postfix = "]") { stringify(it) }
+ is Array<*> -> value.joinToString(prefix = "[", postfix = "]") { stringify(it) }
+ is IntArray -> value.joinToString(prefix = "[", postfix = "]")
+ is LongArray -> value.joinToString(prefix = "[", postfix = "]")
+ is FloatArray -> value.joinToString(prefix = "[", postfix = "]")
+ is DoubleArray -> value.joinToString(prefix = "[", postfix = "]")
+ is BooleanArray -> value.joinToString(prefix = "[", postfix = "]")
+ is CharArray -> value.joinToString(prefix = "[", postfix = "]")
+ else -> value.toString()
+ }
+ }
+
+ @JvmStatic
+ fun testDictionary(d: Dictionary): String {
+ return d.toString()
+ }
+
+ @JvmStatic
+ fun testDictionaryNested(d: Dictionary): String {
+ return stringify(d)
+ }
+
+ @JvmStatic
+ fun testRetDictionary(): Dictionary {
+ var d = Dictionary()
+ d.putAll(mapOf("a" to 1, "b" to 2))
+ return d
+ }
+
+ @JvmStatic
+ fun testRetDictionaryArray(): Array {
+ var d = Dictionary()
+ d.putAll(mapOf("a" to 1, "b" to 2))
+ return arrayOf(d)
+ }
+
+ @JvmStatic
+ fun testMethod(int: Int, array: IntArray): String {
+ return "IntArray: " + array.contentToString()
+ }
+
+ @JvmStatic
+ fun testMethod(int: Int, vararg args: String): String {
+ return "StringArray: " + args.contentToString()
+ }
+
+ @JvmStatic
+ fun testMethod(int: Int, objects: Array): String {
+ return "testObjects: " + objects.joinToString(separator = " ") { it.getValue().toString() }
+ }
+
+ @JvmStatic
+ fun testExc(i: Int): Int {
+ val s: String? = null
+ s!!.length
+ return i
+ }
+
+ @JvmStatic
+ fun testArgBoolArray(a: BooleanArray): String {
+ return a.contentToString();
+ }
+
+ @JvmStatic
+ fun testArgByteArray(a: ByteArray): String {
+ return a.contentToString();
+ }
+
+ @JvmStatic
+ fun testArgCharArray(a: CharArray): String {
+ return a.joinToString("")
+ }
+
+ @JvmStatic
+ fun testArgShortArray(a: ShortArray): String {
+ return a.contentToString();
+ }
+
+ @JvmStatic
+ fun testArgIntArray(a: IntArray): String {
+ return a.contentToString();
+ }
+
+ @JvmStatic
+ fun testArgLongArray(a: LongArray): String {
+ return a.contentToString();
+ }
+
+ @JvmStatic
+ fun testArgFloatArray(a: FloatArray): String {
+ return a.contentToString();
+ }
+
+ @JvmStatic
+ fun testArgDoubleArray(a: DoubleArray): String {
+ return a.contentToString();
+ }
+
+ @JvmStatic
+ fun testRetBoolArray(): BooleanArray {
+ return booleanArrayOf(true, false, true)
+ }
+
+ @JvmStatic
+ fun testRetByteArray(): ByteArray {
+ return byteArrayOf(1, 2, 3)
+ }
+
+ @JvmStatic
+ fun testRetCharArray(): CharArray {
+ return "abc".toCharArray()
+ }
+
+ @JvmStatic
+ fun testRetShortArray(): ShortArray {
+ return shortArrayOf(11, 12, 13)
+ }
+
+ @JvmStatic
+ fun testRetIntArray(): IntArray {
+ return intArrayOf(21, 22, 23)
+ }
+
+ @JvmStatic
+ fun testRetLongArray(): LongArray {
+ return longArrayOf(41, 42, 43)
+ }
+
+ @JvmStatic
+ fun testRetFloatArray(): FloatArray {
+ return floatArrayOf(31.1f, 32.2f, 33.3f)
+ }
+
+ @JvmStatic
+ fun testRetDoubleArray(): DoubleArray {
+ return doubleArrayOf(41.1, 42.2, 43.3)
+ }
+
+ @JvmStatic
+ fun testRetWrappedBoolArray(): Array {
+ return arrayOf(true, false, true)
+ }
+
+ @JvmStatic
+ fun testRetWrappedByteArray(): Array {
+ return arrayOf(1, 2, 3)
+ }
+
+ @JvmStatic
+ fun testRetWrappedCharArray(): Array {
+ return arrayOf('a', 'b', 'c')
+ }
+
+ @JvmStatic
+ fun testRetWrappedShortArray(): Array {
+ return arrayOf(11, 12, 13)
+ }
+
+ @JvmStatic
+ fun testRetWrappedIntArray(): Array {
+ return arrayOf(21, 22, 23)
+ }
+
+ @JvmStatic
+ fun testRetWrappedLongArray(): Array {
+ return arrayOf(41, 42, 43)
+ }
+
+ @JvmStatic
+ fun testRetWrappedFloatArray(): Array {
+ return arrayOf(31.1f, 32.2f, 33.3f)
+ }
+
+ @JvmStatic
+ fun testRetWrappedDoubleArray(): Array {
+ return arrayOf(41.1, 42.2, 43.3)
+ }
+
+ @JvmStatic
+ fun testRetObjectArray(): Array {
+ return arrayOf(TestClass2(51), TestClass2(52));
+ }
+
+ @JvmStatic
+ fun testRetStringArray(): Array {
+ return arrayOf("I", "am", "String")
+ }
+
+ @JvmStatic
+ fun testRetCharSequenceArray(): Array {
+ return arrayOf("I", "am", "CharSequence")
+ }
+
+ @JvmStatic
+ fun testObjectOverload(a: TestClass2): String {
+ return "TestClass2: $a"
+ }
+
+ @JvmStatic
+ fun testObjectOverload(a: TestClass3): String {
+ return "TestClass3: $a"
+ }
+
+ @JvmStatic
+ fun testObjectOverloadArray(a: Array): String {
+ return "TestClass2: " + a.contentToString()
+ }
+
+ @JvmStatic
+ fun testObjectOverloadArray(a: Array): String {
+ return "TestClass3: " + a.contentToString()
+ }
+ }
+}
diff --git a/platform/android/java/app/src/instrumented/java/com/godot/game/test/javaclasswrapper/TestClass2.kt b/platform/android/java/app/src/instrumented/java/com/godot/game/test/javaclasswrapper/TestClass2.kt
new file mode 100644
index 00000000000..a44a3e4e302
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/java/com/godot/game/test/javaclasswrapper/TestClass2.kt
@@ -0,0 +1,40 @@
+/**************************************************************************/
+/* TestClass2.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 com.godot.game.test.javaclasswrapper
+
+class TestClass2(private val value: Int) {
+ fun getValue(): Int {
+ return value
+ }
+ override fun toString(): String {
+ return value.toString()
+ }
+}
diff --git a/platform/android/java/app/src/instrumented/java/com/godot/game/test/javaclasswrapper/TestClass3.kt b/platform/android/java/app/src/instrumented/java/com/godot/game/test/javaclasswrapper/TestClass3.kt
new file mode 100644
index 00000000000..b7ea77bfd84
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/java/com/godot/game/test/javaclasswrapper/TestClass3.kt
@@ -0,0 +1,40 @@
+/**************************************************************************/
+/* TestClass3.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 com.godot.game.test.javaclasswrapper
+
+class TestClass3(private val value: String) {
+ fun getValue(): String {
+ return value
+ }
+ override fun toString(): String {
+ return value
+ }
+}
diff --git a/platform/android/java/app/src/instrumented/res/values/strings.xml b/platform/android/java/app/src/instrumented/res/values/strings.xml
new file mode 100644
index 00000000000..326dc5dc64b
--- /dev/null
+++ b/platform/android/java/app/src/instrumented/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Godot App Instrumented Tests
+
diff --git a/platform/android/java/app/src/main/java/com/godot/game/GodotApp.java b/platform/android/java/app/src/main/java/com/godot/game/GodotApp.java
index dac80282726..4bd7359b8a8 100644
--- a/platform/android/java/app/src/main/java/com/godot/game/GodotApp.java
+++ b/platform/android/java/app/src/main/java/com/godot/game/GodotApp.java
@@ -83,4 +83,13 @@ public class GodotApp extends GodotActivity {
super.onGodotMainLoopStarted();
runOnUiThread(updateWindowAppearance);
}
+
+ @Override
+ public void onGodotForceQuit(Godot instance) {
+ if (!BuildConfig.FLAVOR.equals("instrumented")) {
+ // For instrumented builds, we disable force-quitting to allow the instrumented tests to complete
+ // successfully, otherwise they fail when the process crashes.
+ super.onGodotForceQuit(instance);
+ }
+ }
}