From 8735e0ea2a7e0a2bb417e90fe7bca762816e0adb Mon Sep 17 00:00:00 2001 From: David Snopek Date: Sat, 29 Apr 2023 11:56:33 -0500 Subject: [PATCH] Add automated tests that run a GDExtension (rather than just building it) --- .github/workflows/ci.yml | 31 +++++++++++ test/README.md | 5 +- test/demo/default_env.tres | 2 +- test/demo/icon.png.import | 2 +- test/demo/main.gd | 107 +++++++++++++++++++------------------ test/demo/test_base.gd | 59 ++++++++++++++++++++ test/run-tests.sh | 24 +++++++++ test/src/example.cpp | 34 +++++------- test/src/example.h | 2 +- 9 files changed, 188 insertions(+), 78 deletions(-) create mode 100644 test/demo/test_base.gd create mode 100755 test/run-tests.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 062cefa7c..a555e8760 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ jobs: platform: linux artifact-name: godot-cpp-linux-glibc2.27-x86_64-release artifact-path: bin/libgodot-cpp.linux.template_release.x86_64.a + run-tests: true cache-name: linux-x86_64 - name: 🐧 Linux (GCC, Double Precision) @@ -30,6 +31,7 @@ jobs: artifact-name: godot-cpp-linux-glibc2.27-x86_64-double-release artifact-path: bin/libgodot-cpp.linux.template_release.double.x86_64.a flags: precision=double + run-tests: false cache-name: linux-x86_64-f64 - name: 🏁 Windows (x86_64, MSVC) @@ -37,6 +39,7 @@ jobs: platform: windows artifact-name: godot-cpp-windows-msvc2019-x86_64-release artifact-path: bin/libgodot-cpp.windows.template_release.x86_64.lib + run-tests: false cache-name: windows-x86_64-msvc - name: 🏁 Windows (x86_64, MinGW) @@ -45,6 +48,7 @@ jobs: artifact-name: godot-cpp-linux-mingw-x86_64-release artifact-path: bin/libgodot-cpp.windows.template_release.x86_64.a flags: use_mingw=yes + run-tests: false cache-name: windows-x86_64-mingw - name: 🍎 macOS (universal) @@ -53,6 +57,7 @@ jobs: artifact-name: godot-cpp-macos-universal-release artifact-path: bin/libgodot-cpp.macos.template_release.universal.a flags: arch=universal + run-tests: false cache-name: macos-universal - name: 🤖 Android (arm64) @@ -61,6 +66,7 @@ jobs: artifact-name: godot-cpp-android-arm64-release artifact-path: bin/libgodot-cpp.android.template_release.arm64.a flags: ANDROID_NDK_ROOT=$ANDROID_NDK_LATEST_HOME arch=arm64 + run-tests: false cache-name: android-arm64 - name: 🍏 iOS (arm64) @@ -69,6 +75,7 @@ jobs: artifact-name: godot-cpp-ios-arm64-release artifact-path: bin/libgodot-cpp.ios.template_release.arm64.a flags: arch=arm64 + run-tests: false cache-name: ios-arm64 env: @@ -124,6 +131,30 @@ jobs: cd test scons platform=${{ matrix.platform }} target=template_release ${{ matrix.flags }} + - name: Download latest Godot artifacts + uses: dsnopek/action-download-artifact@91dda23aa09c68860977dd0ed11d93c0ed3795e7 + if: ${{ matrix.run-tests }} + with: + repo: godotengine/godot + branch: master + event: push + workflow: linux_builds.yml + workflow_conclusion: success + name: linux-editor-mono + search_artifacts: true + check_artifacts: true + path: godot-artifacts + + - name: Run tests + if: ${{ matrix.run-tests }} + run: | + chmod +x ./godot-artifacts/godot.linuxbsd.editor.x86_64.mono + ./godot-artifacts/godot.linuxbsd.editor.x86_64.mono --headless --version + cd test + # Need to run the editor so .godot is generated... but it crashes! Ignore that :-) + (cd demo && (../../godot-artifacts/godot.linuxbsd.editor.x86_64.mono --editor --headless --quit >/dev/null 2>&1 || true)) + GODOT=../godot-artifacts/godot.linuxbsd.editor.x86_64.mono ./run-tests.sh + - name: Upload artifact uses: actions/upload-artifact@v3 with: diff --git a/test/README.md b/test/README.md index ac8554bc1..5313e33c3 100644 --- a/test/README.md +++ b/test/README.md @@ -1,11 +1,8 @@ -# godot-cpp example / integration test +# godot-cpp integration test This project is used to perform integration testing of the godot-cpp extension, to validate PRs and implemented APIs. -It can also be used as a quick example of how to set up a godot-cpp -project, both on the C++ side and in the Godot project itself. - ## License This is free and unencumbered software released into the public domain. diff --git a/test/demo/default_env.tres b/test/demo/default_env.tres index 770cd8537..0645b88c4 100644 --- a/test/demo/default_env.tres +++ b/test/demo/default_env.tres @@ -4,4 +4,4 @@ [resource] background_mode = 2 -sky = SubResource( "1" ) +sky = SubResource("1") diff --git a/test/demo/icon.png.import b/test/demo/icon.png.import index 36d7be279..8a7c8b0ce 100644 --- a/test/demo/icon.png.import +++ b/test/demo/icon.png.import @@ -16,9 +16,9 @@ dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.cte [params] compress/mode=0 +compress/high_quality=false compress/lossy_quality=0.7 compress/hdr_compression=1 -compress/bptc_ldr=0 compress/normal_map=0 compress/channel_pack=0 mipmaps/generate=false diff --git a/test/demo/main.gd b/test/demo/main.gd index 3858cc776..14498037a 100644 --- a/test/demo/main.gd +++ b/test/demo/main.gd @@ -1,80 +1,85 @@ -extends Node +extends "res://test_base.gd" + +var custom_signal_emitted = null -func _ready(): - # Bind signals - prints("Signal bind") - $Button.button_up.connect($Example.emit_custom_signal.bind("Button", 42)) - prints("") +func _ready(): + # Signal. + $Example.emit_custom_signal("Button", 42) + assert_equal(custom_signal_emitted, ["Button", 42]) # To string. - prints("To string") - prints(" Example --> ", $Example.to_string()) - prints(" ExampleMin --> ", $Example/ExampleMin.to_string()) + assert_equal($Example.to_string(),'Example:[ GDExtension::Example <--> Instance ID:%s ]' % $Example.get_instance_id()) + # It appears there's a bug with instance ids :-( + #assert_equal($Example/ExampleMin.to_string(), 'ExampleMin:[Wrapped:%s]' % $Example/ExampleMin.get_instance_id()) # Call static methods. - prints("Static method calls") - prints(" static (109)", Example.test_static(9, 100)); - Example.test_static2(); + assert_equal($Example.test_static(9, 100), 109); + # It's void and static, so all we know is that it didn't crash. + $Example.test_static2() # Property list. - prints("Property list") $Example.property_from_list = Vector3(100, 200, 300) - prints(" property value ", $Example.property_from_list) + assert_equal($Example.property_from_list, Vector3(100, 200, 300)) # Call methods. - prints("Instance method calls") $Example.simple_func() + assert_equal(custom_signal_emitted, ['simple_func', 3]) ($Example as Example).simple_const_func() # Force use of ptrcall - prints(" returned", $Example.return_something("some string")) - prints(" returned const", $Example.return_something_const()) + assert_equal(custom_signal_emitted, ['simple_const_func', 4]) + assert_equal($Example.return_something("some string"), "some string") + assert_equal($Example.return_something_const(), get_viewport()) var null_ref = $Example.return_empty_ref() - prints(" returned empty ref", null_ref) + assert_equal(null_ref, null) var ret_ref = $Example.return_extended_ref() - prints(" returned ref", ret_ref.get_instance_id(), ", id:", ret_ref.get_id()) - prints(" returned ", $Example.get_v4()) - prints(" test node argument", $Example.test_node_argument($Example)) + assert_not_equal(ret_ref.get_instance_id(), 0) + assert_not_equal(ret_ref.get_id(), 0) + assert_equal($Example.get_v4(), Vector4(1.2, 3.4, 5.6, 7.8)) + assert_equal($Example.test_node_argument($Example), $Example) - prints("VarArg method calls") + # VarArg method calls. var ref = ExampleRef.new() - prints(" sending ref: ", ref.get_instance_id(), "returned ref: ", $Example.extended_ref_checks(ref).get_instance_id()) - prints(" vararg args", $Example.varargs_func("some", "arguments", "to", "test")) - prints(" vararg_nv ret", $Example.varargs_func_nv("some", "arguments", "to", "test")) + assert_not_equal($Example.extended_ref_checks(ref).get_instance_id(), ref.get_instance_id()) + assert_equal($Example.varargs_func("some", "arguments", "to", "test"), 4) + assert_equal($Example.varargs_func_nv("some", "arguments", "to", "test"), 46) $Example.varargs_func_void("some", "arguments", "to", "test") + assert_equal(custom_signal_emitted, ["varargs_func_void", 5]) - prints("Method calls with default values") - prints(" defval (300)", $Example.def_args()) - prints(" defval (250)", $Example.def_args(50)) - prints(" defval (150)", $Example.def_args(50, 100)) + # Method calls with default values. + assert_equal($Example.def_args(), 300) + assert_equal($Example.def_args(50), 250) + assert_equal($Example.def_args(50, 100), 150) - prints("Array and Dictionary") - prints(" test array", $Example.test_array()) - prints(" test tarray", $Example.test_tarray()) - prints(" test dictionary", $Example.test_dictionary()) + # Array and Dictionary + assert_equal($Example.test_array(), [1, 2]) + assert_equal($Example.test_tarray(), [ Vector2(1, 2), Vector2(2, 3) ]) + assert_equal($Example.test_dictionary(), {"hello": "world", "foo": "bar"}) var array: Array[int] = [1, 2, 3] - $Example.test_tarray_arg(array) + assert_equal($Example.test_tarray_arg(array), 6) - prints("String += operator") - prints(" test string +=", $Example.test_string_ops()) + # String += operator + assert_equal($Example.test_string_ops(), "ABCĎE") - prints("PackedArray iterators") - prints(" test packed array iterators", $Example.test_vector_ops()) + # PackedArray iterators + assert_equal($Example.test_vector_ops(), 105) - prints("Properties") - prints(" custom position is", $Example.group_subgroup_custom_position) + # Properties. + assert_equal($Example.group_subgroup_custom_position, Vector2(0, 0)) $Example.group_subgroup_custom_position = Vector2(50, 50) - prints(" custom position now is", $Example.group_subgroup_custom_position) + assert_equal($Example.group_subgroup_custom_position, Vector2(50, 50)) + + # Constants. + assert_equal($Example.FIRST, 0) + assert_equal($Example.ANSWER_TO_EVERYTHING, 42) + assert_equal($Example.CONSTANT_WITHOUT_ENUM, 314) - prints("Constants") - prints(" FIRST", $Example.FIRST) - prints(" ANSWER_TO_EVERYTHING", $Example.ANSWER_TO_EVERYTHING) - prints(" CONSTANT_WITHOUT_ENUM", $Example.CONSTANT_WITHOUT_ENUM) + # BitFields. + assert_equal(Example.FLAG_ONE, 1) + assert_equal(Example.FLAG_TWO, 2) + assert_equal($Example.test_bitfield(0), 0) + assert_equal($Example.test_bitfield(Example.FLAG_ONE | Example.FLAG_TWO), 3) - prints("BitFields") - prints(" FLAG_ONE", Example.FLAG_ONE) - prints(" FLAG_TWO", Example.FLAG_TWO) - prints(" returned BitField", $Example.test_bitfield(0)) - prints(" returned BitField", $Example.test_bitfield(Example.FLAG_ONE | Example.FLAG_TWO)) + exit_with_status() func _on_Example_custom_signal(signal_name, value): - prints("Example emitted:", signal_name, value) + custom_signal_emitted = [signal_name, value] diff --git a/test/demo/test_base.gd b/test/demo/test_base.gd new file mode 100644 index 000000000..7da393c2d --- /dev/null +++ b/test/demo/test_base.gd @@ -0,0 +1,59 @@ +extends Node + +var test_passes := 0 +var test_failures := 0 + +func __get_stack_frame(): + var me = get_script() + for s in get_stack(): + if s.source == me.resource_path: + return s + return null + +func __assert_pass(): + test_passes += 1 + +func __assert_fail(): + test_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]) + +func assert_true(v): + assert_equal(v, true) + +func assert_false(v): + assert_equal(v, false) + +func assert_not_equal(actual, expected): + if actual != expected: + __assert_pass() + else: + __assert_fail() + print (" |-> Expected '%s' NOT to equal '%s'" % [expected, actual]) + +func exit_with_status() -> void: + var success: bool = (test_failures == 0) + print ("") + print_rich ("[color=%s] ==== TESTS FINISHED ==== [/color]" % ("green" if success else "red")) + print ("") + print_rich (" PASSES: [color=green]%s[/color]" % test_passes) + print_rich (" FAILURES: [color=red]%s[/color]" % test_failures) + print ("") + + if success: + print_rich("[color=green] ******** PASSED ******** [/color]") + else: + print_rich("[color=red] ******** FAILED ********[/color]") + print("") + + get_tree().quit(0 if success else 1) diff --git a/test/run-tests.sh b/test/run-tests.sh new file mode 100755 index 000000000..730cc14e1 --- /dev/null +++ b/test/run-tests.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +GODOT=${GODOT:-godot} + +END_STRING="==== TESTS FINISHED ====" +FAILURE_STRING="******** FAILED ********" + +OUTPUT=$($GODOT --path demo --debug --headless --quit) +ERRCODE=$? + +echo "$OUTPUT" +echo + +if ! echo "$OUTPUT" | grep -e "$END_STRING" >/dev/null; then + echo "ERROR: Tests failed to complete" + exit 1 +fi + +if echo "$OUTPUT" | grep -e "$FAILURE_STRING" >/dev/null; then + exit 1 +fi + +# Success! +exit 0 diff --git a/test/src/example.cpp b/test/src/example.cpp index a94175d1d..6a9b2c344 100644 --- a/test/src/example.cpp +++ b/test/src/example.cpp @@ -28,12 +28,12 @@ ExampleRef::ExampleRef() { id = ++last_id; instance_count++; - UtilityFunctions::print("ExampleRef ", itos(id), " created, current instance count: ", itos(instance_count)); + //UtilityFunctions::print("ExampleRef ", itos(id), " created, current instance count: ", itos(instance_count)); } ExampleRef::~ExampleRef() { instance_count--; - UtilityFunctions::print("ExampleRef ", itos(id), " destroyed, current instance count: ", itos(instance_count)); + //UtilityFunctions::print("ExampleRef ", itos(id), " destroyed, current instance count: ", itos(instance_count)); } int Example::test_static(int p_a, int p_b) { @@ -41,7 +41,7 @@ int Example::test_static(int p_a, int p_b) { } void Example::test_static2() { - UtilityFunctions::print(" void static"); + //UtilityFunctions::print(" void static"); } int Example::def_args(int p_a, int p_b) { @@ -49,7 +49,7 @@ int Example::def_args(int p_a, int p_b) { } void Example::_notification(int p_what) { - UtilityFunctions::print("Notification: ", String::num(p_what)); + //UtilityFunctions::print("Notification: ", String::num(p_what)); } bool Example::_set(const StringName &p_name, const Variant &p_value) { @@ -179,29 +179,27 @@ void Example::_bind_methods() { } Example::Example() { - UtilityFunctions::print("Constructor."); + //UtilityFunctions::print("Constructor."); } Example::~Example() { - UtilityFunctions::print("Destructor."); + //UtilityFunctions::print("Destructor."); } // Methods. void Example::simple_func() { - UtilityFunctions::print(" Simple func called."); + emit_custom_signal("simple_func", 3); } void Example::simple_const_func() const { - UtilityFunctions::print(" Simple const func called."); + ((Example *)this)->emit_custom_signal("simple_const_func", 4); } String Example::return_something(const String &base) { - UtilityFunctions::print(" Return something called."); return base; } Viewport *Example::return_something_const() const { - UtilityFunctions::print(" Return something const called."); if (is_inside_tree()) { Viewport *result = get_viewport(); return result; @@ -222,7 +220,6 @@ ExampleRef *Example::return_extended_ref() const { } Example *Example::test_node_argument(Example *p_node) const { - UtilityFunctions::print(" Test node argument called with ", p_node ? String::num(p_node->get_instance_id()) : "null"); return p_node; } @@ -230,23 +227,19 @@ Ref Example::extended_ref_checks(Ref p_ref) const { // This is therefor the prefered way of instancing and returning a refcounted object: Ref ref; ref.instantiate(); - - UtilityFunctions::print(" Example ref checks called with value: ", p_ref->get_instance_id(), ", returning value: ", ref->get_instance_id()); return ref; } Variant Example::varargs_func(const Variant **args, GDExtensionInt arg_count, GDExtensionCallError &error) { - UtilityFunctions::print(" Varargs (Variant return) called with ", String::num((double)arg_count), " arguments"); return arg_count; } int Example::varargs_func_nv(const Variant **args, GDExtensionInt arg_count, GDExtensionCallError &error) { - UtilityFunctions::print(" Varargs (int return) called with ", String::num((double)arg_count), " arguments"); - return 42; + return 42 + arg_count; } void Example::varargs_func_void(const Variant **args, GDExtensionInt arg_count, GDExtensionCallError &error) { - UtilityFunctions::print(" Varargs (no return) called with ", String::num((double)arg_count), " arguments"); + emit_custom_signal("varargs_func_void", arg_count + 1); } void Example::emit_custom_signal(const String &name, int value) { @@ -285,10 +278,12 @@ int Example::test_vector_ops() const { return ret; } -void Example::test_tarray_arg(const TypedArray &p_array) { +int Example::test_tarray_arg(const TypedArray &p_array) { + int sum = 0; for (int i = 0; i < p_array.size(); i++) { - UtilityFunctions::print(p_array[i]); + sum += (int)p_array[i]; } + return sum; } TypedArray Example::test_tarray() const { @@ -311,7 +306,6 @@ Dictionary Example::test_dictionary() const { } BitField Example::test_bitfield(BitField flags) { - UtilityFunctions::print(" Got BitField: ", String::num_int64(flags)); return flags; } diff --git a/test/src/example.h b/test/src/example.h index ab9c8c201..3f1873683 100644 --- a/test/src/example.h +++ b/test/src/example.h @@ -102,7 +102,7 @@ class Example : public Control { int def_args(int p_a = 100, int p_b = 200); Array test_array() const; - void test_tarray_arg(const TypedArray &p_array); + int test_tarray_arg(const TypedArray &p_array); TypedArray test_tarray() const; Dictionary test_dictionary() const; Example *test_node_argument(Example *p_node) const;