1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-04 12:00:25 +00:00

Add Android instrumented tests to the app module

This builds on the work from @dsnopek in https://github.com/dsnopek/javaclasswrapper-test, by importing the set of JavaClassWrapper tests from that repo within the Godot core repo in order to bootstrap and standardize how we write and run Android instrumented tests.
The approach used here should serve as a base to build upon to expand the set of instrumented tests used to validate the project's stability.

Co-authored-by: David Snopek <dsnopek@gmail.com>
This commit is contained in:
Fredia Huya-Kouadio
2025-09-23 12:06:40 -04:00
parent 5277c94fe4
commit 16bdc8c4f1
27 changed files with 971 additions and 0 deletions

View File

@@ -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 {

View File

@@ -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")
}
}

View File

@@ -0,0 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<meta-data
android:name="org.godotengine.plugin.v2.GodotAppInstrumentedTestPlugin"
android:value="com.godot.game.test.GodotAppInstrumentedTestPlugin"/>
</application>
</manifest>

View File

@@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

View File

@@ -0,0 +1,3 @@
# Godot 4+ specific ignores
/android/
/.godot/editor

View File

@@ -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"
}]

View File

@@ -0,0 +1,2 @@
source_md5="4cdc64b13a9af63279c486903c9b54cc"
dest_md5="ddbdfc47e6405ad8d8e9e6a88a32824e"

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 813 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H447l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c3 34 55 34 58 0v-86c-3-34-55-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

After

Width:  |  Height:  |  Size: 996 B

View File

@@ -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

View File

@@ -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))

View File

@@ -0,0 +1 @@
uid://bv6y7in6otgcm

View File

@@ -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"]

View File

@@ -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

View File

@@ -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])

View File

@@ -0,0 +1 @@
uid://mofa8j0d801f

View File

@@ -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()), '<JavaObject:java.lang.NullPointerException "java.lang.NullPointerException">')
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]), '<JavaObject:com.godot.game.test.javaclasswrapper.TestClass2 "51">')
assert_equal(str(obj_array[1]), '<JavaObject:com.godot.game.test.javaclasswrapper.TestClass2 "52">')
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)

View File

@@ -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<String, Result<Any>>()
private val latches = ConcurrentHashMap<String, CountDownLatch>()
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<Any>? {
return launchTests(JAVACLASSWRAPPER_TESTS)
}
private fun launchTests(testLabel: String): Result<Any>? {
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<Any> = Result.failure(AssertionError(failureMessage))
completeTest(testLabel, result)
}
private fun completeTest(testKey: String, result: Result<Any>) {
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")
}
}
}

View File

@@ -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<Dictionary> {
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<TestClass2>): 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<Boolean> {
return arrayOf(true, false, true)
}
@JvmStatic
fun testRetWrappedByteArray(): Array<Byte> {
return arrayOf(1, 2, 3)
}
@JvmStatic
fun testRetWrappedCharArray(): Array<Char> {
return arrayOf('a', 'b', 'c')
}
@JvmStatic
fun testRetWrappedShortArray(): Array<Short> {
return arrayOf(11, 12, 13)
}
@JvmStatic
fun testRetWrappedIntArray(): Array<Int> {
return arrayOf(21, 22, 23)
}
@JvmStatic
fun testRetWrappedLongArray(): Array<Long> {
return arrayOf(41, 42, 43)
}
@JvmStatic
fun testRetWrappedFloatArray(): Array<Float> {
return arrayOf(31.1f, 32.2f, 33.3f)
}
@JvmStatic
fun testRetWrappedDoubleArray(): Array<Double> {
return arrayOf(41.1, 42.2, 43.3)
}
@JvmStatic
fun testRetObjectArray(): Array<TestClass2> {
return arrayOf(TestClass2(51), TestClass2(52));
}
@JvmStatic
fun testRetStringArray(): Array<String> {
return arrayOf("I", "am", "String")
}
@JvmStatic
fun testRetCharSequenceArray(): Array<CharSequence> {
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<TestClass2>): String {
return "TestClass2: " + a.contentToString()
}
@JvmStatic
fun testObjectOverloadArray(a: Array<TestClass3>): String {
return "TestClass3: " + a.contentToString()
}
}
}

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="godot_project_name_string">Godot App Instrumented Tests</string>
</resources>

View File

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