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); + } + } }