summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/SCsub9
-rw-r--r--tests/core/input/test_input_event_key.h298
-rw-r--r--tests/core/input/test_shortcut.h235
-rw-r--r--tests/core/io/test_config_file.h164
-rw-r--r--tests/core/io/test_file_access.h106
-rw-r--r--tests/core/io/test_image.h304
-rw-r--r--tests/core/io/test_json.h151
-rw-r--r--tests/core/io/test_marshalls.h331
-rw-r--r--tests/core/io/test_pck_packer.h122
-rw-r--r--tests/core/io/test_resource.h114
-rw-r--r--tests/core/io/test_xml_parser.h235
-rw-r--r--tests/core/math/test_aabb.h415
-rw-r--r--tests/core/math/test_astar.h (renamed from tests/test_astar.h)32
-rw-r--r--tests/core/math/test_basis.h (renamed from tests/test_basis.h)245
-rw-r--r--tests/core/math/test_color.h (renamed from tests/test_color.h)44
-rw-r--r--tests/core/math/test_expression.h (renamed from tests/test_expression.h)37
-rw-r--r--tests/core/math/test_geometry_2d.h866
-rw-r--r--tests/core/math/test_geometry_3d.h437
-rw-r--r--tests/core/math/test_plane.h195
-rw-r--r--tests/core/math/test_quaternion.h446
-rw-r--r--tests/core/math/test_random_number_generator.h275
-rw-r--r--tests/core/math/test_rect2.h326
-rw-r--r--tests/core/math/test_rect2i.h311
-rw-r--r--tests/core/math/test_transform_2d.h122
-rw-r--r--tests/core/math/test_transform_3d.h112
-rw-r--r--tests/core/math/test_vector2.h496
-rw-r--r--tests/core/math/test_vector2i.h161
-rw-r--r--tests/core/math/test_vector3.h529
-rw-r--r--tests/core/math/test_vector3i.h149
-rw-r--r--tests/core/math/test_vector4.h397
-rw-r--r--tests/core/math/test_vector4i.h152
-rw-r--r--tests/core/object/test_class_db.h (renamed from tests/test_class_db.h)324
-rw-r--r--tests/core/object/test_method_bind.h175
-rw-r--r--tests/core/object/test_object.h286
-rw-r--r--tests/core/os/test_os.h158
-rw-r--r--tests/core/string/test_node_path.h172
-rw-r--r--tests/core/string/test_string.h (renamed from tests/test_string.h)703
-rw-r--r--tests/core/string/test_translation.h182
-rw-r--r--tests/core/templates/test_command_queue.h427
-rw-r--r--tests/core/templates/test_hash_map.h (renamed from tests/test_ordered_hash_map.h)74
-rw-r--r--tests/core/templates/test_hash_set.h226
-rw-r--r--tests/core/templates/test_list.h (renamed from tests/test_list.h)277
-rw-r--r--tests/core/templates/test_local_vector.h240
-rw-r--r--tests/core/templates/test_lru.h (renamed from tests/test_physics_2d.h)76
-rw-r--r--tests/core/templates/test_paged_array.h153
-rw-r--r--tests/core/templates/test_rid.h101
-rw-r--r--tests/core/templates/test_vector.h537
-rw-r--r--tests/core/test_crypto.h73
-rw-r--r--tests/core/test_hashing_context.h165
-rw-r--r--tests/core/test_time.h148
-rw-r--r--tests/core/threads/test_worker_thread_pool.h158
-rw-r--r--tests/core/variant/test_array.h521
-rw-r--r--tests/core/variant/test_dictionary.h523
-rw-r--r--tests/core/variant/test_variant.h978
-rw-r--r--tests/create_test.py132
-rw-r--r--tests/data/images/icon.bmpbin0 -> 262282 bytes
-rw-r--r--tests/data/images/icon.jpgbin0 -> 14866 bytes
-rw-r--r--tests/data/images/icon.pngbin0 -> 29261 bytes
-rw-r--r--tests/data/images/icon.tgabin0 -> 262162 bytes
-rw-r--r--tests/data/images/icon.webpbin0 -> 4522 bytes
-rw-r--r--tests/data/line_endings_cr.test.txt1
-rw-r--r--tests/data/line_endings_crlf.test.txt4
-rw-r--r--tests/data/line_endings_lf.test.txt4
-rw-r--r--tests/data/translations.csv8
-rw-r--r--tests/python_build/conftest.py26
-rw-r--r--tests/python_build/fixtures/gles3/_included.glsl1
-rw-r--r--tests/python_build/fixtures/gles3/vertex_fragment.glsl34
-rw-r--r--tests/python_build/fixtures/gles3/vertex_fragment_expected_full.glsl50
-rw-r--r--tests/python_build/fixtures/gles3/vertex_fragment_expected_parts.json52
-rw-r--r--tests/python_build/fixtures/glsl/_included.glsl1
-rw-r--r--tests/python_build/fixtures/glsl/compute.glsl12
-rw-r--r--tests/python_build/fixtures/glsl/compute_expected_full.glsl8
-rw-r--r--tests/python_build/fixtures/glsl/compute_expected_parts.json3
-rw-r--r--tests/python_build/fixtures/glsl/vertex_fragment.glsl32
-rw-r--r--tests/python_build/fixtures/glsl/vertex_fragment_expected_full.glsl8
-rw-r--r--tests/python_build/fixtures/glsl/vertex_fragment_expected_parts.json3
-rw-r--r--tests/python_build/fixtures/rd_glsl/_included.glsl1
-rw-r--r--tests/python_build/fixtures/rd_glsl/compute.glsl13
-rw-r--r--tests/python_build/fixtures/rd_glsl/compute_expected_full.glsl20
-rw-r--r--tests/python_build/fixtures/rd_glsl/compute_expected_parts.json28
-rw-r--r--tests/python_build/fixtures/rd_glsl/vertex_fragment.glsl25
-rw-r--r--tests/python_build/fixtures/rd_glsl/vertex_fragment_expected_full.glsl23
-rw-r--r--tests/python_build/fixtures/rd_glsl/vertex_fragment_expected_parts.json40
-rw-r--r--tests/python_build/test_gles3_builder.py31
-rw-r--r--tests/python_build/test_glsl_builder.py37
-rw-r--r--tests/scene/test_animation.h314
-rw-r--r--tests/scene/test_arraymesh.h321
-rw-r--r--tests/scene/test_audio_stream_wav.h243
-rw-r--r--tests/scene/test_bit_map.h441
-rw-r--r--tests/scene/test_code_edit.h3393
-rw-r--r--tests/scene/test_curve.h254
-rw-r--r--tests/scene/test_gradient.h (renamed from tests/test_gradient.h)7
-rw-r--r--tests/scene/test_path_3d.h84
-rw-r--r--tests/scene/test_path_follow_2d.h240
-rw-r--r--tests/scene/test_path_follow_3d.h219
-rw-r--r--tests/scene/test_sprite_frames.h247
-rw-r--r--tests/scene/test_text_edit.h4264
-rw-r--r--tests/scene/test_theme.h271
-rw-r--r--tests/servers/test_text_server.h629
-rw-r--r--tests/test_gui.cpp271
-rw-r--r--tests/test_macros.cpp8
-rw-r--r--tests/test_macros.h315
-rw-r--r--tests/test_main.cpp305
-rw-r--r--tests/test_main.h4
-rw-r--r--tests/test_math.cpp703
-rw-r--r--tests/test_oa_hash_map.cpp299
-rw-r--r--tests/test_physics_2d.cpp407
-rw-r--r--tests/test_physics_3d.cpp413
-rw-r--r--tests/test_physics_3d.h41
-rw-r--r--tests/test_render.cpp240
-rw-r--r--tests/test_render.h41
-rw-r--r--tests/test_shader_lang.cpp361
-rw-r--r--tests/test_shader_lang.h41
-rw-r--r--tests/test_tools.h (renamed from tests/test_oa_hash_map.h)38
-rw-r--r--tests/test_utils.cpp (renamed from tests/test_math.h)21
-rw-r--r--tests/test_utils.h (renamed from tests/test_gui.h)21
-rw-r--r--tests/test_validate_testing.h29
-rw-r--r--tests/test_variant.h111
118 files changed, 26140 insertions, 3546 deletions
diff --git a/tests/SCsub b/tests/SCsub
index 7aab28531d..c59ce69b92 100644
--- a/tests/SCsub
+++ b/tests/SCsub
@@ -6,10 +6,6 @@ env.tests_sources = []
env_tests = env.Clone()
-# Include GDNative headers.
-if env["module_gdnative_enabled"]:
- env_tests.Append(CPPPATH=["#modules/gdnative/include"])
-
# We must disable the THREAD_LOCAL entirely in doctest to prevent crashes on debugging
# Since we link with /MT thread_local is always expired when the header is used
# So the debugger crashes the engine and it causes weird errors
@@ -17,6 +13,11 @@ if env["module_gdnative_enabled"]:
if env_tests["platform"] == "windows":
env_tests.Append(CPPDEFINES=[("DOCTEST_THREAD_LOCAL", "")])
+# Increase number of addressable sections in object files
+# due to doctest's heavy use of templates and macros.
+if env_tests.msvc:
+ env_tests.Append(CCFLAGS=["/bigobj"])
+
env_tests.add_source_files(env.tests_sources, "*.cpp")
lib = env_tests.add_library("tests", env.tests_sources)
diff --git a/tests/core/input/test_input_event_key.h b/tests/core/input/test_input_event_key.h
new file mode 100644
index 0000000000..ef0a656b18
--- /dev/null
+++ b/tests/core/input/test_input_event_key.h
@@ -0,0 +1,298 @@
+/*************************************************************************/
+/* test_input_event_key.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_INPUT_EVENT_KEY_H
+#define TEST_INPUT_EVENT_KEY_H
+
+#include "core/input/input_event.h"
+#include "core/os/keyboard.h"
+
+#include "tests/test_macros.h"
+
+namespace TestInputEventKey {
+
+TEST_CASE("[InputEventKey] Key correctly registers being pressed") {
+ InputEventKey key;
+ key.set_pressed(true);
+ CHECK(key.is_pressed() == true);
+
+ key.set_pressed(false);
+ CHECK(key.is_pressed() == false);
+}
+
+TEST_CASE("[InputEventKey] Key correctly stores and retrieves keycode") {
+ InputEventKey key;
+
+ key.set_keycode(Key::ENTER);
+ CHECK(key.get_keycode() == Key::ENTER);
+ CHECK(key.get_keycode() != Key::PAUSE);
+
+ key.set_physical_keycode(Key::BACKSPACE);
+ CHECK(key.get_physical_keycode() == Key::BACKSPACE);
+ CHECK(key.get_physical_keycode() != Key::PAUSE);
+}
+
+TEST_CASE("[InputEventKey] Key correctly stores and retrieves keycode with modifiers") {
+ InputEventKey key;
+
+ key.set_keycode(Key::ENTER);
+ key.set_ctrl_pressed(true);
+
+ CHECK(key.get_keycode_with_modifiers() == (Key::ENTER | KeyModifierMask::CTRL));
+ CHECK(key.get_keycode_with_modifiers() != (Key::ENTER | KeyModifierMask::SHIFT));
+ CHECK(key.get_keycode_with_modifiers() != Key::ENTER);
+
+ key.set_physical_keycode(Key::SPACE);
+ key.set_ctrl_pressed(true);
+
+ CHECK(key.get_physical_keycode_with_modifiers() == (Key::SPACE | KeyModifierMask::CTRL));
+ CHECK(key.get_physical_keycode_with_modifiers() != (Key::SPACE | KeyModifierMask::SHIFT));
+ CHECK(key.get_physical_keycode_with_modifiers() != Key::SPACE);
+}
+
+TEST_CASE("[InputEventKey] Key correctly stores and retrieves unicode") {
+ InputEventKey key;
+
+ key.set_unicode('x');
+ CHECK(key.get_unicode() == 'x');
+ CHECK(key.get_unicode() != 'y');
+}
+
+TEST_CASE("[InputEventKey] Key correctly stores and checks echo") {
+ InputEventKey key;
+
+ key.set_echo(true);
+ CHECK(key.is_echo() == true);
+
+ key.set_echo(false);
+ CHECK(key.is_echo() == false);
+}
+
+TEST_CASE("[InputEventKey] Key correctly converts itself to text") {
+ InputEventKey none_key;
+
+ // These next three tests test the functionality of getting a key that is set to None
+ // as text. These cases are a bit weird, since None has no textual representation
+ // (find_keycode_name(Key::NONE) results in a nullptr). Thus, these tests look weird
+ // with only (Physical) or a lonely modifier with (Physical) but (as far as I
+ // understand the code, that is intended behaviour.
+
+ // Key is None without a physical key.
+ none_key.set_keycode(Key::NONE);
+ CHECK(none_key.as_text() == " (Physical)");
+
+ // Key is none and has modifiers.
+ none_key.set_ctrl_pressed(true);
+ CHECK(none_key.as_text() == "Ctrl+ (Physical)");
+
+ // Key is None WITH a physical key AND modifiers.
+ none_key.set_physical_keycode(Key::ENTER);
+ CHECK(none_key.as_text() == "Ctrl+Enter (Physical)");
+
+ InputEventKey none_key2;
+
+ // Key is None without modifiers with a physical key.
+ none_key2.set_keycode(Key::NONE);
+ none_key2.set_physical_keycode(Key::ENTER);
+
+ CHECK(none_key2.as_text() == "Enter (Physical)");
+
+ InputEventKey key;
+
+ // Key has keycode.
+ key.set_keycode(Key::SPACE);
+ CHECK(key.as_text() != "");
+ CHECK(key.as_text() == "Space");
+
+ // Key has keycode and modifiers.
+ key.set_ctrl_pressed(true);
+ CHECK(key.as_text() != "Space");
+ CHECK(key.as_text() == "Ctrl+Space");
+
+ // Since the keycode is set to Key::NONE upon initialization of the
+ // InputEventKey and you can only update it with another Key, the keycode
+ // cannot be empty, so the kc.is_empty() case cannot be tested.
+}
+
+TEST_CASE("[InputEventKey] Key correctly converts its state to a string representation") {
+ InputEventKey none_key;
+
+ // Set physical to true.
+ CHECK(none_key.to_string() == "InputEventKey: keycode=0 (), mods=none, physical=true, pressed=false, echo=false");
+ // Set physical key to Escape.
+ none_key.set_physical_keycode(Key::ESCAPE);
+ CHECK(none_key.to_string() == "InputEventKey: keycode=4194305 (Escape), mods=none, physical=true, pressed=false, echo=false");
+
+ InputEventKey key;
+
+ // Set physical to None, set keycode to Space.
+ key.set_keycode(Key::SPACE);
+ CHECK(key.to_string() == "InputEventKey: keycode=32 (Space), mods=none, physical=false, pressed=false, echo=false");
+
+ // Set pressed to true.
+ key.set_pressed(true);
+ CHECK(key.to_string() == "InputEventKey: keycode=32 (Space), mods=none, physical=false, pressed=true, echo=false");
+
+ // set echo to true.
+ key.set_echo(true);
+ CHECK(key.to_string() == "InputEventKey: keycode=32 (Space), mods=none, physical=false, pressed=true, echo=true");
+
+ // Press Ctrl and Alt.
+ key.set_ctrl_pressed(true);
+ key.set_alt_pressed(true);
+#ifdef MACOS_ENABLED
+ CHECK(key.to_string() == "InputEventKey: keycode=32 (Space), mods=Ctrl+Option, physical=false, pressed=true, echo=true");
+#else
+ CHECK(key.to_string() == "InputEventKey: keycode=32 (Space), mods=Ctrl+Alt, physical=false, pressed=true, echo=true");
+#endif
+}
+
+TEST_CASE("[InputEventKey] Key is correctly converted to reference") {
+ InputEventKey base_key;
+ Ref<InputEventKey> key_ref = base_key.create_reference(Key::ENTER);
+
+ CHECK(key_ref->get_keycode() == Key::ENTER);
+}
+
+TEST_CASE("[InputEventKey] Keys are correctly matched based on action") {
+ bool pressed = false;
+ float strength, raw_strength = 0.0;
+
+ InputEventKey key;
+
+ // Nullptr.
+ CHECK_MESSAGE(key.action_match(nullptr, false, 0.0f, &pressed, &strength, &raw_strength) == false, "nullptr as key reference should result in false");
+
+ // Match on keycode.
+ key.set_keycode(Key::SPACE);
+ Ref<InputEventKey> match = key.create_reference(Key::SPACE);
+ Ref<InputEventKey> no_match = key.create_reference(Key::ENTER);
+
+ CHECK(key.action_match(match, false, 0.0f, &pressed, &strength, &raw_strength) == true);
+ CHECK(key.action_match(no_match, false, 0.0f, &pressed, &strength, &raw_strength) == false);
+
+ // Check that values are correctly transferred to the pointers.
+ CHECK(pressed == false);
+ CHECK(strength < 0.5);
+ CHECK(raw_strength < 0.5);
+
+ match->set_pressed(true);
+ key.action_match(match, false, 0.0f, &pressed, &strength, &raw_strength);
+
+ CHECK(pressed == true);
+ CHECK(strength > 0.5);
+ CHECK(raw_strength > 0.5);
+
+ // Tests when keycode is None: Then you rely on physical keycode.
+ InputEventKey none_key;
+ none_key.set_physical_keycode(Key::SPACE);
+
+ Ref<InputEventKey> match_none = none_key.create_reference(Key::NONE);
+ match_none->set_physical_keycode(Key::SPACE);
+
+ Ref<InputEventKey> no_match_none = none_key.create_reference(Key::NONE);
+ no_match_none->set_physical_keycode(Key::ENTER);
+
+ CHECK(none_key.action_match(match_none, false, 0.0f, &pressed, &strength, &raw_strength) == true);
+ CHECK(none_key.action_match(no_match_none, false, 0.0f, &pressed, &strength, &raw_strength) == false);
+
+ // Test exact match.
+ InputEventKey key2, ref_key;
+ key2.set_keycode(Key::SPACE);
+
+ Ref<InputEventKey> match2 = ref_key.create_reference(Key::SPACE);
+
+ // Now both press Ctrl and Shift.
+ key2.set_ctrl_pressed(true);
+ key2.set_shift_pressed(true);
+
+ match2->set_ctrl_pressed(true);
+ match2->set_shift_pressed(true);
+
+ // Now they should match.
+ bool exact_match = true;
+ CHECK(key2.action_match(match2, exact_match, 0.0f, &pressed, &strength, &raw_strength) == true);
+
+ // Modify matching key such that it does no longer match in terms of modifiers: Shift
+ // is no longer pressed.
+ match2->set_shift_pressed(false);
+ CHECK(match2->is_shift_pressed() == false);
+ CHECK(key2.action_match(match2, exact_match, 0.0f, &pressed, &strength, &raw_strength) == false);
+}
+
+TEST_CASE("[IsMatch] Keys are correctly matched") {
+ // Key with NONE as keycode.
+ InputEventKey key;
+ key.set_keycode(Key::NONE);
+ key.set_physical_keycode(Key::SPACE);
+
+ // Nullptr.
+ CHECK(key.is_match(nullptr, false) == false);
+
+ Ref<InputEventKey> none_ref = key.create_reference(Key::NONE);
+
+ none_ref->set_physical_keycode(Key::SPACE);
+ CHECK(key.is_match(none_ref, false) == true);
+
+ none_ref->set_physical_keycode(Key::ENTER);
+ CHECK(key.is_match(none_ref, false) == false);
+
+ none_ref->set_physical_keycode(Key::SPACE);
+
+ key.set_ctrl_pressed(true);
+ none_ref->set_ctrl_pressed(false);
+ CHECK(key.is_match(none_ref, true) == false);
+
+ none_ref->set_ctrl_pressed(true);
+ CHECK(key.is_match(none_ref, true) == true);
+
+ // Ref with actual keycode.
+ InputEventKey key2;
+ key2.set_keycode(Key::SPACE);
+
+ Ref<InputEventKey> match = key2.create_reference(Key::SPACE);
+ Ref<InputEventKey> no_match = key2.create_reference(Key::ENTER);
+
+ CHECK(key2.is_match(match, false) == true);
+ CHECK(key2.is_match(no_match, false) == false);
+
+ // Now the keycode is the same, but the modifiers differ.
+ no_match->set_keycode(Key::SPACE);
+
+ key2.set_ctrl_pressed(true);
+ match->set_ctrl_pressed(true);
+ no_match->set_shift_pressed(true);
+
+ CHECK(key2.is_match(match, true) == true);
+ CHECK(key2.is_match(no_match, true) == false);
+}
+} // namespace TestInputEventKey
+
+#endif // TEST_INPUT_EVENT_KEY_H
diff --git a/tests/core/input/test_shortcut.h b/tests/core/input/test_shortcut.h
new file mode 100644
index 0000000000..93edc685ad
--- /dev/null
+++ b/tests/core/input/test_shortcut.h
@@ -0,0 +1,235 @@
+/*************************************************************************/
+/* test_shortcut.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_SHORTCUT_H
+#define TEST_SHORTCUT_H
+
+#include "core/input/input_event.h"
+#include "core/input/shortcut.h"
+#include "core/io/config_file.h"
+#include "core/object/ref_counted.h"
+#include "core/os/keyboard.h"
+#include "core/os/os.h"
+
+#include "tests/test_macros.h"
+
+namespace TestShortcut {
+
+TEST_CASE("[Shortcut] Empty shortcut should have no valid events and text equal to None") {
+ Shortcut s;
+
+ CHECK(s.get_as_text() == "None");
+ CHECK(s.has_valid_event() == false);
+}
+
+TEST_CASE("[Shortcut] Setting and getting an event should result in the same event as the input") {
+ Ref<InputEventKey> k1;
+ Ref<InputEventKey> k2;
+ k1.instantiate();
+ k2.instantiate();
+
+ k1->set_keycode(Key::ENTER);
+ k2->set_keycode(Key::BACKSPACE);
+
+ // Cast to InputEvent so the internal code recognizes the objects.
+ Ref<InputEvent> e1 = k1;
+ Ref<InputEvent> e2 = k2;
+
+ Array input_array;
+ input_array.append(e1);
+ input_array.append(e2);
+
+ Shortcut s;
+ s.set_events(input_array);
+
+ // Get result, read it out, check whether it equals the input.
+ Array result_array = s.get_events();
+ Ref<InputEventKey> result1 = result_array.front();
+ Ref<InputEventKey> result2 = result_array.back();
+
+ CHECK(result1->get_keycode() == k1->get_keycode());
+ CHECK(result2->get_keycode() == k2->get_keycode());
+}
+
+TEST_CASE("[Shortcut] 'set_events_list' should result in the same events as the input") {
+ Ref<InputEventKey> k1;
+ Ref<InputEventKey> k2;
+ k1.instantiate();
+ k2.instantiate();
+
+ k1->set_keycode(Key::ENTER);
+ k2->set_keycode(Key::BACKSPACE);
+
+ // Cast to InputEvent so the set_events_list() method recognizes the objects.
+ Ref<InputEvent> e1 = k1;
+ Ref<InputEvent> e2 = k2;
+
+ List<Ref<InputEvent>> list;
+ list.push_back(e1);
+ list.push_back(e2);
+
+ Shortcut s;
+ s.set_events_list(&list);
+
+ // Get result, read it out, check whether it equals the input.
+ Array result_array = s.get_events();
+ Ref<InputEventKey> result1 = result_array.front();
+ Ref<InputEventKey> result2 = result_array.back();
+
+ CHECK(result1->get_keycode() == k1->get_keycode());
+ CHECK(result2->get_keycode() == k2->get_keycode());
+}
+
+TEST_CASE("[Shortcut] 'matches_event' should correctly match the same event") {
+ Ref<InputEventKey> original; // The one we compare with.
+ Ref<InputEventKey> similar_but_not_equal; // Same keycode, different event.
+ Ref<InputEventKey> different; // Different event, different keycode.
+ Ref<InputEventKey> copy; // Copy of original event.
+
+ original.instantiate();
+ similar_but_not_equal.instantiate();
+ different.instantiate();
+ copy.instantiate();
+
+ original->set_keycode(Key::ENTER);
+ similar_but_not_equal->set_keycode(Key::ENTER);
+ similar_but_not_equal->set_keycode(Key::ESCAPE);
+ copy = original;
+
+ // Only the copy is really the same, so only that one should match.
+ // The rest should not match.
+
+ Ref<InputEvent> e_original = original;
+
+ Ref<InputEvent> e_similar_but_not_equal = similar_but_not_equal;
+ Ref<InputEvent> e_different = different;
+ Ref<InputEvent> e_copy = copy;
+
+ Array a;
+ a.append(e_original);
+ Shortcut s;
+ s.set_events(a);
+
+ CHECK(s.matches_event(e_similar_but_not_equal) == false);
+ CHECK(s.matches_event(e_different) == false);
+
+ CHECK(s.matches_event(e_copy) == true);
+}
+
+TEST_CASE("[Shortcut] 'get_as_text' text representation should be correct") {
+ Ref<InputEventKey> same;
+ // k2 will not go into the shortcut but only be used to compare.
+ Ref<InputEventKey> different;
+
+ same.instantiate();
+ different.instantiate();
+
+ same->set_keycode(Key::ENTER);
+ different->set_keycode(Key::ESCAPE);
+
+ Ref<InputEvent> key_event1 = same;
+
+ Array a;
+ a.append(key_event1);
+ Shortcut s;
+ s.set_events(a);
+
+ CHECK(s.get_as_text() == same->as_text());
+ CHECK(s.get_as_text() != different->as_text());
+}
+
+TEST_CASE("[Shortcut] Event validity should be correctly checked.") {
+ Ref<InputEventKey> valid;
+ // k2 will not go into the shortcut but only be used to compare.
+ Ref<InputEventKey> invalid = nullptr;
+
+ valid.instantiate();
+ valid->set_keycode(Key::ENTER);
+
+ Ref<InputEvent> valid_event = valid;
+ Ref<InputEvent> invalid_event = invalid;
+
+ Array a;
+ a.append(invalid_event);
+ a.append(valid_event);
+
+ Shortcut s;
+ s.set_events(a);
+
+ CHECK(s.has_valid_event() == true);
+
+ Array b;
+ b.append(invalid_event);
+
+ Shortcut shortcut_with_invalid_event;
+ shortcut_with_invalid_event.set_events(b);
+
+ CHECK(shortcut_with_invalid_event.has_valid_event() == false);
+}
+
+TEST_CASE("[Shortcut] Equal arrays should be recognized as such.") {
+ Ref<InputEventKey> k1;
+ // k2 will not go into the shortcut but only be used to compare.
+ Ref<InputEventKey> k2;
+
+ k1.instantiate();
+ k2.instantiate();
+
+ k1->set_keycode(Key::ENTER);
+ k2->set_keycode(Key::ESCAPE);
+
+ Ref<InputEvent> key_event1 = k1;
+ Ref<InputEvent> key_event2 = k2;
+
+ Array same;
+ same.append(key_event1);
+
+ Array same_as_same;
+ same_as_same.append(key_event1);
+
+ Array different1;
+ different1.append(key_event2);
+
+ Array different2;
+ different2.append(key_event1);
+ different2.append(key_event2);
+
+ Array different3;
+
+ Shortcut s;
+
+ CHECK(s.is_event_array_equal(same, same_as_same) == true);
+ CHECK(s.is_event_array_equal(same, different1) == false);
+ CHECK(s.is_event_array_equal(same, different2) == false);
+ CHECK(s.is_event_array_equal(same, different3) == false);
+}
+} // namespace TestShortcut
+
+#endif // TEST_SHORTCUT_H
diff --git a/tests/core/io/test_config_file.h b/tests/core/io/test_config_file.h
new file mode 100644
index 0000000000..666719febb
--- /dev/null
+++ b/tests/core/io/test_config_file.h
@@ -0,0 +1,164 @@
+/*************************************************************************/
+/* test_config_file.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_CONFIG_FILE_H
+#define TEST_CONFIG_FILE_H
+
+#include "core/io/config_file.h"
+#include "core/os/os.h"
+
+#include "tests/test_macros.h"
+
+namespace TestConfigFile {
+
+TEST_CASE("[ConfigFile] Parsing well-formatted files") {
+ ConfigFile config_file;
+ // Formatting is intentionally hand-edited to see how human-friendly the parser is.
+ const Error error = config_file.parse(R"(
+[player]
+
+name = "Unnamed Player"
+tagline="Waiting
+for
+Godot"
+
+color =Color( 0, 0.5,1, 1) ; Inline comment
+position= Vector2(
+ 3,
+ 4
+)
+
+[graphics]
+
+antialiasing = true
+
+; Testing comments and case-sensitivity...
+antiAliasing = false
+)");
+
+ CHECK_MESSAGE(error == OK, "The configuration file should parse successfully.");
+ CHECK_MESSAGE(
+ String(config_file.get_value("player", "name")) == "Unnamed Player",
+ "Reading `player/name` should return the expected value.");
+ CHECK_MESSAGE(
+ String(config_file.get_value("player", "tagline")) == "Waiting\nfor\nGodot",
+ "Reading `player/tagline` should return the expected value.");
+ CHECK_MESSAGE(
+ Color(config_file.get_value("player", "color")).is_equal_approx(Color(0, 0.5, 1)),
+ "Reading `player/color` should return the expected value.");
+ CHECK_MESSAGE(
+ Vector2(config_file.get_value("player", "position")).is_equal_approx(Vector2(3, 4)),
+ "Reading `player/position` should return the expected value.");
+ CHECK_MESSAGE(
+ bool(config_file.get_value("graphics", "antialiasing")),
+ "Reading `graphics/antialiasing` should return `true`.");
+ CHECK_MESSAGE(
+ bool(config_file.get_value("graphics", "antiAliasing")) == false,
+ "Reading `graphics/antiAliasing` should return `false`.");
+
+ // An empty ConfigFile is valid.
+ const Error error_empty = config_file.parse("");
+ CHECK_MESSAGE(error_empty == OK,
+ "An empty configuration file should parse successfully.");
+}
+
+TEST_CASE("[ConfigFile] Parsing malformatted file") {
+ ConfigFile config_file;
+ ERR_PRINT_OFF;
+ const Error error = config_file.parse(R"(
+[player]
+
+name = "Unnamed Player"" ; Extraneous closing quote.
+tagline = "Waiting\nfor\nGodot"
+
+color = Color(0, 0.5, 1) ; Missing 4th parameter.
+position = Vector2(
+ 3,,
+ 4
+) ; Extraneous comma.
+
+[graphics]
+
+antialiasing = true
+antialiasing = false ; Duplicate key.
+)");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(error == ERR_PARSE_ERROR,
+ "The configuration file shouldn't parse successfully.");
+}
+
+TEST_CASE("[ConfigFile] Saving file") {
+ ConfigFile config_file;
+ config_file.set_value("player", "name", "Unnamed Player");
+ config_file.set_value("player", "tagline", "Waiting\nfor\nGodot");
+ config_file.set_value("player", "color", Color(0, 0.5, 1));
+ config_file.set_value("player", "position", Vector2(3, 4));
+ config_file.set_value("graphics", "antialiasing", true);
+ config_file.set_value("graphics", "antiAliasing", false);
+ config_file.set_value("quoted", String::utf8("静音"), 42);
+ config_file.set_value("quoted", "a=b", 7);
+
+#ifdef WINDOWS_ENABLED
+ const String config_path = OS::get_singleton()->get_environment("TEMP").path_join("config.ini");
+#else
+ const String config_path = "/tmp/config.ini";
+#endif
+
+ config_file.save(config_path);
+
+ // Expected contents of the saved ConfigFile.
+ const String contents = String::utf8(R"([player]
+
+name="Unnamed Player"
+tagline="Waiting
+for
+Godot"
+color=Color(0, 0.5, 1, 1)
+position=Vector2(3, 4)
+
+[graphics]
+
+antialiasing=true
+antiAliasing=false
+
+[quoted]
+
+"静音"=42
+"a=b"=7
+)");
+
+ Ref<FileAccess> file = FileAccess::open(config_path, FileAccess::READ);
+ CHECK_MESSAGE(file->get_as_utf8_string() == contents,
+ "The saved configuration file should match the expected format.");
+}
+} // namespace TestConfigFile
+
+#endif // TEST_CONFIG_FILE_H
diff --git a/tests/core/io/test_file_access.h b/tests/core/io/test_file_access.h
new file mode 100644
index 0000000000..aab62955cb
--- /dev/null
+++ b/tests/core/io/test_file_access.h
@@ -0,0 +1,106 @@
+/*************************************************************************/
+/* test_file_access.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_FILE_ACCESS_H
+#define TEST_FILE_ACCESS_H
+
+#include "core/io/file_access.h"
+#include "tests/test_macros.h"
+#include "tests/test_utils.h"
+
+namespace TestFileAccess {
+
+TEST_CASE("[FileAccess] CSV read") {
+ Ref<FileAccess> f = FileAccess::open(TestUtils::get_data_path("translations.csv"), FileAccess::READ);
+
+ Vector<String> header = f->get_csv_line(); // Default delimiter: ",".
+ REQUIRE(header.size() == 3);
+
+ Vector<String> row1 = f->get_csv_line(","); // Explicit delimiter, should be the same.
+ REQUIRE(row1.size() == 3);
+ CHECK(row1[0] == "GOOD_MORNING");
+ CHECK(row1[1] == "Good Morning");
+ CHECK(row1[2] == "Guten Morgen");
+
+ Vector<String> row2 = f->get_csv_line();
+ REQUIRE(row2.size() == 3);
+ CHECK(row2[0] == "GOOD_EVENING");
+ CHECK(row2[1] == "Good Evening");
+ CHECK(row2[2].is_empty()); // Use case: not yet translated!
+ // https://github.com/godotengine/godot/issues/44269
+ CHECK_MESSAGE(row2[2] != "\"", "Should not parse empty string as a single double quote.");
+
+ Vector<String> row3 = f->get_csv_line();
+ REQUIRE(row3.size() == 6);
+ CHECK(row3[0] == "Without quotes");
+ CHECK(row3[1] == "With, comma");
+ CHECK(row3[2] == "With \"inner\" quotes");
+ CHECK(row3[3] == "With \"inner\", quotes\",\" and comma");
+ CHECK(row3[4] == "With \"inner\nsplit\" quotes and\nline breaks");
+ CHECK(row3[5] == "With \\nnewline chars"); // Escaped, not an actual newline.
+
+ Vector<String> row4 = f->get_csv_line("~"); // Custom delimiter, makes inline commas easier.
+ REQUIRE(row4.size() == 3);
+ CHECK(row4[0] == "Some other");
+ CHECK(row4[1] == "delimiter");
+ CHECK(row4[2] == "should still work, shouldn't it?");
+
+ Vector<String> row5 = f->get_csv_line("\t"); // Tab separated variables.
+ REQUIRE(row5.size() == 3);
+ CHECK(row5[0] == "What about");
+ CHECK(row5[1] == "tab separated");
+ CHECK(row5[2] == "lines, good?");
+}
+
+TEST_CASE("[FileAccess] Get as UTF-8 String") {
+ Ref<FileAccess> f_lf = FileAccess::open(TestUtils::get_data_path("line_endings_lf.test.txt"), FileAccess::READ);
+ String s_lf = f_lf->get_as_utf8_string();
+ f_lf->seek(0);
+ String s_lf_nocr = f_lf->get_as_utf8_string(true);
+ CHECK(s_lf == "Hello darkness\nMy old friend\nI've come to talk\nWith you again\n");
+ CHECK(s_lf_nocr == "Hello darkness\nMy old friend\nI've come to talk\nWith you again\n");
+
+ Ref<FileAccess> f_crlf = FileAccess::open(TestUtils::get_data_path("line_endings_crlf.test.txt"), FileAccess::READ);
+ String s_crlf = f_crlf->get_as_utf8_string();
+ f_crlf->seek(0);
+ String s_crlf_nocr = f_crlf->get_as_utf8_string(true);
+ CHECK(s_crlf == "Hello darkness\r\nMy old friend\r\nI've come to talk\r\nWith you again\r\n");
+ CHECK(s_crlf_nocr == "Hello darkness\nMy old friend\nI've come to talk\nWith you again\n");
+
+ Ref<FileAccess> f_cr = FileAccess::open(TestUtils::get_data_path("line_endings_cr.test.txt"), FileAccess::READ);
+ String s_cr = f_cr->get_as_utf8_string();
+ f_cr->seek(0);
+ String s_cr_nocr = f_cr->get_as_utf8_string(true);
+ CHECK(s_cr == "Hello darkness\rMy old friend\rI've come to talk\rWith you again\r");
+ CHECK(s_cr_nocr == "Hello darknessMy old friendI've come to talkWith you again");
+}
+} // namespace TestFileAccess
+
+#endif // TEST_FILE_ACCESS_H
diff --git a/tests/core/io/test_image.h b/tests/core/io/test_image.h
new file mode 100644
index 0000000000..181d9a8a54
--- /dev/null
+++ b/tests/core/io/test_image.h
@@ -0,0 +1,304 @@
+/*************************************************************************/
+/* test_image.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_IMAGE_H
+#define TEST_IMAGE_H
+
+#include "core/io/image.h"
+#include "core/os/os.h"
+
+#include "tests/test_utils.h"
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestImage {
+
+TEST_CASE("[Image] Instantiation") {
+ Ref<Image> image = memnew(Image(8, 4, false, Image::FORMAT_RGBA8));
+ CHECK_MESSAGE(
+ !image->is_empty(),
+ "An image created with specified size and format should not be empty at first.");
+ CHECK_MESSAGE(
+ image->is_invisible(),
+ "A newly created image should be invisible.");
+ CHECK_MESSAGE(
+ !image->is_compressed(),
+ "A newly created image should not be compressed.");
+ CHECK(!image->has_mipmaps());
+
+ PackedByteArray image_data = image->get_data();
+ for (int i = 0; i < image_data.size(); i++) {
+ CHECK_MESSAGE(
+ image_data[i] == 0,
+ "An image created without data specified should have its data zeroed out.");
+ }
+
+ Ref<Image> image_copy = memnew(Image());
+ CHECK_MESSAGE(
+ image_copy->is_empty(),
+ "An image created without any specified size and format be empty at first.");
+ image_copy->copy_internals_from(image);
+
+ CHECK_MESSAGE(
+ image->get_data() == image_copy->get_data(),
+ "Duplicated images should have the same data.");
+
+ image_data = image->get_data();
+ Ref<Image> image_from_data = memnew(Image(8, 4, false, Image::FORMAT_RGBA8, image_data));
+ CHECK_MESSAGE(
+ image->get_data() == image_from_data->get_data(),
+ "An image created from data of another image should have the same data of the original image.");
+}
+
+TEST_CASE("[Image] Saving and loading") {
+ Ref<Image> image = memnew(Image(4, 4, false, Image::FORMAT_RGBA8));
+ const String save_path_png = OS::get_singleton()->get_cache_path().path_join("image.png");
+ const String save_path_exr = OS::get_singleton()->get_cache_path().path_join("image.exr");
+
+ // Save PNG
+ Error err;
+ err = image->save_png(save_path_png);
+ CHECK_MESSAGE(
+ err == OK,
+ "The image should be saved successfully as a .png file.");
+
+ // Save EXR
+ err = image->save_exr(save_path_exr, false);
+ CHECK_MESSAGE(
+ err == OK,
+ "The image should be saved successfully as an .exr file.");
+
+ // Load using load()
+ Ref<Image> image_load = memnew(Image());
+ err = image_load->load(save_path_png);
+ CHECK_MESSAGE(
+ err == OK,
+ "The image should load successfully using load().");
+ CHECK_MESSAGE(
+ image->get_data() == image_load->get_data(),
+ "The loaded image should have the same data as the one that got saved.");
+
+ // Load BMP
+ Ref<Image> image_bmp = memnew(Image());
+ Ref<FileAccess> f_bmp = FileAccess::open(TestUtils::get_data_path("images/icon.bmp"), FileAccess::READ, &err);
+ PackedByteArray data_bmp;
+ data_bmp.resize(f_bmp->get_length() + 1);
+ f_bmp->get_buffer(data_bmp.ptrw(), f_bmp->get_length());
+ CHECK_MESSAGE(
+ image_bmp->load_bmp_from_buffer(data_bmp) == OK,
+ "The BMP image should load successfully.");
+
+ // Load JPG
+ Ref<Image> image_jpg = memnew(Image());
+ Ref<FileAccess> f_jpg = FileAccess::open(TestUtils::get_data_path("images/icon.jpg"), FileAccess::READ, &err);
+ PackedByteArray data_jpg;
+ data_jpg.resize(f_jpg->get_length() + 1);
+ f_jpg->get_buffer(data_jpg.ptrw(), f_jpg->get_length());
+ CHECK_MESSAGE(
+ image_jpg->load_jpg_from_buffer(data_jpg) == OK,
+ "The JPG image should load successfully.");
+
+ // Load WebP
+ Ref<Image> image_webp = memnew(Image());
+ Ref<FileAccess> f_webp = FileAccess::open(TestUtils::get_data_path("images/icon.webp"), FileAccess::READ, &err);
+ PackedByteArray data_webp;
+ data_webp.resize(f_webp->get_length() + 1);
+ f_webp->get_buffer(data_webp.ptrw(), f_webp->get_length());
+ CHECK_MESSAGE(
+ image_webp->load_webp_from_buffer(data_webp) == OK,
+ "The WebP image should load successfully.");
+
+ // Load PNG
+ Ref<Image> image_png = memnew(Image());
+ Ref<FileAccess> f_png = FileAccess::open(TestUtils::get_data_path("images/icon.png"), FileAccess::READ, &err);
+ PackedByteArray data_png;
+ data_png.resize(f_png->get_length() + 1);
+ f_png->get_buffer(data_png.ptrw(), f_png->get_length());
+ CHECK_MESSAGE(
+ image_png->load_png_from_buffer(data_png) == OK,
+ "The PNG image should load successfully.");
+
+ // Load TGA
+ Ref<Image> image_tga = memnew(Image());
+ Ref<FileAccess> f_tga = FileAccess::open(TestUtils::get_data_path("images/icon.tga"), FileAccess::READ, &err);
+ PackedByteArray data_tga;
+ data_tga.resize(f_tga->get_length() + 1);
+ f_tga->get_buffer(data_tga.ptrw(), f_tga->get_length());
+ CHECK_MESSAGE(
+ image_tga->load_tga_from_buffer(data_tga) == OK,
+ "The TGA image should load successfully.");
+}
+
+TEST_CASE("[Image] Basic getters") {
+ Ref<Image> image = memnew(Image(8, 4, false, Image::FORMAT_LA8));
+ CHECK(image->get_width() == 8);
+ CHECK(image->get_height() == 4);
+ CHECK(image->get_size() == Vector2(8, 4));
+ CHECK(image->get_format() == Image::FORMAT_LA8);
+ CHECK(image->get_used_rect() == Rect2i(0, 0, 0, 0));
+ Ref<Image> image_get_rect = image->get_rect(Rect2i(0, 0, 2, 1));
+ CHECK(image_get_rect->get_size() == Vector2(2, 1));
+}
+
+TEST_CASE("[Image] Resizing") {
+ Ref<Image> image = memnew(Image(8, 8, false, Image::FORMAT_RGBA8));
+ // Crop
+ image->crop(4, 4);
+ CHECK_MESSAGE(
+ image->get_size() == Vector2(4, 4),
+ "get_size() should return the correct size after cropping.");
+ image->set_pixel(0, 0, Color(1, 1, 1, 1));
+
+ // Resize
+ for (int i = 0; i < 5; i++) {
+ Ref<Image> image_resized = memnew(Image());
+ image_resized->copy_internals_from(image);
+ Image::Interpolation interpolation = static_cast<Image::Interpolation>(i);
+ image_resized->resize(8, 8, interpolation);
+ CHECK_MESSAGE(
+ image_resized->get_size() == Vector2(8, 8),
+ "get_size() should return the correct size after resizing.");
+ CHECK_MESSAGE(
+ image_resized->get_pixel(1, 1).a > 0,
+ "Resizing an image should also affect its content.");
+ }
+
+ // shrink_x2()
+ image->shrink_x2();
+ CHECK_MESSAGE(
+ image->get_size() == Vector2(2, 2),
+ "get_size() should return the correct size after shrink_x2().");
+
+ // resize_to_po2()
+ Ref<Image> image_po_2 = memnew(Image(14, 28, false, Image::FORMAT_RGBA8));
+ image_po_2->resize_to_po2();
+ CHECK_MESSAGE(
+ image_po_2->get_size() == Vector2(16, 32),
+ "get_size() should return the correct size after resize_to_po2().");
+}
+
+TEST_CASE("[Image] Modifying pixels of an image") {
+ Ref<Image> image = memnew(Image(3, 3, false, Image::FORMAT_RGBA8));
+ image->set_pixel(0, 0, Color(1, 1, 1, 1));
+ CHECK_MESSAGE(
+ !image->is_invisible(),
+ "Image should not be invisible after drawing on it.");
+ CHECK_MESSAGE(
+ image->get_pixelv(Vector2(0, 0)).is_equal_approx(Color(1, 1, 1, 1)),
+ "Image's get_pixel() should return the same color value as the one being set with set_pixel() in the same position.");
+ CHECK_MESSAGE(
+ image->get_used_rect() == Rect2i(0, 0, 1, 1),
+ "Image's get_used_rect should return the expected value, larger than Rect2i(0, 0, 0, 0) if it's visible.");
+
+ image->set_pixelv(Vector2(0, 0), Color(0.5, 0.5, 0.5, 0.5));
+ Ref<Image> image2 = memnew(Image(3, 3, false, Image::FORMAT_RGBA8));
+
+ // Fill image with color
+ image2->fill(Color(0.5, 0.5, 0.5, 0.5));
+ for (int y = 0; y < image2->get_height(); y++) {
+ for (int x = 0; x < image2->get_width(); x++) {
+ CHECK_MESSAGE(
+ image2->get_pixel(x, y).r > 0.49,
+ "fill() should colorize all pixels of the image.");
+ }
+ }
+
+ // Fill rect with color
+ {
+ const int img_width = 3;
+ const int img_height = 3;
+ Vector<Rect2i> rects;
+ rects.push_back(Rect2i());
+ rects.push_back(Rect2i(-5, -5, 3, 3));
+ rects.push_back(Rect2i(img_width, 0, 12, 12));
+ rects.push_back(Rect2i(0, img_height, 12, 12));
+ rects.push_back(Rect2i(img_width + 1, img_height + 2, 12, 12));
+ rects.push_back(Rect2i(1, 1, 1, 1));
+ rects.push_back(Rect2i(0, 1, 2, 3));
+ rects.push_back(Rect2i(-5, 0, img_width + 10, 2));
+ rects.push_back(Rect2i(0, -5, 2, img_height + 10));
+ rects.push_back(Rect2i(-1, -1, img_width + 1, img_height + 1));
+
+ for (const Rect2i &rect : rects) {
+ Ref<Image> img = memnew(Image(img_width, img_height, false, Image::FORMAT_RGBA8));
+ CHECK_NOTHROW_MESSAGE(
+ img->fill_rect(rect, Color(1, 1, 1, 1)),
+ "fill_rect() shouldn't throw for any rect.");
+ for (int y = 0; y < img->get_height(); y++) {
+ for (int x = 0; x < img->get_width(); x++) {
+ if (rect.abs().has_point(Point2(x, y))) {
+ CHECK_MESSAGE(
+ img->get_pixel(x, y).is_equal_approx(Color(1, 1, 1, 1)),
+ "fill_rect() should colorize all image pixels within rect bounds.");
+ } else {
+ CHECK_MESSAGE(
+ !img->get_pixel(x, y).is_equal_approx(Color(1, 1, 1, 1)),
+ "fill_rect() shouldn't colorize any image pixel out of rect bounds.");
+ }
+ }
+ }
+ }
+ }
+
+ // Blend two images together
+ image->blend_rect(image2, Rect2i(Vector2i(0, 0), image2->get_size()), Vector2i(0, 0));
+ CHECK_MESSAGE(
+ image->get_pixel(0, 0).a > 0.7,
+ "blend_rect() should blend the alpha values of the two images.");
+ CHECK_MESSAGE(
+ image->get_used_rect().size == image->get_size(),
+ "get_used_rect() should return the expected value, its Rect size should be the same as get_size() if there are no transparent pixels.");
+
+ Ref<Image> image3 = memnew(Image(2, 2, false, Image::FORMAT_RGBA8));
+ image3->set_pixel(0, 0, Color(0, 1, 0, 1));
+
+ //blit_rect() two images together
+ image->blit_rect(image3, Rect2i(Vector2i(0, 0), image3->get_size()), Vector2i(0, 0));
+ CHECK_MESSAGE(
+ image->get_pixel(0, 0).is_equal_approx(Color(0, 1, 0, 1)),
+ "blit_rect() should replace old colors and not blend them.");
+ CHECK_MESSAGE(
+ !image->get_pixel(2, 2).is_equal_approx(Color(0, 1, 0, 1)),
+ "blit_rect() should not affect the area of the image that is outside src_rect.");
+
+ // Flip image
+ image3->flip_x();
+ CHECK(image3->get_pixel(1, 0).is_equal_approx(Color(0, 1, 0, 1)));
+ CHECK_MESSAGE(
+ image3->get_pixel(0, 0).is_equal_approx(Color(0, 0, 0, 0)),
+ "flip_x() should not leave old pixels behind.");
+ image3->flip_y();
+ CHECK(image3->get_pixel(1, 1).is_equal_approx(Color(0, 1, 0, 1)));
+ CHECK_MESSAGE(
+ image3->get_pixel(1, 0).is_equal_approx(Color(0, 0, 0, 0)),
+ "flip_y() should not leave old pixels behind.");
+}
+} // namespace TestImage
+
+#endif // TEST_IMAGE_H
diff --git a/tests/core/io/test_json.h b/tests/core/io/test_json.h
new file mode 100644
index 0000000000..478cf1766e
--- /dev/null
+++ b/tests/core/io/test_json.h
@@ -0,0 +1,151 @@
+/*************************************************************************/
+/* test_json.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_JSON_H
+#define TEST_JSON_H
+
+#include "core/io/json.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestJSON {
+
+// NOTE: The current JSON parser accepts many non-conformant strings such as
+// single-quoted strings, duplicate commas and trailing commas.
+// This is intentionally not tested as users shouldn't rely on this behavior.
+
+TEST_CASE("[JSON] Parsing single data types") {
+ // Parsing a single data type as JSON is valid per the JSON specification.
+
+ JSON json;
+
+ json.parse("null");
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing `null` as JSON should parse successfully.");
+ CHECK_MESSAGE(
+ json.get_data() == Variant(),
+ "Parsing a double quoted string as JSON should return the expected value.");
+
+ json.parse("true");
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing boolean `true` as JSON should parse successfully.");
+ CHECK_MESSAGE(
+ json.get_data(),
+ "Parsing boolean `true` as JSON should return the expected value.");
+
+ json.parse("false");
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing boolean `false` as JSON should parse successfully.");
+ CHECK_MESSAGE(
+ !json.get_data(),
+ "Parsing boolean `false` as JSON should return the expected value.");
+
+ json.parse("123456");
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing an integer number as JSON should parse successfully.");
+ CHECK_MESSAGE(
+ (int)(json.get_data()) == 123456,
+ "Parsing an integer number as JSON should return the expected value.");
+
+ json.parse("0.123456");
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing a floating-point number as JSON should parse successfully.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(double(json.get_data()), 0.123456),
+ "Parsing a floating-point number as JSON should return the expected value.");
+
+ json.parse("\"hello\"");
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing a double quoted string as JSON should parse successfully.");
+ CHECK_MESSAGE(
+ json.get_data() == "hello",
+ "Parsing a double quoted string as JSON should return the expected value.");
+}
+
+TEST_CASE("[JSON] Parsing arrays") {
+ JSON json;
+
+ // JSON parsing fails if it's split over several lines (even if leading indentation is removed).
+ json.parse(R"(["Hello", "world.", "This is",["a","json","array.",[]], "Empty arrays ahoy:", [[["Gotcha!"]]]])");
+
+ const Array array = json.get_data();
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing a JSON array should parse successfully.");
+ CHECK_MESSAGE(
+ array[0] == "Hello",
+ "The parsed JSON should contain the expected values.");
+ const Array sub_array = array[3];
+ CHECK_MESSAGE(
+ sub_array.size() == 4,
+ "The parsed JSON should contain the expected values.");
+ CHECK_MESSAGE(
+ sub_array[1] == "json",
+ "The parsed JSON should contain the expected values.");
+ CHECK_MESSAGE(
+ sub_array[3].hash() == Array().hash(),
+ "The parsed JSON should contain the expected values.");
+ const Array deep_array = Array(Array(array[5])[0])[0];
+ CHECK_MESSAGE(
+ deep_array[0] == "Gotcha!",
+ "The parsed JSON should contain the expected values.");
+}
+
+TEST_CASE("[JSON] Parsing objects (dictionaries)") {
+ JSON json;
+
+ json.parse(R"({"name": "Godot Engine", "is_free": true, "bugs": null, "apples": {"red": 500, "green": 0, "blue": -20}, "empty_object": {}})");
+
+ const Dictionary dictionary = json.get_data();
+ CHECK_MESSAGE(
+ dictionary["name"] == "Godot Engine",
+ "The parsed JSON should contain the expected values.");
+ CHECK_MESSAGE(
+ dictionary["is_free"],
+ "The parsed JSON should contain the expected values.");
+ CHECK_MESSAGE(
+ dictionary["bugs"] == Variant(),
+ "The parsed JSON should contain the expected values.");
+ CHECK_MESSAGE(
+ (int)Dictionary(dictionary["apples"])["blue"] == -20,
+ "The parsed JSON should contain the expected values.");
+ CHECK_MESSAGE(
+ dictionary["empty_object"].hash() == Dictionary().hash(),
+ "The parsed JSON should contain the expected values.");
+}
+} // namespace TestJSON
+
+#endif // TEST_JSON_H
diff --git a/tests/core/io/test_marshalls.h b/tests/core/io/test_marshalls.h
new file mode 100644
index 0000000000..7490df2b2c
--- /dev/null
+++ b/tests/core/io/test_marshalls.h
@@ -0,0 +1,331 @@
+/*************************************************************************/
+/* test_marshalls.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_MARSHALLS_H
+#define TEST_MARSHALLS_H
+
+#include "core/io/marshalls.h"
+
+#include "tests/test_macros.h"
+
+namespace TestMarshalls {
+
+TEST_CASE("[Marshalls] Unsigned 16 bit integer encoding") {
+ uint8_t arr[2];
+
+ unsigned int actual_size = encode_uint16(0x1234, arr);
+ CHECK(actual_size == sizeof(uint16_t));
+ CHECK_MESSAGE(arr[0] == 0x34, "First encoded byte value should be equal to low order byte value.");
+ CHECK_MESSAGE(arr[1] == 0x12, "Last encoded byte value should be equal to high order byte value.");
+}
+
+TEST_CASE("[Marshalls] Unsigned 32 bit integer encoding") {
+ uint8_t arr[4];
+
+ unsigned int actual_size = encode_uint32(0x12345678, arr);
+ CHECK(actual_size == sizeof(uint32_t));
+ CHECK_MESSAGE(arr[0] == 0x78, "First encoded byte value should be equal to low order byte value.");
+ CHECK(arr[1] == 0x56);
+ CHECK(arr[2] == 0x34);
+ CHECK_MESSAGE(arr[3] == 0x12, "Last encoded byte value should be equal to high order byte value.");
+}
+
+TEST_CASE("[Marshalls] Unsigned 64 bit integer encoding") {
+ uint8_t arr[8];
+
+ unsigned int actual_size = encode_uint64(0x0f123456789abcdef, arr);
+ CHECK(actual_size == sizeof(uint64_t));
+ CHECK_MESSAGE(arr[0] == 0xef, "First encoded byte value should be equal to low order byte value.");
+ CHECK(arr[1] == 0xcd);
+ CHECK(arr[2] == 0xab);
+ CHECK(arr[3] == 0x89);
+ CHECK(arr[4] == 0x67);
+ CHECK(arr[5] == 0x45);
+ CHECK(arr[6] == 0x23);
+ CHECK_MESSAGE(arr[7] == 0xf1, "Last encoded byte value should be equal to high order byte value.");
+}
+
+TEST_CASE("[Marshalls] Unsigned 16 bit integer decoding") {
+ uint8_t arr[] = { 0x34, 0x12 };
+
+ CHECK(decode_uint16(arr) == 0x1234);
+}
+
+TEST_CASE("[Marshalls] Unsigned 32 bit integer decoding") {
+ uint8_t arr[] = { 0x78, 0x56, 0x34, 0x12 };
+
+ CHECK(decode_uint32(arr) == 0x12345678);
+}
+
+TEST_CASE("[Marshalls] Unsigned 64 bit integer decoding") {
+ uint8_t arr[] = { 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0xf1 };
+
+ CHECK(decode_uint64(arr) == 0x0f123456789abcdef);
+}
+
+TEST_CASE("[Marshalls] Floating point single precision encoding") {
+ uint8_t arr[4];
+
+ // Decimal: 0.15625
+ // IEEE 754 single-precision binary floating-point format:
+ // sign exponent (8 bits) fraction (23 bits)
+ // 0 01111100 01000000000000000000000
+ // Hexadecimal: 0x3E200000
+ unsigned int actual_size = encode_float(0.15625f, arr);
+ CHECK(actual_size == sizeof(uint32_t));
+ CHECK(arr[0] == 0x00);
+ CHECK(arr[1] == 0x00);
+ CHECK(arr[2] == 0x20);
+ CHECK(arr[3] == 0x3e);
+}
+
+TEST_CASE("[Marshalls] Floating point double precision encoding") {
+ uint8_t arr[8];
+
+ // Decimal: 0.333333333333333314829616256247390992939472198486328125
+ // IEEE 754 double-precision binary floating-point format:
+ // sign exponent (11 bits) fraction (52 bits)
+ // 0 01111111101 0101010101010101010101010101010101010101010101010101
+ // Hexadecimal: 0x3FD5555555555555
+ unsigned int actual_size = encode_double(0.33333333333333333, arr);
+ CHECK(actual_size == sizeof(uint64_t));
+ CHECK(arr[0] == 0x55);
+ CHECK(arr[1] == 0x55);
+ CHECK(arr[2] == 0x55);
+ CHECK(arr[3] == 0x55);
+ CHECK(arr[4] == 0x55);
+ CHECK(arr[5] == 0x55);
+ CHECK(arr[6] == 0xd5);
+ CHECK(arr[7] == 0x3f);
+}
+
+TEST_CASE("[Marshalls] Floating point single precision decoding") {
+ uint8_t arr[] = { 0x00, 0x00, 0x20, 0x3e };
+
+ // See floating point encoding test case for details behind expected values
+ CHECK(decode_float(arr) == 0.15625f);
+}
+
+TEST_CASE("[Marshalls] Floating point double precision decoding") {
+ uint8_t arr[] = { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0xd5, 0x3f };
+
+ // See floating point encoding test case for details behind expected values
+ CHECK(decode_double(arr) == 0.33333333333333333);
+}
+
+TEST_CASE("[Marshalls] C string encoding") {
+ char cstring[] = "Godot"; // 5 characters
+ uint8_t data[6];
+
+ int actual_size = encode_cstring(cstring, data);
+ CHECK(actual_size == 6);
+ CHECK(data[0] == 'G');
+ CHECK(data[1] == 'o');
+ CHECK(data[2] == 'd');
+ CHECK(data[3] == 'o');
+ CHECK(data[4] == 't');
+ CHECK(data[5] == '\0');
+}
+
+TEST_CASE("[Marshalls] NIL Variant encoding") {
+ int r_len;
+ Variant variant;
+ uint8_t buffer[4];
+
+ CHECK(encode_variant(variant, buffer, r_len) == OK);
+ CHECK_MESSAGE(r_len == 4, "Length == 4 bytes for Variant::Type");
+ CHECK_MESSAGE(buffer[0] == 0x00, "Variant::NIL");
+ CHECK(buffer[1] == 0x00);
+ CHECK(buffer[2] == 0x00);
+ CHECK(buffer[3] == 0x00);
+ // No value
+}
+
+TEST_CASE("[Marshalls] INT 32 bit Variant encoding") {
+ int r_len;
+ Variant variant(0x12345678);
+ uint8_t buffer[8];
+
+ CHECK(encode_variant(variant, buffer, r_len) == OK);
+ CHECK_MESSAGE(r_len == 8, "Length == 4 bytes for Variant::Type + 4 bytes for int32_t");
+ CHECK_MESSAGE(buffer[0] == 0x02, "Variant::INT");
+ CHECK(buffer[1] == 0x00);
+ CHECK(buffer[2] == 0x00);
+ CHECK(buffer[3] == 0x00);
+ // Check value
+ CHECK(buffer[4] == 0x78);
+ CHECK(buffer[5] == 0x56);
+ CHECK(buffer[6] == 0x34);
+ CHECK(buffer[7] == 0x12);
+}
+
+TEST_CASE("[Marshalls] INT 64 bit Variant encoding") {
+ int r_len;
+ Variant variant(uint64_t(0x0f123456789abcdef));
+ uint8_t buffer[12];
+
+ CHECK(encode_variant(variant, buffer, r_len) == OK);
+ CHECK_MESSAGE(r_len == 12, "Length == 4 bytes for Variant::Type + 8 bytes for int64_t");
+ CHECK_MESSAGE(buffer[0] == 0x02, "Variant::INT");
+ CHECK(buffer[1] == 0x00);
+ CHECK_MESSAGE(buffer[2] == 0x01, "ENCODE_FLAG_64");
+ CHECK(buffer[3] == 0x00);
+ // Check value
+ CHECK(buffer[4] == 0xef);
+ CHECK(buffer[5] == 0xcd);
+ CHECK(buffer[6] == 0xab);
+ CHECK(buffer[7] == 0x89);
+ CHECK(buffer[8] == 0x67);
+ CHECK(buffer[9] == 0x45);
+ CHECK(buffer[10] == 0x23);
+ CHECK(buffer[11] == 0xf1);
+}
+
+TEST_CASE("[Marshalls] FLOAT single precision Variant encoding") {
+ int r_len;
+ Variant variant(0.15625f);
+ uint8_t buffer[8];
+
+ CHECK(encode_variant(variant, buffer, r_len) == OK);
+ CHECK_MESSAGE(r_len == 8, "Length == 4 bytes for Variant::Type + 4 bytes for float");
+ CHECK_MESSAGE(buffer[0] == 0x03, "Variant::FLOAT");
+ CHECK(buffer[1] == 0x00);
+ CHECK(buffer[2] == 0x00);
+ CHECK(buffer[3] == 0x00);
+ // Check value
+ CHECK(buffer[4] == 0x00);
+ CHECK(buffer[5] == 0x00);
+ CHECK(buffer[6] == 0x20);
+ CHECK(buffer[7] == 0x3e);
+}
+
+TEST_CASE("[Marshalls] FLOAT double precision Variant encoding") {
+ int r_len;
+ Variant variant(0.33333333333333333);
+ uint8_t buffer[12];
+
+ CHECK(encode_variant(variant, buffer, r_len) == OK);
+ CHECK_MESSAGE(r_len == 12, "Length == 4 bytes for Variant::Type + 8 bytes for double");
+ CHECK_MESSAGE(buffer[0] == 0x03, "Variant::FLOAT");
+ CHECK(buffer[1] == 0x00);
+ CHECK_MESSAGE(buffer[2] == 0x01, "ENCODE_FLAG_64");
+ CHECK(buffer[3] == 0x00);
+ // Check value
+ CHECK(buffer[4] == 0x55);
+ CHECK(buffer[5] == 0x55);
+ CHECK(buffer[6] == 0x55);
+ CHECK(buffer[7] == 0x55);
+ CHECK(buffer[8] == 0x55);
+ CHECK(buffer[9] == 0x55);
+ CHECK(buffer[10] == 0xd5);
+ CHECK(buffer[11] == 0x3f);
+}
+
+TEST_CASE("[Marshalls] Invalid data Variant decoding") {
+ Variant variant;
+ int r_len = 0;
+ uint8_t some_buffer[1] = { 0x00 };
+ uint8_t out_of_range_type_buffer[4] = { 0xff }; // Greater than Variant::VARIANT_MAX
+
+ ERR_PRINT_OFF;
+ CHECK(decode_variant(variant, some_buffer, /* less than 4 */ 1, &r_len) == ERR_INVALID_DATA);
+ CHECK(r_len == 0);
+
+ CHECK(decode_variant(variant, out_of_range_type_buffer, 4, &r_len) == ERR_INVALID_DATA);
+ CHECK(r_len == 0);
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[Marshalls] NIL Variant decoding") {
+ Variant variant;
+ int r_len;
+ uint8_t buffer[] = {
+ 0x00, 0x00, 0x00, 0x00 // Variant::NIL
+ };
+
+ CHECK(decode_variant(variant, buffer, 4, &r_len) == OK);
+ CHECK(r_len == 4);
+ CHECK(variant == Variant());
+}
+
+TEST_CASE("[Marshalls] INT 32 bit Variant decoding") {
+ Variant variant;
+ int r_len;
+ uint8_t buffer[] = {
+ 0x02, 0x00, 0x00, 0x00, // Variant::INT
+ 0x78, 0x56, 0x34, 0x12 // value
+ };
+
+ CHECK(decode_variant(variant, buffer, 8, &r_len) == OK);
+ CHECK(r_len == 8);
+ CHECK(variant == Variant(0x12345678));
+}
+
+TEST_CASE("[Marshalls] INT 64 bit Variant decoding") {
+ Variant variant;
+ int r_len;
+ uint8_t buffer[] = {
+ 0x02, 0x00, 0x01, 0x00, // Variant::INT & ENCODE_FLAG_64
+ 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0xf1 // value
+ };
+
+ CHECK(decode_variant(variant, buffer, 12, &r_len) == OK);
+ CHECK(r_len == 12);
+ CHECK(variant == Variant(uint64_t(0x0f123456789abcdef)));
+}
+
+TEST_CASE("[Marshalls] FLOAT single precision Variant decoding") {
+ Variant variant;
+ int r_len;
+ uint8_t buffer[] = {
+ 0x03, 0x00, 0x00, 0x00, // Variant::FLOAT
+ 0x00, 0x00, 0x20, 0x3e // value
+ };
+
+ CHECK(decode_variant(variant, buffer, 8, &r_len) == OK);
+ CHECK(r_len == 8);
+ CHECK(variant == Variant(0.15625f));
+}
+
+TEST_CASE("[Marshalls] FLOAT double precision Variant decoding") {
+ Variant variant;
+ int r_len;
+ uint8_t buffer[] = {
+ 0x03, 0x00, 0x01, 0x00, // Variant::FLOAT & ENCODE_FLAG_64
+ 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0xd5, 0x3f // value
+ };
+
+ CHECK(decode_variant(variant, buffer, 12, &r_len) == OK);
+ CHECK(r_len == 12);
+ CHECK(variant == Variant(0.33333333333333333));
+}
+} // namespace TestMarshalls
+
+#endif // TEST_MARSHALLS_H
diff --git a/tests/core/io/test_pck_packer.h b/tests/core/io/test_pck_packer.h
new file mode 100644
index 0000000000..8d0e5c402a
--- /dev/null
+++ b/tests/core/io/test_pck_packer.h
@@ -0,0 +1,122 @@
+/*************************************************************************/
+/* test_pck_packer.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_PCK_PACKER_H
+#define TEST_PCK_PACKER_H
+
+#include "core/io/file_access_pack.h"
+#include "core/io/pck_packer.h"
+#include "core/os/os.h"
+
+#include "tests/test_utils.h"
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestPCKPacker {
+
+TEST_CASE("[PCKPacker] Pack an empty PCK file") {
+ PCKPacker pck_packer;
+ const String output_pck_path = OS::get_singleton()->get_cache_path().path_join("output_empty.pck");
+ CHECK_MESSAGE(
+ pck_packer.pck_start(output_pck_path) == OK,
+ "Starting a PCK file should return an OK error code.");
+
+ CHECK_MESSAGE(
+ pck_packer.flush() == OK,
+ "Flushing the PCK should return an OK error code.");
+
+ Error err;
+ Ref<FileAccess> f = FileAccess::open(output_pck_path, FileAccess::READ, &err);
+ CHECK_MESSAGE(
+ err == OK,
+ "The generated empty PCK file should be opened successfully.");
+ CHECK_MESSAGE(
+ f->get_length() >= 100,
+ "The generated empty PCK file shouldn't be too small (it should have the PCK header).");
+ CHECK_MESSAGE(
+ f->get_length() <= 500,
+ "The generated empty PCK file shouldn't be too large.");
+}
+
+TEST_CASE("[PCKPacker] Pack empty with zero alignment invalid") {
+ PCKPacker pck_packer;
+ const String output_pck_path = OS::get_singleton()->get_cache_path().path_join("output_empty.pck");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(pck_packer.pck_start(output_pck_path, 0) != OK, "PCK with zero alignment should fail.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[PCKPacker] Pack empty with invalid key") {
+ PCKPacker pck_packer;
+ const String output_pck_path = OS::get_singleton()->get_cache_path().path_join("output_empty.pck");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(pck_packer.pck_start(output_pck_path, 32, "") != OK, "PCK with invalid key should fail.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[PCKPacker] Pack a PCK file with some files and directories") {
+ PCKPacker pck_packer;
+ const String output_pck_path = OS::get_singleton()->get_cache_path().path_join("output_with_files.pck");
+ CHECK_MESSAGE(
+ pck_packer.pck_start(output_pck_path) == OK,
+ "Starting a PCK file should return an OK error code.");
+
+ const String base_dir = OS::get_singleton()->get_executable_path().get_base_dir();
+
+ CHECK_MESSAGE(
+ pck_packer.add_file("version.py", base_dir.path_join("../version.py"), "version.py") == OK,
+ "Adding a file to the PCK should return an OK error code.");
+ CHECK_MESSAGE(
+ pck_packer.add_file("some/directories with spaces/to/create/icon.png", base_dir.path_join("../icon.png")) == OK,
+ "Adding a file to a new subdirectory in the PCK should return an OK error code.");
+ CHECK_MESSAGE(
+ pck_packer.add_file("some/directories with spaces/to/create/icon.svg", base_dir.path_join("../icon.svg")) == OK,
+ "Adding a file to an existing subdirectory in the PCK should return an OK error code.");
+ CHECK_MESSAGE(
+ pck_packer.add_file("some/directories with spaces/to/create/icon.png", base_dir.path_join("../logo.png")) == OK,
+ "Overriding a non-flushed file to an existing subdirectory in the PCK should return an OK error code.");
+ CHECK_MESSAGE(
+ pck_packer.flush() == OK,
+ "Flushing the PCK should return an OK error code.");
+
+ Error err;
+ Ref<FileAccess> f = FileAccess::open(output_pck_path, FileAccess::READ, &err);
+ CHECK_MESSAGE(
+ err == OK,
+ "The generated non-empty PCK file should be opened successfully.");
+ CHECK_MESSAGE(
+ f->get_length() >= 25000,
+ "The generated non-empty PCK file should be large enough to actually hold the contents specified above.");
+ CHECK_MESSAGE(
+ f->get_length() <= 35000,
+ "The generated non-empty PCK file shouldn't be too large.");
+}
+} // namespace TestPCKPacker
+
+#endif // TEST_PCK_PACKER_H
diff --git a/tests/core/io/test_resource.h b/tests/core/io/test_resource.h
new file mode 100644
index 0000000000..2457e06ade
--- /dev/null
+++ b/tests/core/io/test_resource.h
@@ -0,0 +1,114 @@
+/*************************************************************************/
+/* test_resource.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_RESOURCE_H
+#define TEST_RESOURCE_H
+
+#include "core/io/resource.h"
+#include "core/io/resource_loader.h"
+#include "core/io/resource_saver.h"
+#include "core/os/os.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestResource {
+
+TEST_CASE("[Resource] Duplication") {
+ Ref<Resource> resource = memnew(Resource);
+ resource->set_name("Hello world");
+ Ref<Resource> child_resource = memnew(Resource);
+ child_resource->set_name("I'm a child resource");
+ resource->set_meta("other_resource", child_resource);
+
+ Ref<Resource> resource_dupe = resource->duplicate();
+ const Ref<Resource> &resource_dupe_reference = resource_dupe;
+ resource_dupe->set_name("Changed name");
+ child_resource->set_name("My name was changed too");
+
+ CHECK_MESSAGE(
+ resource_dupe->get_name() == "Changed name",
+ "Duplicated resource should have the new name.");
+ CHECK_MESSAGE(
+ resource_dupe_reference->get_name() == "Changed name",
+ "Reference to the duplicated resource should have the new name.");
+ CHECK_MESSAGE(
+ resource->get_name() == "Hello world",
+ "Original resource name should not be affected after editing the duplicate's name.");
+ CHECK_MESSAGE(
+ Ref<Resource>(resource_dupe->get_meta("other_resource"))->get_name() == "My name was changed too",
+ "Duplicated resource should share its child resource with the original.");
+}
+
+TEST_CASE("[Resource] Saving and loading") {
+ Ref<Resource> resource = memnew(Resource);
+ resource->set_name("Hello world");
+ resource->set_meta("ExampleMetadata", Vector2i(40, 80));
+ resource->set_meta("string", "The\nstring\nwith\nunnecessary\nline\n\t\\\nbreaks");
+ Ref<Resource> child_resource = memnew(Resource);
+ child_resource->set_name("I'm a child resource");
+ resource->set_meta("other_resource", child_resource);
+ const String save_path_binary = OS::get_singleton()->get_cache_path().path_join("resource.res");
+ const String save_path_text = OS::get_singleton()->get_cache_path().path_join("resource.tres");
+ ResourceSaver::save(resource, save_path_binary);
+ ResourceSaver::save(resource, save_path_text);
+
+ const Ref<Resource> &loaded_resource_binary = ResourceLoader::load(save_path_binary);
+ CHECK_MESSAGE(
+ loaded_resource_binary->get_name() == "Hello world",
+ "The loaded resource name should be equal to the expected value.");
+ CHECK_MESSAGE(
+ loaded_resource_binary->get_meta("ExampleMetadata") == Vector2i(40, 80),
+ "The loaded resource metadata should be equal to the expected value.");
+ CHECK_MESSAGE(
+ loaded_resource_binary->get_meta("string") == "The\nstring\nwith\nunnecessary\nline\n\t\\\nbreaks",
+ "The loaded resource metadata should be equal to the expected value.");
+ const Ref<Resource> &loaded_child_resource_binary = loaded_resource_binary->get_meta("other_resource");
+ CHECK_MESSAGE(
+ loaded_child_resource_binary->get_name() == "I'm a child resource",
+ "The loaded child resource name should be equal to the expected value.");
+
+ const Ref<Resource> &loaded_resource_text = ResourceLoader::load(save_path_text);
+ CHECK_MESSAGE(
+ loaded_resource_text->get_name() == "Hello world",
+ "The loaded resource name should be equal to the expected value.");
+ CHECK_MESSAGE(
+ loaded_resource_text->get_meta("ExampleMetadata") == Vector2i(40, 80),
+ "The loaded resource metadata should be equal to the expected value.");
+ CHECK_MESSAGE(
+ loaded_resource_text->get_meta("string") == "The\nstring\nwith\nunnecessary\nline\n\t\\\nbreaks",
+ "The loaded resource metadata should be equal to the expected value.");
+ const Ref<Resource> &loaded_child_resource_text = loaded_resource_text->get_meta("other_resource");
+ CHECK_MESSAGE(
+ loaded_child_resource_text->get_name() == "I'm a child resource",
+ "The loaded child resource name should be equal to the expected value.");
+}
+} // namespace TestResource
+
+#endif // TEST_RESOURCE_H
diff --git a/tests/core/io/test_xml_parser.h b/tests/core/io/test_xml_parser.h
new file mode 100644
index 0000000000..35e86d8203
--- /dev/null
+++ b/tests/core/io/test_xml_parser.h
@@ -0,0 +1,235 @@
+/*************************************************************************/
+/* test_xml_parser.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_XML_PARSER_H
+#define TEST_XML_PARSER_H
+
+#include "core/io/xml_parser.h"
+
+#include "tests/test_macros.h"
+
+namespace TestXMLParser {
+TEST_CASE("[XMLParser] End-to-end") {
+ String source = "<?xml version = \"1.0\" encoding=\"UTF-8\" ?>\
+<top attr=\"attr value\">\
+ Text&lt;&#65;&#x42;&gt;\
+</top>";
+ Vector<uint8_t> buff = source.to_utf8_buffer();
+
+ XMLParser parser;
+ parser.open_buffer(buff);
+
+ // <?xml ...?> gets parsed as NODE_UNKNOWN
+ CHECK(parser.read() == OK);
+ CHECK(parser.get_node_type() == XMLParser::NodeType::NODE_UNKNOWN);
+
+ CHECK(parser.read() == OK);
+ CHECK(parser.get_node_type() == XMLParser::NodeType::NODE_ELEMENT);
+ CHECK(parser.get_node_name() == "top");
+ CHECK(parser.has_attribute("attr"));
+ CHECK(parser.get_attribute_value("attr") == "attr value");
+
+ CHECK(parser.read() == OK);
+ CHECK(parser.get_node_type() == XMLParser::NodeType::NODE_TEXT);
+ CHECK(parser.get_node_data().lstrip(" \t") == "Text<AB>");
+
+ CHECK(parser.read() == OK);
+ CHECK(parser.get_node_type() == XMLParser::NodeType::NODE_ELEMENT_END);
+ CHECK(parser.get_node_name() == "top");
+
+ parser.close();
+}
+
+TEST_CASE("[XMLParser] Comments") {
+ XMLParser parser;
+
+ SUBCASE("Missing end of comment") {
+ const String input = "<first></first><!-- foo";
+ REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT_END);
+ REQUIRE_EQ(parser.read(), OK);
+ CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_COMMENT);
+ CHECK_EQ(parser.get_node_name(), " foo");
+ }
+ SUBCASE("Bad start of comment") {
+ const String input = "<first></first><!-";
+ REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT_END);
+ REQUIRE_EQ(parser.read(), OK);
+ CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_COMMENT);
+ CHECK_EQ(parser.get_node_name(), "-");
+ }
+ SUBCASE("Unblanced angle brackets in comment") {
+ const String input = "<!-- example << --><next-tag></next-tag>";
+ REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_COMMENT);
+ CHECK_EQ(parser.get_node_name(), " example << ");
+ }
+ SUBCASE("Doctype") {
+ const String input = "<!DOCTYPE greeting [<!ELEMENT greeting (#PCDATA)>]>";
+ REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_COMMENT);
+ CHECK_EQ(parser.get_node_name(), "DOCTYPE greeting [<!ELEMENT greeting (#PCDATA)>]");
+ }
+}
+
+TEST_CASE("[XMLParser] Premature endings") {
+ SUBCASE("Simple cases") {
+ String input;
+ String expected_name;
+ XMLParser::NodeType expected_type;
+
+ SUBCASE("Incomplete Unknown") {
+ input = "<first></first><?xml";
+ expected_type = XMLParser::NodeType::NODE_UNKNOWN;
+ expected_name = "?xml";
+ }
+ SUBCASE("Incomplete CDStart") {
+ input = "<first></first><![CD";
+ expected_type = XMLParser::NodeType::NODE_CDATA;
+ expected_name = "";
+ }
+ SUBCASE("Incomplete CData") {
+ input = "<first></first><![CDATA[example";
+ expected_type = XMLParser::NodeType::NODE_CDATA;
+ expected_name = "example";
+ }
+ SUBCASE("Incomplete CDEnd") {
+ input = "<first></first><![CDATA[example]]";
+ expected_type = XMLParser::NodeType::NODE_CDATA;
+ expected_name = "example]]";
+ }
+ SUBCASE("Incomplete start-tag name") {
+ input = "<first></first><second";
+ expected_type = XMLParser::NodeType::NODE_ELEMENT;
+ expected_name = "second";
+ }
+
+ XMLParser parser;
+ REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT_END);
+ REQUIRE_EQ(parser.read(), OK);
+ CHECK_EQ(parser.get_node_type(), expected_type);
+ CHECK_EQ(parser.get_node_name(), expected_name);
+ }
+
+ SUBCASE("With attributes and texts") {
+ XMLParser parser;
+
+ SUBCASE("Incomplete start-tag attribute name") {
+ const String input = "<first></first><second attr1=\"foo\" attr2";
+ REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
+ CHECK_EQ(parser.get_node_name(), "second");
+ CHECK_EQ(parser.get_attribute_count(), 1);
+ CHECK_EQ(parser.get_attribute_name(0), "attr1");
+ CHECK_EQ(parser.get_attribute_value(0), "foo");
+ }
+
+ SUBCASE("Incomplete start-tag attribute unquoted value") {
+ const String input = "<first></first><second attr1=\"foo\" attr2=bar";
+ REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
+ CHECK_EQ(parser.get_node_name(), "second");
+ CHECK_EQ(parser.get_attribute_count(), 1);
+ CHECK_EQ(parser.get_attribute_name(0), "attr1");
+ CHECK_EQ(parser.get_attribute_value(0), "foo");
+ }
+
+ SUBCASE("Incomplete start-tag attribute quoted value") {
+ const String input = "<first></first><second attr1=\"foo\" attr2=\"bar";
+ REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
+ CHECK_EQ(parser.get_node_name(), "second");
+ CHECK_EQ(parser.get_attribute_count(), 2);
+ CHECK_EQ(parser.get_attribute_name(0), "attr1");
+ CHECK_EQ(parser.get_attribute_value(0), "foo");
+ CHECK_EQ(parser.get_attribute_name(1), "attr2");
+ CHECK_EQ(parser.get_attribute_value(1), "bar");
+ }
+
+ SUBCASE("Incomplete end-tag name") {
+ const String input = "<first></fir";
+ REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT_END);
+ CHECK_EQ(parser.get_node_name(), "fir");
+ }
+
+ SUBCASE("Trailing text") {
+ const String input = "<first></first>example";
+ REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_TEXT);
+ CHECK_EQ(parser.get_node_data(), "example");
+ }
+ }
+}
+
+TEST_CASE("[XMLParser] CDATA") {
+ const String input = "<a><![CDATA[my cdata content goes here]]></a>";
+ XMLParser parser;
+ REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
+ REQUIRE_EQ(parser.read(), OK);
+ CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
+ CHECK_EQ(parser.get_node_name(), "a");
+ REQUIRE_EQ(parser.read(), OK);
+ CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_CDATA);
+ CHECK_EQ(parser.get_node_name(), "my cdata content goes here");
+ REQUIRE_EQ(parser.read(), OK);
+ CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT_END);
+ CHECK_EQ(parser.get_node_name(), "a");
+}
+} // namespace TestXMLParser
+
+#endif // TEST_XML_PARSER_H
diff --git a/tests/core/math/test_aabb.h b/tests/core/math/test_aabb.h
new file mode 100644
index 0000000000..ebaf441abf
--- /dev/null
+++ b/tests/core/math/test_aabb.h
@@ -0,0 +1,415 @@
+/*************************************************************************/
+/* test_aabb.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_AABB_H
+#define TEST_AABB_H
+
+#include "core/math/aabb.h"
+
+#include "tests/test_macros.h"
+
+namespace TestAABB {
+
+TEST_CASE("[AABB] Constructor methods") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ const AABB aabb_copy = AABB(aabb);
+
+ CHECK_MESSAGE(
+ aabb == aabb_copy,
+ "AABBs created with the same dimensions but by different methods should be equal.");
+}
+
+TEST_CASE("[AABB] String conversion") {
+ CHECK_MESSAGE(
+ String(AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6))) == "[P: (-1.5, 2, -2.5), S: (4, 5, 6)]",
+ "The string representation should match the expected value.");
+}
+
+TEST_CASE("[AABB] Basic getters") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.get_position().is_equal_approx(Vector3(-1.5, 2, -2.5)),
+ "get_position() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_size().is_equal_approx(Vector3(4, 5, 6)),
+ "get_size() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_end().is_equal_approx(Vector3(2.5, 7, 3.5)),
+ "get_end() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_center().is_equal_approx(Vector3(0.5, 4.5, 0.5)),
+ "get_center() should return the expected value.");
+}
+
+TEST_CASE("[AABB] Basic setters") {
+ AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ aabb.set_end(Vector3(100, 0, 100));
+ CHECK_MESSAGE(
+ aabb.is_equal_approx(AABB(Vector3(-1.5, 2, -2.5), Vector3(101.5, -2, 102.5))),
+ "set_end() should result in the expected AABB.");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ aabb.set_position(Vector3(-1000, -2000, -3000));
+ CHECK_MESSAGE(
+ aabb.is_equal_approx(AABB(Vector3(-1000, -2000, -3000), Vector3(4, 5, 6))),
+ "set_position() should result in the expected AABB.");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ aabb.set_size(Vector3(0, 0, -50));
+ CHECK_MESSAGE(
+ aabb.is_equal_approx(AABB(Vector3(-1.5, 2, -2.5), Vector3(0, 0, -50))),
+ "set_size() should result in the expected AABB.");
+}
+
+TEST_CASE("[AABB] Volume getters") {
+ AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ Math::is_equal_approx(aabb.get_volume(), 120),
+ "get_volume() should return the expected value with positive size.");
+ CHECK_MESSAGE(
+ aabb.has_volume(),
+ "Non-empty volumetric AABB should have a volume.");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(-4, 5, 6));
+ CHECK_MESSAGE(
+ Math::is_equal_approx(aabb.get_volume(), -120),
+ "get_volume() should return the expected value with negative size (1 component).");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(-4, -5, 6));
+ CHECK_MESSAGE(
+ Math::is_equal_approx(aabb.get_volume(), 120),
+ "get_volume() should return the expected value with negative size (2 components).");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(-4, -5, -6));
+ CHECK_MESSAGE(
+ Math::is_equal_approx(aabb.get_volume(), -120),
+ "get_volume() should return the expected value with negative size (3 components).");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 0, 6));
+ CHECK_MESSAGE(
+ !aabb.has_volume(),
+ "Non-empty flat AABB should not have a volume.");
+
+ CHECK_MESSAGE(
+ !AABB().has_volume(),
+ "Empty AABB should not have a volume.");
+}
+
+TEST_CASE("[AABB] Surface getters") {
+ AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.has_surface(),
+ "Non-empty volumetric AABB should have an surface.");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 0, 6));
+ CHECK_MESSAGE(
+ aabb.has_surface(),
+ "Non-empty flat AABB should have a surface.");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 0, 0));
+ CHECK_MESSAGE(
+ aabb.has_surface(),
+ "Non-empty linear AABB should have a surface.");
+
+ CHECK_MESSAGE(
+ !AABB().has_surface(),
+ "Empty AABB should not have an surface.");
+}
+
+TEST_CASE("[AABB] Intersection") {
+ const AABB aabb_big = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+
+ AABB aabb_small = AABB(Vector3(-1.5, 2, -2.5), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.intersects(aabb_small),
+ "intersects() with fully contained AABB (touching the edge) should return the expected result.");
+
+ aabb_small = AABB(Vector3(0.5, 1.5, -2), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.intersects(aabb_small),
+ "intersects() with partially contained AABB (overflowing on Y axis) should return the expected result.");
+
+ aabb_small = AABB(Vector3(10, -10, -10), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ !aabb_big.intersects(aabb_small),
+ "intersects() with non-contained AABB should return the expected result.");
+
+ aabb_small = AABB(Vector3(-1.5, 2, -2.5), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.intersection(aabb_small).is_equal_approx(aabb_small),
+ "intersection() with fully contained AABB (touching the edge) should return the expected result.");
+
+ aabb_small = AABB(Vector3(0.5, 1.5, -2), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.intersection(aabb_small).is_equal_approx(AABB(Vector3(0.5, 2, -2), Vector3(1, 0.5, 1))),
+ "intersection() with partially contained AABB (overflowing on Y axis) should return the expected result.");
+
+ aabb_small = AABB(Vector3(10, -10, -10), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.intersection(aabb_small).is_equal_approx(AABB()),
+ "intersection() with non-contained AABB should return the expected result.");
+
+ CHECK_MESSAGE(
+ aabb_big.intersects_plane(Plane(Vector3(0, 1, 0), 4)),
+ "intersects_plane() should return the expected result.");
+ CHECK_MESSAGE(
+ aabb_big.intersects_plane(Plane(Vector3(0, -1, 0), -4)),
+ "intersects_plane() should return the expected result.");
+ CHECK_MESSAGE(
+ !aabb_big.intersects_plane(Plane(Vector3(0, 1, 0), 200)),
+ "intersects_plane() should return the expected result.");
+
+ CHECK_MESSAGE(
+ aabb_big.intersects_segment(Vector3(1, 3, 0), Vector3(0, 3, 0)),
+ "intersects_segment() should return the expected result.");
+ CHECK_MESSAGE(
+ aabb_big.intersects_segment(Vector3(0, 3, 0), Vector3(0, -300, 0)),
+ "intersects_segment() should return the expected result.");
+ CHECK_MESSAGE(
+ aabb_big.intersects_segment(Vector3(-50, 3, -50), Vector3(50, 3, 50)),
+ "intersects_segment() should return the expected result.");
+ CHECK_MESSAGE(
+ !aabb_big.intersects_segment(Vector3(-50, 25, -50), Vector3(50, 25, 50)),
+ "intersects_segment() should return the expected result.");
+ CHECK_MESSAGE(
+ aabb_big.intersects_segment(Vector3(0, 3, 0), Vector3(0, 3, 0)),
+ "intersects_segment() should return the expected result with segment of length 0.");
+ CHECK_MESSAGE(
+ !aabb_big.intersects_segment(Vector3(0, 300, 0), Vector3(0, 300, 0)),
+ "intersects_segment() should return the expected result with segment of length 0.");
+}
+
+TEST_CASE("[AABB] Merging") {
+ const AABB aabb_big = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+
+ AABB aabb_small = AABB(Vector3(-1.5, 2, -2.5), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.merge(aabb_small).is_equal_approx(aabb_big),
+ "merge() with fully contained AABB (touching the edge) should return the expected result.");
+
+ aabb_small = AABB(Vector3(0.5, 1.5, -2), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.merge(aabb_small).is_equal_approx(AABB(Vector3(-1.5, 1.5, -2.5), Vector3(4, 5.5, 6))),
+ "merge() with partially contained AABB (overflowing on Y axis) should return the expected result.");
+
+ aabb_small = AABB(Vector3(10, -10, -10), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.merge(aabb_small).is_equal_approx(AABB(Vector3(-1.5, -10, -10), Vector3(12.5, 17, 13.5))),
+ "merge() with non-contained AABB should return the expected result.");
+}
+
+TEST_CASE("[AABB] Encloses") {
+ const AABB aabb_big = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+
+ AABB aabb_small = AABB(Vector3(-1.5, 2, -2.5), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.encloses(aabb_small),
+ "encloses() with fully contained AABB (touching the edge) should return the expected result.");
+
+ aabb_small = AABB(Vector3(0.5, 1.5, -2), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ !aabb_big.encloses(aabb_small),
+ "encloses() with partially contained AABB (overflowing on Y axis) should return the expected result.");
+
+ aabb_small = AABB(Vector3(10, -10, -10), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ !aabb_big.encloses(aabb_small),
+ "encloses() with non-contained AABB should return the expected result.");
+}
+
+TEST_CASE("[AABB] Get endpoints") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.get_endpoint(0).is_equal_approx(Vector3(-1.5, 2, -2.5)),
+ "The endpoint at index 0 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(1).is_equal_approx(Vector3(-1.5, 2, 3.5)),
+ "The endpoint at index 1 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(2).is_equal_approx(Vector3(-1.5, 7, -2.5)),
+ "The endpoint at index 2 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(3).is_equal_approx(Vector3(-1.5, 7, 3.5)),
+ "The endpoint at index 3 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(4).is_equal_approx(Vector3(2.5, 2, -2.5)),
+ "The endpoint at index 4 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(5).is_equal_approx(Vector3(2.5, 2, 3.5)),
+ "The endpoint at index 5 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(6).is_equal_approx(Vector3(2.5, 7, -2.5)),
+ "The endpoint at index 6 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(7).is_equal_approx(Vector3(2.5, 7, 3.5)),
+ "The endpoint at index 7 should match the expected value.");
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ aabb.get_endpoint(8).is_equal_approx(Vector3()),
+ "The endpoint at invalid index 8 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(-1).is_equal_approx(Vector3()),
+ "The endpoint at invalid index -1 should match the expected value.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[AABB] Get longest/shortest axis") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.get_longest_axis() == Vector3(0, 0, 1),
+ "get_longest_axis() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_longest_axis_index() == Vector3::AXIS_Z,
+ "get_longest_axis_index() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_longest_axis_size() == 6,
+ "get_longest_axis_size() should return the expected value.");
+
+ CHECK_MESSAGE(
+ aabb.get_shortest_axis() == Vector3(1, 0, 0),
+ "get_shortest_axis() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_shortest_axis_index() == Vector3::AXIS_X,
+ "get_shortest_axis_index() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_shortest_axis_size() == 4,
+ "get_shortest_axis_size() should return the expected value.");
+}
+
+TEST_CASE("[AABB] Get support") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.get_support(Vector3(1, 0, 0)).is_equal_approx(Vector3(2.5, 2, -2.5)),
+ "get_support() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_support(Vector3(0.5, 1, 0)).is_equal_approx(Vector3(2.5, 7, -2.5)),
+ "get_support() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_support(Vector3(0.5, 1, -400)).is_equal_approx(Vector3(2.5, 7, -2.5)),
+ "get_support() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_support(Vector3(0, -1, 0)).is_equal_approx(Vector3(-1.5, 2, -2.5)),
+ "get_support() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_support(Vector3(0, -0.1, 0)).is_equal_approx(Vector3(-1.5, 2, -2.5)),
+ "get_support() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_support(Vector3()).is_equal_approx(Vector3(-1.5, 2, -2.5)),
+ "get_support() should return the expected value with a null vector.");
+}
+
+TEST_CASE("[AABB] Grow") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.grow(0.25).is_equal_approx(AABB(Vector3(-1.75, 1.75, -2.75), Vector3(4.5, 5.5, 6.5))),
+ "grow() with positive value should return the expected AABB.");
+ CHECK_MESSAGE(
+ aabb.grow(-0.25).is_equal_approx(AABB(Vector3(-1.25, 2.25, -2.25), Vector3(3.5, 4.5, 5.5))),
+ "grow() with negative value should return the expected AABB.");
+ CHECK_MESSAGE(
+ aabb.grow(-10).is_equal_approx(AABB(Vector3(8.5, 12, 7.5), Vector3(-16, -15, -14))),
+ "grow() with large negative value should return the expected AABB.");
+}
+
+TEST_CASE("[AABB] Has point") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(-1, 3, 0)),
+ "has_point() with contained point should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(2, 3, 0)),
+ "has_point() with contained point should return the expected value.");
+ CHECK_MESSAGE(
+ !aabb.has_point(Vector3(-20, 0, 0)),
+ "has_point() with non-contained point should return the expected value.");
+
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(-1.5, 3, 0)),
+ "has_point() with positive size should include point on near face (X axis).");
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(2.5, 3, 0)),
+ "has_point() with positive size should include point on far face (X axis).");
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(0, 2, 0)),
+ "has_point() with positive size should include point on near face (Y axis).");
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(0, 7, 0)),
+ "has_point() with positive size should include point on far face (Y axis).");
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(0, 3, -2.5)),
+ "has_point() with positive size should include point on near face (Z axis).");
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(0, 3, 3.5)),
+ "has_point() with positive size should include point on far face (Z axis).");
+}
+
+TEST_CASE("[AABB] Expanding") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.expand(Vector3(-1, 3, 0)).is_equal_approx(aabb),
+ "expand() with contained point should return the expected AABB.");
+ CHECK_MESSAGE(
+ aabb.expand(Vector3(2, 3, 0)).is_equal_approx(aabb),
+ "expand() with contained point should return the expected AABB.");
+ CHECK_MESSAGE(
+ aabb.expand(Vector3(-1.5, 3, 0)).is_equal_approx(aabb),
+ "expand() with contained point on negative edge should return the expected AABB.");
+ CHECK_MESSAGE(
+ aabb.expand(Vector3(2.5, 3, 0)).is_equal_approx(aabb),
+ "expand() with contained point on positive edge should return the expected AABB.");
+ CHECK_MESSAGE(
+ aabb.expand(Vector3(-20, 0, 0)).is_equal_approx(AABB(Vector3(-20, 0, -2.5), Vector3(22.5, 7, 6))),
+ "expand() with non-contained point should return the expected AABB.");
+}
+
+TEST_CASE("[AABB] Finite number checks") {
+ const Vector3 x(0, 1, 2);
+ const Vector3 infinite(NAN, NAN, NAN);
+
+ CHECK_MESSAGE(
+ AABB(x, x).is_finite(),
+ "AABB with all components finite should be finite");
+
+ CHECK_FALSE_MESSAGE(
+ AABB(infinite, x).is_finite(),
+ "AABB with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ AABB(x, infinite).is_finite(),
+ "AABB with one component infinite should not be finite.");
+
+ CHECK_FALSE_MESSAGE(
+ AABB(infinite, infinite).is_finite(),
+ "AABB with two components infinite should not be finite.");
+}
+
+} // namespace TestAABB
+
+#endif // TEST_AABB_H
diff --git a/tests/test_astar.h b/tests/core/math/test_astar.h
index bef6127471..9f5e98ef94 100644
--- a/tests/test_astar.h
+++ b/tests/core/math/test_astar.h
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -32,17 +32,12 @@
#define TEST_ASTAR_H
#include "core/math/a_star.h"
-#include "core/math/math_funcs.h"
-#include "core/os/os.h"
-
-#include <math.h>
-#include <stdio.h>
#include "tests/test_macros.h"
namespace TestAStar {
-class ABCX : public AStar {
+class ABCX : public AStar3D {
public:
enum {
A,
@@ -63,7 +58,7 @@ public:
}
// Disable heuristic completely.
- float _compute_cost(int p_from, int p_to) {
+ real_t _compute_cost(int64_t p_from, int64_t p_to) {
if (p_from == A && p_to == C) {
return 1000;
}
@@ -71,18 +66,18 @@ public:
}
};
-TEST_CASE("[AStar] ABC path") {
+TEST_CASE("[AStar3D] ABC path") {
ABCX abcx;
- Vector<int> path = abcx.get_id_path(ABCX::A, ABCX::C);
+ Vector<int64_t> path = abcx.get_id_path(ABCX::A, ABCX::C);
REQUIRE(path.size() == 3);
CHECK(path[0] == ABCX::A);
CHECK(path[1] == ABCX::B);
CHECK(path[2] == ABCX::C);
}
-TEST_CASE("[AStar] ABCX path") {
+TEST_CASE("[AStar3D] ABCX path") {
ABCX abcx;
- Vector<int> path = abcx.get_id_path(ABCX::X, ABCX::C);
+ Vector<int64_t> path = abcx.get_id_path(ABCX::X, ABCX::C);
REQUIRE(path.size() == 4);
CHECK(path[0] == ABCX::X);
CHECK(path[1] == ABCX::A);
@@ -90,8 +85,8 @@ TEST_CASE("[AStar] ABCX path") {
CHECK(path[3] == ABCX::C);
}
-TEST_CASE("[AStar] Add/Remove") {
- AStar a;
+TEST_CASE("[AStar3D] Add/Remove") {
+ AStar3D a;
// Manual tests.
a.add_point(1, Vector3(0, 0, 0));
@@ -218,13 +213,13 @@ TEST_CASE("[AStar] Add/Remove") {
// It's been great work, cheers. \(^ ^)/
}
-TEST_CASE("[Stress][AStar] Find paths") {
+TEST_CASE("[Stress][AStar3D] Find paths") {
// Random stress tests with Floyd-Warshall.
const int N = 30;
Math::seed(0);
for (int test = 0; test < 1000; test++) {
- AStar a;
+ AStar3D a;
Vector3 p[N];
bool adj[N][N] = { { false } };
@@ -323,7 +318,7 @@ TEST_CASE("[Stress][AStar] Find paths") {
for (int u = 0; u < N; u++) {
for (int v = 0; v < N; v++) {
if (u != v) {
- Vector<int> route = a.get_id_path(u, v);
+ Vector<int64_t> route = a.get_id_path(u, v);
if (!Math::is_inf(d[u][v])) {
// Reachable.
if (route.size() == 0) {
@@ -362,7 +357,6 @@ TEST_CASE("[Stress][AStar] Find paths") {
CHECK_MESSAGE(match, "Found all paths.");
}
}
-
} // namespace TestAStar
#endif // TEST_ASTAR_H
diff --git a/tests/test_basis.h b/tests/core/math/test_basis.h
index 05efe33788..f52b715cd7 100644
--- a/tests/test_basis.h
+++ b/tests/core/math/test_basis.h
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -31,24 +31,14 @@
#ifndef TEST_BASIS_H
#define TEST_BASIS_H
+#include "core/math/basis.h"
#include "core/math/random_number_generator.h"
-#include "core/os/os.h"
-#include "core/ustring.h"
#include "tests/test_macros.h"
namespace TestBasis {
-enum RotOrder {
- EulerXYZ,
- EulerXZY,
- EulerYZX,
- EulerYXZ,
- EulerZXY,
- EulerZYX
-};
-
-Vector3 deg2rad(const Vector3 &p_rotation) {
+Vector3 deg_to_rad(const Vector3 &p_rotation) {
return p_rotation / 180.0 * Math_PI;
}
@@ -56,88 +46,26 @@ Vector3 rad2deg(const Vector3 &p_rotation) {
return p_rotation / Math_PI * 180.0;
}
-Basis EulerToBasis(RotOrder mode, const Vector3 &p_rotation) {
- Basis ret;
- switch (mode) {
- case EulerXYZ:
- ret.set_euler_xyz(p_rotation);
- break;
-
- case EulerXZY:
- ret.set_euler_xzy(p_rotation);
- break;
-
- case EulerYZX:
- ret.set_euler_yzx(p_rotation);
- break;
-
- case EulerYXZ:
- ret.set_euler_yxz(p_rotation);
- break;
-
- case EulerZXY:
- ret.set_euler_zxy(p_rotation);
- break;
-
- case EulerZYX:
- ret.set_euler_zyx(p_rotation);
- break;
-
- default:
- // If you land here, Please integrate all rotation orders.
- FAIL("This is not unreachable.");
- }
-
- return ret;
-}
-
-Vector3 BasisToEuler(RotOrder mode, const Basis &p_rotation) {
- switch (mode) {
- case EulerXYZ:
- return p_rotation.get_euler_xyz();
-
- case EulerXZY:
- return p_rotation.get_euler_xzy();
-
- case EulerYZX:
- return p_rotation.get_euler_yzx();
-
- case EulerYXZ:
- return p_rotation.get_euler_yxz();
-
- case EulerZXY:
- return p_rotation.get_euler_zxy();
-
- case EulerZYX:
- return p_rotation.get_euler_zyx();
-
- default:
- // If you land here, Please integrate all rotation orders.
- FAIL("This is not unreachable.");
- return Vector3();
- }
-}
-
-String get_rot_order_name(RotOrder ro) {
+String get_rot_order_name(Basis::EulerOrder ro) {
switch (ro) {
- case EulerXYZ:
+ case Basis::EULER_ORDER_XYZ:
return "XYZ";
- case EulerXZY:
+ case Basis::EULER_ORDER_XZY:
return "XZY";
- case EulerYZX:
+ case Basis::EULER_ORDER_YZX:
return "YZX";
- case EulerYXZ:
+ case Basis::EULER_ORDER_YXZ:
return "YXZ";
- case EulerZXY:
+ case Basis::EULER_ORDER_ZXY:
return "ZXY";
- case EulerZYX:
+ case Basis::EULER_ORDER_ZYX:
return "ZYX";
default:
return "[Not supported]";
}
}
-void test_rotation(Vector3 deg_original_euler, RotOrder rot_order) {
+void test_rotation(Vector3 deg_original_euler, Basis::EulerOrder rot_order) {
// This test:
// 1. Converts the rotation vector from deg to rad.
// 2. Converts euler to basis.
@@ -156,29 +84,28 @@ void test_rotation(Vector3 deg_original_euler, RotOrder rot_order) {
// are correct.
// Euler to rotation
- const Vector3 original_euler = deg2rad(deg_original_euler);
- const Basis to_rotation = EulerToBasis(rot_order, original_euler);
+ const Vector3 original_euler = deg_to_rad(deg_original_euler);
+ const Basis to_rotation = Basis::from_euler(original_euler, rot_order);
// Euler from rotation
- const Vector3 euler_from_rotation = BasisToEuler(rot_order, to_rotation);
- const Basis rotation_from_computed_euler = EulerToBasis(rot_order, euler_from_rotation);
+ const Vector3 euler_from_rotation = to_rotation.get_euler(rot_order);
+ const Basis rotation_from_computed_euler = Basis::from_euler(euler_from_rotation, rot_order);
Basis res = to_rotation.inverse() * rotation_from_computed_euler;
- CHECK_MESSAGE((res.get_axis(0) - Vector3(1.0, 0.0, 0.0)).length() <= 0.1, vformat("Fail due to X %s\n", String(res.get_axis(0))).utf8().ptr());
- CHECK_MESSAGE((res.get_axis(1) - Vector3(0.0, 1.0, 0.0)).length() <= 0.1, vformat("Fail due to Y %s\n", String(res.get_axis(1))).utf8().ptr());
- CHECK_MESSAGE((res.get_axis(2) - Vector3(0.0, 0.0, 1.0)).length() <= 0.1, vformat("Fail due to Z %s\n", String(res.get_axis(2))).utf8().ptr());
+ CHECK_MESSAGE((res.get_column(0) - Vector3(1.0, 0.0, 0.0)).length() <= 0.1, vformat("Fail due to X %s\n", String(res.get_column(0))).utf8().ptr());
+ CHECK_MESSAGE((res.get_column(1) - Vector3(0.0, 1.0, 0.0)).length() <= 0.1, vformat("Fail due to Y %s\n", String(res.get_column(1))).utf8().ptr());
+ CHECK_MESSAGE((res.get_column(2) - Vector3(0.0, 0.0, 1.0)).length() <= 0.1, vformat("Fail due to Z %s\n", String(res.get_column(2))).utf8().ptr());
// Double check `to_rotation` decomposing with XYZ rotation order.
- const Vector3 euler_xyz_from_rotation = to_rotation.get_euler_xyz();
- Basis rotation_from_xyz_computed_euler;
- rotation_from_xyz_computed_euler.set_euler_xyz(euler_xyz_from_rotation);
+ const Vector3 euler_xyz_from_rotation = to_rotation.get_euler(Basis::EULER_ORDER_XYZ);
+ Basis rotation_from_xyz_computed_euler = Basis::from_euler(euler_xyz_from_rotation, Basis::EULER_ORDER_XYZ);
res = to_rotation.inverse() * rotation_from_xyz_computed_euler;
- CHECK_MESSAGE((res.get_axis(0) - Vector3(1.0, 0.0, 0.0)).length() <= 0.1, vformat("Double check with XYZ rot order failed, due to X %s\n", String(res.get_axis(0))).utf8().ptr());
- CHECK_MESSAGE((res.get_axis(1) - Vector3(0.0, 1.0, 0.0)).length() <= 0.1, vformat("Double check with XYZ rot order failed, due to Y %s\n", String(res.get_axis(1))).utf8().ptr());
- CHECK_MESSAGE((res.get_axis(2) - Vector3(0.0, 0.0, 1.0)).length() <= 0.1, vformat("Double check with XYZ rot order failed, due to Z %s\n", String(res.get_axis(2))).utf8().ptr());
+ CHECK_MESSAGE((res.get_column(0) - Vector3(1.0, 0.0, 0.0)).length() <= 0.1, vformat("Double check with XYZ rot order failed, due to X %s\n", String(res.get_column(0))).utf8().ptr());
+ CHECK_MESSAGE((res.get_column(1) - Vector3(0.0, 1.0, 0.0)).length() <= 0.1, vformat("Double check with XYZ rot order failed, due to Y %s\n", String(res.get_column(1))).utf8().ptr());
+ CHECK_MESSAGE((res.get_column(2) - Vector3(0.0, 0.0, 1.0)).length() <= 0.1, vformat("Double check with XYZ rot order failed, due to Z %s\n", String(res.get_column(2))).utf8().ptr());
INFO(vformat("Rotation order: %s\n.", get_rot_order_name(rot_order)).utf8().ptr());
INFO(vformat("Original Rotation: %s\n", String(deg_original_euler)).utf8().ptr());
@@ -186,13 +113,13 @@ void test_rotation(Vector3 deg_original_euler, RotOrder rot_order) {
}
TEST_CASE("[Basis] Euler conversions") {
- Vector<RotOrder> rotorder_to_test;
- rotorder_to_test.push_back(EulerXYZ);
- rotorder_to_test.push_back(EulerXZY);
- rotorder_to_test.push_back(EulerYZX);
- rotorder_to_test.push_back(EulerYXZ);
- rotorder_to_test.push_back(EulerZXY);
- rotorder_to_test.push_back(EulerZYX);
+ Vector<Basis::EulerOrder> euler_order_to_test;
+ euler_order_to_test.push_back(Basis::EULER_ORDER_XYZ);
+ euler_order_to_test.push_back(Basis::EULER_ORDER_XZY);
+ euler_order_to_test.push_back(Basis::EULER_ORDER_YZX);
+ euler_order_to_test.push_back(Basis::EULER_ORDER_YXZ);
+ euler_order_to_test.push_back(Basis::EULER_ORDER_ZXY);
+ euler_order_to_test.push_back(Basis::EULER_ORDER_ZYX);
Vector<Vector3> vectors_to_test;
@@ -250,21 +177,21 @@ TEST_CASE("[Basis] Euler conversions") {
vectors_to_test.push_back(Vector3(120.0, 150.0, -130.0));
vectors_to_test.push_back(Vector3(120.0, 150.0, 130.0));
- for (int h = 0; h < rotorder_to_test.size(); h += 1) {
+ for (int h = 0; h < euler_order_to_test.size(); h += 1) {
for (int i = 0; i < vectors_to_test.size(); i += 1) {
- test_rotation(vectors_to_test[i], rotorder_to_test[h]);
+ test_rotation(vectors_to_test[i], euler_order_to_test[h]);
}
}
}
TEST_CASE("[Stress][Basis] Euler conversions") {
- Vector<RotOrder> rotorder_to_test;
- rotorder_to_test.push_back(EulerXYZ);
- rotorder_to_test.push_back(EulerXZY);
- rotorder_to_test.push_back(EulerYZX);
- rotorder_to_test.push_back(EulerYXZ);
- rotorder_to_test.push_back(EulerZXY);
- rotorder_to_test.push_back(EulerZYX);
+ Vector<Basis::EulerOrder> euler_order_to_test;
+ euler_order_to_test.push_back(Basis::EULER_ORDER_XYZ);
+ euler_order_to_test.push_back(Basis::EULER_ORDER_XZY);
+ euler_order_to_test.push_back(Basis::EULER_ORDER_YZX);
+ euler_order_to_test.push_back(Basis::EULER_ORDER_YXZ);
+ euler_order_to_test.push_back(Basis::EULER_ORDER_ZXY);
+ euler_order_to_test.push_back(Basis::EULER_ORDER_ZYX);
Vector<Vector3> vectors_to_test;
// Add 1000 random vectors with weirds numbers.
@@ -276,13 +203,99 @@ TEST_CASE("[Stress][Basis] Euler conversions") {
rng.randf_range(-1800, 1800)));
}
- for (int h = 0; h < rotorder_to_test.size(); h += 1) {
+ for (int h = 0; h < euler_order_to_test.size(); h += 1) {
for (int i = 0; i < vectors_to_test.size(); i += 1) {
- test_rotation(vectors_to_test[i], rotorder_to_test[h]);
+ test_rotation(vectors_to_test[i], euler_order_to_test[h]);
}
}
}
+TEST_CASE("[Basis] Set axis angle") {
+ Vector3 axis;
+ real_t angle;
+ real_t pi = (real_t)Math_PI;
+
+ // Testing the singularity when the angle is 0°.
+ Basis identity(1, 0, 0, 0, 1, 0, 0, 0, 1);
+ identity.get_axis_angle(axis, angle);
+ CHECK(angle == 0);
+
+ // Testing the singularity when the angle is 180°.
+ Basis singularityPi(-1, 0, 0, 0, 1, 0, 0, 0, -1);
+ singularityPi.get_axis_angle(axis, angle);
+ CHECK(Math::is_equal_approx(angle, pi));
+
+ // Testing reversing the an axis (of an 30° angle).
+ float cos30deg = Math::cos(Math::deg_to_rad((real_t)30.0));
+ Basis z_positive(cos30deg, -0.5, 0, 0.5, cos30deg, 0, 0, 0, 1);
+ Basis z_negative(cos30deg, 0.5, 0, -0.5, cos30deg, 0, 0, 0, 1);
+
+ z_positive.get_axis_angle(axis, angle);
+ CHECK(Math::is_equal_approx(angle, Math::deg_to_rad((real_t)30.0)));
+ CHECK(axis == Vector3(0, 0, 1));
+
+ z_negative.get_axis_angle(axis, angle);
+ CHECK(Math::is_equal_approx(angle, Math::deg_to_rad((real_t)30.0)));
+ CHECK(axis == Vector3(0, 0, -1));
+
+ // Testing a rotation of 90° on x-y-z.
+ Basis x90deg(1, 0, 0, 0, 0, -1, 0, 1, 0);
+ x90deg.get_axis_angle(axis, angle);
+ CHECK(Math::is_equal_approx(angle, pi / (real_t)2));
+ CHECK(axis == Vector3(1, 0, 0));
+
+ Basis y90deg(0, 0, 1, 0, 1, 0, -1, 0, 0);
+ y90deg.get_axis_angle(axis, angle);
+ CHECK(axis == Vector3(0, 1, 0));
+
+ Basis z90deg(0, -1, 0, 1, 0, 0, 0, 0, 1);
+ z90deg.get_axis_angle(axis, angle);
+ CHECK(axis == Vector3(0, 0, 1));
+
+ // Regression test: checks that the method returns a small angle (not 0).
+ Basis tiny(1, 0, 0, 0, 0.9999995, -0.001, 0, 001, 0.9999995); // The min angle possible with float is 0.001rad.
+ tiny.get_axis_angle(axis, angle);
+ CHECK(Math::is_equal_approx(angle, (real_t)0.001, (real_t)0.0001));
+
+ // Regression test: checks that the method returns an angle which is a number (not NaN)
+ Basis bugNan(1.00000024, 0, 0.000100001693, 0, 1, 0, -0.000100009143, 0, 1.00000024);
+ bugNan.get_axis_angle(axis, angle);
+ CHECK(!Math::is_nan(angle));
+}
+
+TEST_CASE("[Basis] Finite number checks") {
+ const Vector3 x(0, 1, 2);
+ const Vector3 infinite(NAN, NAN, NAN);
+
+ CHECK_MESSAGE(
+ Basis(x, x, x).is_finite(),
+ "Basis with all components finite should be finite");
+
+ CHECK_FALSE_MESSAGE(
+ Basis(infinite, x, x).is_finite(),
+ "Basis with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Basis(x, infinite, x).is_finite(),
+ "Basis with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Basis(x, x, infinite).is_finite(),
+ "Basis with one component infinite should not be finite.");
+
+ CHECK_FALSE_MESSAGE(
+ Basis(infinite, infinite, x).is_finite(),
+ "Basis with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Basis(infinite, x, infinite).is_finite(),
+ "Basis with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Basis(x, infinite, infinite).is_finite(),
+ "Basis with two components infinite should not be finite.");
+
+ CHECK_FALSE_MESSAGE(
+ Basis(infinite, infinite, infinite).is_finite(),
+ "Basis with three components infinite should not be finite.");
+}
+
} // namespace TestBasis
-#endif
+#endif // TEST_BASIS_H
diff --git a/tests/test_color.h b/tests/core/math/test_color.h
index dfdc29ec7d..51c3bc8bdc 100644
--- a/tests/test_color.h
+++ b/tests/core/math/test_color.h
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -31,9 +31,9 @@
#ifndef TEST_COLOR_H
#define TEST_COLOR_H
-#include "core/color.h"
+#include "core/math/color.h"
-#include "thirdparty/doctest/doctest.h"
+#include "tests/test_macros.h"
namespace TestColor {
@@ -101,13 +101,13 @@ TEST_CASE("[Color] Reading methods") {
const Color dark_blue = Color(0, 0, 0.5, 0.4);
CHECK_MESSAGE(
- Math::is_equal_approx(dark_blue.get_h(), 240 / 360.0),
+ Math::is_equal_approx(dark_blue.get_h(), 240.0f / 360.0f),
"The returned HSV hue should match the expected value.");
CHECK_MESSAGE(
- Math::is_equal_approx(dark_blue.get_s(), 1),
+ Math::is_equal_approx(dark_blue.get_s(), 1.0f),
"The returned HSV saturation should match the expected value.");
CHECK_MESSAGE(
- Math::is_equal_approx(dark_blue.get_v(), 0.5),
+ Math::is_equal_approx(dark_blue.get_v(), 0.5f),
"The returned HSV value should match the expected value.");
}
@@ -140,21 +140,39 @@ TEST_CASE("[Color] Conversion methods") {
cyan.to_rgba64() == 0x0000'ffff'ffff'ffff,
"The returned 64-bit BGR number should match the expected value.");
CHECK_MESSAGE(
- String(cyan) == "0, 1, 1, 1",
+ String(cyan) == "(0, 1, 1, 1)",
"The string representation should match the expected value.");
}
+TEST_CASE("[Color] Linear <-> sRGB conversion") {
+ const Color color = Color(0.35, 0.5, 0.6, 0.7);
+ const Color color_linear = color.srgb_to_linear();
+ const Color color_srgb = color.linear_to_srgb();
+ CHECK_MESSAGE(
+ color_linear.is_equal_approx(Color(0.100481, 0.214041, 0.318547, 0.7)),
+ "The color converted to linear color space should match the expected value.");
+ CHECK_MESSAGE(
+ color_srgb.is_equal_approx(Color(0.62621, 0.735357, 0.797738, 0.7)),
+ "The color converted to sRGB color space should match the expected value.");
+ CHECK_MESSAGE(
+ color_linear.linear_to_srgb().is_equal_approx(Color(0.35, 0.5, 0.6, 0.7)),
+ "The linear color converted back to sRGB color space should match the expected value.");
+ CHECK_MESSAGE(
+ color_srgb.srgb_to_linear().is_equal_approx(Color(0.35, 0.5, 0.6, 0.7)),
+ "The sRGB color converted back to linear color space should match the expected value.");
+}
+
TEST_CASE("[Color] Named colors") {
CHECK_MESSAGE(
- Color::named("red").is_equal_approx(Color(1, 0, 0)),
+ Color::named("red").is_equal_approx(Color::hex(0xFF0000FF)),
"The named color \"red\" should match the expected value.");
// Named colors have their names automatically normalized.
CHECK_MESSAGE(
- Color::named("white_smoke").is_equal_approx(Color(0.96, 0.96, 0.96)),
+ Color::named("white_smoke").is_equal_approx(Color::hex(0xF5F5F5FF)),
"The named color \"white_smoke\" should match the expected value.");
CHECK_MESSAGE(
- Color::named("Slate Blue").is_equal_approx(Color(0.42, 0.35, 0.80)),
+ Color::named("Slate Blue").is_equal_approx(Color::hex(0x6A5ACDFF)),
"The named color \"Slate Blue\" should match the expected value.");
ERR_PRINT_OFF;
@@ -185,9 +203,6 @@ TEST_CASE("[Color] Manipulation methods") {
CHECK_MESSAGE(
blue.inverted().is_equal_approx(Color(1, 1, 0, 0.4)),
"Inverted color should have its red, green and blue components inverted.");
- CHECK_MESSAGE(
- blue.contrasted().is_equal_approx(Color(0.5, 0.5, 0.5, 0.4)),
- "Contrasted pure blue should be fully gray.");
const Color purple = Color(0.5, 0.2, 0.5, 0.25);
@@ -205,7 +220,6 @@ TEST_CASE("[Color] Manipulation methods") {
red.lerp(yellow, 0.5).is_equal_approx(Color(1, 0.5, 0, 0.5)),
"Red interpolated with yellow should be orange (with interpolated alpha).");
}
-
} // namespace TestColor
#endif // TEST_COLOR_H
diff --git a/tests/test_expression.h b/tests/core/math/test_expression.h
index a3d4877d52..6e3be541b0 100644
--- a/tests/test_expression.h
+++ b/tests/core/math/test_expression.h
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -83,42 +83,42 @@ TEST_CASE("[Expression] Floating-point arithmetic") {
expression.parse("-123.456") == OK,
"Float identity should parse successfully.");
CHECK_MESSAGE(
- Math::is_equal_approx(float(expression.execute()), -123.456),
+ Math::is_equal_approx(double(expression.execute()), -123.456),
"Float identity should return the expected result.");
CHECK_MESSAGE(
expression.parse("2.0 + 3.0") == OK,
"Float addition should parse successfully.");
CHECK_MESSAGE(
- Math::is_equal_approx(float(expression.execute()), 5),
+ Math::is_equal_approx(double(expression.execute()), 5),
"Float addition should return the expected result.");
CHECK_MESSAGE(
expression.parse("3.0 / 10") == OK,
"Float / integer division should parse successfully.");
CHECK_MESSAGE(
- Math::is_equal_approx(float(expression.execute()), 0.3),
+ Math::is_equal_approx(double(expression.execute()), 0.3),
"Float / integer division should return the expected result.");
CHECK_MESSAGE(
expression.parse("3 / 10.0") == OK,
"Basic integer / float division should parse successfully.");
CHECK_MESSAGE(
- Math::is_equal_approx(float(expression.execute()), 0.3),
+ Math::is_equal_approx(double(expression.execute()), 0.3),
"Basic integer / float division should return the expected result.");
CHECK_MESSAGE(
expression.parse("3.0 / 10.0") == OK,
"Float / float division should parse successfully.");
CHECK_MESSAGE(
- Math::is_equal_approx(float(expression.execute()), 0.3),
+ Math::is_equal_approx(double(expression.execute()), 0.3),
"Float / float division should return the expected result.");
CHECK_MESSAGE(
expression.parse("2.5 * (6.0 + 14.25) / 2.0 - 5.12345") == OK,
"Float multiplication-addition-subtraction-division should parse successfully.");
CHECK_MESSAGE(
- Math::is_equal_approx(float(expression.execute()), 20.18905),
+ Math::is_equal_approx(double(expression.execute()), 20.18905),
"Float multiplication-addition-subtraction-division should return the expected result.");
}
@@ -129,7 +129,7 @@ TEST_CASE("[Expression] Scientific notation") {
expression.parse("2.e5") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
- Math::is_equal_approx(float(expression.execute()), 200'000),
+ Math::is_equal_approx(double(expression.execute()), 200'000),
"The expression should return the expected result.");
// The middle "e" is ignored here.
@@ -137,14 +137,14 @@ TEST_CASE("[Expression] Scientific notation") {
expression.parse("2e5") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
- Math::is_equal_approx(float(expression.execute()), 25),
+ Math::is_equal_approx(double(expression.execute()), 2e5),
"The expression should return the expected result.");
CHECK_MESSAGE(
expression.parse("2e.5") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
- Math::is_equal_approx(float(expression.execute()), 2),
+ Math::is_equal_approx(double(expression.execute()), 2),
"The expression should return the expected result.");
}
@@ -173,17 +173,17 @@ TEST_CASE("[Expression] Built-in functions") {
"`sqrt(pow(3, 2) + pow(4, 2))` should return the expected result.");
CHECK_MESSAGE(
- expression.parse("stepify(sin(0.5), 0.01)") == OK,
+ expression.parse("snapped(sin(0.5), 0.01)") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
- Math::is_equal_approx(float(expression.execute()), 0.48),
- "`stepify(sin(0.5), 0.01)` should return the expected result.");
+ Math::is_equal_approx(double(expression.execute()), 0.48),
+ "`snapped(sin(0.5), 0.01)` should return the expected result.");
CHECK_MESSAGE(
expression.parse("pow(2.0, -2500)") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
- Math::is_zero_approx(float(expression.execute())),
+ Math::is_zero_approx(double(expression.execute())),
"`pow(2.0, -2500)` should return the expected result (asymptotically zero).");
}
@@ -381,7 +381,7 @@ TEST_CASE("[Expression] Unusual expressions") {
ERR_PRINT_OFF;
CHECK_MESSAGE(
- expression.parse("$1.00 + €5") == OK,
+ expression.parse("$1.00 + ???5") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
int(expression.execute()) == 0,
@@ -410,8 +410,8 @@ TEST_CASE("[Expression] Unusual expressions") {
"The expression should parse successfully.");
ERR_PRINT_OFF;
CHECK_MESSAGE(
- Math::is_zero_approx(float(expression.execute())),
- "`-25.4 / 0` should return 0.");
+ Math::is_inf(double(expression.execute())),
+ "`-25.4 / 0` should return inf.");
ERR_PRINT_ON;
CHECK_MESSAGE(
@@ -439,7 +439,6 @@ TEST_CASE("[Expression] Unusual expressions") {
// int64_t(expression.execute()) == 0,
// "`(-9223372036854775807 - 1) / -1` should return the expected result.");
}
-
} // namespace TestExpression
#endif // TEST_EXPRESSION_H
diff --git a/tests/core/math/test_geometry_2d.h b/tests/core/math/test_geometry_2d.h
new file mode 100644
index 0000000000..db4e6e2177
--- /dev/null
+++ b/tests/core/math/test_geometry_2d.h
@@ -0,0 +1,866 @@
+/*************************************************************************/
+/* test_geometry_2d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_GEOMETRY_2D_H
+#define TEST_GEOMETRY_2D_H
+
+#include "core/math/geometry_2d.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestGeometry2D {
+
+TEST_CASE("[Geometry2D] Point in circle") {
+ CHECK(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(0, 0), 1.0));
+
+ CHECK(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(11.99, 0), 12));
+ CHECK(Geometry2D::is_point_in_circle(Vector2(-11.99, 0), Vector2(0, 0), 12));
+
+ CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(12.01, 0), 12));
+ CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(-12.01, 0), Vector2(0, 0), 12));
+
+ CHECK(Geometry2D::is_point_in_circle(Vector2(7, -42), Vector2(4, -40), 3.7));
+ CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(7, -42), Vector2(4, -40), 3.5));
+
+ // This tests points on the edge of the circle. They are treated as being inside the circle.
+ CHECK(Geometry2D::is_point_in_circle(Vector2(1.0, 0.0), Vector2(0, 0), 1.0));
+ CHECK(Geometry2D::is_point_in_circle(Vector2(0.0, -1.0), Vector2(0, 0), 1.0));
+}
+
+TEST_CASE("[Geometry2D] Point in triangle") {
+ CHECK(Geometry2D::is_point_in_triangle(Vector2(0, 0), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));
+ CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(-1.01, 1.0), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));
+
+ CHECK(Geometry2D::is_point_in_triangle(Vector2(3, 2.5), Vector2(1, 4), Vector2(3, 2), Vector2(5, 4)));
+ CHECK(Geometry2D::is_point_in_triangle(Vector2(-3, -2.5), Vector2(-1, -4), Vector2(-3, -2), Vector2(-5, -4)));
+ CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(0, 0), Vector2(1, 4), Vector2(3, 2), Vector2(5, 4)));
+
+ // This tests points on the edge of the triangle. They are treated as being outside the triangle.
+ // In `is_point_in_circle` and `is_point_in_polygon` they are treated as being inside, so in order the make
+ // the behaviour consistent this may change in the future (see issue #44717 and PR #44274).
+ CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(1, 1), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));
+ CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(0, 1), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));
+}
+
+TEST_CASE("[Geometry2D] Point in polygon") {
+ Vector<Vector2> p;
+ CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(0, 0), p));
+
+ p.push_back(Vector2(-88, 120));
+ p.push_back(Vector2(-74, -38));
+ p.push_back(Vector2(135, -145));
+ p.push_back(Vector2(425, 70));
+ p.push_back(Vector2(68, 112));
+ p.push_back(Vector2(-120, 370));
+ p.push_back(Vector2(-323, -145));
+ CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-350, 0), p));
+ CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-110, 60), p));
+ CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(412, 96), p));
+ CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(83, 130), p));
+ CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-320, -153), p));
+
+ CHECK(Geometry2D::is_point_in_polygon(Vector2(0, 0), p));
+ CHECK(Geometry2D::is_point_in_polygon(Vector2(-230, 0), p));
+ CHECK(Geometry2D::is_point_in_polygon(Vector2(130, -110), p));
+ CHECK(Geometry2D::is_point_in_polygon(Vector2(370, 55), p));
+ CHECK(Geometry2D::is_point_in_polygon(Vector2(-160, 190), p));
+
+ // This tests points on the edge of the polygon. They are treated as being inside the polygon.
+ int c = p.size();
+ for (int i = 0; i < c; i++) {
+ const Vector2 &p1 = p[i];
+ CHECK(Geometry2D::is_point_in_polygon(p1, p));
+
+ const Vector2 &p2 = p[(i + 1) % c];
+ Vector2 midpoint((p1 + p2) * 0.5);
+ CHECK(Geometry2D::is_point_in_polygon(midpoint, p));
+ }
+}
+
+TEST_CASE("[Geometry2D] Polygon clockwise") {
+ Vector<Vector2> p;
+ CHECK_FALSE(Geometry2D::is_polygon_clockwise(p));
+
+ p.push_back(Vector2(5, -5));
+ p.push_back(Vector2(-1, -5));
+ p.push_back(Vector2(-5, -1));
+ p.push_back(Vector2(-1, 3));
+ p.push_back(Vector2(1, 5));
+ CHECK(Geometry2D::is_polygon_clockwise(p));
+
+ p.reverse();
+ CHECK_FALSE(Geometry2D::is_polygon_clockwise(p));
+}
+
+TEST_CASE("[Geometry2D] Line intersection") {
+ Vector2 r;
+ CHECK(Geometry2D::line_intersects_line(Vector2(2, 0), Vector2(0, 1), Vector2(0, 2), Vector2(1, 0), r));
+ CHECK(r.is_equal_approx(Vector2(2, 2)));
+
+ CHECK(Geometry2D::line_intersects_line(Vector2(-1, 1), Vector2(1, -1), Vector2(4, 1), Vector2(-1, -1), r));
+ CHECK(r.is_equal_approx(Vector2(1.5, -1.5)));
+
+ CHECK(Geometry2D::line_intersects_line(Vector2(-1, 0), Vector2(-1, -1), Vector2(1, 0), Vector2(1, -1), r));
+ CHECK(r.is_equal_approx(Vector2(0, 1)));
+
+ CHECK_FALSE_MESSAGE(
+ Geometry2D::line_intersects_line(Vector2(-1, 1), Vector2(1, -1), Vector2(0, 1), Vector2(1, -1), r),
+ "Parallel lines should not intersect.");
+}
+
+TEST_CASE("[Geometry2D] Segment intersection") {
+ Vector2 r;
+
+ CHECK(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(-1, -1), &r));
+ CHECK(r.is_equal_approx(Vector2(0, 0)));
+
+ CHECK_FALSE(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(0.1, 0.1), &r));
+ CHECK_FALSE(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(0.1, 0.1), Vector2(1, 1), &r));
+
+ CHECK_FALSE_MESSAGE(
+ Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(0, 1), Vector2(2, -1), &r),
+ "Parallel segments should not intersect.");
+
+ CHECK_FALSE_MESSAGE(
+ Geometry2D::segment_intersects_segment(Vector2(1, 2), Vector2(3, 2), Vector2(0, 2), Vector2(-2, 2), &r),
+ "Non-overlapping collinear segments should not intersect.");
+
+ CHECK_MESSAGE(
+ Geometry2D::segment_intersects_segment(Vector2(0, 0), Vector2(0, 1), Vector2(0, 0), Vector2(1, 0), &r),
+ "Touching segments should intersect.");
+ CHECK(r.is_equal_approx(Vector2(0, 0)));
+
+ CHECK_MESSAGE(
+ Geometry2D::segment_intersects_segment(Vector2(0, 1), Vector2(0, 0), Vector2(0, 0), Vector2(1, 0), &r),
+ "Touching segments should intersect.");
+ CHECK(r.is_equal_approx(Vector2(0, 0)));
+}
+
+TEST_CASE("[Geometry2D] Segment intersection with circle") {
+ real_t minus_one = -1.0;
+ real_t zero = 0.0;
+ real_t one_quarter = 0.25;
+ real_t three_quarters = 0.75;
+ real_t one = 1.0;
+
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Geometry2D::segment_intersects_circle(Vector2(0, 0), Vector2(4, 0), Vector2(0, 0), 1.0), one_quarter),
+ "Segment from inside to outside of circle should intersect it.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Geometry2D::segment_intersects_circle(Vector2(4, 0), Vector2(0, 0), Vector2(0, 0), 1.0), three_quarters),
+ "Segment from outside to inside of circle should intersect it.");
+
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Geometry2D::segment_intersects_circle(Vector2(-2, 0), Vector2(2, 0), Vector2(0, 0), 1.0), one_quarter),
+ "Segment running through circle should intersect it.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Geometry2D::segment_intersects_circle(Vector2(2, 0), Vector2(-2, 0), Vector2(0, 0), 1.0), one_quarter),
+ "Segment running through circle should intersect it.");
+
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Geometry2D::segment_intersects_circle(Vector2(0, 0), Vector2(1, 0), Vector2(0, 0), 1.0), one),
+ "Segment starting inside the circle and ending on the circle should intersect it");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Geometry2D::segment_intersects_circle(Vector2(1, 0), Vector2(0, 0), Vector2(0, 0), 1.0), zero),
+ "Segment starting on the circle and going inwards should intersect it");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Geometry2D::segment_intersects_circle(Vector2(1, 0), Vector2(2, 0), Vector2(0, 0), 1.0), zero),
+ "Segment starting on the circle and going outwards should intersect it");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Geometry2D::segment_intersects_circle(Vector2(2, 0), Vector2(1, 0), Vector2(0, 0), 1.0), one),
+ "Segment starting outside the circle and ending on the circle intersect it");
+
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Geometry2D::segment_intersects_circle(Vector2(-1, 0), Vector2(1, 0), Vector2(0, 0), 2.0), minus_one),
+ "Segment completely within the circle should not intersect it");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Geometry2D::segment_intersects_circle(Vector2(1, 0), Vector2(-1, 0), Vector2(0, 0), 2.0), minus_one),
+ "Segment completely within the circle should not intersect it");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Geometry2D::segment_intersects_circle(Vector2(2, 0), Vector2(3, 0), Vector2(0, 0), 1.0), minus_one),
+ "Segment completely outside the circle should not intersect it");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Geometry2D::segment_intersects_circle(Vector2(3, 0), Vector2(2, 0), Vector2(0, 0), 1.0), minus_one),
+ "Segment completely outside the circle should not intersect it");
+}
+
+TEST_CASE("[Geometry2D] Segment intersection with polygon") {
+ Vector<Point2> a;
+
+ a.push_back(Point2(-2, 2));
+ a.push_back(Point2(3, 4));
+ a.push_back(Point2(1, 1));
+ a.push_back(Point2(2, -2));
+ a.push_back(Point2(-1, -1));
+
+ CHECK_MESSAGE(
+ Geometry2D::is_segment_intersecting_polygon(Vector2(0, 2), Vector2(2, 2), a),
+ "Segment from inside to outside of polygon should intersect it.");
+ CHECK_MESSAGE(
+ Geometry2D::is_segment_intersecting_polygon(Vector2(2, 2), Vector2(0, 2), a),
+ "Segment from outside to inside of polygon should intersect it.");
+
+ CHECK_MESSAGE(
+ Geometry2D::is_segment_intersecting_polygon(Vector2(2, 4), Vector2(3, 3), a),
+ "Segment running through polygon should intersect it.");
+ CHECK_MESSAGE(
+ Geometry2D::is_segment_intersecting_polygon(Vector2(3, 3), Vector2(2, 4), a),
+ "Segment running through polygon should intersect it.");
+
+ CHECK_MESSAGE(
+ Geometry2D::is_segment_intersecting_polygon(Vector2(0, 0), Vector2(1, 1), a),
+ "Segment starting inside the polygon and ending on the polygon should intersect it");
+ CHECK_MESSAGE(
+ Geometry2D::is_segment_intersecting_polygon(Vector2(1, 1), Vector2(0, 0), a),
+ "Segment starting on the polygon and going inwards should intersect it");
+ CHECK_MESSAGE(
+ Geometry2D::is_segment_intersecting_polygon(Vector2(-2, 2), Vector2(-2, -1), a),
+ "Segment starting on the polygon and going outwards should intersect it");
+ CHECK_MESSAGE(
+ Geometry2D::is_segment_intersecting_polygon(Vector2(-2, 1), Vector2(-2, 2), a),
+ "Segment starting outside the polygon and ending on the polygon intersect it");
+
+ CHECK_FALSE_MESSAGE(
+ Geometry2D::is_segment_intersecting_polygon(Vector2(-1, 2), Vector2(1, -1), a),
+ "Segment completely within the polygon should not intersect it");
+ CHECK_FALSE_MESSAGE(
+ Geometry2D::is_segment_intersecting_polygon(Vector2(1, -1), Vector2(-1, 2), a),
+ "Segment completely within the polygon should not intersect it");
+ CHECK_FALSE_MESSAGE(
+ Geometry2D::is_segment_intersecting_polygon(Vector2(2, 2), Vector2(2, -1), a),
+ "Segment completely outside the polygon should not intersect it");
+ CHECK_FALSE_MESSAGE(
+ Geometry2D::is_segment_intersecting_polygon(Vector2(2, -1), Vector2(2, 2), a),
+ "Segment completely outside the polygon should not intersect it");
+}
+
+TEST_CASE("[Geometry2D] Closest point to segment") {
+ Vector2 s[] = { Vector2(-4, -4), Vector2(4, 4) };
+ CHECK(Geometry2D::get_closest_point_to_segment(Vector2(4.1, 4.1), s).is_equal_approx(Vector2(4, 4)));
+ CHECK(Geometry2D::get_closest_point_to_segment(Vector2(-4.1, -4.1), s).is_equal_approx(Vector2(-4, -4)));
+ CHECK(Geometry2D::get_closest_point_to_segment(Vector2(-1, 1), s).is_equal_approx(Vector2(0, 0)));
+
+ Vector2 t[] = { Vector2(1, -2), Vector2(1, -2) };
+ CHECK_MESSAGE(
+ Geometry2D::get_closest_point_to_segment(Vector2(-3, 4), t).is_equal_approx(Vector2(1, -2)),
+ "Line segment is only a single point. This point should be the closest.");
+}
+
+TEST_CASE("[Geometry2D] Closest point to uncapped segment") {
+ Vector2 s[] = { Vector2(-4, -4), Vector2(4, 4) };
+ CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(-1, 1), s).is_equal_approx(Vector2(0, 0)));
+ CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(-4, -6), s).is_equal_approx(Vector2(-5, -5)));
+ CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(4, 6), s).is_equal_approx(Vector2(5, 5)));
+}
+
+TEST_CASE("[Geometry2D] Closest points between segments") {
+ Vector2 c1, c2;
+ Geometry2D::get_closest_points_between_segments(Vector2(2, 2), Vector2(3, 3), Vector2(4, 4), Vector2(4, 5), c1, c2);
+ CHECK(c1.is_equal_approx(Vector2(3, 3)));
+ CHECK(c2.is_equal_approx(Vector2(4, 4)));
+
+ Geometry2D::get_closest_points_between_segments(Vector2(0, 1), Vector2(-2, -1), Vector2(0, 0), Vector2(2, -2), c1, c2);
+ CHECK(c1.is_equal_approx(Vector2(-0.5, 0.5)));
+ CHECK(c2.is_equal_approx(Vector2(0, 0)));
+
+ Geometry2D::get_closest_points_between_segments(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(-1, -1), c1, c2);
+ CHECK(c1.is_equal_approx(Vector2(0, 0)));
+ CHECK(c2.is_equal_approx(Vector2(0, 0)));
+
+ Geometry2D::get_closest_points_between_segments(Vector2(-3, 4), Vector2(-3, 4), Vector2(-4, 3), Vector2(-2, 3), c1, c2);
+ CHECK_MESSAGE(
+ c1.is_equal_approx(Vector2(-3, 4)),
+ "1st line segment is only a point, this point should be the closest point to the 2nd line segment.");
+ CHECK_MESSAGE(
+ c2.is_equal_approx(Vector2(-3, 3)),
+ "1st line segment is only a point, this should not matter when determining the closest point on the 2nd line segment.");
+
+ Geometry2D::get_closest_points_between_segments(Vector2(-4, 3), Vector2(-2, 3), Vector2(-3, 4), Vector2(-3, 4), c1, c2);
+ CHECK_MESSAGE(
+ c1.is_equal_approx(Vector2(-3, 3)),
+ "2nd line segment is only a point, this should not matter when determining the closest point on the 1st line segment.");
+ CHECK_MESSAGE(
+ c2.is_equal_approx(Vector2(-3, 4)),
+ "2nd line segment is only a point, this point should be the closest point to the 1st line segment.");
+
+ Geometry2D::get_closest_points_between_segments(Vector2(5, -4), Vector2(5, -4), Vector2(-2, 1), Vector2(-2, 1), c1, c2);
+ CHECK_MESSAGE(
+ c1.is_equal_approx(Vector2(5, -4)),
+ "Both line segments are only a point. On the 1st line segment, that point should be the closest point to the 2nd line segment.");
+ CHECK_MESSAGE(
+ c2.is_equal_approx(Vector2(-2, 1)),
+ "Both line segments are only a point. On the 2nd line segment, that point should be the closest point to the 1st line segment.");
+}
+
+TEST_CASE("[Geometry2D] Make atlas") {
+ Vector<Point2i> result;
+ Size2i size;
+
+ Vector<Size2i> r;
+ r.push_back(Size2i(2, 2));
+ Geometry2D::make_atlas(r, result, size);
+ CHECK(size == Size2i(2, 2));
+ CHECK(result.size() == r.size());
+
+ r.clear();
+ result.clear();
+ r.push_back(Size2i(1, 2));
+ r.push_back(Size2i(3, 4));
+ r.push_back(Size2i(5, 6));
+ r.push_back(Size2i(7, 8));
+ Geometry2D::make_atlas(r, result, size);
+ CHECK(result.size() == r.size());
+}
+
+TEST_CASE("[Geometry2D] Polygon intersection") {
+ Vector<Point2> a;
+ Vector<Point2> b;
+ Vector<Vector<Point2>> r;
+
+ a.push_back(Point2(30, 60));
+ a.push_back(Point2(70, 5));
+ a.push_back(Point2(200, 40));
+ a.push_back(Point2(80, 200));
+
+ SUBCASE("[Geometry2D] Both polygons are empty") {
+ r = Geometry2D::intersect_polygons(Vector<Point2>(), Vector<Point2>());
+ CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The intersection should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] One polygon is empty") {
+ r = Geometry2D::intersect_polygons(a, b);
+ REQUIRE_MESSAGE(r.is_empty(), "One polygon is empty. The intersection should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] Basic intersection") {
+ b.push_back(Point2(200, 300));
+ b.push_back(Point2(90, 200));
+ b.push_back(Point2(50, 100));
+ b.push_back(Point2(200, 90));
+ r = Geometry2D::intersect_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 1, "The polygons should intersect each other with 1 resulting intersection polygon.");
+ REQUIRE_MESSAGE(r[0].size() == 3, "The resulting intersection polygon should have 3 vertices.");
+ CHECK(r[0][0].is_equal_approx(Point2(86.52174, 191.30436)));
+ CHECK(r[0][1].is_equal_approx(Point2(50, 100)));
+ CHECK(r[0][2].is_equal_approx(Point2(160.52632, 92.63157)));
+ }
+
+ SUBCASE("[Geometry2D] Intersection with one polygon being completely inside the other polygon") {
+ b.push_back(Point2(80, 100));
+ b.push_back(Point2(50, 50));
+ b.push_back(Point2(150, 50));
+ r = Geometry2D::intersect_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 1, "The polygons should intersect each other with 1 resulting intersection polygon.");
+ REQUIRE_MESSAGE(r[0].size() == 3, "The resulting intersection polygon should have 3 vertices.");
+ CHECK(r[0][0].is_equal_approx(b[0]));
+ CHECK(r[0][1].is_equal_approx(b[1]));
+ CHECK(r[0][2].is_equal_approx(b[2]));
+ }
+
+ SUBCASE("[Geometry2D] No intersection with 2 non-empty polygons") {
+ b.push_back(Point2(150, 150));
+ b.push_back(Point2(250, 100));
+ b.push_back(Point2(300, 200));
+ r = Geometry2D::intersect_polygons(a, b);
+ REQUIRE_MESSAGE(r.is_empty(), "The polygons should not intersect each other.");
+ }
+
+ SUBCASE("[Geometry2D] Intersection with 2 resulting polygons") {
+ a.clear();
+ a.push_back(Point2(70, 5));
+ a.push_back(Point2(140, 7));
+ a.push_back(Point2(100, 52));
+ a.push_back(Point2(170, 50));
+ a.push_back(Point2(60, 125));
+ b.push_back(Point2(70, 105));
+ b.push_back(Point2(115, 55));
+ b.push_back(Point2(90, 15));
+ b.push_back(Point2(160, 50));
+ r = Geometry2D::intersect_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 2, "The polygons should intersect each other with 2 resulting intersection polygons.");
+ REQUIRE_MESSAGE(r[0].size() == 4, "The resulting intersection polygon should have 4 vertices.");
+ CHECK(r[0][0].is_equal_approx(Point2(70, 105)));
+ CHECK(r[0][1].is_equal_approx(Point2(115, 55)));
+ CHECK(r[0][2].is_equal_approx(Point2(112.894737, 51.63158)));
+ CHECK(r[0][3].is_equal_approx(Point2(159.509537, 50.299728)));
+
+ REQUIRE_MESSAGE(r[1].size() == 3, "The intersection polygon should have 3 vertices.");
+ CHECK(r[1][0].is_equal_approx(Point2(119.692307, 29.846149)));
+ CHECK(r[1][1].is_equal_approx(Point2(107.706421, 43.33028)));
+ CHECK(r[1][2].is_equal_approx(Point2(90, 15)));
+ }
+}
+
+TEST_CASE("[Geometry2D] Merge polygons") {
+ Vector<Point2> a;
+ Vector<Point2> b;
+ Vector<Vector<Point2>> r;
+
+ a.push_back(Point2(225, 180));
+ a.push_back(Point2(160, 230));
+ a.push_back(Point2(20, 212));
+ a.push_back(Point2(50, 115));
+
+ SUBCASE("[Geometry2D] Both polygons are empty") {
+ r = Geometry2D::merge_polygons(Vector<Point2>(), Vector<Point2>());
+ REQUIRE_MESSAGE(r.is_empty(), "Both polygons are empty. The union should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] One polygon is empty") {
+ r = Geometry2D::merge_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 1, "One polygon is non-empty. There should be 1 resulting merged polygon.");
+ REQUIRE_MESSAGE(r[0].size() == 4, "The resulting merged polygon should have 4 vertices.");
+ CHECK(r[0][0].is_equal_approx(a[0]));
+ CHECK(r[0][1].is_equal_approx(a[1]));
+ CHECK(r[0][2].is_equal_approx(a[2]));
+ CHECK(r[0][3].is_equal_approx(a[3]));
+ }
+
+ SUBCASE("[Geometry2D] Basic merge with 2 polygons") {
+ b.push_back(Point2(180, 190));
+ b.push_back(Point2(60, 140));
+ b.push_back(Point2(160, 80));
+ r = Geometry2D::merge_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 1, "The merged polygons should result in 1 polygon.");
+ REQUIRE_MESSAGE(r[0].size() == 7, "The resulting merged polygon should have 7 vertices.");
+ CHECK(r[0][0].is_equal_approx(Point2(174.791077, 161.350967)));
+ CHECK(r[0][1].is_equal_approx(Point2(225, 180)));
+ CHECK(r[0][2].is_equal_approx(Point2(160, 230)));
+ CHECK(r[0][3].is_equal_approx(Point2(20, 212)));
+ CHECK(r[0][4].is_equal_approx(Point2(50, 115)));
+ CHECK(r[0][5].is_equal_approx(Point2(81.911758, 126.852943)));
+ CHECK(r[0][6].is_equal_approx(Point2(160, 80)));
+ }
+
+ SUBCASE("[Geometry2D] Merge with 2 resulting merged polygons (outline and hole)") {
+ b.push_back(Point2(180, 190));
+ b.push_back(Point2(140, 125));
+ b.push_back(Point2(60, 140));
+ b.push_back(Point2(160, 80));
+ r = Geometry2D::merge_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 2, "The merged polygons should result in 2 polygons.");
+
+ REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The merged polygon (outline) should be counter-clockwise.");
+ REQUIRE_MESSAGE(r[0].size() == 7, "The resulting merged polygon (outline) should have 7 vertices.");
+ CHECK(r[0][0].is_equal_approx(Point2(174.791077, 161.350967)));
+ CHECK(r[0][1].is_equal_approx(Point2(225, 180)));
+ CHECK(r[0][2].is_equal_approx(Point2(160, 230)));
+ CHECK(r[0][3].is_equal_approx(Point2(20, 212)));
+ CHECK(r[0][4].is_equal_approx(Point2(50, 115)));
+ CHECK(r[0][5].is_equal_approx(Point2(81.911758, 126.852943)));
+ CHECK(r[0][6].is_equal_approx(Point2(160, 80)));
+
+ REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting merged polygon (hole) should be clockwise.");
+ REQUIRE_MESSAGE(r[1].size() == 3, "The resulting merged polygon (hole) should have 3 vertices.");
+ CHECK(r[1][0].is_equal_approx(Point2(98.083069, 132.859421)));
+ CHECK(r[1][1].is_equal_approx(Point2(158.689453, 155.370377)));
+ CHECK(r[1][2].is_equal_approx(Point2(140, 125)));
+ }
+}
+
+TEST_CASE("[Geometry2D] Clip polygons") {
+ Vector<Point2> a;
+ Vector<Point2> b;
+ Vector<Vector<Point2>> r;
+
+ a.push_back(Point2(225, 180));
+ a.push_back(Point2(160, 230));
+ a.push_back(Point2(20, 212));
+ a.push_back(Point2(50, 115));
+
+ SUBCASE("[Geometry2D] Both polygons are empty") {
+ r = Geometry2D::clip_polygons(Vector<Point2>(), Vector<Point2>());
+ CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The clip should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] Basic clip with one result polygon") {
+ b.push_back(Point2(250, 170));
+ b.push_back(Point2(175, 270));
+ b.push_back(Point2(120, 260));
+ b.push_back(Point2(25, 80));
+ r = Geometry2D::clip_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 1, "The clipped polygons should result in 1 polygon.");
+ REQUIRE_MESSAGE(r[0].size() == 3, "The resulting clipped polygon should have 3 vertices.");
+ CHECK(r[0][0].is_equal_approx(Point2(100.102173, 222.298843)));
+ CHECK(r[0][1].is_equal_approx(Point2(20, 212)));
+ CHECK(r[0][2].is_equal_approx(Point2(47.588089, 122.798492)));
+ }
+
+ SUBCASE("[Geometry2D] Polygon b completely overlaps polygon a") {
+ b.push_back(Point2(250, 170));
+ b.push_back(Point2(175, 270));
+ b.push_back(Point2(10, 210));
+ b.push_back(Point2(55, 80));
+ r = Geometry2D::clip_polygons(a, b);
+ CHECK_MESSAGE(r.is_empty(), "Polygon 'b' completely overlaps polygon 'a'. This should result in no clipped polygons.");
+ }
+
+ SUBCASE("[Geometry2D] Polygon a completely overlaps polygon b") {
+ b.push_back(Point2(150, 200));
+ b.push_back(Point2(65, 190));
+ b.push_back(Point2(80, 140));
+ r = Geometry2D::clip_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 2, "Polygon 'a' completely overlaps polygon 'b'. This should result in 2 clipped polygons.");
+ REQUIRE_MESSAGE(r[0].size() == 4, "The resulting clipped polygon should have 4 vertices.");
+ REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The resulting clipped polygon (outline) should be counter-clockwise.");
+ CHECK(r[0][0].is_equal_approx(a[0]));
+ CHECK(r[0][1].is_equal_approx(a[1]));
+ CHECK(r[0][2].is_equal_approx(a[2]));
+ CHECK(r[0][3].is_equal_approx(a[3]));
+ REQUIRE_MESSAGE(r[1].size() == 3, "The resulting clipped polygon should have 3 vertices.");
+ REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting clipped polygon (hole) should be clockwise.");
+ CHECK(r[1][0].is_equal_approx(b[1]));
+ CHECK(r[1][1].is_equal_approx(b[0]));
+ CHECK(r[1][2].is_equal_approx(b[2]));
+ }
+}
+
+TEST_CASE("[Geometry2D] Exclude polygons") {
+ Vector<Point2> a;
+ Vector<Point2> b;
+ Vector<Vector<Point2>> r;
+
+ a.push_back(Point2(225, 180));
+ a.push_back(Point2(160, 230));
+ a.push_back(Point2(20, 212));
+ a.push_back(Point2(50, 115));
+
+ SUBCASE("[Geometry2D] Both polygons are empty") {
+ r = Geometry2D::exclude_polygons(Vector<Point2>(), Vector<Point2>());
+ CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The excluded polygon should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] One polygon is empty") {
+ r = Geometry2D::exclude_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 1, "One polygon is non-empty. There should be 1 resulting excluded polygon.");
+ REQUIRE_MESSAGE(r[0].size() == 4, "The resulting excluded polygon should have 4 vertices.");
+ CHECK(r[0][0].is_equal_approx(a[0]));
+ CHECK(r[0][1].is_equal_approx(a[1]));
+ CHECK(r[0][2].is_equal_approx(a[2]));
+ CHECK(r[0][3].is_equal_approx(a[3]));
+ }
+
+ SUBCASE("[Geometry2D] Exclude with 2 resulting polygons (outline and hole)") {
+ b.push_back(Point2(140, 160));
+ b.push_back(Point2(150, 220));
+ b.push_back(Point2(40, 200));
+ b.push_back(Point2(60, 140));
+ r = Geometry2D::exclude_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting excluded polygons (outline and hole).");
+ REQUIRE_MESSAGE(r[0].size() == 4, "The resulting excluded polygon should have 4 vertices.");
+ REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The resulting excluded polygon (outline) should be counter-clockwise.");
+ CHECK(r[0][0].is_equal_approx(a[0]));
+ CHECK(r[0][1].is_equal_approx(a[1]));
+ CHECK(r[0][2].is_equal_approx(a[2]));
+ CHECK(r[0][3].is_equal_approx(a[3]));
+ REQUIRE_MESSAGE(r[1].size() == 4, "The resulting excluded polygon should have 4 vertices.");
+ REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting excluded polygon (hole) should be clockwise.");
+ CHECK(r[1][0].is_equal_approx(Point2(40, 200)));
+ CHECK(r[1][1].is_equal_approx(Point2(150, 220)));
+ CHECK(r[1][2].is_equal_approx(Point2(140, 160)));
+ CHECK(r[1][3].is_equal_approx(Point2(60, 140)));
+ }
+}
+
+TEST_CASE("[Geometry2D] Intersect polyline with polygon") {
+ Vector<Vector2> l;
+ Vector<Vector2> p;
+ Vector<Vector<Point2>> r;
+
+ l.push_back(Vector2(100, 90));
+ l.push_back(Vector2(120, 250));
+
+ p.push_back(Vector2(225, 180));
+ p.push_back(Vector2(160, 230));
+ p.push_back(Vector2(20, 212));
+ p.push_back(Vector2(50, 115));
+
+ SUBCASE("[Geometry2D] Both line and polygon are empty") {
+ r = Geometry2D::intersect_polyline_with_polygon(Vector<Vector2>(), Vector<Vector2>());
+ CHECK_MESSAGE(r.is_empty(), "Both line and polygon are empty. The intersection line should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] Line is non-empty and polygon is empty") {
+ r = Geometry2D::intersect_polyline_with_polygon(l, Vector<Vector2>());
+ CHECK_MESSAGE(r.is_empty(), "The polygon is empty while the line is non-empty. The intersection line should be empty.");
+ }
+
+ SUBCASE("[Geometry2D] Basic intersection with 1 resulting intersection line") {
+ r = Geometry2D::intersect_polyline_with_polygon(l, p);
+ REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting intersection line.");
+ REQUIRE_MESSAGE(r[0].size() == 2, "The resulting intersection line should have 2 vertices.");
+ CHECK(r[0][0].is_equal_approx(Vector2(105.711609, 135.692886)));
+ CHECK(r[0][1].is_equal_approx(Vector2(116.805809, 224.446457)));
+ }
+
+ SUBCASE("[Geometry2D] Complex intersection with 2 resulting intersection lines") {
+ l.clear();
+ l.push_back(Vector2(100, 90));
+ l.push_back(Vector2(190, 255));
+ l.push_back(Vector2(135, 260));
+ l.push_back(Vector2(57, 200));
+ l.push_back(Vector2(50, 170));
+ l.push_back(Vector2(15, 155));
+ r = Geometry2D::intersect_polyline_with_polygon(l, p);
+ REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting intersection lines.");
+ REQUIRE_MESSAGE(r[0].size() == 2, "The resulting intersection line should have 2 vertices.");
+ CHECK(r[0][0].is_equal_approx(Vector2(129.804565, 144.641693)));
+ CHECK(r[0][1].is_equal_approx(Vector2(171.527084, 221.132996)));
+ REQUIRE_MESSAGE(r[1].size() == 4, "The resulting intersection line should have 4 vertices.");
+ CHECK(r[1][0].is_equal_approx(Vector2(83.15609, 220.120087)));
+ CHECK(r[1][1].is_equal_approx(Vector2(57, 200)));
+ CHECK(r[1][2].is_equal_approx(Vector2(50, 170)));
+ CHECK(r[1][3].is_equal_approx(Vector2(34.980492, 163.563065)));
+ }
+}
+
+TEST_CASE("[Geometry2D] Clip polyline with polygon") {
+ Vector<Vector2> l;
+ Vector<Vector2> p;
+ Vector<Vector<Point2>> r;
+
+ l.push_back(Vector2(70, 140));
+ l.push_back(Vector2(160, 320));
+
+ p.push_back(Vector2(225, 180));
+ p.push_back(Vector2(160, 230));
+ p.push_back(Vector2(20, 212));
+ p.push_back(Vector2(50, 115));
+
+ SUBCASE("[Geometry2D] Both line and polygon are empty") {
+ r = Geometry2D::clip_polyline_with_polygon(Vector<Vector2>(), Vector<Vector2>());
+ CHECK_MESSAGE(r.is_empty(), "Both line and polygon are empty. The clipped line should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] Polygon is empty and line is non-empty") {
+ r = Geometry2D::clip_polyline_with_polygon(l, Vector<Vector2>());
+ REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting clipped line.");
+ REQUIRE_MESSAGE(r[0].size() == 2, "The resulting clipped line should have 2 vertices.");
+ CHECK(r[0][0].is_equal_approx(l[0]));
+ CHECK(r[0][1].is_equal_approx(l[1]));
+ }
+
+ SUBCASE("[Geometry2D] Basic clip with 1 resulting clipped line") {
+ r = Geometry2D::clip_polyline_with_polygon(l, p);
+ REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting clipped line.");
+ REQUIRE_MESSAGE(r[0].size() == 2, "The resulting clipped line should have 2 vertices.");
+ CHECK(r[0][0].is_equal_approx(Vector2(111.908401, 223.816803)));
+ CHECK(r[0][1].is_equal_approx(Vector2(160, 320)));
+ }
+
+ SUBCASE("[Geometry2D] Complex clip with 2 resulting clipped lines") {
+ l.clear();
+ l.push_back(Vector2(55, 70));
+ l.push_back(Vector2(50, 190));
+ l.push_back(Vector2(120, 165));
+ l.push_back(Vector2(122, 250));
+ l.push_back(Vector2(160, 320));
+ r = Geometry2D::clip_polyline_with_polygon(l, p);
+ REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting clipped lines.");
+ REQUIRE_MESSAGE(r[0].size() == 3, "The resulting clipped line should have 3 vertices.");
+ CHECK(r[0][0].is_equal_approx(Vector2(160, 320)));
+ CHECK(r[0][1].is_equal_approx(Vector2(122, 250)));
+ CHECK(r[0][2].is_equal_approx(Vector2(121.412682, 225.038757)));
+ REQUIRE_MESSAGE(r[1].size() == 2, "The resulting clipped line should have 2 vertices.");
+ CHECK(r[1][0].is_equal_approx(Vector2(53.07737, 116.143021)));
+ CHECK(r[1][1].is_equal_approx(Vector2(55, 70)));
+ }
+}
+
+TEST_CASE("[Geometry2D] Convex hull") {
+ Vector<Point2> a;
+ Vector<Point2> r;
+
+ a.push_back(Point2(-4, -8));
+ a.push_back(Point2(-10, -4));
+ a.push_back(Point2(8, 2));
+ a.push_back(Point2(-6, 10));
+ a.push_back(Point2(-12, 4));
+ a.push_back(Point2(10, -8));
+ a.push_back(Point2(4, 8));
+
+ SUBCASE("[Geometry2D] No points") {
+ r = Geometry2D::convex_hull(Vector<Vector2>());
+
+ CHECK_MESSAGE(r.is_empty(), "The convex hull should be empty if there are no input points.");
+ }
+
+ SUBCASE("[Geometry2D] Single point") {
+ Vector<Point2> b;
+ b.push_back(Point2(4, -3));
+
+ r = Geometry2D::convex_hull(b);
+ REQUIRE_MESSAGE(r.size() == 1, "Convex hull should contain 1 point.");
+ CHECK(r[0].is_equal_approx(b[0]));
+ }
+
+ SUBCASE("[Geometry2D] All points form the convex hull") {
+ r = Geometry2D::convex_hull(a);
+ REQUIRE_MESSAGE(r.size() == 8, "Convex hull should contain 8 points.");
+ CHECK(r[0].is_equal_approx(Point2(-12, 4)));
+ CHECK(r[1].is_equal_approx(Point2(-10, -4)));
+ CHECK(r[2].is_equal_approx(Point2(-4, -8)));
+ CHECK(r[3].is_equal_approx(Point2(10, -8)));
+ CHECK(r[4].is_equal_approx(Point2(8, 2)));
+ CHECK(r[5].is_equal_approx(Point2(4, 8)));
+ CHECK(r[6].is_equal_approx(Point2(-6, 10)));
+ CHECK(r[7].is_equal_approx(Point2(-12, 4)));
+ }
+
+ SUBCASE("[Geometry2D] Add extra points inside original convex hull") {
+ a.push_back(Point2(-4, -8));
+ a.push_back(Point2(0, 0));
+ a.push_back(Point2(0, 8));
+ a.push_back(Point2(-10, -3));
+ a.push_back(Point2(9, -4));
+ a.push_back(Point2(6, 4));
+
+ r = Geometry2D::convex_hull(a);
+ REQUIRE_MESSAGE(r.size() == 8, "Convex hull should contain 8 points.");
+ CHECK(r[0].is_equal_approx(Point2(-12, 4)));
+ CHECK(r[1].is_equal_approx(Point2(-10, -4)));
+ CHECK(r[2].is_equal_approx(Point2(-4, -8)));
+ CHECK(r[3].is_equal_approx(Point2(10, -8)));
+ CHECK(r[4].is_equal_approx(Point2(8, 2)));
+ CHECK(r[5].is_equal_approx(Point2(4, 8)));
+ CHECK(r[6].is_equal_approx(Point2(-6, 10)));
+ CHECK(r[7].is_equal_approx(Point2(-12, 4)));
+ }
+
+ SUBCASE("[Geometry2D] Add extra points on border of original convex hull") {
+ a.push_back(Point2(9, -3));
+ a.push_back(Point2(-2, -8));
+
+ r = Geometry2D::convex_hull(a);
+ REQUIRE_MESSAGE(r.size() == 8, "Convex hull should contain 8 points.");
+ CHECK(r[0].is_equal_approx(Point2(-12, 4)));
+ CHECK(r[1].is_equal_approx(Point2(-10, -4)));
+ CHECK(r[2].is_equal_approx(Point2(-4, -8)));
+ CHECK(r[3].is_equal_approx(Point2(10, -8)));
+ CHECK(r[4].is_equal_approx(Point2(8, 2)));
+ CHECK(r[5].is_equal_approx(Point2(4, 8)));
+ CHECK(r[6].is_equal_approx(Point2(-6, 10)));
+ CHECK(r[7].is_equal_approx(Point2(-12, 4)));
+ }
+
+ SUBCASE("[Geometry2D] Add extra points outside border of original convex hull") {
+ a.push_back(Point2(-11, -1));
+ a.push_back(Point2(7, 6));
+
+ r = Geometry2D::convex_hull(a);
+ REQUIRE_MESSAGE(r.size() == 10, "Convex hull should contain 10 points.");
+ CHECK(r[0].is_equal_approx(Point2(-12, 4)));
+ CHECK(r[1].is_equal_approx(Point2(-11, -1)));
+ CHECK(r[2].is_equal_approx(Point2(-10, -4)));
+ CHECK(r[3].is_equal_approx(Point2(-4, -8)));
+ CHECK(r[4].is_equal_approx(Point2(10, -8)));
+ CHECK(r[5].is_equal_approx(Point2(8, 2)));
+ CHECK(r[6].is_equal_approx(Point2(7, 6)));
+ CHECK(r[7].is_equal_approx(Point2(4, 8)));
+ CHECK(r[8].is_equal_approx(Point2(-6, 10)));
+ CHECK(r[9].is_equal_approx(Point2(-12, 4)));
+ }
+}
+
+TEST_CASE("[Geometry2D] Bresenham line") {
+ Vector<Vector2i> r;
+
+ SUBCASE("[Geometry2D] Single point") {
+ r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(0, 0));
+
+ REQUIRE_MESSAGE(r.size() == 1, "The Bresenham line should contain exactly one point.");
+ CHECK(r[0] == Vector2i(0, 0));
+ }
+
+ SUBCASE("[Geometry2D] Line parallel to x-axis") {
+ r = Geometry2D::bresenham_line(Point2i(1, 2), Point2i(5, 2));
+
+ REQUIRE_MESSAGE(r.size() == 5, "The Bresenham line should contain exactly five points.");
+ CHECK(r[0] == Vector2i(1, 2));
+ CHECK(r[1] == Vector2i(2, 2));
+ CHECK(r[2] == Vector2i(3, 2));
+ CHECK(r[3] == Vector2i(4, 2));
+ CHECK(r[4] == Vector2i(5, 2));
+ }
+
+ SUBCASE("[Geometry2D] 45 degree line from the origin") {
+ r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(4, 4));
+
+ REQUIRE_MESSAGE(r.size() == 5, "The Bresenham line should contain exactly five points.");
+ CHECK(r[0] == Vector2i(0, 0));
+ CHECK(r[1] == Vector2i(1, 1));
+ CHECK(r[2] == Vector2i(2, 2));
+ CHECK(r[3] == Vector2i(3, 3));
+ CHECK(r[4] == Vector2i(4, 4));
+ }
+
+ SUBCASE("[Geometry2D] Sloped line going up one unit") {
+ r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(4, 1));
+
+ REQUIRE_MESSAGE(r.size() == 5, "The Bresenham line should contain exactly five points.");
+ CHECK(r[0] == Vector2i(0, 0));
+ CHECK(r[1] == Vector2i(1, 0));
+ CHECK(r[2] == Vector2i(2, 0));
+ CHECK(r[3] == Vector2i(3, 1));
+ CHECK(r[4] == Vector2i(4, 1));
+ }
+
+ SUBCASE("[Geometry2D] Sloped line going up two units") {
+ r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(4, 2));
+
+ REQUIRE_MESSAGE(r.size() == 5, "The Bresenham line should contain exactly five points.");
+ CHECK(r[0] == Vector2i(0, 0));
+ CHECK(r[1] == Vector2i(1, 0));
+ CHECK(r[2] == Vector2i(2, 1));
+ CHECK(r[3] == Vector2i(3, 1));
+ CHECK(r[4] == Vector2i(4, 2));
+ }
+
+ SUBCASE("[Geometry2D] Long sloped line") {
+ r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(11, 5));
+
+ REQUIRE_MESSAGE(r.size() == 12, "The Bresenham line should contain exactly twelve points.");
+ CHECK(r[0] == Vector2i(0, 0));
+ CHECK(r[1] == Vector2i(1, 0));
+ CHECK(r[2] == Vector2i(2, 1));
+ CHECK(r[3] == Vector2i(3, 1));
+ CHECK(r[4] == Vector2i(4, 2));
+ CHECK(r[5] == Vector2i(5, 2));
+ CHECK(r[6] == Vector2i(6, 3));
+ CHECK(r[7] == Vector2i(7, 3));
+ CHECK(r[8] == Vector2i(8, 4));
+ CHECK(r[9] == Vector2i(9, 4));
+ CHECK(r[10] == Vector2i(10, 5));
+ CHECK(r[11] == Vector2i(11, 5));
+ }
+}
+} // namespace TestGeometry2D
+
+#endif // TEST_GEOMETRY_2D_H
diff --git a/tests/core/math/test_geometry_3d.h b/tests/core/math/test_geometry_3d.h
new file mode 100644
index 0000000000..23bbf1e183
--- /dev/null
+++ b/tests/core/math/test_geometry_3d.h
@@ -0,0 +1,437 @@
+/*************************************************************************/
+/* test_geometry_3d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_GEOMETRY_3D_H
+#define TEST_GEOMETRY_3D_H
+
+#include "core/math/geometry_3d.h"
+#include "tests/test_macros.h"
+
+namespace TestGeometry3D {
+TEST_CASE("[Geometry3D] Closest Points Between Segments") {
+ struct Case {
+ Vector3 p_1, p_2, p_3, p_4;
+ Vector3 got_1, got_2;
+ Vector3 want_1, want_2;
+ Case(){};
+ Case(Vector3 p_p_1, Vector3 p_p_2, Vector3 p_p_3, Vector3 p_p_4, Vector3 p_want_1, Vector3 p_want_2) :
+ p_1(p_p_1), p_2(p_p_2), p_3(p_p_3), p_4(p_p_4), want_1(p_want_1), want_2(p_want_2){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(1, -1, 1), Vector3(1, 1, -1), Vector3(-1, -2, -1), Vector3(-1, 1, 1), Vector3(1, -0.2, 0.2), Vector3(-1, -0.2, 0.2)));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Geometry3D::get_closest_points_between_segments(current_case.p_1, current_case.p_2, current_case.p_3, current_case.p_4, current_case.got_1, current_case.got_2);
+ CHECK(current_case.got_1.is_equal_approx(current_case.want_1));
+ CHECK(current_case.got_2.is_equal_approx(current_case.want_2));
+ }
+}
+
+TEST_CASE("[Geometry3D] Closest Distance Between Segments") {
+ struct Case {
+ Vector3 p_1, p_2, p_3, p_4;
+ float want;
+ Case(){};
+ Case(Vector3 p_p_1, Vector3 p_p_2, Vector3 p_p_3, Vector3 p_p_4, float p_want) :
+ p_1(p_p_1), p_2(p_p_2), p_3(p_p_3), p_4(p_p_4), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(1, -2, 0), Vector3(1, 2, 0), Vector3(-1, 2, 0), Vector3(-1, -2, 0), 2.0f));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ float out = Geometry3D::get_closest_distance_between_segments(current_case.p_1, current_case.p_2, current_case.p_3, current_case.p_4);
+ CHECK(out == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Build Box Planes") {
+ const Vector3 extents = Vector3(5, 5, 20);
+ Vector<Plane> box = Geometry3D::build_box_planes(extents);
+ CHECK(box.size() == 6);
+ CHECK(extents.x == box[0].d);
+ CHECK(box[0].normal == Vector3(1, 0, 0));
+ CHECK(extents.x == box[1].d);
+ CHECK(box[1].normal == Vector3(-1, 0, 0));
+ CHECK(extents.y == box[2].d);
+ CHECK(box[2].normal == Vector3(0, 1, 0));
+ CHECK(extents.y == box[3].d);
+ CHECK(box[3].normal == Vector3(0, -1, 0));
+ CHECK(extents.z == box[4].d);
+ CHECK(box[4].normal == Vector3(0, 0, 1));
+ CHECK(extents.z == box[5].d);
+ CHECK(box[5].normal == Vector3(0, 0, -1));
+}
+
+TEST_CASE("[Geometry3D] Build Capsule Planes") {
+ struct Case {
+ real_t radius, height;
+ int sides, lats;
+ Vector3::Axis axis;
+ int want_size;
+ Case(){};
+ Case(real_t p_radius, real_t p_height, int p_sides, int p_lats, Vector3::Axis p_axis, int p_want) :
+ radius(p_radius), height(p_height), sides(p_sides), lats(p_lats), axis(p_axis), want_size(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(10, 20, 6, 10, Vector3::Axis(), 126));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Vector<Plane> capsule = Geometry3D::build_capsule_planes(current_case.radius, current_case.height, current_case.sides, current_case.lats, current_case.axis);
+ // Should equal (p_sides * p_lats) * 2 + p_sides
+ CHECK(capsule.size() == current_case.want_size);
+ }
+}
+
+TEST_CASE("[Geometry3D] Build Cylinder Planes") {
+ struct Case {
+ real_t radius, height;
+ int sides;
+ Vector3::Axis axis;
+ int want_size;
+ Case(){};
+ Case(real_t p_radius, real_t p_height, int p_sides, Vector3::Axis p_axis, int p_want) :
+ radius(p_radius), height(p_height), sides(p_sides), axis(p_axis), want_size(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(3.0f, 10.0f, 10, Vector3::Axis(), 12));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Vector<Plane> planes = Geometry3D::build_cylinder_planes(current_case.radius, current_case.height, current_case.sides, current_case.axis);
+ CHECK(planes.size() == current_case.want_size);
+ }
+}
+
+TEST_CASE("[Geometry3D] Build Sphere Planes") {
+ struct Case {
+ real_t radius;
+ int lats, lons;
+ Vector3::Axis axis;
+ int want_size;
+ Case(){};
+ Case(real_t p_radius, int p_lat, int p_lons, Vector3::Axis p_axis, int p_want) :
+ radius(p_radius), lats(p_lat), lons(p_lons), axis(p_axis), want_size(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(10.0f, 10, 3, Vector3::Axis(), 63));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Vector<Plane> planes = Geometry3D::build_sphere_planes(current_case.radius, current_case.lats, current_case.lons, current_case.axis);
+ CHECK(planes.size() == 63);
+ }
+}
+
+#if false
+// This test has been temporarily disabled because it's really fragile and
+// breaks if calculations change very slightly. For example, it breaks when
+// using doubles, and it breaks when making Plane calculations more accurate.
+TEST_CASE("[Geometry3D] Build Convex Mesh") {
+ struct Case {
+ Vector<Plane> object;
+ int want_faces, want_edges, want_vertices;
+ Case(){};
+ Case(Vector<Plane> p_object, int p_want_faces, int p_want_edges, int p_want_vertices) :
+ object(p_object), want_faces(p_want_faces), want_edges(p_want_edges), want_vertices(p_want_vertices){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Geometry3D::build_box_planes(Vector3(5, 10, 5)), 6, 12, 8));
+ tt.push_back(Case(Geometry3D::build_capsule_planes(5, 5, 20, 20, Vector3::Axis()), 820, 7603, 6243));
+ tt.push_back(Case(Geometry3D::build_cylinder_planes(5, 5, 20, Vector3::Axis()), 22, 100, 80));
+ tt.push_back(Case(Geometry3D::build_sphere_planes(5, 5, 20), 220, 1011, 522));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Geometry3D::MeshData mesh = Geometry3D::build_convex_mesh(current_case.object);
+ CHECK(mesh.faces.size() == current_case.want_faces);
+ CHECK(mesh.edges.size() == current_case.want_edges);
+ CHECK(mesh.vertices.size() == current_case.want_vertices);
+ }
+}
+#endif
+
+TEST_CASE("[Geometry3D] Clip Polygon") {
+ struct Case {
+ Plane clipping_plane;
+ Vector<Vector3> polygon;
+ bool want;
+ Case(){};
+ Case(Plane p_clipping_plane, Vector<Vector3> p_polygon, bool p_want) :
+ clipping_plane(p_clipping_plane), polygon(p_polygon), want(p_want){};
+ };
+ Vector<Case> tt;
+ Vector<Plane> box_planes = Geometry3D::build_box_planes(Vector3(5, 10, 5));
+ Vector<Vector3> box = Geometry3D::compute_convex_mesh_points(&box_planes[0], box_planes.size());
+ tt.push_back(Case(Plane(), box, true));
+ tt.push_back(Case(Plane(Vector3(0, 1, 0), Vector3(0, 3, 0)), box, false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Vector<Vector3> output = Geometry3D::clip_polygon(current_case.polygon, current_case.clipping_plane);
+ if (current_case.want) {
+ CHECK(output == current_case.polygon);
+ } else {
+ CHECK(output != current_case.polygon);
+ }
+ }
+}
+
+TEST_CASE("[Geometry3D] Compute Convex Mesh Points") {
+ struct Case {
+ Vector<Plane> mesh;
+ Vector<Vector3> want;
+ Case(){};
+ Case(Vector<Plane> p_mesh, Vector<Vector3> p_want) :
+ mesh(p_mesh), want(p_want){};
+ };
+ Vector<Case> tt;
+ Vector<Vector3> cube;
+ cube.push_back(Vector3(-5, -5, -5));
+ cube.push_back(Vector3(5, -5, -5));
+ cube.push_back(Vector3(-5, 5, -5));
+ cube.push_back(Vector3(5, 5, -5));
+ cube.push_back(Vector3(-5, -5, 5));
+ cube.push_back(Vector3(5, -5, 5));
+ cube.push_back(Vector3(-5, 5, 5));
+ cube.push_back(Vector3(5, 5, 5));
+ tt.push_back(Case(Geometry3D::build_box_planes(Vector3(5, 5, 5)), cube));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Vector<Vector3> vectors = Geometry3D::compute_convex_mesh_points(&current_case.mesh[0], current_case.mesh.size());
+ CHECK(vectors == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Get Closest Point To Segment") {
+ struct Case {
+ Vector3 point;
+ Vector<Vector3> segment;
+ Vector3 want;
+ Case(){};
+ Case(Vector3 p_point, Vector<Vector3> p_segment, Vector3 p_want) :
+ point(p_point), segment(p_segment), want(p_want){};
+ };
+ Vector<Case> tt;
+ Vector<Vector3> test_segment;
+ test_segment.push_back(Vector3(1, 1, 1));
+ test_segment.push_back(Vector3(5, 5, 5));
+ tt.push_back(Case(Vector3(2, 1, 4), test_segment, Vector3(2.33333, 2.33333, 2.33333)));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Vector3 output = Geometry3D::get_closest_point_to_segment(current_case.point, &current_case.segment[0]);
+ CHECK(output.is_equal_approx(current_case.want));
+ }
+}
+
+TEST_CASE("[Geometry3D] Plane and Box Overlap") {
+ struct Case {
+ Vector3 normal, max_box;
+ float d;
+ bool want;
+ Case(){};
+ Case(Vector3 p_normal, float p_d, Vector3 p_max_box, bool p_want) :
+ normal(p_normal), max_box(p_max_box), d(p_d), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(3, 4, 2), 5, Vector3(5, 5, 5), true));
+ tt.push_back(Case(Vector3(0, 1, 0), -10, Vector3(5, 5, 5), false));
+ tt.push_back(Case(Vector3(1, 0, 0), -6, Vector3(5, 5, 5), false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool overlap = Geometry3D::planeBoxOverlap(current_case.normal, current_case.d, current_case.max_box);
+ CHECK(overlap == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Is Point in Projected Triangle") {
+ struct Case {
+ Vector3 point, v_1, v_2, v_3;
+ bool want;
+ Case(){};
+ Case(Vector3 p_point, Vector3 p_v_1, Vector3 p_v_2, Vector3 p_v_3, bool p_want) :
+ point(p_point), v_1(p_v_1), v_2(p_v_2), v_3(p_v_3), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(1, 1, 0), Vector3(3, 0, 0), Vector3(0, 3, 0), Vector3(-3, 0, 0), true));
+ tt.push_back(Case(Vector3(5, 1, 0), Vector3(3, 0, 0), Vector3(0, 3, 0), Vector3(-3, 0, 0), false));
+ tt.push_back(Case(Vector3(3, 0, 0), Vector3(3, 0, 0), Vector3(0, 3, 0), Vector3(-3, 0, 0), true));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::point_in_projected_triangle(current_case.point, current_case.v_1, current_case.v_2, current_case.v_3);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Does Ray Intersect Triangle") {
+ struct Case {
+ Vector3 from, direction, v_1, v_2, v_3;
+ Vector3 *result = nullptr;
+ bool want;
+ Case(){};
+ Case(Vector3 p_from, Vector3 p_direction, Vector3 p_v_1, Vector3 p_v_2, Vector3 p_v_3, bool p_want) :
+ from(p_from), direction(p_direction), v_1(p_v_1), v_2(p_v_2), v_3(p_v_3), result(nullptr), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(0, 1, 1), Vector3(0, 0, -10), Vector3(0, 3, 0), Vector3(-3, 0, 0), Vector3(3, 0, 0), true));
+ tt.push_back(Case(Vector3(5, 10, 1), Vector3(0, 0, -10), Vector3(0, 3, 0), Vector3(-3, 0, 0), Vector3(3, 0, 0), false));
+ tt.push_back(Case(Vector3(0, 1, 1), Vector3(0, 0, 10), Vector3(0, 3, 0), Vector3(-3, 0, 0), Vector3(3, 0, 0), false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::ray_intersects_triangle(current_case.from, current_case.direction, current_case.v_1, current_case.v_2, current_case.v_3, current_case.result);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Does Segment Intersect Convex") {
+ struct Case {
+ Vector3 from, to;
+ Vector<Plane> planes;
+ Vector3 *result, *normal;
+ bool want;
+ Case(){};
+ Case(Vector3 p_from, Vector3 p_to, Vector<Plane> p_planes, bool p_want) :
+ from(p_from), to(p_to), planes(p_planes), result(nullptr), normal(nullptr), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(0, 0, 0), Geometry3D::build_box_planes(Vector3(5, 5, 5)), true));
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(5, 5, 5), Geometry3D::build_box_planes(Vector3(5, 5, 5)), true));
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(6, 5, 5), Geometry3D::build_box_planes(Vector3(5, 5, 5)), false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::segment_intersects_convex(current_case.from, current_case.to, &current_case.planes[0], current_case.planes.size(), current_case.result, current_case.normal);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Segment Intersects Cylinder") {
+ struct Case {
+ Vector3 from, to;
+ real_t height, radius;
+ Vector3 *result, *normal;
+ bool want;
+ Case(){};
+ Case(Vector3 p_from, Vector3 p_to, real_t p_height, real_t p_radius, bool p_want) :
+ from(p_from), to(p_to), height(p_height), radius(p_radius), result(nullptr), normal(nullptr), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(0, 0, 0), 5, 5, true));
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(6, 6, 6), 5, 5, false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::segment_intersects_cylinder(current_case.from, current_case.to, current_case.height, current_case.radius, current_case.result, current_case.normal);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Segment Intersects Cylinder") {
+ struct Case {
+ Vector3 from, to, sphere_pos;
+ real_t radius;
+ Vector3 *result, *normal;
+ bool want;
+ Case(){};
+ Case(Vector3 p_from, Vector3 p_to, Vector3 p_sphere_pos, real_t p_radius, bool p_want) :
+ from(p_from), to(p_to), sphere_pos(p_sphere_pos), radius(p_radius), result(nullptr), normal(nullptr), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(0, 0, 0), Vector3(0, 0, 0), 5, true));
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(0, 0, 2.5), Vector3(0, 0, 0), 5, true));
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(5, 5, 5), Vector3(0, 0, 0), 5, false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::segment_intersects_sphere(current_case.from, current_case.to, current_case.sphere_pos, current_case.radius, current_case.result, current_case.normal);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Segment Intersects Triangle") {
+ struct Case {
+ Vector3 from, to, v_1, v_2, v_3, *result;
+ bool want;
+ Case(){};
+ Case(Vector3 p_from, Vector3 p_to, Vector3 p_v_1, Vector3 p_v_2, Vector3 p_v_3, bool p_want) :
+ from(p_from), to(p_to), v_1(p_v_1), v_2(p_v_2), v_3(p_v_3), result(nullptr), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(1, 1, 1), Vector3(-1, -1, -1), Vector3(-3, 0, 0), Vector3(0, 3, 0), Vector3(3, 0, 0), true));
+ tt.push_back(Case(Vector3(1, 1, 1), Vector3(3, 0, 0), Vector3(-3, 0, 0), Vector3(0, 3, 0), Vector3(3, 0, 0), true));
+ tt.push_back(Case(Vector3(1, 1, 1), Vector3(10, -1, -1), Vector3(-3, 0, 0), Vector3(0, 3, 0), Vector3(3, 0, 0), false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::segment_intersects_triangle(current_case.from, current_case.to, current_case.v_1, current_case.v_2, current_case.v_3, current_case.result);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Triangle and Box Overlap") {
+ struct Case {
+ Vector3 box_centre;
+ Vector3 box_half_size;
+ Vector3 *tri_verts = nullptr;
+ bool want;
+ Case(){};
+ Case(Vector3 p_centre, Vector3 p_half_size, Vector3 *p_verts, bool p_want) :
+ box_centre(p_centre), box_half_size(p_half_size), tri_verts(p_verts), want(p_want){};
+ };
+ Vector<Case> tt;
+ Vector3 GoodTriangle[3] = { Vector3(3, 2, 3), Vector3(2, 2, 1), Vector3(2, 1, 1) };
+ tt.push_back(Case(Vector3(0, 0, 0), Vector3(5, 5, 5), GoodTriangle, true));
+ Vector3 BadTriangle[3] = { Vector3(100, 100, 100), Vector3(-100, -100, -100), Vector3(10, 10, 10) };
+ tt.push_back(Case(Vector3(1000, 1000, 1000), Vector3(1, 1, 1), BadTriangle, false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::triangle_box_overlap(current_case.box_centre, current_case.box_half_size, current_case.tri_verts);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Triangle and Sphere Intersect") {
+ struct Case {
+ Vector<Vector3> triangle;
+ Vector3 normal, sphere_pos, triangle_contact, sphere_contact;
+ real_t sphere_radius;
+ bool want;
+ Case(){};
+ Case(Vector<Vector3> p_triangle, Vector3 p_normal, Vector3 p_sphere_pos, real_t p_sphere_radius, bool p_want) :
+ triangle(p_triangle), normal(p_normal), sphere_pos(p_sphere_pos), triangle_contact(Vector3()), sphere_contact(Vector3()), sphere_radius(p_sphere_radius), want(p_want){};
+ };
+ Vector<Case> tt;
+ Vector<Vector3> triangle;
+ triangle.push_back(Vector3(3, 0, 0));
+ triangle.push_back(Vector3(-3, 0, 0));
+ triangle.push_back(Vector3(0, 3, 0));
+ tt.push_back(Case(triangle, Vector3(0, -1, 0), Vector3(0, 0, 0), 5, true));
+ tt.push_back(Case(triangle, Vector3(0, 1, 0), Vector3(0, 0, 0), 5, true));
+ tt.push_back(Case(triangle, Vector3(0, 1, 0), Vector3(20, 0, 0), 5, false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::triangle_sphere_intersection_test(&current_case.triangle[0], current_case.normal, current_case.sphere_pos, current_case.sphere_radius, current_case.triangle_contact, current_case.sphere_contact);
+ CHECK(output == current_case.want);
+ }
+}
+} // namespace TestGeometry3D
+
+#endif // TEST_GEOMETRY_3D_H
diff --git a/tests/core/math/test_plane.h b/tests/core/math/test_plane.h
new file mode 100644
index 0000000000..84d9a0ff7d
--- /dev/null
+++ b/tests/core/math/test_plane.h
@@ -0,0 +1,195 @@
+/*************************************************************************/
+/* test_plane.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_PLANE_H
+#define TEST_PLANE_H
+
+#include "core/math/plane.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestPlane {
+
+// Plane
+
+TEST_CASE("[Plane] Constructor methods") {
+ const Plane plane = Plane(32, 22, 16, 3);
+ const Plane plane_vector = Plane(Vector3(32, 22, 16), 3);
+ const Plane plane_copy_plane = Plane(plane);
+
+ CHECK_MESSAGE(
+ plane == plane_vector,
+ "Planes created with same values but different methods should be equal.");
+
+ CHECK_MESSAGE(
+ plane == plane_copy_plane,
+ "Planes created with same values but different methods should be equal.");
+}
+
+TEST_CASE("[Plane] Basic getters") {
+ const Plane plane = Plane(32, 22, 16, 3);
+ const Plane plane_normalized = Plane(32.0 / 42, 22.0 / 42, 16.0 / 42, 3.0 / 42);
+
+ CHECK_MESSAGE(
+ plane.get_normal().is_equal_approx(Vector3(32, 22, 16)),
+ "get_normal() should return the expected value.");
+
+ CHECK_MESSAGE(
+ plane.normalized().is_equal_approx(plane_normalized),
+ "normalized() should return a copy of the normalized value.");
+}
+
+TEST_CASE("[Plane] Basic setters") {
+ Plane plane = Plane(32, 22, 16, 3);
+ plane.set_normal(Vector3(4, 2, 3));
+
+ CHECK_MESSAGE(
+ plane.is_equal_approx(Plane(4, 2, 3, 3)),
+ "set_normal() should result in the expected plane.");
+
+ plane = Plane(32, 22, 16, 3);
+ plane.normalize();
+
+ CHECK_MESSAGE(
+ plane.is_equal_approx(Plane(32.0 / 42, 22.0 / 42, 16.0 / 42, 3.0 / 42)),
+ "normalize() should result in the expected plane.");
+}
+
+TEST_CASE("[Plane] Plane-point operations") {
+ const Plane plane = Plane(32, 22, 16, 3);
+ const Plane y_facing_plane = Plane(0, 1, 0, 4);
+
+ CHECK_MESSAGE(
+ plane.center().is_equal_approx(Vector3(32 * 3, 22 * 3, 16 * 3)),
+ "center() should return a vector pointing to the center of the plane.");
+
+ CHECK_MESSAGE(
+ y_facing_plane.is_point_over(Vector3(0, 5, 0)),
+ "is_point_over() should return the expected result.");
+
+ CHECK_MESSAGE(
+ y_facing_plane.get_any_perpendicular_normal().is_equal_approx(Vector3(1, 0, 0)),
+ "get_any_perpindicular_normal() should return the expected result.");
+
+ // TODO distance_to()
+}
+
+TEST_CASE("[Plane] Has point") {
+ const Plane x_facing_plane = Plane(1, 0, 0, 0);
+ const Plane y_facing_plane = Plane(0, 1, 0, 0);
+ const Plane z_facing_plane = Plane(0, 0, 1, 0);
+
+ const Vector3 x_axis_point = Vector3(10, 0, 0);
+ const Vector3 y_axis_point = Vector3(0, 10, 0);
+ const Vector3 z_axis_point = Vector3(0, 0, 10);
+
+ const Plane x_facing_plane_with_d_offset = Plane(1, 0, 0, 1);
+ const Vector3 y_axis_point_with_d_offset = Vector3(1, 10, 0);
+
+ CHECK_MESSAGE(
+ x_facing_plane.has_point(y_axis_point),
+ "has_point() with contained Vector3 should return the expected result.");
+ CHECK_MESSAGE(
+ x_facing_plane.has_point(z_axis_point),
+ "has_point() with contained Vector3 should return the expected result.");
+
+ CHECK_MESSAGE(
+ y_facing_plane.has_point(x_axis_point),
+ "has_point() with contained Vector3 should return the expected result.");
+ CHECK_MESSAGE(
+ y_facing_plane.has_point(z_axis_point),
+ "has_point() with contained Vector3 should return the expected result.");
+
+ CHECK_MESSAGE(
+ z_facing_plane.has_point(y_axis_point),
+ "has_point() with contained Vector3 should return the expected result.");
+ CHECK_MESSAGE(
+ z_facing_plane.has_point(x_axis_point),
+ "has_point() with contained Vector3 should return the expected result.");
+
+ CHECK_MESSAGE(
+ x_facing_plane_with_d_offset.has_point(y_axis_point_with_d_offset),
+ "has_point() with passed Vector3 should return the expected result.");
+}
+
+TEST_CASE("[Plane] Intersection") {
+ const Plane x_facing_plane = Plane(1, 0, 0, 1);
+ const Plane y_facing_plane = Plane(0, 1, 0, 2);
+ const Plane z_facing_plane = Plane(0, 0, 1, 3);
+
+ Vector3 vec_out;
+
+ CHECK_MESSAGE(
+ x_facing_plane.intersect_3(y_facing_plane, z_facing_plane, &vec_out),
+ "intersect_3() should return the expected result.");
+ CHECK_MESSAGE(
+ vec_out.is_equal_approx(Vector3(1, 2, 3)),
+ "intersect_3() should modify vec_out to the expected result.");
+
+ CHECK_MESSAGE(
+ x_facing_plane.intersects_ray(Vector3(0, 1, 1), Vector3(2, 0, 0), &vec_out),
+ "intersects_ray() should return the expected result.");
+ CHECK_MESSAGE(
+ vec_out.is_equal_approx(Vector3(1, 1, 1)),
+ "intersects_ray() should modify vec_out to the expected result.");
+
+ CHECK_MESSAGE(
+ x_facing_plane.intersects_segment(Vector3(0, 1, 1), Vector3(2, 1, 1), &vec_out),
+ "intersects_segment() should return the expected result.");
+ CHECK_MESSAGE(
+ vec_out.is_equal_approx(Vector3(1, 1, 1)),
+ "intersects_segment() should modify vec_out to the expected result.");
+}
+
+TEST_CASE("[Plane] Finite number checks") {
+ const Vector3 x(0, 1, 2);
+ const Vector3 infinite_vec(NAN, NAN, NAN);
+ const real_t y = 0;
+ const real_t infinite_y = NAN;
+
+ CHECK_MESSAGE(
+ Plane(x, y).is_finite(),
+ "Plane with all components finite should be finite");
+
+ CHECK_FALSE_MESSAGE(
+ Plane(x, infinite_y).is_finite(),
+ "Plane with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Plane(infinite_vec, y).is_finite(),
+ "Plane with one component infinite should not be finite.");
+
+ CHECK_FALSE_MESSAGE(
+ Plane(infinite_vec, infinite_y).is_finite(),
+ "Plane with two components infinite should not be finite.");
+}
+
+} // namespace TestPlane
+
+#endif // TEST_PLANE_H
diff --git a/tests/core/math/test_quaternion.h b/tests/core/math/test_quaternion.h
new file mode 100644
index 0000000000..eb5fea1d2d
--- /dev/null
+++ b/tests/core/math/test_quaternion.h
@@ -0,0 +1,446 @@
+/*************************************************************************/
+/* test_quaternion.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_QUATERNION_H
+#define TEST_QUATERNION_H
+
+#include "core/math/math_defs.h"
+#include "core/math/math_funcs.h"
+#include "core/math/quaternion.h"
+#include "core/math/vector3.h"
+
+#include "tests/test_macros.h"
+
+namespace TestQuaternion {
+
+Quaternion quat_euler_yxz_deg(Vector3 angle) {
+ double yaw = Math::deg_to_rad(angle[1]);
+ double pitch = Math::deg_to_rad(angle[0]);
+ double roll = Math::deg_to_rad(angle[2]);
+
+ // Generate YXZ (Z-then-X-then-Y) Quaternion using single-axis Euler
+ // constructor and quaternion product, both tested separately.
+ Quaternion q_y(Vector3(0.0, yaw, 0.0));
+ Quaternion q_p(Vector3(pitch, 0.0, 0.0));
+ Quaternion q_r(Vector3(0.0, 0.0, roll));
+ // Roll-Z is followed by Pitch-X, then Yaw-Y.
+ Quaternion q_yxz = q_y * q_p * q_r;
+
+ return q_yxz;
+}
+
+TEST_CASE("[Quaternion] Default Construct") {
+ Quaternion q;
+
+ CHECK(q[0] == 0.0);
+ CHECK(q[1] == 0.0);
+ CHECK(q[2] == 0.0);
+ CHECK(q[3] == 1.0);
+}
+
+TEST_CASE("[Quaternion] Construct x,y,z,w") {
+ // Values are taken from actual use in another project & are valid (except roundoff error).
+ Quaternion q(0.2391, 0.099, 0.3696, 0.8924);
+
+ CHECK(q[0] == doctest::Approx(0.2391));
+ CHECK(q[1] == doctest::Approx(0.099));
+ CHECK(q[2] == doctest::Approx(0.3696));
+ CHECK(q[3] == doctest::Approx(0.8924));
+}
+
+TEST_CASE("[Quaternion] Construct AxisAngle 1") {
+ // Easy to visualize: 120 deg about X-axis.
+ Quaternion q(Vector3(1.0, 0.0, 0.0), Math::deg_to_rad(120.0));
+
+ // 0.866 isn't close enough; doctest::Approx doesn't cut much slack!
+ CHECK(q[0] == doctest::Approx(0.866025)); // Sine of half the angle.
+ CHECK(q[1] == doctest::Approx(0.0));
+ CHECK(q[2] == doctest::Approx(0.0));
+ CHECK(q[3] == doctest::Approx(0.5)); // Cosine of half the angle.
+}
+
+TEST_CASE("[Quaternion] Construct AxisAngle 2") {
+ // Easy to visualize: 30 deg about Y-axis.
+ Quaternion q(Vector3(0.0, 1.0, 0.0), Math::deg_to_rad(30.0));
+
+ CHECK(q[0] == doctest::Approx(0.0));
+ CHECK(q[1] == doctest::Approx(0.258819)); // Sine of half the angle.
+ CHECK(q[2] == doctest::Approx(0.0));
+ CHECK(q[3] == doctest::Approx(0.965926)); // Cosine of half the angle.
+}
+
+TEST_CASE("[Quaternion] Construct AxisAngle 3") {
+ // Easy to visualize: 60 deg about Z-axis.
+ Quaternion q(Vector3(0.0, 0.0, 1.0), Math::deg_to_rad(60.0));
+
+ CHECK(q[0] == doctest::Approx(0.0));
+ CHECK(q[1] == doctest::Approx(0.0));
+ CHECK(q[2] == doctest::Approx(0.5)); // Sine of half the angle.
+ CHECK(q[3] == doctest::Approx(0.866025)); // Cosine of half the angle.
+}
+
+TEST_CASE("[Quaternion] Construct AxisAngle 4") {
+ // More complex & hard to visualize, so test w/ data from online calculator.
+ Vector3 axis(1.0, 2.0, 0.5);
+ Quaternion q(axis.normalized(), Math::deg_to_rad(35.0));
+
+ CHECK(q[0] == doctest::Approx(0.131239));
+ CHECK(q[1] == doctest::Approx(0.262478));
+ CHECK(q[2] == doctest::Approx(0.0656194));
+ CHECK(q[3] == doctest::Approx(0.953717));
+}
+
+TEST_CASE("[Quaternion] Construct from Quaternion") {
+ Vector3 axis(1.0, 2.0, 0.5);
+ Quaternion q_src(axis.normalized(), Math::deg_to_rad(35.0));
+ Quaternion q(q_src);
+
+ CHECK(q[0] == doctest::Approx(0.131239));
+ CHECK(q[1] == doctest::Approx(0.262478));
+ CHECK(q[2] == doctest::Approx(0.0656194));
+ CHECK(q[3] == doctest::Approx(0.953717));
+}
+
+TEST_CASE("[Quaternion] Construct Euler SingleAxis") {
+ double yaw = Math::deg_to_rad(45.0);
+ double pitch = Math::deg_to_rad(30.0);
+ double roll = Math::deg_to_rad(10.0);
+
+ Vector3 euler_y(0.0, yaw, 0.0);
+ Quaternion q_y(euler_y);
+ CHECK(q_y[0] == doctest::Approx(0.0));
+ CHECK(q_y[1] == doctest::Approx(0.382684));
+ CHECK(q_y[2] == doctest::Approx(0.0));
+ CHECK(q_y[3] == doctest::Approx(0.923879));
+
+ Vector3 euler_p(pitch, 0.0, 0.0);
+ Quaternion q_p(euler_p);
+ CHECK(q_p[0] == doctest::Approx(0.258819));
+ CHECK(q_p[1] == doctest::Approx(0.0));
+ CHECK(q_p[2] == doctest::Approx(0.0));
+ CHECK(q_p[3] == doctest::Approx(0.965926));
+
+ Vector3 euler_r(0.0, 0.0, roll);
+ Quaternion q_r(euler_r);
+ CHECK(q_r[0] == doctest::Approx(0.0));
+ CHECK(q_r[1] == doctest::Approx(0.0));
+ CHECK(q_r[2] == doctest::Approx(0.0871558));
+ CHECK(q_r[3] == doctest::Approx(0.996195));
+}
+
+TEST_CASE("[Quaternion] Construct Euler YXZ dynamic axes") {
+ double yaw = Math::deg_to_rad(45.0);
+ double pitch = Math::deg_to_rad(30.0);
+ double roll = Math::deg_to_rad(10.0);
+
+ // Generate YXZ comparison data (Z-then-X-then-Y) using single-axis Euler
+ // constructor and quaternion product, both tested separately.
+ Vector3 euler_y(0.0, yaw, 0.0);
+ Quaternion q_y(euler_y);
+ Vector3 euler_p(pitch, 0.0, 0.0);
+ Quaternion q_p(euler_p);
+ Vector3 euler_r(0.0, 0.0, roll);
+ Quaternion q_r(euler_r);
+
+ // Roll-Z is followed by Pitch-X.
+ Quaternion check_xz = q_p * q_r;
+ // Then Yaw-Y follows both.
+ Quaternion check_yxz = q_y * check_xz;
+
+ // Test construction from YXZ Euler angles.
+ Vector3 euler_yxz(pitch, yaw, roll);
+ Quaternion q(euler_yxz);
+ CHECK(q[0] == doctest::Approx(check_yxz[0]));
+ CHECK(q[1] == doctest::Approx(check_yxz[1]));
+ CHECK(q[2] == doctest::Approx(check_yxz[2]));
+ CHECK(q[3] == doctest::Approx(check_yxz[3]));
+
+ // Sneak in a test of is_equal_approx.
+ CHECK(q.is_equal_approx(check_yxz));
+}
+
+TEST_CASE("[Quaternion] Construct Basis Euler") {
+ double yaw = Math::deg_to_rad(45.0);
+ double pitch = Math::deg_to_rad(30.0);
+ double roll = Math::deg_to_rad(10.0);
+ Vector3 euler_yxz(pitch, yaw, roll);
+ Quaternion q_yxz(euler_yxz);
+ Basis basis_axes = Basis::from_euler(euler_yxz);
+ Quaternion q(basis_axes);
+ CHECK(q.is_equal_approx(q_yxz));
+}
+
+TEST_CASE("[Quaternion] Construct Basis Axes") {
+ // Arbitrary Euler angles.
+ Vector3 euler_yxz(Math::deg_to_rad(31.41), Math::deg_to_rad(-49.16), Math::deg_to_rad(12.34));
+ // Basis vectors from online calculation of rotation matrix.
+ Vector3 i_unit(0.5545787, 0.1823950, 0.8118957);
+ Vector3 j_unit(-0.5249245, 0.8337420, 0.1712555);
+ Vector3 k_unit(-0.6456754, -0.5211586, 0.5581192);
+ // Quaternion from online calculation.
+ Quaternion q_calc(0.2016913, -0.4245716, 0.206033, 0.8582598);
+ // Quaternion from local calculation.
+ Quaternion q_local = quat_euler_yxz_deg(Vector3(31.41, -49.16, 12.34));
+ // Quaternion from Euler angles constructor.
+ Quaternion q_euler(euler_yxz);
+ CHECK(q_calc.is_equal_approx(q_local));
+ CHECK(q_local.is_equal_approx(q_euler));
+
+ // Calculate Basis and construct Quaternion.
+ // When this is written, C++ Basis class does not construct from basis vectors.
+ // This is by design, but may be subject to change.
+ // Workaround by constructing Basis from Euler angles.
+ // basis_axes = Basis(i_unit, j_unit, k_unit);
+ Basis basis_axes = Basis::from_euler(euler_yxz);
+ Quaternion q(basis_axes);
+
+ CHECK(basis_axes.get_column(0).is_equal_approx(i_unit));
+ CHECK(basis_axes.get_column(1).is_equal_approx(j_unit));
+ CHECK(basis_axes.get_column(2).is_equal_approx(k_unit));
+
+ CHECK(q.is_equal_approx(q_calc));
+ CHECK_FALSE(q.inverse().is_equal_approx(q_calc));
+ CHECK(q.is_equal_approx(q_local));
+ CHECK(q.is_equal_approx(q_euler));
+ CHECK(q[0] == doctest::Approx(0.2016913));
+ CHECK(q[1] == doctest::Approx(-0.4245716));
+ CHECK(q[2] == doctest::Approx(0.206033));
+ CHECK(q[3] == doctest::Approx(0.8582598));
+}
+
+TEST_CASE("[Quaternion] Product (book)") {
+ // Example from "Quaternions and Rotation Sequences" by Jack Kuipers, p. 108.
+ Quaternion p(1.0, -2.0, 1.0, 3.0);
+ Quaternion q(-1.0, 2.0, 3.0, 2.0);
+
+ Quaternion pq = p * q;
+ CHECK(pq[0] == doctest::Approx(-9.0));
+ CHECK(pq[1] == doctest::Approx(-2.0));
+ CHECK(pq[2] == doctest::Approx(11.0));
+ CHECK(pq[3] == doctest::Approx(8.0));
+}
+
+TEST_CASE("[Quaternion] Product") {
+ double yaw = Math::deg_to_rad(45.0);
+ double pitch = Math::deg_to_rad(30.0);
+ double roll = Math::deg_to_rad(10.0);
+
+ Vector3 euler_y(0.0, yaw, 0.0);
+ Quaternion q_y(euler_y);
+ CHECK(q_y[0] == doctest::Approx(0.0));
+ CHECK(q_y[1] == doctest::Approx(0.382684));
+ CHECK(q_y[2] == doctest::Approx(0.0));
+ CHECK(q_y[3] == doctest::Approx(0.923879));
+
+ Vector3 euler_p(pitch, 0.0, 0.0);
+ Quaternion q_p(euler_p);
+ CHECK(q_p[0] == doctest::Approx(0.258819));
+ CHECK(q_p[1] == doctest::Approx(0.0));
+ CHECK(q_p[2] == doctest::Approx(0.0));
+ CHECK(q_p[3] == doctest::Approx(0.965926));
+
+ Vector3 euler_r(0.0, 0.0, roll);
+ Quaternion q_r(euler_r);
+ CHECK(q_r[0] == doctest::Approx(0.0));
+ CHECK(q_r[1] == doctest::Approx(0.0));
+ CHECK(q_r[2] == doctest::Approx(0.0871558));
+ CHECK(q_r[3] == doctest::Approx(0.996195));
+
+ // Test ZYX dynamic-axes since test data is available online.
+ // Rotate first about X axis, then new Y axis, then new Z axis.
+ // (Godot uses YXZ Yaw-Pitch-Roll order).
+ Quaternion q_yp = q_y * q_p;
+ CHECK(q_yp[0] == doctest::Approx(0.239118));
+ CHECK(q_yp[1] == doctest::Approx(0.369644));
+ CHECK(q_yp[2] == doctest::Approx(-0.099046));
+ CHECK(q_yp[3] == doctest::Approx(0.892399));
+
+ Quaternion q_ryp = q_r * q_yp;
+ CHECK(q_ryp[0] == doctest::Approx(0.205991));
+ CHECK(q_ryp[1] == doctest::Approx(0.389078));
+ CHECK(q_ryp[2] == doctest::Approx(-0.0208912));
+ CHECK(q_ryp[3] == doctest::Approx(0.897636));
+}
+
+TEST_CASE("[Quaternion] xform unit vectors") {
+ // Easy to visualize: 120 deg about X-axis.
+ // Transform the i, j, & k unit vectors.
+ Quaternion q(Vector3(1.0, 0.0, 0.0), Math::deg_to_rad(120.0));
+ Vector3 i_t = q.xform(Vector3(1.0, 0.0, 0.0));
+ Vector3 j_t = q.xform(Vector3(0.0, 1.0, 0.0));
+ Vector3 k_t = q.xform(Vector3(0.0, 0.0, 1.0));
+ //
+ CHECK(i_t.is_equal_approx(Vector3(1.0, 0.0, 0.0)));
+ CHECK(j_t.is_equal_approx(Vector3(0.0, -0.5, 0.866025)));
+ CHECK(k_t.is_equal_approx(Vector3(0.0, -0.866025, -0.5)));
+ CHECK(i_t.length_squared() == doctest::Approx(1.0));
+ CHECK(j_t.length_squared() == doctest::Approx(1.0));
+ CHECK(k_t.length_squared() == doctest::Approx(1.0));
+
+ // Easy to visualize: 30 deg about Y-axis.
+ q = Quaternion(Vector3(0.0, 1.0, 0.0), Math::deg_to_rad(30.0));
+ i_t = q.xform(Vector3(1.0, 0.0, 0.0));
+ j_t = q.xform(Vector3(0.0, 1.0, 0.0));
+ k_t = q.xform(Vector3(0.0, 0.0, 1.0));
+ //
+ CHECK(i_t.is_equal_approx(Vector3(0.866025, 0.0, -0.5)));
+ CHECK(j_t.is_equal_approx(Vector3(0.0, 1.0, 0.0)));
+ CHECK(k_t.is_equal_approx(Vector3(0.5, 0.0, 0.866025)));
+ CHECK(i_t.length_squared() == doctest::Approx(1.0));
+ CHECK(j_t.length_squared() == doctest::Approx(1.0));
+ CHECK(k_t.length_squared() == doctest::Approx(1.0));
+
+ // Easy to visualize: 60 deg about Z-axis.
+ q = Quaternion(Vector3(0.0, 0.0, 1.0), Math::deg_to_rad(60.0));
+ i_t = q.xform(Vector3(1.0, 0.0, 0.0));
+ j_t = q.xform(Vector3(0.0, 1.0, 0.0));
+ k_t = q.xform(Vector3(0.0, 0.0, 1.0));
+ //
+ CHECK(i_t.is_equal_approx(Vector3(0.5, 0.866025, 0.0)));
+ CHECK(j_t.is_equal_approx(Vector3(-0.866025, 0.5, 0.0)));
+ CHECK(k_t.is_equal_approx(Vector3(0.0, 0.0, 1.0)));
+ CHECK(i_t.length_squared() == doctest::Approx(1.0));
+ CHECK(j_t.length_squared() == doctest::Approx(1.0));
+ CHECK(k_t.length_squared() == doctest::Approx(1.0));
+}
+
+TEST_CASE("[Quaternion] xform vector") {
+ // Arbitrary quaternion rotates an arbitrary vector.
+ Vector3 euler_yzx(Math::deg_to_rad(31.41), Math::deg_to_rad(-49.16), Math::deg_to_rad(12.34));
+ Basis basis_axes = Basis::from_euler(euler_yzx);
+ Quaternion q(basis_axes);
+
+ Vector3 v_arb(3.0, 4.0, 5.0);
+ Vector3 v_rot = q.xform(v_arb);
+ Vector3 v_compare = basis_axes.xform(v_arb);
+
+ CHECK(v_rot.length_squared() == doctest::Approx(v_arb.length_squared()));
+ CHECK(v_rot.is_equal_approx(v_compare));
+}
+
+// Test vector xform for a single combination of Quaternion and Vector.
+void test_quat_vec_rotate(Vector3 euler_yzx, Vector3 v_in) {
+ Basis basis_axes = Basis::from_euler(euler_yzx);
+ Quaternion q(basis_axes);
+
+ Vector3 v_rot = q.xform(v_in);
+ Vector3 v_compare = basis_axes.xform(v_in);
+
+ CHECK(v_rot.length_squared() == doctest::Approx(v_in.length_squared()));
+ CHECK(v_rot.is_equal_approx(v_compare));
+}
+
+TEST_CASE("[Stress][Quaternion] Many vector xforms") {
+ // Many arbitrary quaternions rotate many arbitrary vectors.
+ // For each trial, check that rotation by Quaternion yields same result as
+ // rotation by Basis.
+ const int STEPS = 100; // Number of test steps in each dimension
+ const double delta = 2.0 * Math_PI / STEPS; // Angle increment per step
+ const double delta_vec = 20.0 / STEPS; // Vector increment per step
+ Vector3 vec_arb(1.0, 1.0, 1.0);
+ double x_angle = -Math_PI;
+ double y_angle = -Math_PI;
+ double z_angle = -Math_PI;
+ for (double i = 0; i < STEPS; ++i) {
+ vec_arb[0] = -10.0 + i * delta_vec;
+ x_angle = i * delta - Math_PI;
+ for (double j = 0; j < STEPS; ++j) {
+ vec_arb[1] = -10.0 + j * delta_vec;
+ y_angle = j * delta - Math_PI;
+ for (double k = 0; k < STEPS; ++k) {
+ vec_arb[2] = -10.0 + k * delta_vec;
+ z_angle = k * delta - Math_PI;
+ Vector3 euler_yzx(x_angle, y_angle, z_angle);
+ test_quat_vec_rotate(euler_yzx, vec_arb);
+ }
+ }
+ }
+}
+
+TEST_CASE("[Quaternion] Finite number checks") {
+ const real_t x = NAN;
+
+ CHECK_MESSAGE(
+ Quaternion(0, 1, 2, 3).is_finite(),
+ "Quaternion with all components finite should be finite");
+
+ CHECK_FALSE_MESSAGE(
+ Quaternion(x, 1, 2, 3).is_finite(),
+ "Quaternion with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Quaternion(0, x, 2, 3).is_finite(),
+ "Quaternion with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Quaternion(0, 1, x, 3).is_finite(),
+ "Quaternion with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Quaternion(0, 1, 2, x).is_finite(),
+ "Quaternion with one component infinite should not be finite.");
+
+ CHECK_FALSE_MESSAGE(
+ Quaternion(x, x, 2, 3).is_finite(),
+ "Quaternion with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Quaternion(x, 1, x, 3).is_finite(),
+ "Quaternion with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Quaternion(x, 1, 2, x).is_finite(),
+ "Quaternion with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Quaternion(0, x, x, 3).is_finite(),
+ "Quaternion with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Quaternion(0, x, 2, x).is_finite(),
+ "Quaternion with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Quaternion(0, 1, x, x).is_finite(),
+ "Quaternion with two components infinite should not be finite.");
+
+ CHECK_FALSE_MESSAGE(
+ Quaternion(0, x, x, x).is_finite(),
+ "Quaternion with three components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Quaternion(x, 1, x, x).is_finite(),
+ "Quaternion with three components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Quaternion(x, x, 2, x).is_finite(),
+ "Quaternion with three components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Quaternion(x, x, x, 3).is_finite(),
+ "Quaternion with three components infinite should not be finite.");
+
+ CHECK_FALSE_MESSAGE(
+ Quaternion(x, x, x, x).is_finite(),
+ "Quaternion with four components infinite should not be finite.");
+}
+
+} // namespace TestQuaternion
+
+#endif // TEST_QUATERNION_H
diff --git a/tests/core/math/test_random_number_generator.h b/tests/core/math/test_random_number_generator.h
new file mode 100644
index 0000000000..e8cd47b9d7
--- /dev/null
+++ b/tests/core/math/test_random_number_generator.h
@@ -0,0 +1,275 @@
+/*************************************************************************/
+/* test_random_number_generator.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_RANDOM_NUMBER_GENERATOR_H
+#define TEST_RANDOM_NUMBER_GENERATOR_H
+
+#include "core/math/random_number_generator.h"
+#include "tests/test_macros.h"
+
+namespace TestRandomNumberGenerator {
+
+TEST_CASE("[RandomNumberGenerator] Float") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(0);
+
+ INFO("Should give float between 0.0 and 1.0.");
+ for (int i = 0; i < 1000; i++) {
+ real_t n = rng->randf();
+ CHECK(n >= 0.0);
+ CHECK(n <= 1.0);
+ }
+}
+
+TEST_CASE("[RandomNumberGenerator] Integer range via modulo") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(0);
+
+ INFO("Should give integer between 0 and 100.");
+ for (int i = 0; i < 1000; i++) {
+ uint32_t n = rng->randi() % 100;
+ CHECK(n >= 0);
+ CHECK(n <= 100);
+ }
+}
+
+TEST_CASE_MAY_FAIL("[RandomNumberGenerator] Integer 32 bit") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(0); // Change the seed if this fails.
+
+ bool higher = false;
+ int i;
+ for (i = 0; i < 1000; i++) {
+ uint32_t n = rng->randi();
+ if (n > 0x0fff'ffff) {
+ higher = true;
+ break;
+ }
+ }
+ INFO("Current seed: ", rng->get_seed());
+ INFO("Current iteration: ", i);
+ CHECK_MESSAGE(higher, "Given current seed, this should give an integer higher than 0x0fff'ffff at least once.");
+}
+
+TEST_CASE("[RandomNumberGenerator] Float and integer range") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(0);
+ uint64_t initial_state = rng->get_state();
+ uint32_t initial_seed = rng->get_seed();
+
+ INFO("Should give float between -100.0 and 100.0, base test.");
+ for (int i = 0; i < 1000; i++) {
+ real_t n0 = rng->randf_range(-100.0, 100.0);
+ CHECK(n0 >= -100);
+ CHECK(n0 <= 100);
+ }
+
+ rng->randomize();
+ INFO("Should give float between -75.0 and 75.0.");
+ INFO("Shouldn't be affected by randomize.");
+ for (int i = 0; i < 1000; i++) {
+ real_t n1 = rng->randf_range(-75.0, 75.0);
+ CHECK(n1 >= -75);
+ CHECK(n1 <= 75);
+ }
+
+ rng->set_state(initial_state);
+ INFO("Should give integer between -50 and 50.");
+ INFO("Shouldn't be affected by set_state.");
+ for (int i = 0; i < 1000; i++) {
+ real_t n2 = rng->randi_range(-50, 50);
+ CHECK(n2 >= -50);
+ CHECK(n2 <= 50);
+ }
+
+ rng->set_seed(initial_seed);
+ INFO("Should give integer between -25 and 25.");
+ INFO("Shouldn't be affected by set_seed.");
+ for (int i = 0; i < 1000; i++) {
+ int32_t n3 = rng->randi_range(-25, 25);
+ CHECK(n3 >= -25);
+ CHECK(n3 <= 25);
+ }
+
+ rng->randf();
+ rng->randf();
+
+ INFO("Should give float between -10.0 and 10.0.");
+ INFO("Shouldn't be affected after generating new numbers.");
+ for (int i = 0; i < 1000; i++) {
+ real_t n4 = rng->randf_range(-10.0, 10.0);
+ CHECK(n4 >= -10);
+ CHECK(n4 <= 10);
+ }
+
+ rng->randi();
+ rng->randi();
+
+ INFO("Should give integer between -5 and 5.");
+ INFO("Shouldn't be affected after generating new numbers.");
+ for (int i = 0; i < 1000; i++) {
+ real_t n5 = rng->randf_range(-5, 5);
+ CHECK(n5 >= -5);
+ CHECK(n5 <= 5);
+ }
+}
+
+TEST_CASE_MAY_FAIL("[RandomNumberGenerator] Normal distribution") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(1); // Change the seed if this fails.
+ INFO("Should give a number between -5 to 5 (5 std deviations away; above 99.7% chance it will be in this range).");
+ INFO("Standard randfn function call.");
+ for (int i = 0; i < 100; i++) {
+ real_t n = rng->randfn();
+ CHECK(n >= -5);
+ CHECK(n <= 5);
+ }
+
+ INFO("Should give number between -5 to 5 after multiple randi/randf calls.");
+ INFO("5 std deviations away; above 99.7% chance it will be in this range.");
+ rng->randf();
+ rng->randi();
+ for (int i = 0; i < 100; i++) {
+ real_t n = rng->randfn();
+ CHECK(n >= -5);
+ CHECK(n <= 5);
+ }
+
+ INFO("Checks if user defined mean and deviation work properly.");
+ INFO("5 std deviations away; above 99.7% chance it will be in this range.");
+ for (int i = 0; i < 100; i++) {
+ real_t n = rng->randfn(5, 10);
+ CHECK(n >= -45);
+ CHECK(n <= 55);
+ }
+
+ INFO("Checks if randfn works with changed seeds.");
+ INFO("5 std deviations away; above 99.7% chance it will be in this range.");
+ rng->randomize();
+ for (int i = 0; i < 100; i++) {
+ real_t n = rng->randfn(3, 3);
+ CHECK(n >= -12);
+ CHECK(n <= 18);
+ }
+}
+
+TEST_CASE("[RandomNumberGenerator] Zero for first number immediately after seeding") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(0);
+ uint32_t n1 = rng->randi();
+ uint32_t n2 = rng->randi();
+ INFO("Initial random values: ", n1, " ", n2);
+ CHECK(n1 != 0);
+
+ rng->set_seed(1);
+ uint32_t n3 = rng->randi();
+ uint32_t n4 = rng->randi();
+ INFO("Values after changing the seed: ", n3, " ", n4);
+ CHECK(n3 != 0);
+}
+
+TEST_CASE("[RandomNumberGenerator] Restore state") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->randomize();
+ uint64_t last_seed = rng->get_seed();
+ INFO("Current seed: ", last_seed);
+
+ rng->randi();
+ rng->randi();
+
+ CHECK_MESSAGE(rng->get_seed() == last_seed,
+ "The seed should remain the same after generating some numbers");
+
+ uint64_t saved_state = rng->get_state();
+ INFO("Current state: ", saved_state);
+
+ real_t f1_before = rng->randf();
+ real_t f2_before = rng->randf();
+ INFO("This seed produces: ", f1_before, " ", f2_before);
+
+ // Restore now.
+ rng->set_state(saved_state);
+
+ real_t f1_after = rng->randf();
+ real_t f2_after = rng->randf();
+ INFO("Resetting the state produces: ", f1_after, " ", f2_after);
+
+ String msg = "Should restore the sequence of numbers after resetting the state";
+ CHECK_MESSAGE(f1_before == f1_after, msg);
+ CHECK_MESSAGE(f2_before == f2_after, msg);
+}
+
+TEST_CASE("[RandomNumberGenerator] Restore from seed") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(0);
+ INFO("Current seed: ", rng->get_seed());
+ uint32_t s0_1_before = rng->randi();
+ uint32_t s0_2_before = rng->randi();
+ INFO("This seed produces: ", s0_1_before, " ", s0_2_before);
+
+ rng->set_seed(9000);
+ INFO("Current seed: ", rng->get_seed());
+ uint32_t s9000_1 = rng->randi();
+ uint32_t s9000_2 = rng->randi();
+ INFO("This seed produces: ", s9000_1, " ", s9000_2);
+
+ rng->set_seed(0);
+ INFO("Current seed: ", rng->get_seed());
+ uint32_t s0_1_after = rng->randi();
+ uint32_t s0_2_after = rng->randi();
+ INFO("This seed produces: ", s0_1_after, " ", s0_2_after);
+
+ String msg = "Should restore the sequence of numbers after resetting the seed";
+ CHECK_MESSAGE(s0_1_before == s0_1_after, msg);
+ CHECK_MESSAGE(s0_2_before == s0_2_after, msg);
+}
+
+TEST_CASE_MAY_FAIL("[RandomNumberGenerator] randi_range bias check") {
+ int zeros = 0;
+ int ones = 0;
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ for (int i = 0; i < 10000; i++) {
+ int val = rng->randi_range(0, 1);
+ val == 0 ? zeros++ : ones++;
+ }
+ CHECK_MESSAGE(abs(zeros * 1.0 / ones - 1.0) < 0.1, "The ratio of zeros to ones should be nearly 1");
+
+ int vals[10] = { 0 };
+ for (int i = 0; i < 1000000; i++) {
+ vals[rng->randi_range(0, 9)]++;
+ }
+
+ for (int i = 0; i < 10; i++) {
+ CHECK_MESSAGE(abs(vals[i] / 1000000.0 - 0.1) < 0.01, "Each element should appear roughly 10% of the time");
+ }
+}
+} // namespace TestRandomNumberGenerator
+
+#endif // TEST_RANDOM_NUMBER_GENERATOR_H
diff --git a/tests/core/math/test_rect2.h b/tests/core/math/test_rect2.h
new file mode 100644
index 0000000000..d784875c1c
--- /dev/null
+++ b/tests/core/math/test_rect2.h
@@ -0,0 +1,326 @@
+/*************************************************************************/
+/* test_rect2.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_RECT2_H
+#define TEST_RECT2_H
+
+#include "core/math/rect2.h"
+#include "core/math/rect2i.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestRect2 {
+TEST_CASE("[Rect2] Constructor methods") {
+ const Rect2 rect = Rect2(0, 100, 1280, 720);
+ const Rect2 rect_vector = Rect2(Vector2(0, 100), Vector2(1280, 720));
+ const Rect2 rect_copy_rect = Rect2(rect);
+ const Rect2 rect_copy_recti = Rect2(Rect2i(0, 100, 1280, 720));
+
+ CHECK_MESSAGE(
+ rect == rect_vector,
+ "Rect2s created with the same dimensions but by different methods should be equal.");
+ CHECK_MESSAGE(
+ rect == rect_copy_rect,
+ "Rect2s created with the same dimensions but by different methods should be equal.");
+ CHECK_MESSAGE(
+ rect == rect_copy_recti,
+ "Rect2s created with the same dimensions but by different methods should be equal.");
+}
+
+TEST_CASE("[Rect2] String conversion") {
+ // Note: This also depends on the Vector2 string representation.
+ CHECK_MESSAGE(
+ String(Rect2(0, 100, 1280, 720)) == "[P: (0, 100), S: (1280, 720)]",
+ "The string representation should match the expected value.");
+}
+
+TEST_CASE("[Rect2] Basic getters") {
+ const Rect2 rect = Rect2(0, 100, 1280, 720);
+ CHECK_MESSAGE(
+ rect.get_position().is_equal_approx(Vector2(0, 100)),
+ "get_position() should return the expected value.");
+ CHECK_MESSAGE(
+ rect.get_size().is_equal_approx(Vector2(1280, 720)),
+ "get_size() should return the expected value.");
+ CHECK_MESSAGE(
+ rect.get_end().is_equal_approx(Vector2(1280, 820)),
+ "get_end() should return the expected value.");
+ CHECK_MESSAGE(
+ rect.get_center().is_equal_approx(Vector2(640, 460)),
+ "get_center() should return the expected value.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1281, 721).get_center().is_equal_approx(Vector2(640.5, 460.5)),
+ "get_center() should return the expected value.");
+}
+
+TEST_CASE("[Rect2] Basic setters") {
+ Rect2 rect = Rect2(0, 100, 1280, 720);
+ rect.set_end(Vector2(4000, 4000));
+ CHECK_MESSAGE(
+ rect.is_equal_approx(Rect2(0, 100, 4000, 3900)),
+ "set_end() should result in the expected Rect2.");
+
+ rect = Rect2(0, 100, 1280, 720);
+ rect.set_position(Vector2(4000, 4000));
+ CHECK_MESSAGE(
+ rect.is_equal_approx(Rect2(4000, 4000, 1280, 720)),
+ "set_position() should result in the expected Rect2.");
+
+ rect = Rect2(0, 100, 1280, 720);
+ rect.set_size(Vector2(4000, 4000));
+ CHECK_MESSAGE(
+ rect.is_equal_approx(Rect2(0, 100, 4000, 4000)),
+ "set_size() should result in the expected Rect2.");
+}
+
+TEST_CASE("[Rect2] Area getters") {
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Rect2(0, 100, 1280, 720).get_area(), 921'600),
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Rect2(0, 100, -1280, -720).get_area(), 921'600),
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Rect2(0, 100, 1280, -720).get_area(), -921'600),
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Rect2(0, 100, -1280, 720).get_area(), -921'600),
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(Rect2(0, 100, 0, 720).get_area()),
+ "get_area() should return the expected value.");
+
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).has_area(),
+ "has_area() should return the expected value on Rect2 with an area.");
+ CHECK_MESSAGE(
+ !Rect2(0, 100, 0, 500).has_area(),
+ "has_area() should return the expected value on Rect2 with no area.");
+ CHECK_MESSAGE(
+ !Rect2(0, 100, 500, 0).has_area(),
+ "has_area() should return the expected value on Rect2 with no area.");
+ CHECK_MESSAGE(
+ !Rect2(0, 100, 0, 0).has_area(),
+ "has_area() should return the expected value on Rect2 with no area.");
+}
+
+TEST_CASE("[Rect2] Absolute coordinates") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).abs().is_equal_approx(Rect2(0, 100, 1280, 720)),
+ "abs() should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, -100, 1280, 720).abs().is_equal_approx(Rect2(0, -100, 1280, 720)),
+ "abs() should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, -100, -1280, -720).abs().is_equal_approx(Rect2(-1280, -820, 1280, 720)),
+ "abs() should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, -1280, 720).abs().is_equal_approx(Rect2(-1280, 100, 1280, 720)),
+ "abs() should return the expected Rect2.");
+}
+
+TEST_CASE("[Rect2] Intersection") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).intersection(Rect2(0, 300, 100, 100)).is_equal_approx(Rect2(0, 300, 100, 100)),
+ "intersection() with fully enclosed Rect2 should return the expected result.");
+ // The resulting Rect2 is 100 pixels high because the first Rect2 is vertically offset by 100 pixels.
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).intersection(Rect2(1200, 700, 100, 100)).is_equal_approx(Rect2(1200, 700, 80, 100)),
+ "intersection() with partially enclosed Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).intersection(Rect2(-4000, -4000, 100, 100)).is_equal_approx(Rect2()),
+ "intersection() with non-enclosed Rect2 should return the expected result.");
+}
+
+TEST_CASE("[Rect2] Enclosing") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).encloses(Rect2(0, 300, 100, 100)),
+ "encloses() with fully contained Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2(0, 100, 1280, 720).encloses(Rect2(1200, 700, 100, 100)),
+ "encloses() with partially contained Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2(0, 100, 1280, 720).encloses(Rect2(-4000, -4000, 100, 100)),
+ "encloses() with non-contained Rect2 should return the expected result.");
+}
+
+TEST_CASE("[Rect2] Expanding") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).expand(Vector2(500, 600)).is_equal_approx(Rect2(0, 100, 1280, 720)),
+ "expand() with contained Vector2 should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).expand(Vector2(0, 0)).is_equal_approx(Rect2(0, 0, 1280, 820)),
+ "expand() with non-contained Vector2 should return the expected result.");
+}
+
+TEST_CASE("[Rect2] Growing") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow(100).is_equal_approx(Rect2(-100, 0, 1480, 920)),
+ "grow() with positive value should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow(-100).is_equal_approx(Rect2(100, 200, 1080, 520)),
+ "grow() with negative value should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow(-4000).is_equal_approx(Rect2(4000, 4100, -6720, -7280)),
+ "grow() with large negative value should return the expected Rect2.");
+
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow_individual(100, 200, 300, 400).is_equal_approx(Rect2(-100, -100, 1680, 1320)),
+ "grow_individual() with positive values should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow_individual(-100, 200, 300, -400).is_equal_approx(Rect2(100, -100, 1480, 520)),
+ "grow_individual() with positive and negative values should return the expected Rect2.");
+
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow_side(SIDE_TOP, 500).is_equal_approx(Rect2(0, -400, 1280, 1220)),
+ "grow_side() with positive value should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow_side(SIDE_TOP, -500).is_equal_approx(Rect2(0, 600, 1280, 220)),
+ "grow_side() with negative value should return the expected Rect2.");
+}
+
+TEST_CASE("[Rect2] Has point") {
+ Rect2 rect = Rect2(0, 100, 1280, 720);
+ CHECK_MESSAGE(
+ rect.has_point(Vector2(500, 600)),
+ "has_point() with contained Vector2 should return the expected result.");
+ CHECK_MESSAGE(
+ !rect.has_point(Vector2(0, 0)),
+ "has_point() with non-contained Vector2 should return the expected result.");
+
+ CHECK_MESSAGE(
+ rect.has_point(rect.position),
+ "has_point() with positive size should include `position`.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2(1, 1)),
+ "has_point() with positive size should include `position + (1, 1)`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2(1, -1)),
+ "has_point() with positive size should not include `position + (1, -1)`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size),
+ "has_point() with positive size should not include `position + size`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size + Vector2(1, 1)),
+ "has_point() with positive size should not include `position + size + (1, 1)`.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + rect.size + Vector2(-1, -1)),
+ "has_point() with positive size should include `position + size + (-1, -1)`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size + Vector2(-1, 1)),
+ "has_point() with positive size should not include `position + size + (-1, 1)`.");
+
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2(0, 10)),
+ "has_point() with point located on left edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2(rect.size.x, 10)),
+ "has_point() with point located on right edge should return false.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2(10, 0)),
+ "has_point() with point located on top edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2(10, rect.size.y)),
+ "has_point() with point located on bottom edge should return false.");
+
+ /*
+ // FIXME: Disabled for now until GH-37617 is fixed one way or another.
+ // More tests should then be written like for the positive size case.
+ rect = Rect2(0, 100, -1280, -720);
+ CHECK_MESSAGE(
+ rect.has_point(rect.position),
+ "has_point() with negative size should include `position`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size),
+ "has_point() with negative size should not include `position + size`.");
+ */
+
+ rect = Rect2(-4000, -200, 1280, 720);
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2(0, 10)),
+ "has_point() with negative position and point located on left edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2(rect.size.x, 10)),
+ "has_point() with negative position and point located on right edge should return false.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2(10, 0)),
+ "has_point() with negative position and point located on top edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2(10, rect.size.y)),
+ "has_point() with negative position and point located on bottom edge should return false.");
+}
+
+TEST_CASE("[Rect2] Intersection") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).intersects(Rect2(0, 300, 100, 100)),
+ "intersects() with fully enclosed Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).intersects(Rect2(1200, 700, 100, 100)),
+ "intersects() with partially enclosed Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2(0, 100, 1280, 720).intersects(Rect2(-4000, -4000, 100, 100)),
+ "intersects() with non-enclosed Rect2 should return the expected result.");
+}
+
+TEST_CASE("[Rect2] Merging") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).merge(Rect2(0, 300, 100, 100)).is_equal_approx(Rect2(0, 100, 1280, 720)),
+ "merge() with fully enclosed Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).merge(Rect2(1200, 700, 100, 100)).is_equal_approx(Rect2(0, 100, 1300, 720)),
+ "merge() with partially enclosed Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).merge(Rect2(-4000, -4000, 100, 100)).is_equal_approx(Rect2(-4000, -4000, 5280, 4820)),
+ "merge() with non-enclosed Rect2 should return the expected result.");
+}
+
+TEST_CASE("[Rect2] Finite number checks") {
+ const Vector2 x(0, 1);
+ const Vector2 infinite(NAN, NAN);
+
+ CHECK_MESSAGE(
+ Rect2(x, x).is_finite(),
+ "Rect2 with all components finite should be finite");
+
+ CHECK_FALSE_MESSAGE(
+ Rect2(infinite, x).is_finite(),
+ "Rect2 with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Rect2(x, infinite).is_finite(),
+ "Rect2 with one component infinite should not be finite.");
+
+ CHECK_FALSE_MESSAGE(
+ Rect2(infinite, infinite).is_finite(),
+ "Rect2 with two components infinite should not be finite.");
+}
+
+} // namespace TestRect2
+
+#endif // TEST_RECT2_H
diff --git a/tests/core/math/test_rect2i.h b/tests/core/math/test_rect2i.h
new file mode 100644
index 0000000000..4005300e1f
--- /dev/null
+++ b/tests/core/math/test_rect2i.h
@@ -0,0 +1,311 @@
+/*************************************************************************/
+/* test_rect2i.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_RECT2I_H
+#define TEST_RECT2I_H
+
+#include "core/math/rect2.h"
+#include "core/math/rect2i.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestRect2i {
+TEST_CASE("[Rect2i] Constructor methods") {
+ Rect2i recti = Rect2i(0, 100, 1280, 720);
+ Rect2i recti_vector = Rect2i(Vector2i(0, 100), Vector2i(1280, 720));
+ Rect2i recti_copy_recti = Rect2i(recti);
+ Rect2i recti_copy_rect = Rect2i(Rect2(0, 100, 1280, 720));
+
+ CHECK_MESSAGE(
+ recti == recti_vector,
+ "Rect2is created with the same dimensions but by different methods should be equal.");
+ CHECK_MESSAGE(
+ recti == recti_copy_recti,
+ "Rect2is created with the same dimensions but by different methods should be equal.");
+ CHECK_MESSAGE(
+ recti == recti_copy_rect,
+ "Rect2is created with the same dimensions but by different methods should be equal.");
+}
+
+TEST_CASE("[Rect2i] String conversion") {
+ // Note: This also depends on the Vector2 string representation.
+ CHECK_MESSAGE(
+ String(Rect2i(0, 100, 1280, 720)) == "[P: (0, 100), S: (1280, 720)]",
+ "The string representation should match the expected value.");
+}
+
+TEST_CASE("[Rect2i] Basic getters") {
+ const Rect2i rect = Rect2i(0, 100, 1280, 720);
+ CHECK_MESSAGE(
+ rect.get_position() == Vector2i(0, 100),
+ "get_position() should return the expected value.");
+ CHECK_MESSAGE(
+ rect.get_size() == Vector2i(1280, 720),
+ "get_size() should return the expected value.");
+ CHECK_MESSAGE(
+ rect.get_end() == Vector2i(1280, 820),
+ "get_end() should return the expected value.");
+ CHECK_MESSAGE(
+ rect.get_center() == Vector2i(640, 460),
+ "get_center() should return the expected value.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1281, 721).get_center() == Vector2i(640, 460),
+ "get_center() should return the expected value.");
+}
+
+TEST_CASE("[Rect2i] Basic setters") {
+ Rect2i rect = Rect2i(0, 100, 1280, 720);
+ rect.set_end(Vector2i(4000, 4000));
+ CHECK_MESSAGE(
+ rect == Rect2i(0, 100, 4000, 3900),
+ "set_end() should result in the expected Rect2i.");
+
+ rect = Rect2i(0, 100, 1280, 720);
+ rect.set_position(Vector2i(4000, 4000));
+ CHECK_MESSAGE(
+ rect == Rect2i(4000, 4000, 1280, 720),
+ "set_position() should result in the expected Rect2i.");
+
+ rect = Rect2i(0, 100, 1280, 720);
+ rect.set_size(Vector2i(4000, 4000));
+ CHECK_MESSAGE(
+ rect == Rect2i(0, 100, 4000, 4000),
+ "set_size() should result in the expected Rect2i.");
+}
+
+TEST_CASE("[Rect2i] Area getters") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).get_area() == 921'600,
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, -1280, -720).get_area() == 921'600,
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, -720).get_area() == -921'600,
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, -1280, 720).get_area() == -921'600,
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 0, 720).get_area() == 0,
+ "get_area() should return the expected value.");
+
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).has_area(),
+ "has_area() should return the expected value on Rect2i with an area.");
+ CHECK_MESSAGE(
+ !Rect2i(0, 100, 0, 500).has_area(),
+ "has_area() should return the expected value on Rect2i with no area.");
+ CHECK_MESSAGE(
+ !Rect2i(0, 100, 500, 0).has_area(),
+ "has_area() should return the expected value on Rect2i with no area.");
+ CHECK_MESSAGE(
+ !Rect2i(0, 100, 0, 0).has_area(),
+ "has_area() should return the expected value on Rect2i with no area.");
+}
+
+TEST_CASE("[Rect2i] Absolute coordinates") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).abs() == Rect2i(0, 100, 1280, 720),
+ "abs() should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, -100, 1280, 720).abs() == Rect2i(0, -100, 1280, 720),
+ "abs() should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, -100, -1280, -720).abs() == Rect2i(-1280, -820, 1280, 720),
+ "abs() should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, -1280, 720).abs() == Rect2i(-1280, 100, 1280, 720),
+ "abs() should return the expected Rect2i.");
+}
+
+TEST_CASE("[Rect2i] Intersection") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).intersection(Rect2i(0, 300, 100, 100)) == Rect2i(0, 300, 100, 100),
+ "intersection() with fully enclosed Rect2i should return the expected result.");
+ // The resulting Rect2i is 100 pixels high because the first Rect2i is vertically offset by 100 pixels.
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).intersection(Rect2i(1200, 700, 100, 100)) == Rect2i(1200, 700, 80, 100),
+ "intersection() with partially enclosed Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).intersection(Rect2i(-4000, -4000, 100, 100)) == Rect2i(),
+ "intersection() with non-enclosed Rect2i should return the expected result.");
+}
+
+TEST_CASE("[Rect2i] Enclosing") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).encloses(Rect2i(0, 300, 100, 100)),
+ "encloses() with fully contained Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2i(0, 100, 1280, 720).encloses(Rect2i(1200, 700, 100, 100)),
+ "encloses() with partially contained Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2i(0, 100, 1280, 720).encloses(Rect2i(-4000, -4000, 100, 100)),
+ "encloses() with non-contained Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).encloses(Rect2i(0, 100, 1280, 720)),
+ "encloses() with identical Rect2i should return the expected result.");
+}
+
+TEST_CASE("[Rect2i] Expanding") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).expand(Vector2i(500, 600)) == Rect2i(0, 100, 1280, 720),
+ "expand() with contained Vector2i should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).expand(Vector2i(0, 0)) == Rect2i(0, 0, 1280, 820),
+ "expand() with non-contained Vector2i should return the expected result.");
+}
+
+TEST_CASE("[Rect2i] Growing") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow(100) == Rect2i(-100, 0, 1480, 920),
+ "grow() with positive value should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow(-100) == Rect2i(100, 200, 1080, 520),
+ "grow() with negative value should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow(-4000) == Rect2i(4000, 4100, -6720, -7280),
+ "grow() with large negative value should return the expected Rect2i.");
+
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow_individual(100, 200, 300, 400) == Rect2i(-100, -100, 1680, 1320),
+ "grow_individual() with positive values should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow_individual(-100, 200, 300, -400) == Rect2i(100, -100, 1480, 520),
+ "grow_individual() with positive and negative values should return the expected Rect2i.");
+
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow_side(SIDE_TOP, 500) == Rect2i(0, -400, 1280, 1220),
+ "grow_side() with positive value should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow_side(SIDE_TOP, -500) == Rect2i(0, 600, 1280, 220),
+ "grow_side() with negative value should return the expected Rect2i.");
+}
+
+TEST_CASE("[Rect2i] Has point") {
+ Rect2i rect = Rect2i(0, 100, 1280, 720);
+ CHECK_MESSAGE(
+ rect.has_point(Vector2i(500, 600)),
+ "has_point() with contained Vector2i should return the expected result.");
+ CHECK_MESSAGE(
+ !rect.has_point(Vector2i(0, 0)),
+ "has_point() with non-contained Vector2i should return the expected result.");
+
+ CHECK_MESSAGE(
+ rect.has_point(rect.position),
+ "has_point() with positive size should include `position`.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2i(1, 1)),
+ "has_point() with positive size should include `position + (1, 1)`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2i(1, -1)),
+ "has_point() with positive size should not include `position + (1, -1)`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size),
+ "has_point() with positive size should not include `position + size`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size + Vector2i(1, 1)),
+ "has_point() with positive size should not include `position + size + (1, 1)`.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + rect.size + Vector2i(-1, -1)),
+ "has_point() with positive size should include `position + size + (-1, -1)`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size + Vector2i(-1, 1)),
+ "has_point() with positive size should not include `position + size + (-1, 1)`.");
+
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2i(0, 10)),
+ "has_point() with point located on left edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2i(rect.size.x, 10)),
+ "has_point() with point located on right edge should return false.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2i(10, 0)),
+ "has_point() with point located on top edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2i(10, rect.size.y)),
+ "has_point() with point located on bottom edge should return false.");
+
+ /*
+ // FIXME: Disabled for now until GH-37617 is fixed one way or another.
+ // More tests should then be written like for the positive size case.
+ rect = Rect2i(0, 100, -1280, -720);
+ CHECK_MESSAGE(
+ rect.has_point(rect.position),
+ "has_point() with negative size should include `position`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size),
+ "has_point() with negative size should not include `position + size`.");
+ */
+
+ rect = Rect2i(-4000, -200, 1280, 720);
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2i(0, 10)),
+ "has_point() with negative position and point located on left edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2i(rect.size.x, 10)),
+ "has_point() with negative position and point located on right edge should return false.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2i(10, 0)),
+ "has_point() with negative position and point located on top edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2i(10, rect.size.y)),
+ "has_point() with negative position and point located on bottom edge should return false.");
+}
+
+TEST_CASE("[Rect2i] Intersection") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).intersects(Rect2i(0, 300, 100, 100)),
+ "intersects() with fully enclosed Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).intersects(Rect2i(1200, 700, 100, 100)),
+ "intersects() with partially enclosed Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2i(0, 100, 1280, 720).intersects(Rect2i(-4000, -4000, 100, 100)),
+ "intersects() with non-enclosed Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2i(0, 0, 2, 2).intersects(Rect2i(2, 2, 2, 2)),
+ "intersects() with adjacent Rect2i should return the expected result.");
+}
+
+TEST_CASE("[Rect2i] Merging") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).merge(Rect2i(0, 300, 100, 100)) == Rect2i(0, 100, 1280, 720),
+ "merge() with fully enclosed Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).merge(Rect2i(1200, 700, 100, 100)) == Rect2i(0, 100, 1300, 720),
+ "merge() with partially enclosed Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).merge(Rect2i(-4000, -4000, 100, 100)) == Rect2i(-4000, -4000, 5280, 4820),
+ "merge() with non-enclosed Rect2i should return the expected result.");
+}
+} // namespace TestRect2i
+
+#endif // TEST_RECT2I_H
diff --git a/tests/core/math/test_transform_2d.h b/tests/core/math/test_transform_2d.h
new file mode 100644
index 0000000000..ab51bcd7da
--- /dev/null
+++ b/tests/core/math/test_transform_2d.h
@@ -0,0 +1,122 @@
+/*************************************************************************/
+/* test_transform_2d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_TRANSFORM_2D_H
+#define TEST_TRANSFORM_2D_H
+
+#include "core/math/transform_2d.h"
+
+#include "tests/test_macros.h"
+
+namespace TestTransform2D {
+
+Transform2D create_dummy_transform() {
+ return Transform2D(Vector2(1, 2), Vector2(3, 4), Vector2(5, 6));
+}
+
+Transform2D identity() {
+ return Transform2D();
+}
+
+TEST_CASE("[Transform2D] translation") {
+ Vector2 offset = Vector2(1, 2);
+
+ // Both versions should give the same result applied to identity.
+ CHECK(identity().translated(offset) == identity().translated_local(offset));
+
+ // Check both versions against left and right multiplications.
+ Transform2D orig = create_dummy_transform();
+ Transform2D T = identity().translated(offset);
+ CHECK(orig.translated(offset) == T * orig);
+ CHECK(orig.translated_local(offset) == orig * T);
+}
+
+TEST_CASE("[Transform2D] scaling") {
+ Vector2 scaling = Vector2(1, 2);
+
+ // Both versions should give the same result applied to identity.
+ CHECK(identity().scaled(scaling) == identity().scaled_local(scaling));
+
+ // Check both versions against left and right multiplications.
+ Transform2D orig = create_dummy_transform();
+ Transform2D S = identity().scaled(scaling);
+ CHECK(orig.scaled(scaling) == S * orig);
+ CHECK(orig.scaled_local(scaling) == orig * S);
+}
+
+TEST_CASE("[Transform2D] rotation") {
+ real_t phi = 1.0;
+
+ // Both versions should give the same result applied to identity.
+ CHECK(identity().rotated(phi) == identity().rotated_local(phi));
+
+ // Check both versions against left and right multiplications.
+ Transform2D orig = create_dummy_transform();
+ Transform2D R = identity().rotated(phi);
+ CHECK(orig.rotated(phi) == R * orig);
+ CHECK(orig.rotated_local(phi) == orig * R);
+}
+
+TEST_CASE("[Transform2D] Finite number checks") {
+ const Vector2 x(0, 1);
+ const Vector2 infinite(NAN, NAN);
+
+ CHECK_MESSAGE(
+ Transform2D(x, x, x).is_finite(),
+ "Transform2D with all components finite should be finite");
+
+ CHECK_FALSE_MESSAGE(
+ Transform2D(infinite, x, x).is_finite(),
+ "Transform2D with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Transform2D(x, infinite, x).is_finite(),
+ "Transform2D with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Transform2D(x, x, infinite).is_finite(),
+ "Transform2D with one component infinite should not be finite.");
+
+ CHECK_FALSE_MESSAGE(
+ Transform2D(infinite, infinite, x).is_finite(),
+ "Transform2D with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Transform2D(infinite, x, infinite).is_finite(),
+ "Transform2D with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Transform2D(x, infinite, infinite).is_finite(),
+ "Transform2D with two components infinite should not be finite.");
+
+ CHECK_FALSE_MESSAGE(
+ Transform2D(infinite, infinite, infinite).is_finite(),
+ "Transform2D with three components infinite should not be finite.");
+}
+
+} // namespace TestTransform2D
+
+#endif // TEST_TRANSFORM_2D_H
diff --git a/tests/core/math/test_transform_3d.h b/tests/core/math/test_transform_3d.h
new file mode 100644
index 0000000000..d2730f3577
--- /dev/null
+++ b/tests/core/math/test_transform_3d.h
@@ -0,0 +1,112 @@
+/*************************************************************************/
+/* test_transform_3d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_TRANSFORM_3D_H
+#define TEST_TRANSFORM_3D_H
+
+#include "core/math/transform_3d.h"
+
+#include "tests/test_macros.h"
+
+namespace TestTransform3D {
+
+Transform3D create_dummy_transform() {
+ return Transform3D(Basis(Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9)), Vector3(10, 11, 12));
+}
+
+Transform3D identity() {
+ return Transform3D();
+}
+
+TEST_CASE("[Transform3D] translation") {
+ Vector3 offset = Vector3(1, 2, 3);
+
+ // Both versions should give the same result applied to identity.
+ CHECK(identity().translated(offset) == identity().translated_local(offset));
+
+ // Check both versions against left and right multiplications.
+ Transform3D orig = create_dummy_transform();
+ Transform3D T = identity().translated(offset);
+ CHECK(orig.translated(offset) == T * orig);
+ CHECK(orig.translated_local(offset) == orig * T);
+}
+
+TEST_CASE("[Transform3D] scaling") {
+ Vector3 scaling = Vector3(1, 2, 3);
+
+ // Both versions should give the same result applied to identity.
+ CHECK(identity().scaled(scaling) == identity().scaled_local(scaling));
+
+ // Check both versions against left and right multiplications.
+ Transform3D orig = create_dummy_transform();
+ Transform3D S = identity().scaled(scaling);
+ CHECK(orig.scaled(scaling) == S * orig);
+ CHECK(orig.scaled_local(scaling) == orig * S);
+}
+
+TEST_CASE("[Transform3D] rotation") {
+ Vector3 axis = Vector3(1, 2, 3).normalized();
+ real_t phi = 1.0;
+
+ // Both versions should give the same result applied to identity.
+ CHECK(identity().rotated(axis, phi) == identity().rotated_local(axis, phi));
+
+ // Check both versions against left and right multiplications.
+ Transform3D orig = create_dummy_transform();
+ Transform3D R = identity().rotated(axis, phi);
+ CHECK(orig.rotated(axis, phi) == R * orig);
+ CHECK(orig.rotated_local(axis, phi) == orig * R);
+}
+
+TEST_CASE("[Transform3D] Finite number checks") {
+ const Vector3 y(0, 1, 2);
+ const Vector3 infinite_vec(NAN, NAN, NAN);
+ const Basis x(y, y, y);
+ const Basis infinite_basis(infinite_vec, infinite_vec, infinite_vec);
+
+ CHECK_MESSAGE(
+ Transform3D(x, y).is_finite(),
+ "Transform3D with all components finite should be finite");
+
+ CHECK_FALSE_MESSAGE(
+ Transform3D(x, infinite_vec).is_finite(),
+ "Transform3D with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Transform3D(infinite_basis, y).is_finite(),
+ "Transform3D with one component infinite should not be finite.");
+
+ CHECK_FALSE_MESSAGE(
+ Transform3D(infinite_basis, infinite_vec).is_finite(),
+ "Transform3D with two components infinite should not be finite.");
+}
+
+} // namespace TestTransform3D
+
+#endif // TEST_TRANSFORM_3D_H
diff --git a/tests/core/math/test_vector2.h b/tests/core/math/test_vector2.h
new file mode 100644
index 0000000000..a87b9ffc02
--- /dev/null
+++ b/tests/core/math/test_vector2.h
@@ -0,0 +1,496 @@
+/*************************************************************************/
+/* test_vector2.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_VECTOR2_H
+#define TEST_VECTOR2_H
+
+#include "core/math/vector2.h"
+#include "core/math/vector2i.h"
+#include "tests/test_macros.h"
+
+namespace TestVector2 {
+
+TEST_CASE("[Vector2] Constructor methods") {
+ const Vector2 vector_empty = Vector2();
+ const Vector2 vector_zero = Vector2(0.0, 0.0);
+ CHECK_MESSAGE(
+ vector_empty == vector_zero,
+ "Vector2 Constructor with no inputs should return a zero Vector2.");
+}
+
+TEST_CASE("[Vector2] Angle methods") {
+ const Vector2 vector_x = Vector2(1, 0);
+ const Vector2 vector_y = Vector2(0, 1);
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector_x.angle_to(vector_y), (real_t)Math_TAU / 4),
+ "Vector2 angle_to should work as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector_y.angle_to(vector_x), (real_t)-Math_TAU / 4),
+ "Vector2 angle_to should work as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector_x.angle_to_point(vector_y), (real_t)Math_TAU * 3 / 8),
+ "Vector2 angle_to_point should work as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector_y.angle_to_point(vector_x), (real_t)-Math_TAU / 8),
+ "Vector2 angle_to_point should work as expected.");
+}
+
+TEST_CASE("[Vector2] Axis methods") {
+ Vector2 vector = Vector2(1.2, 3.4);
+ CHECK_MESSAGE(
+ vector.max_axis_index() == Vector2::Axis::AXIS_Y,
+ "Vector2 max_axis_index should work as expected.");
+ CHECK_MESSAGE(
+ vector.min_axis_index() == Vector2::Axis::AXIS_X,
+ "Vector2 min_axis_index should work as expected.");
+ CHECK_MESSAGE(
+ vector[vector.min_axis_index()] == (real_t)1.2,
+ "Vector2 array operator should work as expected.");
+ vector[Vector2::Axis::AXIS_Y] = 3.7;
+ CHECK_MESSAGE(
+ vector[Vector2::Axis::AXIS_Y] == (real_t)3.7,
+ "Vector2 array operator setter should work as expected.");
+}
+
+TEST_CASE("[Vector2] Interpolation methods") {
+ const Vector2 vector1 = Vector2(1, 2);
+ const Vector2 vector2 = Vector2(4, 5);
+ CHECK_MESSAGE(
+ vector1.lerp(vector2, 0.5) == Vector2(2.5, 3.5),
+ "Vector2 lerp should work as expected.");
+ CHECK_MESSAGE(
+ vector1.lerp(vector2, 1.0 / 3.0).is_equal_approx(Vector2(2, 3)),
+ "Vector2 lerp should work as expected.");
+ CHECK_MESSAGE(
+ vector1.normalized().slerp(vector2.normalized(), 0.5).is_equal_approx(Vector2(0.538953602313995361, 0.84233558177947998)),
+ "Vector2 slerp should work as expected.");
+ CHECK_MESSAGE(
+ vector1.normalized().slerp(vector2.normalized(), 1.0 / 3.0).is_equal_approx(Vector2(0.508990883827209473, 0.860771894454956055)),
+ "Vector2 slerp should work as expected.");
+ CHECK_MESSAGE(
+ Vector2(5, 0).slerp(Vector2(0, 5), 0.5).is_equal_approx(Vector2(5, 5) * Math_SQRT12),
+ "Vector2 slerp with non-normalized values should work as expected.");
+ CHECK_MESSAGE(
+ Vector2(1, 1).slerp(Vector2(2, 2), 0.5).is_equal_approx(Vector2(1.5, 1.5)),
+ "Vector2 slerp with colinear inputs should behave as expected.");
+ CHECK_MESSAGE(
+ Vector2().slerp(Vector2(), 0.5) == Vector2(),
+ "Vector2 slerp with both inputs as zero vectors should return a zero vector.");
+ CHECK_MESSAGE(
+ Vector2().slerp(Vector2(1, 1), 0.5) == Vector2(0.5, 0.5),
+ "Vector2 slerp with one input as zero should behave like a regular lerp.");
+ CHECK_MESSAGE(
+ Vector2(1, 1).slerp(Vector2(), 0.5) == Vector2(0.5, 0.5),
+ "Vector2 slerp with one input as zero should behave like a regular lerp.");
+ CHECK_MESSAGE(
+ Vector2(4, 6).slerp(Vector2(8, 10), 0.5).is_equal_approx(Vector2(5.9076470794008017626, 8.07918879020090480697)),
+ "Vector2 slerp should work as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.slerp(vector2, 0.5).length(), (real_t)4.31959610746631919),
+ "Vector2 slerp with different length input should return a vector with an interpolated length.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.angle_to(vector1.slerp(vector2, 0.5)) * 2, vector1.angle_to(vector2)),
+ "Vector2 slerp with different length input should return a vector with an interpolated angle.");
+ CHECK_MESSAGE(
+ vector1.cubic_interpolate(vector2, Vector2(), Vector2(7, 7), 0.5) == Vector2(2.375, 3.5),
+ "Vector2 cubic_interpolate should work as expected.");
+ CHECK_MESSAGE(
+ vector1.cubic_interpolate(vector2, Vector2(), Vector2(7, 7), 1.0 / 3.0).is_equal_approx(Vector2(1.851851940155029297, 2.962963104248046875)),
+ "Vector2 cubic_interpolate should work as expected.");
+ CHECK_MESSAGE(
+ Vector2(1, 0).move_toward(Vector2(10, 0), 3) == Vector2(4, 0),
+ "Vector2 move_toward should work as expected.");
+}
+
+TEST_CASE("[Vector2] Length methods") {
+ const Vector2 vector1 = Vector2(10, 10);
+ const Vector2 vector2 = Vector2(20, 30);
+ CHECK_MESSAGE(
+ vector1.length_squared() == 200,
+ "Vector2 length_squared should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.length(), 10 * (real_t)Math_SQRT2),
+ "Vector2 length should work as expected.");
+ CHECK_MESSAGE(
+ vector2.length_squared() == 1300,
+ "Vector2 length_squared should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector2.length(), (real_t)36.05551275463989293119),
+ "Vector2 length should work as expected.");
+ CHECK_MESSAGE(
+ vector1.distance_squared_to(vector2) == 500,
+ "Vector2 distance_squared_to should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.distance_to(vector2), (real_t)22.36067977499789696409),
+ "Vector2 distance_to should work as expected.");
+}
+
+TEST_CASE("[Vector2] Limiting methods") {
+ const Vector2 vector = Vector2(10, 10);
+ CHECK_MESSAGE(
+ vector.limit_length().is_equal_approx(Vector2(Math_SQRT12, Math_SQRT12)),
+ "Vector2 limit_length should work as expected.");
+ CHECK_MESSAGE(
+ vector.limit_length(5).is_equal_approx(5 * Vector2(Math_SQRT12, Math_SQRT12)),
+ "Vector2 limit_length should work as expected.");
+
+ CHECK_MESSAGE(
+ Vector2(-5, 15).clamp(Vector2(), vector).is_equal_approx(Vector2(0, 10)),
+ "Vector2 clamp should work as expected.");
+ CHECK_MESSAGE(
+ vector.clamp(Vector2(0, 15), Vector2(5, 20)).is_equal_approx(Vector2(5, 15)),
+ "Vector2 clamp should work as expected.");
+}
+
+TEST_CASE("[Vector2] Normalization methods") {
+ CHECK_MESSAGE(
+ Vector2(1, 0).is_normalized() == true,
+ "Vector2 is_normalized should return true for a normalized vector.");
+ CHECK_MESSAGE(
+ Vector2(1, 1).is_normalized() == false,
+ "Vector2 is_normalized should return false for a non-normalized vector.");
+ CHECK_MESSAGE(
+ Vector2(1, 0).normalized() == Vector2(1, 0),
+ "Vector2 normalized should return the same vector for a normalized vector.");
+ CHECK_MESSAGE(
+ Vector2(1, 1).normalized().is_equal_approx(Vector2(Math_SQRT12, Math_SQRT12)),
+ "Vector2 normalized should work as expected.");
+
+ Vector2 vector = Vector2(3.2, -5.4);
+ vector.normalize();
+ CHECK_MESSAGE(
+ vector == Vector2(3.2, -5.4).normalized(),
+ "Vector2 normalize should convert same way as Vector2 normalized.");
+ CHECK_MESSAGE(
+ vector.is_equal_approx(Vector2(0.509802390301732898898, -0.860291533634174266891)),
+ "Vector2 normalize should work as expected.");
+}
+
+TEST_CASE("[Vector2] Operators") {
+ const Vector2 decimal1 = Vector2(2.3, 4.9);
+ const Vector2 decimal2 = Vector2(1.2, 3.4);
+ const Vector2 power1 = Vector2(0.75, 1.5);
+ const Vector2 power2 = Vector2(0.5, 0.125);
+ const Vector2 int1 = Vector2(4, 5);
+ const Vector2 int2 = Vector2(1, 2);
+
+ CHECK_MESSAGE(
+ (decimal1 + decimal2).is_equal_approx(Vector2(3.5, 8.3)),
+ "Vector2 addition should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 + power2) == Vector2(1.25, 1.625),
+ "Vector2 addition with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 + int2) == Vector2(5, 7),
+ "Vector2 addition with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 - decimal2).is_equal_approx(Vector2(1.1, 1.5)),
+ "Vector2 subtraction should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 - power2) == Vector2(0.25, 1.375),
+ "Vector2 subtraction with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 - int2) == Vector2(3, 3),
+ "Vector2 subtraction with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 * decimal2).is_equal_approx(Vector2(2.76, 16.66)),
+ "Vector2 multiplication should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 * power2) == Vector2(0.375, 0.1875),
+ "Vector2 multiplication with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 * int2) == Vector2(4, 10),
+ "Vector2 multiplication with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 / decimal2).is_equal_approx(Vector2(1.91666666666666666, 1.44117647058823529)),
+ "Vector2 division should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 / power2) == Vector2(1.5, 12.0),
+ "Vector2 division with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 / int2) == Vector2(4, 2.5),
+ "Vector2 division with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 * 2).is_equal_approx(Vector2(4.6, 9.8)),
+ "Vector2 multiplication should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 * 2) == Vector2(1.5, 3),
+ "Vector2 multiplication with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 * 2) == Vector2(8, 10),
+ "Vector2 multiplication with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 / 2).is_equal_approx(Vector2(1.15, 2.45)),
+ "Vector2 division should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 / 2) == Vector2(0.375, 0.75),
+ "Vector2 division with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 / 2) == Vector2(2, 2.5),
+ "Vector2 division with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ ((Vector2i)decimal1) == Vector2i(2, 4),
+ "Vector2 cast to Vector2i should work as expected.");
+ CHECK_MESSAGE(
+ ((Vector2i)decimal2) == Vector2i(1, 3),
+ "Vector2 cast to Vector2i should work as expected.");
+ CHECK_MESSAGE(
+ Vector2(Vector2i(1, 2)) == Vector2(1, 2),
+ "Vector2 constructed from Vector2i should work as expected.");
+
+ CHECK_MESSAGE(
+ ((String)decimal1) == "(2.3, 4.9)",
+ "Vector2 cast to String should work as expected.");
+ CHECK_MESSAGE(
+ ((String)decimal2) == "(1.2, 3.4)",
+ "Vector2 cast to String should work as expected.");
+ CHECK_MESSAGE(
+ ((String)Vector2(9.8, 9.9)) == "(9.8, 9.9)",
+ "Vector2 cast to String should work as expected.");
+#ifdef REAL_T_IS_DOUBLE
+ CHECK_MESSAGE(
+ ((String)Vector2(Math_PI, Math_TAU)) == "(3.14159265358979, 6.28318530717959)",
+ "Vector2 cast to String should print the correct amount of digits for real_t = double.");
+#else
+ CHECK_MESSAGE(
+ ((String)Vector2(Math_PI, Math_TAU)) == "(3.141593, 6.283185)",
+ "Vector2 cast to String should print the correct amount of digits for real_t = float.");
+#endif // REAL_T_IS_DOUBLE
+}
+
+TEST_CASE("[Vector2] Other methods") {
+ const Vector2 vector = Vector2(1.2, 3.4);
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector.aspect(), (real_t)1.2 / (real_t)3.4),
+ "Vector2 aspect should work as expected.");
+
+ CHECK_MESSAGE(
+ vector.direction_to(Vector2()).is_equal_approx(-vector.normalized()),
+ "Vector2 direction_to should work as expected.");
+ CHECK_MESSAGE(
+ Vector2(1, 1).direction_to(Vector2(2, 2)).is_equal_approx(Vector2(Math_SQRT12, Math_SQRT12)),
+ "Vector2 direction_to should work as expected.");
+
+ CHECK_MESSAGE(
+ vector.posmod(2).is_equal_approx(Vector2(1.2, 1.4)),
+ "Vector2 posmod should work as expected.");
+ CHECK_MESSAGE(
+ (-vector).posmod(2).is_equal_approx(Vector2(0.8, 0.6)),
+ "Vector2 posmod should work as expected.");
+ CHECK_MESSAGE(
+ vector.posmodv(Vector2(1, 2)).is_equal_approx(Vector2(0.2, 1.4)),
+ "Vector2 posmodv should work as expected.");
+ CHECK_MESSAGE(
+ (-vector).posmodv(Vector2(2, 3)).is_equal_approx(Vector2(0.8, 2.6)),
+ "Vector2 posmodv should work as expected.");
+
+ CHECK_MESSAGE(
+ vector.rotated(Math_TAU).is_equal_approx(Vector2(1.2, 3.4)),
+ "Vector2 rotated should work as expected.");
+ CHECK_MESSAGE(
+ vector.rotated(Math_TAU / 4).is_equal_approx(Vector2(-3.4, 1.2)),
+ "Vector2 rotated should work as expected.");
+ CHECK_MESSAGE(
+ vector.rotated(Math_TAU / 3).is_equal_approx(Vector2(-3.544486372867091398996, -0.660769515458673623883)),
+ "Vector2 rotated should work as expected.");
+ CHECK_MESSAGE(
+ vector.rotated(Math_TAU / 2).is_equal_approx(vector.rotated(Math_TAU / -2)),
+ "Vector2 rotated should work as expected.");
+
+ CHECK_MESSAGE(
+ vector.snapped(Vector2(1, 1)) == Vector2(1, 3),
+ "Vector2 snapped to integers should be the same as rounding.");
+ CHECK_MESSAGE(
+ Vector2(3.4, 5.6).snapped(Vector2(1, 1)) == Vector2(3, 6),
+ "Vector2 snapped to integers should be the same as rounding.");
+ CHECK_MESSAGE(
+ vector.snapped(Vector2(0.25, 0.25)) == Vector2(1.25, 3.5),
+ "Vector2 snapped to 0.25 should give exact results.");
+
+ CHECK_MESSAGE(
+ Vector2(1.2, 2.5).is_equal_approx(vector.min(Vector2(3.0, 2.5))),
+ "Vector2 min should return expected value.");
+
+ CHECK_MESSAGE(
+ Vector2(5.3, 3.4).is_equal_approx(vector.max(Vector2(5.3, 2.0))),
+ "Vector2 max should return expected value.");
+}
+
+TEST_CASE("[Vector2] Plane methods") {
+ const Vector2 vector = Vector2(1.2, 3.4);
+ const Vector2 vector_y = Vector2(0, 1);
+ const Vector2 vector_normal = Vector2(0.95879811270838721622267, 0.2840883296913739899919);
+ const Vector2 vector_non_normal = Vector2(5.4, 1.6);
+ CHECK_MESSAGE(
+ vector.bounce(vector_y) == Vector2(1.2, -3.4),
+ "Vector2 bounce on a plane with normal of the Y axis should.");
+ CHECK_MESSAGE(
+ vector.bounce(vector_normal).is_equal_approx(Vector2(-2.85851197982345523329, 2.197477931904161412358)),
+ "Vector2 bounce with normal should return expected value.");
+ CHECK_MESSAGE(
+ vector.reflect(vector_y) == Vector2(-1.2, 3.4),
+ "Vector2 reflect on a plane with normal of the Y axis should.");
+ CHECK_MESSAGE(
+ vector.reflect(vector_normal).is_equal_approx(Vector2(2.85851197982345523329, -2.197477931904161412358)),
+ "Vector2 reflect with normal should return expected value.");
+ CHECK_MESSAGE(
+ vector.project(vector_y) == Vector2(0, 3.4),
+ "Vector2 projected on the Y axis should only give the Y component.");
+ CHECK_MESSAGE(
+ vector.project(vector_normal).is_equal_approx(Vector2(2.0292559899117276166, 0.60126103404791929382)),
+ "Vector2 projected on a normal should return expected value.");
+ CHECK_MESSAGE(
+ vector.slide(vector_y) == Vector2(1.2, 0),
+ "Vector2 slide on a plane with normal of the Y axis should set the Y to zero.");
+ CHECK_MESSAGE(
+ vector.slide(vector_normal).is_equal_approx(Vector2(-0.8292559899117276166456, 2.798738965952080706179)),
+ "Vector2 slide with normal should return expected value.");
+ // There's probably a better way to test these ones?
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ vector.bounce(vector_non_normal).is_equal_approx(Vector2()),
+ "Vector2 bounce should return empty Vector2 with non-normalised input.");
+ CHECK_MESSAGE(
+ vector.reflect(vector_non_normal).is_equal_approx(Vector2()),
+ "Vector2 reflect should return empty Vector2 with non-normalised input.");
+ CHECK_MESSAGE(
+ vector.slide(vector_non_normal).is_equal_approx(Vector2()),
+ "Vector2 slide should return empty Vector2 with non-normalised input.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[Vector2] Rounding methods") {
+ const Vector2 vector1 = Vector2(1.2, 5.6);
+ const Vector2 vector2 = Vector2(1.2, -5.6);
+ CHECK_MESSAGE(
+ vector1.abs() == vector1,
+ "Vector2 abs should work as expected.");
+ CHECK_MESSAGE(
+ vector2.abs() == vector1,
+ "Vector2 abs should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.ceil() == Vector2(2, 6),
+ "Vector2 ceil should work as expected.");
+ CHECK_MESSAGE(
+ vector2.ceil() == Vector2(2, -5),
+ "Vector2 ceil should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.floor() == Vector2(1, 5),
+ "Vector2 floor should work as expected.");
+ CHECK_MESSAGE(
+ vector2.floor() == Vector2(1, -6),
+ "Vector2 floor should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.round() == Vector2(1, 6),
+ "Vector2 round should work as expected.");
+ CHECK_MESSAGE(
+ vector2.round() == Vector2(1, -6),
+ "Vector2 round should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.sign() == Vector2(1, 1),
+ "Vector2 sign should work as expected.");
+ CHECK_MESSAGE(
+ vector2.sign() == Vector2(1, -1),
+ "Vector2 sign should work as expected.");
+}
+
+TEST_CASE("[Vector2] Linear algebra methods") {
+ const Vector2 vector_x = Vector2(1, 0);
+ const Vector2 vector_y = Vector2(0, 1);
+ const Vector2 a = Vector2(3.5, 8.5);
+ const Vector2 b = Vector2(5.2, 4.6);
+ CHECK_MESSAGE(
+ vector_x.cross(vector_y) == 1,
+ "Vector2 cross product of X and Y should give 1.");
+ CHECK_MESSAGE(
+ vector_y.cross(vector_x) == -1,
+ "Vector2 cross product of Y and X should give negative 1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(a.cross(b), (real_t)-28.1),
+ "Vector2 cross should return expected value.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Vector2(-a.x, a.y).cross(Vector2(b.x, -b.y)), (real_t)-28.1),
+ "Vector2 cross should return expected value.");
+
+ CHECK_MESSAGE(
+ vector_x.dot(vector_y) == 0.0,
+ "Vector2 dot product of perpendicular vectors should be zero.");
+ CHECK_MESSAGE(
+ vector_x.dot(vector_x) == 1.0,
+ "Vector2 dot product of identical unit vectors should be one.");
+ CHECK_MESSAGE(
+ (vector_x * 10).dot(vector_x * 10) == 100.0,
+ "Vector2 dot product of same direction vectors should behave as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(a.dot(b), (real_t)57.3),
+ "Vector2 dot should return expected value.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Vector2(-a.x, a.y).dot(Vector2(b.x, -b.y)), (real_t)-57.3),
+ "Vector2 dot should return expected value.");
+}
+
+TEST_CASE("[Vector2] Finite number checks") {
+ const double infinite[] = { NAN, INFINITY, -INFINITY };
+
+ CHECK_MESSAGE(
+ Vector2(0, 1).is_finite(),
+ "Vector2(0, 1) should be finite");
+
+ for (double x : infinite) {
+ CHECK_FALSE_MESSAGE(
+ Vector2(x, 1).is_finite(),
+ "Vector2 with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector2(0, x).is_finite(),
+ "Vector2 with one component infinite should not be finite.");
+ }
+
+ for (double x : infinite) {
+ for (double y : infinite) {
+ CHECK_FALSE_MESSAGE(
+ Vector2(x, y).is_finite(),
+ "Vector2 with two components infinite should not be finite.");
+ }
+ }
+}
+
+} // namespace TestVector2
+
+#endif // TEST_VECTOR2_H
diff --git a/tests/core/math/test_vector2i.h b/tests/core/math/test_vector2i.h
new file mode 100644
index 0000000000..49b0632e3c
--- /dev/null
+++ b/tests/core/math/test_vector2i.h
@@ -0,0 +1,161 @@
+/*************************************************************************/
+/* test_vector2i.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_VECTOR2I_H
+#define TEST_VECTOR2I_H
+
+#include "core/math/vector2.h"
+#include "core/math/vector2i.h"
+#include "tests/test_macros.h"
+
+namespace TestVector2i {
+
+TEST_CASE("[Vector2i] Constructor methods") {
+ const Vector2i vector_empty = Vector2i();
+ const Vector2i vector_zero = Vector2i(0, 0);
+ CHECK_MESSAGE(
+ vector_empty == vector_zero,
+ "Vector2i Constructor with no inputs should return a zero Vector2i.");
+}
+
+TEST_CASE("[Vector2i] Axis methods") {
+ Vector2i vector = Vector2i(2, 3);
+ CHECK_MESSAGE(
+ vector.max_axis_index() == Vector2i::Axis::AXIS_Y,
+ "Vector2i max_axis_index should work as expected.");
+ CHECK_MESSAGE(
+ vector.min_axis_index() == Vector2i::Axis::AXIS_X,
+ "Vector2i min_axis_index should work as expected.");
+ CHECK_MESSAGE(
+ vector[vector.min_axis_index()] == 2,
+ "Vector2i array operator should work as expected.");
+ vector[Vector2i::Axis::AXIS_Y] = 5;
+ CHECK_MESSAGE(
+ vector[Vector2i::Axis::AXIS_Y] == 5,
+ "Vector2i array operator setter should work as expected.");
+}
+
+TEST_CASE("[Vector2i] Clamp method") {
+ const Vector2i vector = Vector2i(10, 10);
+ CHECK_MESSAGE(
+ Vector2i(-5, 15).clamp(Vector2i(), vector) == Vector2i(0, 10),
+ "Vector2i clamp should work as expected.");
+ CHECK_MESSAGE(
+ vector.clamp(Vector2i(0, 15), Vector2i(5, 20)) == Vector2i(5, 15),
+ "Vector2i clamp should work as expected.");
+}
+
+TEST_CASE("[Vector2i] Length methods") {
+ const Vector2i vector1 = Vector2i(10, 10);
+ const Vector2i vector2 = Vector2i(20, 30);
+ CHECK_MESSAGE(
+ vector1.length_squared() == 200,
+ "Vector2i length_squared should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.length(), 10 * Math_SQRT2),
+ "Vector2i length should work as expected.");
+ CHECK_MESSAGE(
+ vector2.length_squared() == 1300,
+ "Vector2i length_squared should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector2.length(), 36.05551275463989293119),
+ "Vector2i length should work as expected.");
+}
+
+TEST_CASE("[Vector2i] Operators") {
+ const Vector2i vector1 = Vector2i(5, 9);
+ const Vector2i vector2 = Vector2i(2, 3);
+
+ CHECK_MESSAGE(
+ (vector1 + vector2) == Vector2i(7, 12),
+ "Vector2i addition with integers should give exact results.");
+ CHECK_MESSAGE(
+ (vector1 - vector2) == Vector2i(3, 6),
+ "Vector2i subtraction with integers should give exact results.");
+ CHECK_MESSAGE(
+ (vector1 * vector2) == Vector2i(10, 27),
+ "Vector2i multiplication with integers should give exact results.");
+ CHECK_MESSAGE(
+ (vector1 / vector2) == Vector2i(2, 3),
+ "Vector2i division with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (vector1 * 2) == Vector2i(10, 18),
+ "Vector2i multiplication with integers should give exact results.");
+ CHECK_MESSAGE(
+ (vector1 / 2) == Vector2i(2, 4),
+ "Vector2i division with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ ((Vector2)vector1) == Vector2(5, 9),
+ "Vector2i cast to Vector2 should work as expected.");
+ CHECK_MESSAGE(
+ ((Vector2)vector2) == Vector2(2, 3),
+ "Vector2i cast to Vector2 should work as expected.");
+ CHECK_MESSAGE(
+ Vector2i(Vector2(1.1, 2.9)) == Vector2i(1, 2),
+ "Vector2i constructed from Vector2 should work as expected.");
+}
+
+TEST_CASE("[Vector2i] Other methods") {
+ const Vector2i vector = Vector2i(1, 3);
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector.aspect(), (real_t)1.0 / (real_t)3.0),
+ "Vector2i aspect should work as expected.");
+
+ CHECK_MESSAGE(
+ Vector2i(1, 2) == vector.min(Vector2i(3, 2)),
+ "Vector2i min should return expected value.");
+
+ CHECK_MESSAGE(
+ Vector2i(5, 3) == vector.max(Vector2i(5, 2)),
+ "Vector2i max should return expected value.");
+}
+
+TEST_CASE("[Vector2i] Abs and sign methods") {
+ const Vector2i vector1 = Vector2i(1, 3);
+ const Vector2i vector2 = Vector2i(1, -3);
+ CHECK_MESSAGE(
+ vector1.abs() == vector1,
+ "Vector2i abs should work as expected.");
+ CHECK_MESSAGE(
+ vector2.abs() == vector1,
+ "Vector2i abs should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.sign() == Vector2i(1, 1),
+ "Vector2i sign should work as expected.");
+ CHECK_MESSAGE(
+ vector2.sign() == Vector2i(1, -1),
+ "Vector2i sign should work as expected.");
+}
+} // namespace TestVector2i
+
+#endif // TEST_VECTOR2I_H
diff --git a/tests/core/math/test_vector3.h b/tests/core/math/test_vector3.h
new file mode 100644
index 0000000000..4932cd04db
--- /dev/null
+++ b/tests/core/math/test_vector3.h
@@ -0,0 +1,529 @@
+/*************************************************************************/
+/* test_vector3.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_VECTOR3_H
+#define TEST_VECTOR3_H
+
+#include "core/math/vector3.h"
+#include "tests/test_macros.h"
+
+#define Math_SQRT13 0.57735026918962576450914878050196
+#define Math_SQRT3 1.7320508075688772935274463415059
+
+namespace TestVector3 {
+
+TEST_CASE("[Vector3] Constructor methods") {
+ const Vector3 vector_empty = Vector3();
+ const Vector3 vector_zero = Vector3(0.0, 0.0, 0.0);
+ CHECK_MESSAGE(
+ vector_empty == vector_zero,
+ "Vector3 Constructor with no inputs should return a zero Vector3.");
+}
+
+TEST_CASE("[Vector3] Angle methods") {
+ const Vector3 vector_x = Vector3(1, 0, 0);
+ const Vector3 vector_y = Vector3(0, 1, 0);
+ const Vector3 vector_yz = Vector3(0, 1, 1);
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector_x.angle_to(vector_y), (real_t)Math_TAU / 4),
+ "Vector3 angle_to should work as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector_x.angle_to(vector_yz), (real_t)Math_TAU / 4),
+ "Vector3 angle_to should work as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector_yz.angle_to(vector_x), (real_t)Math_TAU / 4),
+ "Vector3 angle_to should work as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector_y.angle_to(vector_yz), (real_t)Math_TAU / 8),
+ "Vector3 angle_to should work as expected.");
+
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector_x.signed_angle_to(vector_y, vector_y), (real_t)Math_TAU / 4),
+ "Vector3 signed_angle_to edge case should be positive.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector_x.signed_angle_to(vector_yz, vector_y), (real_t)Math_TAU / -4),
+ "Vector3 signed_angle_to should work as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector_yz.signed_angle_to(vector_x, vector_y), (real_t)Math_TAU / 4),
+ "Vector3 signed_angle_to should work as expected.");
+}
+
+TEST_CASE("[Vector3] Axis methods") {
+ Vector3 vector = Vector3(1.2, 3.4, 5.6);
+ CHECK_MESSAGE(
+ vector.max_axis_index() == Vector3::Axis::AXIS_Z,
+ "Vector3 max_axis_index should work as expected.");
+ CHECK_MESSAGE(
+ vector.min_axis_index() == Vector3::Axis::AXIS_X,
+ "Vector3 min_axis_index should work as expected.");
+ CHECK_MESSAGE(
+ vector[vector.max_axis_index()] == (real_t)5.6,
+ "Vector3 array operator should work as expected.");
+ CHECK_MESSAGE(
+ vector[vector.min_axis_index()] == (real_t)1.2,
+ "Vector3 array operator should work as expected.");
+
+ vector[Vector3::Axis::AXIS_Y] = 3.7;
+ CHECK_MESSAGE(
+ vector[Vector3::Axis::AXIS_Y] == (real_t)3.7,
+ "Vector3 array operator setter should work as expected.");
+}
+
+TEST_CASE("[Vector3] Interpolation methods") {
+ const Vector3 vector1 = Vector3(1, 2, 3);
+ const Vector3 vector2 = Vector3(4, 5, 6);
+ CHECK_MESSAGE(
+ vector1.lerp(vector2, 0.5) == Vector3(2.5, 3.5, 4.5),
+ "Vector3 lerp should work as expected.");
+ CHECK_MESSAGE(
+ vector1.lerp(vector2, 1.0 / 3.0).is_equal_approx(Vector3(2, 3, 4)),
+ "Vector3 lerp should work as expected.");
+ CHECK_MESSAGE(
+ vector1.normalized().slerp(vector2.normalized(), 0.5).is_equal_approx(Vector3(0.363866806030273438, 0.555698215961456299, 0.747529566287994385)),
+ "Vector3 slerp should work as expected.");
+ CHECK_MESSAGE(
+ vector1.normalized().slerp(vector2.normalized(), 1.0 / 3.0).is_equal_approx(Vector3(0.332119762897491455, 0.549413740634918213, 0.766707837581634521)),
+ "Vector3 slerp should work as expected.");
+ CHECK_MESSAGE(
+ Vector3(5, 0, 0).slerp(Vector3(0, 3, 4), 0.5).is_equal_approx(Vector3(3.535533905029296875, 2.121320486068725586, 2.828427314758300781)),
+ "Vector3 slerp with non-normalized values should work as expected.");
+ CHECK_MESSAGE(
+ Vector3(1, 1, 1).slerp(Vector3(2, 2, 2), 0.5).is_equal_approx(Vector3(1.5, 1.5, 1.5)),
+ "Vector3 slerp with colinear inputs should behave as expected.");
+ CHECK_MESSAGE(
+ Vector3().slerp(Vector3(), 0.5) == Vector3(),
+ "Vector3 slerp with both inputs as zero vectors should return a zero vector.");
+ CHECK_MESSAGE(
+ Vector3().slerp(Vector3(1, 1, 1), 0.5) == Vector3(0.5, 0.5, 0.5),
+ "Vector3 slerp with one input as zero should behave like a regular lerp.");
+ CHECK_MESSAGE(
+ Vector3(1, 1, 1).slerp(Vector3(), 0.5) == Vector3(0.5, 0.5, 0.5),
+ "Vector3 slerp with one input as zero should behave like a regular lerp.");
+ CHECK_MESSAGE(
+ Vector3(4, 6, 2).slerp(Vector3(8, 10, 3), 0.5).is_equal_approx(Vector3(5.90194219811429941053, 8.06758688849378394534, 2.558307894718317120038)),
+ "Vector3 slerp should work as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.slerp(vector2, 0.5).length(), (real_t)6.25831088708303172),
+ "Vector3 slerp with different length input should return a vector with an interpolated length.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.angle_to(vector1.slerp(vector2, 0.5)) * 2, vector1.angle_to(vector2)),
+ "Vector3 slerp with different length input should return a vector with an interpolated angle.");
+ CHECK_MESSAGE(
+ vector1.cubic_interpolate(vector2, Vector3(), Vector3(7, 7, 7), 0.5) == Vector3(2.375, 3.5, 4.625),
+ "Vector3 cubic_interpolate should work as expected.");
+ CHECK_MESSAGE(
+ vector1.cubic_interpolate(vector2, Vector3(), Vector3(7, 7, 7), 1.0 / 3.0).is_equal_approx(Vector3(1.851851940155029297, 2.962963104248046875, 4.074074268341064453)),
+ "Vector3 cubic_interpolate should work as expected.");
+ CHECK_MESSAGE(
+ Vector3(1, 0, 0).move_toward(Vector3(10, 0, 0), 3) == Vector3(4, 0, 0),
+ "Vector3 move_toward should work as expected.");
+}
+
+TEST_CASE("[Vector3] Length methods") {
+ const Vector3 vector1 = Vector3(10, 10, 10);
+ const Vector3 vector2 = Vector3(20, 30, 40);
+ CHECK_MESSAGE(
+ vector1.length_squared() == 300,
+ "Vector3 length_squared should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.length(), 10 * (real_t)Math_SQRT3),
+ "Vector3 length should work as expected.");
+ CHECK_MESSAGE(
+ vector2.length_squared() == 2900,
+ "Vector3 length_squared should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector2.length(), (real_t)53.8516480713450403125),
+ "Vector3 length should work as expected.");
+ CHECK_MESSAGE(
+ vector1.distance_squared_to(vector2) == 1400,
+ "Vector3 distance_squared_to should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.distance_to(vector2), (real_t)37.41657386773941385584),
+ "Vector3 distance_to should work as expected.");
+}
+
+TEST_CASE("[Vector3] Limiting methods") {
+ const Vector3 vector = Vector3(10, 10, 10);
+ CHECK_MESSAGE(
+ vector.limit_length().is_equal_approx(Vector3(Math_SQRT13, Math_SQRT13, Math_SQRT13)),
+ "Vector3 limit_length should work as expected.");
+ CHECK_MESSAGE(
+ vector.limit_length(5).is_equal_approx(5 * Vector3(Math_SQRT13, Math_SQRT13, Math_SQRT13)),
+ "Vector3 limit_length should work as expected.");
+
+ CHECK_MESSAGE(
+ Vector3(-5, 5, 15).clamp(Vector3(), vector) == Vector3(0, 5, 10),
+ "Vector3 clamp should work as expected.");
+ CHECK_MESSAGE(
+ vector.clamp(Vector3(0, 10, 15), Vector3(5, 10, 20)) == Vector3(5, 10, 15),
+ "Vector3 clamp should work as expected.");
+}
+
+TEST_CASE("[Vector3] Normalization methods") {
+ CHECK_MESSAGE(
+ Vector3(1, 0, 0).is_normalized() == true,
+ "Vector3 is_normalized should return true for a normalized vector.");
+ CHECK_MESSAGE(
+ Vector3(1, 1, 1).is_normalized() == false,
+ "Vector3 is_normalized should return false for a non-normalized vector.");
+ CHECK_MESSAGE(
+ Vector3(1, 0, 0).normalized() == Vector3(1, 0, 0),
+ "Vector3 normalized should return the same vector for a normalized vector.");
+ CHECK_MESSAGE(
+ Vector3(1, 1, 0).normalized().is_equal_approx(Vector3(Math_SQRT12, Math_SQRT12, 0)),
+ "Vector3 normalized should work as expected.");
+ CHECK_MESSAGE(
+ Vector3(1, 1, 1).normalized().is_equal_approx(Vector3(Math_SQRT13, Math_SQRT13, Math_SQRT13)),
+ "Vector3 normalized should work as expected.");
+
+ Vector3 vector = Vector3(3.2, -5.4, 6);
+ vector.normalize();
+ CHECK_MESSAGE(
+ vector == Vector3(3.2, -5.4, 6).normalized(),
+ "Vector3 normalize should convert same way as Vector3 normalized.");
+ CHECK_MESSAGE(
+ vector.is_equal_approx(Vector3(0.368522751763902980457, -0.621882143601586279522, 0.6909801595573180883585)),
+ "Vector3 normalize should work as expected.");
+}
+
+TEST_CASE("[Vector3] Operators") {
+ const Vector3 decimal1 = Vector3(2.3, 4.9, 7.8);
+ const Vector3 decimal2 = Vector3(1.2, 3.4, 5.6);
+ const Vector3 power1 = Vector3(0.75, 1.5, 0.625);
+ const Vector3 power2 = Vector3(0.5, 0.125, 0.25);
+ const Vector3 int1 = Vector3(4, 5, 9);
+ const Vector3 int2 = Vector3(1, 2, 3);
+
+ CHECK_MESSAGE(
+ (decimal1 + decimal2).is_equal_approx(Vector3(3.5, 8.3, 13.4)),
+ "Vector3 addition should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 + power2) == Vector3(1.25, 1.625, 0.875),
+ "Vector3 addition with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 + int2) == Vector3(5, 7, 12),
+ "Vector3 addition with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 - decimal2).is_equal_approx(Vector3(1.1, 1.5, 2.2)),
+ "Vector3 subtraction should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 - power2) == Vector3(0.25, 1.375, 0.375),
+ "Vector3 subtraction with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 - int2) == Vector3(3, 3, 6),
+ "Vector3 subtraction with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 * decimal2).is_equal_approx(Vector3(2.76, 16.66, 43.68)),
+ "Vector3 multiplication should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 * power2) == Vector3(0.375, 0.1875, 0.15625),
+ "Vector3 multiplication with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 * int2) == Vector3(4, 10, 27),
+ "Vector3 multiplication with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 / decimal2).is_equal_approx(Vector3(1.91666666666666666, 1.44117647058823529, 1.39285714285714286)),
+ "Vector3 division should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 / power2) == Vector3(1.5, 12.0, 2.5),
+ "Vector3 division with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 / int2) == Vector3(4, 2.5, 3),
+ "Vector3 division with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 * 2).is_equal_approx(Vector3(4.6, 9.8, 15.6)),
+ "Vector3 multiplication should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 * 2) == Vector3(1.5, 3, 1.25),
+ "Vector3 multiplication with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 * 2) == Vector3(8, 10, 18),
+ "Vector3 multiplication with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 / 2).is_equal_approx(Vector3(1.15, 2.45, 3.9)),
+ "Vector3 division should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 / 2) == Vector3(0.375, 0.75, 0.3125),
+ "Vector3 division with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 / 2) == Vector3(2, 2.5, 4.5),
+ "Vector3 division with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ ((Vector3i)decimal1) == Vector3i(2, 4, 7),
+ "Vector3 cast to Vector3i should work as expected.");
+ CHECK_MESSAGE(
+ ((Vector3i)decimal2) == Vector3i(1, 3, 5),
+ "Vector3 cast to Vector3i should work as expected.");
+ CHECK_MESSAGE(
+ Vector3(Vector3i(1, 2, 3)) == Vector3(1, 2, 3),
+ "Vector3 constructed from Vector3i should work as expected.");
+
+ CHECK_MESSAGE(
+ ((String)decimal1) == "(2.3, 4.9, 7.8)",
+ "Vector3 cast to String should work as expected.");
+ CHECK_MESSAGE(
+ ((String)decimal2) == "(1.2, 3.4, 5.6)",
+ "Vector3 cast to String should work as expected.");
+ CHECK_MESSAGE(
+ ((String)Vector3(9.7, 9.8, 9.9)) == "(9.7, 9.8, 9.9)",
+ "Vector3 cast to String should work as expected.");
+#ifdef REAL_T_IS_DOUBLE
+ CHECK_MESSAGE(
+ ((String)Vector3(Math_E, Math_SQRT2, Math_SQRT3)) == "(2.71828182845905, 1.4142135623731, 1.73205080756888)",
+ "Vector3 cast to String should print the correct amount of digits for real_t = double.");
+#else
+ CHECK_MESSAGE(
+ ((String)Vector3(Math_E, Math_SQRT2, Math_SQRT3)) == "(2.718282, 1.414214, 1.732051)",
+ "Vector3 cast to String should print the correct amount of digits for real_t = float.");
+#endif // REAL_T_IS_DOUBLE
+}
+
+TEST_CASE("[Vector3] Other methods") {
+ const Vector3 vector = Vector3(1.2, 3.4, 5.6);
+ CHECK_MESSAGE(
+ vector.direction_to(Vector3()).is_equal_approx(-vector.normalized()),
+ "Vector3 direction_to should work as expected.");
+ CHECK_MESSAGE(
+ Vector3(1, 1, 1).direction_to(Vector3(2, 2, 2)).is_equal_approx(Vector3(Math_SQRT13, Math_SQRT13, Math_SQRT13)),
+ "Vector3 direction_to should work as expected.");
+ CHECK_MESSAGE(
+ vector.inverse().is_equal_approx(Vector3(1 / 1.2, 1 / 3.4, 1 / 5.6)),
+ "Vector3 inverse should work as expected.");
+ CHECK_MESSAGE(
+ vector.posmod(2).is_equal_approx(Vector3(1.2, 1.4, 1.6)),
+ "Vector3 posmod should work as expected.");
+ CHECK_MESSAGE(
+ (-vector).posmod(2).is_equal_approx(Vector3(0.8, 0.6, 0.4)),
+ "Vector3 posmod should work as expected.");
+ CHECK_MESSAGE(
+ vector.posmodv(Vector3(1, 2, 3)).is_equal_approx(Vector3(0.2, 1.4, 2.6)),
+ "Vector3 posmodv should work as expected.");
+ CHECK_MESSAGE(
+ (-vector).posmodv(Vector3(2, 3, 4)).is_equal_approx(Vector3(0.8, 2.6, 2.4)),
+ "Vector3 posmodv should work as expected.");
+
+ CHECK_MESSAGE(
+ vector.rotated(Vector3(0, 1, 0), Math_TAU).is_equal_approx(vector),
+ "Vector3 rotated should work as expected.");
+ CHECK_MESSAGE(
+ vector.rotated(Vector3(0, 1, 0), Math_TAU / 4).is_equal_approx(Vector3(5.6, 3.4, -1.2)),
+ "Vector3 rotated should work as expected.");
+ CHECK_MESSAGE(
+ vector.rotated(Vector3(1, 0, 0), Math_TAU / 3).is_equal_approx(Vector3(1.2, -6.54974226119285642, 0.1444863728670914)),
+ "Vector3 rotated should work as expected.");
+ CHECK_MESSAGE(
+ vector.rotated(Vector3(0, 0, 1), Math_TAU / 2).is_equal_approx(vector.rotated(Vector3(0, 0, 1), Math_TAU / -2)),
+ "Vector3 rotated should work as expected.");
+
+ CHECK_MESSAGE(
+ vector.snapped(Vector3(1, 1, 1)) == Vector3(1, 3, 6),
+ "Vector3 snapped to integers should be the same as rounding.");
+ CHECK_MESSAGE(
+ vector.snapped(Vector3(0.25, 0.25, 0.25)) == Vector3(1.25, 3.5, 5.5),
+ "Vector3 snapped to 0.25 should give exact results.");
+}
+
+TEST_CASE("[Vector3] Plane methods") {
+ const Vector3 vector = Vector3(1.2, 3.4, 5.6);
+ const Vector3 vector_y = Vector3(0, 1, 0);
+ const Vector3 vector_normal = Vector3(0.88763458893247992491, 0.26300284116517923701, 0.37806658417494515320);
+ const Vector3 vector_non_normal = Vector3(5.4, 1.6, 2.3);
+ CHECK_MESSAGE(
+ vector.bounce(vector_y) == Vector3(1.2, -3.4, 5.6),
+ "Vector3 bounce on a plane with normal of the Y axis should.");
+ CHECK_MESSAGE(
+ vector.bounce(vector_normal).is_equal_approx(Vector3(-6.0369629829775736287, 1.25571467171034855444, 2.517589840583626047)),
+ "Vector3 bounce with normal should return expected value.");
+ CHECK_MESSAGE(
+ vector.reflect(vector_y) == Vector3(-1.2, 3.4, -5.6),
+ "Vector3 reflect on a plane with normal of the Y axis should.");
+ CHECK_MESSAGE(
+ vector.reflect(vector_normal).is_equal_approx(Vector3(6.0369629829775736287, -1.25571467171034855444, -2.517589840583626047)),
+ "Vector3 reflect with normal should return expected value.");
+ CHECK_MESSAGE(
+ vector.project(vector_y) == Vector3(0, 3.4, 0),
+ "Vector3 projected on the Y axis should only give the Y component.");
+ CHECK_MESSAGE(
+ vector.project(vector_normal).is_equal_approx(Vector3(3.61848149148878681437, 1.0721426641448257227776, 1.54120507970818697649)),
+ "Vector3 projected on a normal should return expected value.");
+ CHECK_MESSAGE(
+ vector.slide(vector_y) == Vector3(1.2, 0, 5.6),
+ "Vector3 slide on a plane with normal of the Y axis should set the Y to zero.");
+ CHECK_MESSAGE(
+ vector.slide(vector_normal).is_equal_approx(Vector3(-2.41848149148878681437, 2.32785733585517427722237, 4.0587949202918130235)),
+ "Vector3 slide with normal should return expected value.");
+ // There's probably a better way to test these ones?
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ vector.bounce(vector_non_normal).is_equal_approx(Vector3()),
+ "Vector3 bounce should return empty Vector3 with non-normalised input.");
+ CHECK_MESSAGE(
+ vector.reflect(vector_non_normal).is_equal_approx(Vector3()),
+ "Vector3 reflect should return empty Vector3 with non-normalised input.");
+ CHECK_MESSAGE(
+ vector.slide(vector_non_normal).is_equal_approx(Vector3()),
+ "Vector3 slide should return empty Vector3 with non-normalised input.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[Vector3] Rounding methods") {
+ const Vector3 vector1 = Vector3(1.2, 3.4, 5.6);
+ const Vector3 vector2 = Vector3(1.2, -3.4, -5.6);
+ CHECK_MESSAGE(
+ vector1.abs() == vector1,
+ "Vector3 abs should work as expected.");
+ CHECK_MESSAGE(
+ vector2.abs() == vector1,
+ "Vector3 abs should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.ceil() == Vector3(2, 4, 6),
+ "Vector3 ceil should work as expected.");
+ CHECK_MESSAGE(
+ vector2.ceil() == Vector3(2, -3, -5),
+ "Vector3 ceil should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.floor() == Vector3(1, 3, 5),
+ "Vector3 floor should work as expected.");
+ CHECK_MESSAGE(
+ vector2.floor() == Vector3(1, -4, -6),
+ "Vector3 floor should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.round() == Vector3(1, 3, 6),
+ "Vector3 round should work as expected.");
+ CHECK_MESSAGE(
+ vector2.round() == Vector3(1, -3, -6),
+ "Vector3 round should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.sign() == Vector3(1, 1, 1),
+ "Vector3 sign should work as expected.");
+ CHECK_MESSAGE(
+ vector2.sign() == Vector3(1, -1, -1),
+ "Vector3 sign should work as expected.");
+}
+
+TEST_CASE("[Vector3] Linear algebra methods") {
+ const Vector3 vector_x = Vector3(1, 0, 0);
+ const Vector3 vector_y = Vector3(0, 1, 0);
+ const Vector3 vector_z = Vector3(0, 0, 1);
+ const Vector3 a = Vector3(3.5, 8.5, 2.3);
+ const Vector3 b = Vector3(5.2, 4.6, 7.8);
+ CHECK_MESSAGE(
+ vector_x.cross(vector_y) == vector_z,
+ "Vector3 cross product of X and Y should give Z.");
+ CHECK_MESSAGE(
+ vector_y.cross(vector_x) == -vector_z,
+ "Vector3 cross product of Y and X should give negative Z.");
+ CHECK_MESSAGE(
+ vector_y.cross(vector_z) == vector_x,
+ "Vector3 cross product of Y and Z should give X.");
+ CHECK_MESSAGE(
+ vector_z.cross(vector_x) == vector_y,
+ "Vector3 cross product of Z and X should give Y.");
+ CHECK_MESSAGE(
+ a.cross(b).is_equal_approx(Vector3(55.72, -15.34, -28.1)),
+ "Vector3 cross should return expected value.");
+ CHECK_MESSAGE(
+ Vector3(-a.x, a.y, -a.z).cross(Vector3(b.x, -b.y, b.z)).is_equal_approx(Vector3(55.72, 15.34, -28.1)),
+ "Vector2 cross should return expected value.");
+
+ CHECK_MESSAGE(
+ vector_x.dot(vector_y) == 0.0,
+ "Vector3 dot product of perpendicular vectors should be zero.");
+ CHECK_MESSAGE(
+ vector_x.dot(vector_x) == 1.0,
+ "Vector3 dot product of identical unit vectors should be one.");
+ CHECK_MESSAGE(
+ (vector_x * 10).dot(vector_x * 10) == 100.0,
+ "Vector3 dot product of same direction vectors should behave as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(a.dot(b), (real_t)75.24),
+ "Vector3 dot should return expected value.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Vector3(-a.x, a.y, -a.z).dot(Vector3(b.x, -b.y, b.z)), (real_t)-75.24),
+ "Vector3 dot should return expected value.");
+}
+
+TEST_CASE("[Vector3] Finite number checks") {
+ const double infinite[] = { NAN, INFINITY, -INFINITY };
+
+ CHECK_MESSAGE(
+ Vector3(0, 1, 2).is_finite(),
+ "Vector3(0, 1, 2) should be finite");
+
+ for (double x : infinite) {
+ CHECK_FALSE_MESSAGE(
+ Vector3(x, 1, 2).is_finite(),
+ "Vector3 with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector3(0, x, 2).is_finite(),
+ "Vector3 with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector3(0, 1, x).is_finite(),
+ "Vector3 with one component infinite should not be finite.");
+ }
+
+ for (double x : infinite) {
+ for (double y : infinite) {
+ CHECK_FALSE_MESSAGE(
+ Vector3(x, y, 2).is_finite(),
+ "Vector3 with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector3(x, 1, y).is_finite(),
+ "Vector3 with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector3(0, x, y).is_finite(),
+ "Vector3 with two components infinite should not be finite.");
+ }
+ }
+
+ for (double x : infinite) {
+ for (double y : infinite) {
+ for (double z : infinite) {
+ CHECK_FALSE_MESSAGE(
+ Vector3(x, y, z).is_finite(),
+ "Vector3 with three components infinite should not be finite.");
+ }
+ }
+ }
+}
+
+} // namespace TestVector3
+
+#endif // TEST_VECTOR3_H
diff --git a/tests/core/math/test_vector3i.h b/tests/core/math/test_vector3i.h
new file mode 100644
index 0000000000..2050b222d0
--- /dev/null
+++ b/tests/core/math/test_vector3i.h
@@ -0,0 +1,149 @@
+/*************************************************************************/
+/* test_vector3i.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_VECTOR3I_H
+#define TEST_VECTOR3I_H
+
+#include "core/math/vector3i.h"
+#include "tests/test_macros.h"
+
+namespace TestVector3i {
+
+TEST_CASE("[Vector3i] Constructor methods") {
+ const Vector3i vector_empty = Vector3i();
+ const Vector3i vector_zero = Vector3i(0, 0, 0);
+ CHECK_MESSAGE(
+ vector_empty == vector_zero,
+ "Vector3i Constructor with no inputs should return a zero Vector3i.");
+}
+
+TEST_CASE("[Vector3i] Axis methods") {
+ Vector3i vector = Vector3i(1, 2, 3);
+ CHECK_MESSAGE(
+ vector.max_axis_index() == Vector3i::Axis::AXIS_Z,
+ "Vector3i max_axis_index should work as expected.");
+ CHECK_MESSAGE(
+ vector.min_axis_index() == Vector3i::Axis::AXIS_X,
+ "Vector3i min_axis_index should work as expected.");
+ CHECK_MESSAGE(
+ vector[vector.max_axis_index()] == 3,
+ "Vector3i array operator should work as expected.");
+ CHECK_MESSAGE(
+ vector[vector.min_axis_index()] == 1,
+ "Vector3i array operator should work as expected.");
+
+ vector[Vector3i::Axis::AXIS_Y] = 5;
+ CHECK_MESSAGE(
+ vector[Vector3i::Axis::AXIS_Y] == 5,
+ "Vector3i array operator setter should work as expected.");
+}
+
+TEST_CASE("[Vector3i] Clamp method") {
+ const Vector3i vector = Vector3i(10, 10, 10);
+ CHECK_MESSAGE(
+ Vector3i(-5, 5, 15).clamp(Vector3i(), vector) == Vector3i(0, 5, 10),
+ "Vector3i clamp should work as expected.");
+ CHECK_MESSAGE(
+ vector.clamp(Vector3i(0, 10, 15), Vector3i(5, 10, 20)) == Vector3i(5, 10, 15),
+ "Vector3i clamp should work as expected.");
+}
+
+TEST_CASE("[Vector3i] Length methods") {
+ const Vector3i vector1 = Vector3i(10, 10, 10);
+ const Vector3i vector2 = Vector3i(20, 30, 40);
+ CHECK_MESSAGE(
+ vector1.length_squared() == 300,
+ "Vector3i length_squared should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.length(), 10 * Math_SQRT3),
+ "Vector3i length should work as expected.");
+ CHECK_MESSAGE(
+ vector2.length_squared() == 2900,
+ "Vector3i length_squared should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector2.length(), 53.8516480713450403125),
+ "Vector3i length should work as expected.");
+}
+
+TEST_CASE("[Vector3i] Operators") {
+ const Vector3i vector1 = Vector3i(4, 5, 9);
+ const Vector3i vector2 = Vector3i(1, 2, 3);
+
+ CHECK_MESSAGE(
+ (vector1 + vector2) == Vector3i(5, 7, 12),
+ "Vector3i addition with integers should give exact results.");
+ CHECK_MESSAGE(
+ (vector1 - vector2) == Vector3i(3, 3, 6),
+ "Vector3i subtraction with integers should give exact results.");
+ CHECK_MESSAGE(
+ (vector1 * vector2) == Vector3i(4, 10, 27),
+ "Vector3i multiplication with integers should give exact results.");
+ CHECK_MESSAGE(
+ (vector1 / vector2) == Vector3i(4, 2, 3),
+ "Vector3i division with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (vector1 * 2) == Vector3i(8, 10, 18),
+ "Vector3i multiplication with integers should give exact results.");
+ CHECK_MESSAGE(
+ (vector1 / 2) == Vector3i(2, 2, 4),
+ "Vector3i division with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ ((Vector3)vector1) == Vector3(4, 5, 9),
+ "Vector3i cast to Vector3 should work as expected.");
+ CHECK_MESSAGE(
+ ((Vector3)vector2) == Vector3(1, 2, 3),
+ "Vector3i cast to Vector3 should work as expected.");
+ CHECK_MESSAGE(
+ Vector3i(Vector3(1.1, 2.9, 3.9)) == Vector3i(1, 2, 3),
+ "Vector3i constructed from Vector3 should work as expected.");
+}
+
+TEST_CASE("[Vector3i] Abs and sign methods") {
+ const Vector3i vector1 = Vector3i(1, 3, 5);
+ const Vector3i vector2 = Vector3i(1, -3, -5);
+ CHECK_MESSAGE(
+ vector1.abs() == vector1,
+ "Vector3i abs should work as expected.");
+ CHECK_MESSAGE(
+ vector2.abs() == vector1,
+ "Vector3i abs should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.sign() == Vector3i(1, 1, 1),
+ "Vector3i sign should work as expected.");
+ CHECK_MESSAGE(
+ vector2.sign() == Vector3i(1, -1, -1),
+ "Vector3i sign should work as expected.");
+}
+} // namespace TestVector3i
+
+#endif // TEST_VECTOR3I_H
diff --git a/tests/core/math/test_vector4.h b/tests/core/math/test_vector4.h
new file mode 100644
index 0000000000..b31db56f67
--- /dev/null
+++ b/tests/core/math/test_vector4.h
@@ -0,0 +1,397 @@
+/*************************************************************************/
+/* test_vector4.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_VECTOR4_H
+#define TEST_VECTOR4_H
+
+#include "core/math/vector4.h"
+#include "tests/test_macros.h"
+
+#define Math_SQRT3 1.7320508075688772935274463415059
+
+namespace TestVector4 {
+
+TEST_CASE("[Vector4] Constructor methods") {
+ const Vector4 vector_empty = Vector4();
+ const Vector4 vector_zero = Vector4(0.0, 0.0, 0.0, 0.0);
+ CHECK_MESSAGE(
+ vector_empty == vector_zero,
+ "Vector4 Constructor with no inputs should return a zero Vector4.");
+}
+
+TEST_CASE("[Vector4] Axis methods") {
+ Vector4 vector = Vector4(1.2, 3.4, 5.6, -0.9);
+ CHECK_MESSAGE(
+ vector.max_axis_index() == Vector4::Axis::AXIS_Z,
+ "Vector4 max_axis_index should work as expected.");
+ CHECK_MESSAGE(
+ vector.min_axis_index() == Vector4::Axis::AXIS_W,
+ "Vector4 min_axis_index should work as expected.");
+ CHECK_MESSAGE(
+ vector[vector.max_axis_index()] == (real_t)5.6,
+ "Vector4 array operator should work as expected.");
+ CHECK_MESSAGE(
+ vector[vector.min_axis_index()] == (real_t)-0.9,
+ "Vector4 array operator should work as expected.");
+
+ vector[Vector4::Axis::AXIS_Y] = 3.7;
+ CHECK_MESSAGE(
+ vector[Vector4::Axis::AXIS_Y] == (real_t)3.7,
+ "Vector4 array operator setter should work as expected.");
+}
+
+TEST_CASE("[Vector4] Interpolation methods") {
+ const Vector4 vector1 = Vector4(1, 2, 3, 4);
+ const Vector4 vector2 = Vector4(4, 5, 6, 7);
+ CHECK_MESSAGE(
+ vector1.lerp(vector2, 0.5) == Vector4(2.5, 3.5, 4.5, 5.5),
+ "Vector4 lerp should work as expected.");
+ CHECK_MESSAGE(
+ vector1.lerp(vector2, 1.0 / 3.0).is_equal_approx(Vector4(2, 3, 4, 5)),
+ "Vector4 lerp should work as expected.");
+ CHECK_MESSAGE(
+ vector1.cubic_interpolate(vector2, Vector4(), Vector4(7, 7, 7, 7), 0.5) == Vector4(2.375, 3.5, 4.625, 5.75),
+ "Vector4 cubic_interpolate should work as expected.");
+ CHECK_MESSAGE(
+ vector1.cubic_interpolate(vector2, Vector4(), Vector4(7, 7, 7, 7), 1.0 / 3.0).is_equal_approx(Vector4(1.851851940155029297, 2.962963104248046875, 4.074074268341064453, 5.185185185185)),
+ "Vector4 cubic_interpolate should work as expected.");
+}
+
+TEST_CASE("[Vector4] Length methods") {
+ const Vector4 vector1 = Vector4(10, 10, 10, 10);
+ const Vector4 vector2 = Vector4(20, 30, 40, 50);
+ CHECK_MESSAGE(
+ vector1.length_squared() == 400,
+ "Vector4 length_squared should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.length(), 20),
+ "Vector4 length should work as expected.");
+ CHECK_MESSAGE(
+ vector2.length_squared() == 5400,
+ "Vector4 length_squared should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector2.length(), (real_t)73.484692283495),
+ "Vector4 length should work as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.distance_to(vector2), (real_t)54.772255750517),
+ "Vector4 distance_to should work as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.distance_squared_to(vector2), 3000),
+ "Vector4 distance_squared_to should work as expected.");
+}
+
+TEST_CASE("[Vector4] Limiting methods") {
+ const Vector4 vector = Vector4(10, 10, 10, 10);
+ CHECK_MESSAGE(
+ Vector4(-5, 5, 15, -15).clamp(Vector4(), vector) == Vector4(0, 5, 10, 0),
+ "Vector4 clamp should work as expected.");
+ CHECK_MESSAGE(
+ vector.clamp(Vector4(0, 10, 15, 18), Vector4(5, 10, 20, 25)) == Vector4(5, 10, 15, 18),
+ "Vector4 clamp should work as expected.");
+}
+
+TEST_CASE("[Vector4] Normalization methods") {
+ CHECK_MESSAGE(
+ Vector4(1, 0, 0, 0).is_normalized() == true,
+ "Vector4 is_normalized should return true for a normalized vector.");
+ CHECK_MESSAGE(
+ Vector4(1, 1, 1, 1).is_normalized() == false,
+ "Vector4 is_normalized should return false for a non-normalized vector.");
+ CHECK_MESSAGE(
+ Vector4(1, 0, 0, 0).normalized() == Vector4(1, 0, 0, 0),
+ "Vector4 normalized should return the same vector for a normalized vector.");
+ CHECK_MESSAGE(
+ Vector4(1, 1, 0, 0).normalized().is_equal_approx(Vector4(Math_SQRT12, Math_SQRT12, 0, 0)),
+ "Vector4 normalized should work as expected.");
+ CHECK_MESSAGE(
+ Vector4(1, 1, 1, 1).normalized().is_equal_approx(Vector4(0.5, 0.5, 0.5, 0.5)),
+ "Vector4 normalized should work as expected.");
+}
+
+TEST_CASE("[Vector4] Operators") {
+ const Vector4 decimal1 = Vector4(2.3, 4.9, 7.8, 3.2);
+ const Vector4 decimal2 = Vector4(1.2, 3.4, 5.6, 1.7);
+ const Vector4 power1 = Vector4(0.75, 1.5, 0.625, 0.125);
+ const Vector4 power2 = Vector4(0.5, 0.125, 0.25, 0.75);
+ const Vector4 int1 = Vector4(4, 5, 9, 2);
+ const Vector4 int2 = Vector4(1, 2, 3, 1);
+
+ CHECK_MESSAGE(
+ -decimal1 == Vector4(-2.3, -4.9, -7.8, -3.2),
+ "Vector4 change of sign should work as expected.");
+ CHECK_MESSAGE(
+ (decimal1 + decimal2).is_equal_approx(Vector4(3.5, 8.3, 13.4, 4.9)),
+ "Vector4 addition should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 + power2) == Vector4(1.25, 1.625, 0.875, 0.875),
+ "Vector4 addition with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 + int2) == Vector4(5, 7, 12, 3),
+ "Vector4 addition with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 - decimal2).is_equal_approx(Vector4(1.1, 1.5, 2.2, 1.5)),
+ "Vector4 subtraction should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 - power2) == Vector4(0.25, 1.375, 0.375, -0.625),
+ "Vector4 subtraction with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 - int2) == Vector4(3, 3, 6, 1),
+ "Vector4 subtraction with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 * decimal2).is_equal_approx(Vector4(2.76, 16.66, 43.68, 5.44)),
+ "Vector4 multiplication should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 * power2) == Vector4(0.375, 0.1875, 0.15625, 0.09375),
+ "Vector4 multiplication with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 * int2) == Vector4(4, 10, 27, 2),
+ "Vector4 multiplication with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 / decimal2).is_equal_approx(Vector4(1.91666666666666666, 1.44117647058823529, 1.39285714285714286, 1.88235294118)),
+ "Vector4 division should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 / power2) == Vector4(1.5, 12.0, 2.5, 1.0 / 6.0),
+ "Vector4 division with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 / int2) == Vector4(4, 2.5, 3, 2),
+ "Vector4 division with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 * 2).is_equal_approx(Vector4(4.6, 9.8, 15.6, 6.4)),
+ "Vector4 multiplication should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 * 2) == Vector4(1.5, 3, 1.25, 0.25),
+ "Vector4 multiplication with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 * 2) == Vector4(8, 10, 18, 4),
+ "Vector4 multiplication with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (decimal1 / 2).is_equal_approx(Vector4(1.15, 2.45, 3.9, 1.6)),
+ "Vector4 division should behave as expected.");
+ CHECK_MESSAGE(
+ (power1 / 2) == Vector4(0.375, 0.75, 0.3125, 0.0625),
+ "Vector4 division with powers of two should give exact results.");
+ CHECK_MESSAGE(
+ (int1 / 2) == Vector4(2, 2.5, 4.5, 1),
+ "Vector4 division with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ ((String)decimal1) == "(2.3, 4.9, 7.8, 3.2)",
+ "Vector4 cast to String should work as expected.");
+ CHECK_MESSAGE(
+ ((String)decimal2) == "(1.2, 3.4, 5.6, 1.7)",
+ "Vector4 cast to String should work as expected.");
+ CHECK_MESSAGE(
+ ((String)Vector4(9.7, 9.8, 9.9, -1.8)) == "(9.7, 9.8, 9.9, -1.8)",
+ "Vector4 cast to String should work as expected.");
+#ifdef REAL_T_IS_DOUBLE
+ CHECK_MESSAGE(
+ ((String)Vector4(Math_E, Math_SQRT2, Math_SQRT3, Math_SQRT3)) == "(2.71828182845905, 1.4142135623731, 1.73205080756888, 1.73205080756888)",
+ "Vector4 cast to String should print the correct amount of digits for real_t = double.");
+#else
+ CHECK_MESSAGE(
+ ((String)Vector4(Math_E, Math_SQRT2, Math_SQRT3, Math_SQRT3)) == "(2.718282, 1.414214, 1.732051, 1.732051)",
+ "Vector4 cast to String should print the correct amount of digits for real_t = float.");
+#endif // REAL_T_IS_DOUBLE
+}
+
+TEST_CASE("[Vector4] Other methods") {
+ const Vector4 vector = Vector4(1.2, 3.4, 5.6, 1.6);
+ CHECK_MESSAGE(
+ vector.direction_to(Vector4()).is_equal_approx(-vector.normalized()),
+ "Vector4 direction_to should work as expected.");
+ CHECK_MESSAGE(
+ Vector4(1, 1, 1, 1).direction_to(Vector4(2, 2, 2, 2)).is_equal_approx(Vector4(0.5, 0.5, 0.5, 0.5)),
+ "Vector4 direction_to should work as expected.");
+ CHECK_MESSAGE(
+ vector.inverse().is_equal_approx(Vector4(1 / 1.2, 1 / 3.4, 1 / 5.6, 1 / 1.6)),
+ "Vector4 inverse should work as expected.");
+ CHECK_MESSAGE(
+ vector.posmod(2).is_equal_approx(Vector4(1.2, 1.4, 1.6, 1.6)),
+ "Vector4 posmod should work as expected.");
+ CHECK_MESSAGE(
+ (-vector).posmod(2).is_equal_approx(Vector4(0.8, 0.6, 0.4, 0.4)),
+ "Vector4 posmod should work as expected.");
+ CHECK_MESSAGE(
+ vector.posmodv(Vector4(1, 2, 3, 4)).is_equal_approx(Vector4(0.2, 1.4, 2.6, 1.6)),
+ "Vector4 posmodv should work as expected.");
+ CHECK_MESSAGE(
+ (-vector).posmodv(Vector4(2, 3, 4, 5)).is_equal_approx(Vector4(0.8, 2.6, 2.4, 3.4)),
+ "Vector4 posmodv should work as expected.");
+ CHECK_MESSAGE(
+ vector.snapped(Vector4(1, 1, 1, 1)) == Vector4(1, 3, 6, 2),
+ "Vector4 snapped to integers should be the same as rounding.");
+ CHECK_MESSAGE(
+ vector.snapped(Vector4(0.25, 0.25, 0.25, 0.25)) == Vector4(1.25, 3.5, 5.5, 1.5),
+ "Vector4 snapped to 0.25 should give exact results.");
+}
+
+TEST_CASE("[Vector4] Rounding methods") {
+ const Vector4 vector1 = Vector4(1.2, 3.4, 5.6, 1.6);
+ const Vector4 vector2 = Vector4(1.2, -3.4, -5.6, -1.6);
+ CHECK_MESSAGE(
+ vector1.abs() == vector1,
+ "Vector4 abs should work as expected.");
+ CHECK_MESSAGE(
+ vector2.abs() == vector1,
+ "Vector4 abs should work as expected.");
+ CHECK_MESSAGE(
+ vector1.ceil() == Vector4(2, 4, 6, 2),
+ "Vector4 ceil should work as expected.");
+ CHECK_MESSAGE(
+ vector2.ceil() == Vector4(2, -3, -5, -1),
+ "Vector4 ceil should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.floor() == Vector4(1, 3, 5, 1),
+ "Vector4 floor should work as expected.");
+ CHECK_MESSAGE(
+ vector2.floor() == Vector4(1, -4, -6, -2),
+ "Vector4 floor should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.round() == Vector4(1, 3, 6, 2),
+ "Vector4 round should work as expected.");
+ CHECK_MESSAGE(
+ vector2.round() == Vector4(1, -3, -6, -2),
+ "Vector4 round should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.sign() == Vector4(1, 1, 1, 1),
+ "Vector4 sign should work as expected.");
+ CHECK_MESSAGE(
+ vector2.sign() == Vector4(1, -1, -1, -1),
+ "Vector4 sign should work as expected.");
+}
+
+TEST_CASE("[Vector4] Linear algebra methods") {
+ const Vector4 vector_x = Vector4(1, 0, 0, 0);
+ const Vector4 vector_y = Vector4(0, 1, 0, 0);
+ const Vector4 vector1 = Vector4(1.7, 2.3, 1, 9.1);
+ const Vector4 vector2 = Vector4(-8.2, -16, 3, 2.4);
+
+ CHECK_MESSAGE(
+ vector_x.dot(vector_y) == 0.0,
+ "Vector4 dot product of perpendicular vectors should be zero.");
+ CHECK_MESSAGE(
+ vector_x.dot(vector_x) == 1.0,
+ "Vector4 dot product of identical unit vectors should be one.");
+ CHECK_MESSAGE(
+ (vector_x * 10).dot(vector_x * 10) == 100.0,
+ "Vector4 dot product of same direction vectors should behave as expected.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx((vector1 * 2).dot(vector2 * 4), (real_t)-25.9 * 8),
+ "Vector4 dot product should work as expected.");
+}
+
+TEST_CASE("[Vector4] Finite number checks") {
+ const double infinite[] = { NAN, INFINITY, -INFINITY };
+
+ CHECK_MESSAGE(
+ Vector4(0, 1, 2, 3).is_finite(),
+ "Vector4(0, 1, 2, 3) should be finite");
+
+ for (double x : infinite) {
+ CHECK_FALSE_MESSAGE(
+ Vector4(x, 1, 2, 3).is_finite(),
+ "Vector4 with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector4(0, x, 2, 3).is_finite(),
+ "Vector4 with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector4(0, 1, x, 3).is_finite(),
+ "Vector4 with one component infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector4(0, 1, 2, x).is_finite(),
+ "Vector4 with one component infinite should not be finite.");
+ }
+
+ for (double x : infinite) {
+ for (double y : infinite) {
+ CHECK_FALSE_MESSAGE(
+ Vector4(x, y, 2, 3).is_finite(),
+ "Vector4 with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector4(x, 1, y, 3).is_finite(),
+ "Vector4 with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector4(x, 1, 2, y).is_finite(),
+ "Vector4 with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector4(0, x, y, 3).is_finite(),
+ "Vector4 with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector4(0, x, 2, y).is_finite(),
+ "Vector4 with two components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector4(0, 1, x, y).is_finite(),
+ "Vector4 with two components infinite should not be finite.");
+ }
+ }
+
+ for (double x : infinite) {
+ for (double y : infinite) {
+ for (double z : infinite) {
+ CHECK_FALSE_MESSAGE(
+ Vector4(0, x, y, z).is_finite(),
+ "Vector4 with three components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector4(x, 1, y, z).is_finite(),
+ "Vector4 with three components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector4(x, y, 2, z).is_finite(),
+ "Vector4 with three components infinite should not be finite.");
+ CHECK_FALSE_MESSAGE(
+ Vector4(x, y, z, 3).is_finite(),
+ "Vector4 with three components infinite should not be finite.");
+ }
+ }
+ }
+
+ for (double x : infinite) {
+ for (double y : infinite) {
+ for (double z : infinite) {
+ for (double w : infinite) {
+ CHECK_FALSE_MESSAGE(
+ Vector4(x, y, z, w).is_finite(),
+ "Vector4 with four components infinite should not be finite.");
+ }
+ }
+ }
+ }
+}
+
+} // namespace TestVector4
+
+#endif // TEST_VECTOR4_H
diff --git a/tests/core/math/test_vector4i.h b/tests/core/math/test_vector4i.h
new file mode 100644
index 0000000000..309162c3f7
--- /dev/null
+++ b/tests/core/math/test_vector4i.h
@@ -0,0 +1,152 @@
+/*************************************************************************/
+/* test_vector4i.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_VECTOR4I_H
+#define TEST_VECTOR4I_H
+
+#include "core/math/vector4i.h"
+#include "tests/test_macros.h"
+
+namespace TestVector4i {
+
+TEST_CASE("[Vector4i] Constructor methods") {
+ const Vector4i vector_empty = Vector4i();
+ const Vector4i vector_zero = Vector4i(0, 0, 0, 0);
+ CHECK_MESSAGE(
+ vector_empty == vector_zero,
+ "Vector4i Constructor with no inputs should return a zero Vector4i.");
+}
+
+TEST_CASE("[Vector4i] Axis methods") {
+ Vector4i vector = Vector4i(1, 2, 3, 4);
+ CHECK_MESSAGE(
+ vector.max_axis_index() == Vector4i::Axis::AXIS_W,
+ "Vector4i max_axis_index should work as expected.");
+ CHECK_MESSAGE(
+ vector.min_axis_index() == Vector4i::Axis::AXIS_X,
+ "Vector4i min_axis_index should work as expected.");
+ CHECK_MESSAGE(
+ vector[vector.max_axis_index()] == 4,
+ "Vector4i array operator should work as expected.");
+ CHECK_MESSAGE(
+ vector[vector.min_axis_index()] == 1,
+ "Vector4i array operator should work as expected.");
+
+ vector[Vector4i::Axis::AXIS_Y] = 5;
+ CHECK_MESSAGE(
+ vector[Vector4i::Axis::AXIS_Y] == 5,
+ "Vector4i array operator setter should work as expected.");
+}
+
+TEST_CASE("[Vector4i] Clamp method") {
+ const Vector4i vector = Vector4i(10, 10, 10, 10);
+ CHECK_MESSAGE(
+ Vector4i(-5, 5, 15, INT_MAX).clamp(Vector4i(), vector) == Vector4i(0, 5, 10, 10),
+ "Vector4i clamp should work as expected.");
+ CHECK_MESSAGE(
+ vector.clamp(Vector4i(0, 10, 15, -10), Vector4i(5, 10, 20, -5)) == Vector4i(5, 10, 15, -5),
+ "Vector4i clamp should work as expected.");
+}
+
+TEST_CASE("[Vector4i] Length methods") {
+ const Vector4i vector1 = Vector4i(10, 10, 10, 10);
+ const Vector4i vector2 = Vector4i(20, 30, 40, 50);
+ CHECK_MESSAGE(
+ vector1.length_squared() == 400,
+ "Vector4i length_squared should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector1.length(), 20),
+ "Vector4i length should work as expected.");
+ CHECK_MESSAGE(
+ vector2.length_squared() == 5400,
+ "Vector4i length_squared should work as expected and return exact result.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(vector2.length(), 73.4846922835),
+ "Vector4i length should work as expected.");
+}
+
+TEST_CASE("[Vector4i] Operators") {
+ const Vector4i vector1 = Vector4i(4, 5, 9, 2);
+ const Vector4i vector2 = Vector4i(1, 2, 3, 4);
+
+ CHECK_MESSAGE(
+ -vector1 == Vector4i(-4, -5, -9, -2),
+ "Vector4i change of sign should work as expected.");
+ CHECK_MESSAGE(
+ (vector1 + vector2) == Vector4i(5, 7, 12, 6),
+ "Vector4i addition with integers should give exact results.");
+ CHECK_MESSAGE(
+ (vector1 - vector2) == Vector4i(3, 3, 6, -2),
+ "Vector4i subtraction with integers should give exact results.");
+ CHECK_MESSAGE(
+ (vector1 * vector2) == Vector4i(4, 10, 27, 8),
+ "Vector4i multiplication with integers should give exact results.");
+ CHECK_MESSAGE(
+ (vector1 / vector2) == Vector4i(4, 2, 3, 0),
+ "Vector4i division with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ (vector1 * 2) == Vector4i(8, 10, 18, 4),
+ "Vector4i multiplication with integers should give exact results.");
+ CHECK_MESSAGE(
+ (vector1 / 2) == Vector4i(2, 2, 4, 1),
+ "Vector4i division with integers should give exact results.");
+
+ CHECK_MESSAGE(
+ ((Vector4)vector1) == Vector4(4, 5, 9, 2),
+ "Vector4i cast to Vector4 should work as expected.");
+ CHECK_MESSAGE(
+ ((Vector4)vector2) == Vector4(1, 2, 3, 4),
+ "Vector4i cast to Vector4 should work as expected.");
+ CHECK_MESSAGE(
+ Vector4i(Vector4(1.1, 2.9, 3.9, 100.5)) == Vector4i(1, 2, 3, 100),
+ "Vector4i constructed from Vector4 should work as expected.");
+}
+
+TEST_CASE("[Vector4i] Abs and sign methods") {
+ const Vector4i vector1 = Vector4i(1, 3, 5, 7);
+ const Vector4i vector2 = Vector4i(1, -3, -5, 7);
+ CHECK_MESSAGE(
+ vector1.abs() == vector1,
+ "Vector4i abs should work as expected.");
+ CHECK_MESSAGE(
+ vector2.abs() == vector1,
+ "Vector4i abs should work as expected.");
+
+ CHECK_MESSAGE(
+ vector1.sign() == Vector4i(1, 1, 1, 1),
+ "Vector4i sign should work as expected.");
+ CHECK_MESSAGE(
+ vector2.sign() == Vector4i(1, -1, -1, 1),
+ "Vector4i sign should work as expected.");
+}
+} // namespace TestVector4i
+
+#endif // TEST_VECTOR4I_H
diff --git a/tests/test_class_db.h b/tests/core/object/test_class_db.h
index d0d8136874..b0375c63b9 100644
--- a/tests/test_class_db.h
+++ b/tests/core/object/test_class_db.h
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -28,25 +28,15 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-#ifndef GODOT_TEST_CLASS_DB_H
-#define GODOT_TEST_CLASS_DB_H
+#ifndef TEST_CLASS_DB_H
+#define TEST_CLASS_DB_H
-#include "core/register_core_types.h"
-
-#include "core/global_constants.h"
-#include "core/ordered_hash_map.h"
-#include "core/os/os.h"
-#include "core/string_name.h"
-#include "core/ustring.h"
-#include "core/variant.h"
+#include "core/core_bind.h"
+#include "core/core_constants.h"
+#include "core/object/class_db.h"
#include "tests/test_macros.h"
-#define TEST_COND DOCTEST_CHECK_FALSE_MESSAGE
-#define TEST_FAIL DOCTEST_FAIL
-#define TEST_FAIL_COND DOCTEST_REQUIRE_FALSE_MESSAGE
-#define TEST_FAIL_COND_WARN DOCTEST_WARN_FALSE_MESSAGE
-
namespace TestClassDB {
struct TypeReference {
@@ -56,7 +46,7 @@ struct TypeReference {
struct ConstantData {
String name;
- int value = 0;
+ int64_t value = 0;
};
struct EnumData {
@@ -81,6 +71,7 @@ struct ArgumentData {
String name;
bool has_defval = false;
Variant defval;
+ int position;
};
struct MethodData {
@@ -102,7 +93,7 @@ struct ExposedClass {
bool is_singleton = false;
bool is_instantiable = false;
- bool is_reference = false;
+ bool is_ref_counted = false;
ClassDB::APIType api_type;
@@ -113,9 +104,9 @@ struct ExposedClass {
List<SignalData> signals_;
const PropertyData *find_property_by_name(const StringName &p_name) const {
- for (const List<PropertyData>::Element *E = properties.front(); E; E = E->next()) {
- if (E->get().name == p_name) {
- return &E->get();
+ for (const PropertyData &E : properties) {
+ if (E.name == p_name) {
+ return &E;
}
}
@@ -123,9 +114,9 @@ struct ExposedClass {
}
const MethodData *find_method_by_name(const StringName &p_name) const {
- for (const List<MethodData>::Element *E = methods.front(); E; E = E->next()) {
- if (E->get().name == p_name) {
- return &E->get();
+ for (const MethodData &E : methods) {
+ if (E.name == p_name) {
+ return &E;
}
}
@@ -136,7 +127,7 @@ struct ExposedClass {
struct NamesCache {
StringName variant_type = StaticCString::create("Variant");
StringName object_class = StaticCString::create("Object");
- StringName reference_class = StaticCString::create("Reference");
+ StringName ref_counted_class = StaticCString::create("RefCounted");
StringName string_type = StaticCString::create("String");
StringName string_name_type = StaticCString::create("StringName");
StringName node_path_type = StaticCString::create("NodePath");
@@ -183,7 +174,7 @@ struct NamesCache {
}
};
-typedef OrderedHashMap<StringName, ExposedClass> ExposedClasses;
+typedef HashMap<StringName, ExposedClass> ExposedClasses;
struct Context {
Vector<StringName> enum_types;
@@ -193,13 +184,13 @@ struct Context {
NamesCache names_cache;
const ExposedClass *find_exposed_class(const StringName &p_name) const {
- ExposedClasses::ConstElement elem = exposed_classes.find(p_name);
- return elem ? &elem.value() : nullptr;
+ ExposedClasses::ConstIterator elem = exposed_classes.find(p_name);
+ return elem ? &elem->value : nullptr;
}
const ExposedClass *find_exposed_class(const TypeReference &p_type_ref) const {
- ExposedClasses::ConstElement elem = exposed_classes.find(p_type_ref.name);
- return elem ? &elem.value() : nullptr;
+ ExposedClasses::ConstIterator elem = exposed_classes.find(p_type_ref.name);
+ return elem ? &elem->value : nullptr;
}
bool has_type(const TypeReference &p_type_ref) const {
@@ -229,33 +220,33 @@ bool arg_default_value_is_assignable_to_type(const Context &p_context, const Var
switch (p_val.get_type()) {
case Variant::NIL:
return p_context.find_exposed_class(p_arg_type) ||
- p_context.names_cache.is_nullable_type(p_arg_type.name);
+ p_context.names_cache.is_nullable_type(p_arg_type.name);
case Variant::BOOL:
return p_arg_type.name == p_context.names_cache.bool_type;
case Variant::INT:
return p_arg_type.name == p_context.names_cache.int_type ||
- p_arg_type.name == p_context.names_cache.float_type ||
- p_arg_type.is_enum;
+ p_arg_type.name == p_context.names_cache.float_type ||
+ p_arg_type.is_enum;
case Variant::FLOAT:
return p_arg_type.name == p_context.names_cache.float_type;
case Variant::STRING:
case Variant::STRING_NAME:
return p_arg_type.name == p_context.names_cache.string_type ||
- p_arg_type.name == p_context.names_cache.string_name_type ||
- p_arg_type.name == p_context.names_cache.node_path_type;
+ p_arg_type.name == p_context.names_cache.string_name_type ||
+ p_arg_type.name == p_context.names_cache.node_path_type;
case Variant::NODE_PATH:
return p_arg_type.name == p_context.names_cache.node_path_type;
- case Variant::TRANSFORM:
+ case Variant::TRANSFORM3D:
case Variant::TRANSFORM2D:
case Variant::BASIS:
- case Variant::QUAT:
+ case Variant::QUATERNION:
case Variant::PLANE:
case Variant::AABB:
case Variant::COLOR:
case Variant::VECTOR2:
case Variant::RECT2:
case Variant::VECTOR3:
- case Variant::_RID:
+ case Variant::RID:
case Variant::ARRAY:
case Variant::DICTIONARY:
case Variant::PACKED_BYTE_ARRAY:
@@ -274,13 +265,13 @@ bool arg_default_value_is_assignable_to_type(const Context &p_context, const Var
return p_context.find_exposed_class(p_arg_type);
case Variant::VECTOR2I:
return p_arg_type.name == p_context.names_cache.vector2_type ||
- p_arg_type.name == Variant::get_type_name(p_val.get_type());
+ p_arg_type.name == Variant::get_type_name(p_val.get_type());
case Variant::RECT2I:
return p_arg_type.name == p_context.names_cache.rect2_type ||
- p_arg_type.name == Variant::get_type_name(p_val.get_type());
+ p_arg_type.name == Variant::get_type_name(p_val.get_type());
case Variant::VECTOR3I:
return p_arg_type.name == p_context.names_cache.vector3_type ||
- p_arg_type.name == Variant::get_type_name(p_val.get_type());
+ p_arg_type.name == Variant::get_type_name(p_val.get_type());
default:
if (r_err_msg) {
*r_err_msg = "Unexpected Variant type: " + itos(p_val.get_type());
@@ -298,7 +289,7 @@ void validate_property(const Context &p_context, const ExposedClass &p_class, co
const ExposedClass *top = &p_class;
while (!setter && top->base != StringName()) {
top = p_context.find_exposed_class(top->base);
- TEST_FAIL_COND(!top, "Class not found '" + top->base + "'. Inherited by '" + top->name + "'.");
+ TEST_FAIL_COND(!top, "Class not found '", top->base, "'. Inherited by '", top->name, "'.");
setter = top->find_method_by_name(p_prop.setter);
}
@@ -308,23 +299,23 @@ void validate_property(const Context &p_context, const ExposedClass &p_class, co
top = &p_class;
while (!getter && top->base != StringName()) {
top = p_context.find_exposed_class(top->base);
- TEST_FAIL_COND(!top, "Class not found '" + top->base + "'. Inherited by '" + top->name + "'.");
+ TEST_FAIL_COND(!top, "Class not found '", top->base, "'. Inherited by '", top->name, "'.");
getter = top->find_method_by_name(p_prop.getter);
}
TEST_FAIL_COND((!setter && !getter),
- "Couldn't find neither the setter nor the getter for property: '" + p_class.name + "." + String(p_prop.name) + "'.");
+ "Couldn't find neither the setter nor the getter for property: '", p_class.name, ".", String(p_prop.name), "'.");
if (setter) {
int setter_argc = p_prop.index != -1 ? 2 : 1;
TEST_FAIL_COND(setter->arguments.size() != setter_argc,
- "Invalid property setter argument count: '" + p_class.name + "." + String(p_prop.name) + "'.");
+ "Invalid property setter argument count: '", p_class.name, ".", String(p_prop.name), "'.");
}
if (getter) {
int getter_argc = p_prop.index != -1 ? 1 : 0;
TEST_FAIL_COND(getter->arguments.size() != getter_argc,
- "Invalid property setter argument count: '" + p_class.name + "." + String(p_prop.name) + "'.");
+ "Invalid property setter argument count: '", p_class.name, ".", String(p_prop.name), "'.");
}
if (getter && setter) {
@@ -332,10 +323,10 @@ void validate_property(const Context &p_context, const ExposedClass &p_class, co
if (getter->return_type.name != setter_first_arg.type.name) {
// Special case for Node::set_name
bool whitelisted = getter->return_type.name == p_context.names_cache.string_name_type &&
- setter_first_arg.type.name == p_context.names_cache.string_type;
+ setter_first_arg.type.name == p_context.names_cache.string_type;
TEST_FAIL_COND(!whitelisted,
- "Return type from getter doesn't match first argument of setter, for property: '" + p_class.name + "." + String(p_prop.name) + "'.");
+ "Return type from getter doesn't match first argument of setter, for property: '", p_class.name, ".", String(p_prop.name), "'.");
}
}
@@ -344,10 +335,17 @@ void validate_property(const Context &p_context, const ExposedClass &p_class, co
const ExposedClass *prop_class = p_context.find_exposed_class(prop_type_ref);
if (prop_class) {
TEST_COND(prop_class->is_singleton,
- "Property type is a singleton: '" + p_class.name + "." + String(p_prop.name) + "'.");
+ "Property type is a singleton: '", p_class.name, ".", String(p_prop.name), "'.");
+
+ if (p_class.api_type == ClassDB::API_CORE) {
+ TEST_COND(prop_class->api_type == ClassDB::API_EDITOR,
+ "Property '", p_class.name, ".", p_prop.name, "' has type '", prop_class->name,
+ "' from the editor API. Core API cannot have dependencies on the editor API.");
+ }
} else {
+ // Look for types that don't inherit Object
TEST_FAIL_COND(!p_context.has_type(prop_type_ref),
- "Property type '" + prop_type_ref.name + "' not found: '" + p_class.name + "." + String(p_prop.name) + "'.");
+ "Property type '", prop_type_ref.name, "' not found: '", p_class.name, ".", String(p_prop.name), "'.");
}
if (getter) {
@@ -356,7 +354,7 @@ void validate_property(const Context &p_context, const ExposedClass &p_class, co
if (idx_arg.type.name != p_context.names_cache.int_type) {
// If not an int, it can be an enum
TEST_COND(p_context.enum_types.find(idx_arg.type.name) < 0,
- "Invalid type '" + idx_arg.type.name + "' for index argument of property getter: '" + p_class.name + "." + String(p_prop.name) + "'.");
+ "Invalid type '", idx_arg.type.name, "' for index argument of property getter: '", p_class.name, ".", String(p_prop.name), "'.");
}
}
}
@@ -368,55 +366,74 @@ void validate_property(const Context &p_context, const ExposedClass &p_class, co
// Assume the index parameter is an enum
// If not an int, it can be an enum
TEST_COND(p_context.enum_types.find(idx_arg.type.name) < 0,
- "Invalid type '" + idx_arg.type.name + "' for index argument of property setter: '" + p_class.name + "." + String(p_prop.name) + "'.");
+ "Invalid type '", idx_arg.type.name, "' for index argument of property setter: '", p_class.name, ".", String(p_prop.name), "'.");
}
}
}
}
-void validate_method(const Context &p_context, const ExposedClass &p_class, const MethodData &p_method) {
- const ExposedClass *return_class = p_context.find_exposed_class(p_method.return_type);
- if (return_class) {
- TEST_COND(return_class->is_singleton,
- "Method return type is a singleton: '" + p_class.name + "." + p_method.name + "'.");
+void validate_argument(const Context &p_context, const ExposedClass &p_class, const String &p_owner_name, const String &p_owner_type, const ArgumentData &p_arg) {
+ TEST_COND((p_arg.name.is_empty() || p_arg.name.begins_with("_unnamed_arg")),
+ vformat("Unnamed argument in position %d of %s '%s.%s'.", p_arg.position, p_owner_type, p_class.name, p_owner_name));
+
+ const ExposedClass *arg_class = p_context.find_exposed_class(p_arg.type);
+ if (arg_class) {
+ TEST_COND(arg_class->is_singleton,
+ vformat("Argument type is a singleton: '%s' of %s '%s.%s'.", p_arg.name, p_owner_type, p_class.name, p_owner_name));
+
+ if (p_class.api_type == ClassDB::API_CORE) {
+ TEST_COND(arg_class->api_type == ClassDB::API_EDITOR,
+ vformat("Argument '%s' of %s '%s.%s' has type '%s' from the editor API. Core API cannot have dependencies on the editor API.",
+ p_arg.name, p_owner_type, p_class.name, p_owner_name, arg_class->name));
+ }
+ } else {
+ // Look for types that don't inherit Object.
+ TEST_FAIL_COND(!p_context.has_type(p_arg.type),
+ vformat("Argument type '%s' not found: '%s' of %s '%s.%s'.", p_arg.type.name, p_arg.name, p_owner_type, p_class.name, p_owner_name));
}
- for (const List<ArgumentData>::Element *F = p_method.arguments.front(); F; F = F->next()) {
- const ArgumentData &arg = F->get();
+ if (p_arg.has_defval) {
+ String type_error_msg;
+ bool arg_defval_assignable_to_type = arg_default_value_is_assignable_to_type(p_context, p_arg.defval, p_arg.type, &type_error_msg);
- const ExposedClass *arg_class = p_context.find_exposed_class(arg.type);
- if (arg_class) {
- TEST_COND(arg_class->is_singleton,
- "Argument type is a singleton: '" + arg.name + "' of method '" + p_class.name + "." + p_method.name + "'.");
- } else {
- TEST_FAIL_COND(!p_context.has_type(arg.type),
- "Argument type '" + arg.type.name + "' not found: '" + arg.name + "' of method" + p_class.name + "." + p_method.name + "'.");
+ String err_msg = vformat("Invalid default value for parameter '%s' of %s '%s.%s'.", p_arg.name, p_owner_type, p_class.name, p_owner_name);
+ if (!type_error_msg.is_empty()) {
+ err_msg += " " + type_error_msg;
}
- if (arg.has_defval) {
- String type_error_msg;
- bool arg_defval_assignable_to_type = arg_default_value_is_assignable_to_type(p_context, arg.defval, arg.type, &type_error_msg);
- String err_msg = vformat("Invalid default value for parameter '%s' of method '%s.%s'.", arg.name, p_class.name, p_method.name);
- if (!type_error_msg.empty()) {
- err_msg += " " + type_error_msg;
+ TEST_COND(!arg_defval_assignable_to_type, err_msg.utf8().get_data());
+ }
+}
+
+void validate_method(const Context &p_context, const ExposedClass &p_class, const MethodData &p_method) {
+ if (p_method.return_type.name != StringName()) {
+ const ExposedClass *return_class = p_context.find_exposed_class(p_method.return_type);
+ if (return_class) {
+ TEST_COND(return_class->is_singleton,
+ "Method return type is a singleton: '", p_class.name, ".", p_method.name, "'.");
+
+ if (p_class.api_type == ClassDB::API_CORE) {
+ TEST_COND(return_class->api_type == ClassDB::API_EDITOR,
+ "Method '", p_class.name, ".", p_method.name, "' has return type '", return_class->name,
+ "' from the editor API. Core API cannot have dependencies on the editor API.");
}
- TEST_COND(!arg_defval_assignable_to_type, err_msg.utf8().get_data());
+ } else {
+ // Look for types that don't inherit Object
+ TEST_FAIL_COND(!p_context.has_type(p_method.return_type),
+ "Method return type '", p_method.return_type.name, "' not found: '", p_class.name, ".", p_method.name, "'.");
}
}
+
+ for (const ArgumentData &F : p_method.arguments) {
+ const ArgumentData &arg = F;
+ validate_argument(p_context, p_class, p_method.name, "method", arg);
+ }
}
void validate_signal(const Context &p_context, const ExposedClass &p_class, const SignalData &p_signal) {
- for (const List<ArgumentData>::Element *F = p_signal.arguments.front(); F; F = F->next()) {
- const ArgumentData &arg = F->get();
-
- const ExposedClass *arg_class = p_context.find_exposed_class(arg.type);
- if (arg_class) {
- TEST_COND(arg_class->is_singleton,
- "Argument class is a singleton: '" + arg.name + "' of signal" + p_class.name + "." + p_signal.name + "'.");
- } else {
- TEST_FAIL_COND(!p_context.has_type(arg.type),
- "Argument type '" + arg.type.name + "' not found: '" + arg.name + "' of signal" + p_class.name + "." + p_signal.name + "'.");
- }
+ for (const ArgumentData &F : p_signal.arguments) {
+ const ArgumentData &arg = F;
+ validate_argument(p_context, p_class, p_signal.name, "signal", arg);
}
}
@@ -426,7 +443,7 @@ void validate_class(const Context &p_context, const ExposedClass &p_exposed_clas
if (!is_derived_type) {
// Asserts about the base Object class
TEST_FAIL_COND(p_exposed_class.name != p_context.names_cache.object_class,
- "Class '" + p_exposed_class.name + "' has no base class.");
+ "Class '", p_exposed_class.name, "' has no base class.");
TEST_FAIL_COND(!p_exposed_class.is_instantiable,
"Object class is not instantiable.");
TEST_FAIL_COND(p_exposed_class.api_type != ClassDB::API_CORE,
@@ -436,21 +453,21 @@ void validate_class(const Context &p_context, const ExposedClass &p_exposed_clas
}
TEST_FAIL_COND((p_exposed_class.is_singleton && p_exposed_class.base != p_context.names_cache.object_class),
- "Singleton base class '" + String(p_exposed_class.base) + "' is not Object, for class '" + p_exposed_class.name + "'.");
+ "Singleton base class '", String(p_exposed_class.base), "' is not Object, for class '", p_exposed_class.name, "'.");
TEST_FAIL_COND((is_derived_type && !p_context.exposed_classes.has(p_exposed_class.base)),
- "Base type '" + p_exposed_class.base.operator String() + "' does not exist, for class '" + p_exposed_class.name + "'.");
+ "Base type '", p_exposed_class.base.operator String(), "' does not exist, for class '", p_exposed_class.name, "'.");
- for (const List<PropertyData>::Element *F = p_exposed_class.properties.front(); F; F = F->next()) {
- validate_property(p_context, p_exposed_class, F->get());
+ for (const PropertyData &F : p_exposed_class.properties) {
+ validate_property(p_context, p_exposed_class, F);
}
- for (const List<MethodData>::Element *F = p_exposed_class.methods.front(); F; F = F->next()) {
- validate_method(p_context, p_exposed_class, F->get());
+ for (const MethodData &F : p_exposed_class.methods) {
+ validate_method(p_context, p_exposed_class, F);
}
- for (const List<SignalData>::Element *F = p_exposed_class.signals_.front(); F; F = F->next()) {
- validate_signal(p_context, p_exposed_class, F->get());
+ for (const SignalData &F : p_exposed_class.signals_) {
+ validate_signal(p_context, p_exposed_class, F);
}
}
@@ -470,13 +487,13 @@ void add_exposed_classes(Context &r_context) {
}
if (!ClassDB::is_class_exposed(class_name)) {
- MESSAGE(vformat("Ignoring class '%s' because it's not exposed.", class_name).utf8().get_data());
+ MESSAGE(vformat("Ignoring class '%s' because it's not exposed.", class_name));
class_list.pop_front();
continue;
}
if (!ClassDB::is_class_enabled(class_name)) {
- MESSAGE(vformat("Ignoring class '%s' because it's not enabled.", class_name).utf8().get_data());
+ MESSAGE(vformat("Ignoring class '%s' because it's not enabled.", class_name));
class_list.pop_front();
continue;
}
@@ -488,7 +505,7 @@ void add_exposed_classes(Context &r_context) {
exposed_class.api_type = api_type;
exposed_class.is_singleton = Engine::get_singleton()->has_singleton(class_name);
exposed_class.is_instantiable = class_info->creation_func && !exposed_class.is_singleton;
- exposed_class.is_reference = ClassDB::is_parent_class(class_name, "Reference");
+ exposed_class.is_ref_counted = ClassDB::is_parent_class(class_name, "RefCounted");
exposed_class.base = ClassDB::get_parent_class(class_name);
// Add properties
@@ -496,12 +513,10 @@ void add_exposed_classes(Context &r_context) {
List<PropertyInfo> property_list;
ClassDB::get_property_list(class_name, &property_list, true);
- Map<StringName, StringName> accessor_methods;
-
- for (const List<PropertyInfo>::Element *E = property_list.front(); E; E = E->next()) {
- const PropertyInfo &property = E->get();
+ HashMap<StringName, StringName> accessor_methods;
- if (property.usage & PROPERTY_USAGE_GROUP || property.usage & PROPERTY_USAGE_SUBGROUP || property.usage & PROPERTY_USAGE_CATEGORY) {
+ for (const PropertyInfo &property : property_list) {
+ if (property.usage & PROPERTY_USAGE_GROUP || property.usage & PROPERTY_USAGE_SUBGROUP || property.usage & PROPERTY_USAGE_CATEGORY || (property.type == Variant::NIL && property.usage & PROPERTY_USAGE_ARRAY)) {
continue;
}
@@ -519,7 +534,7 @@ void add_exposed_classes(Context &r_context) {
bool valid = false;
prop.index = ClassDB::get_property_index(class_name, prop.name, &valid);
- TEST_FAIL_COND(!valid, "Invalid property: '" + exposed_class.name + "." + String(prop.name) + "'.");
+ TEST_FAIL_COND(!valid, "Invalid property: '", exposed_class.name, ".", String(prop.name), "'.");
exposed_class.properties.push_back(prop);
}
@@ -533,12 +548,12 @@ void add_exposed_classes(Context &r_context) {
ClassDB::get_method_list(class_name, &method_list, true);
method_list.sort();
- for (List<MethodInfo>::Element *E = method_list.front(); E; E = E->next()) {
- const MethodInfo &method_info = E->get();
+ for (const MethodInfo &E : method_list) {
+ const MethodInfo &method_info = E;
int argc = method_info.arguments.size();
- if (method_info.name.empty()) {
+ if (method_info.name.is_empty()) {
continue;
}
@@ -557,7 +572,7 @@ void add_exposed_classes(Context &r_context) {
if (!m && !method.is_virtual) {
TEST_FAIL_COND(!virtual_method_list.find(method_info),
- "Missing MethodBind for non-virtual method: '" + exposed_class.name + "." + method.name + "'.");
+ "Missing MethodBind for non-virtual method: '", exposed_class.name, ".", method.name, "'.");
// A virtual method without the virtual flag. This is a special case.
@@ -576,17 +591,16 @@ void add_exposed_classes(Context &r_context) {
(exposed_class.name != r_context.names_cache.object_class || String(method.name) != "free"),
warn_msg.utf8().get_data());
- } else if (return_info.type == Variant::INT && return_info.usage & PROPERTY_USAGE_CLASS_IS_ENUM) {
+ } else if (return_info.type == Variant::INT && return_info.usage & (PROPERTY_USAGE_CLASS_IS_ENUM | PROPERTY_USAGE_CLASS_IS_BITFIELD)) {
method.return_type.name = return_info.class_name;
method.return_type.is_enum = true;
} else if (return_info.class_name != StringName()) {
method.return_type.name = return_info.class_name;
bool bad_reference_hint = !method.is_virtual && return_info.hint != PROPERTY_HINT_RESOURCE_TYPE &&
- ClassDB::is_parent_class(return_info.class_name, r_context.names_cache.reference_class);
- TEST_COND(bad_reference_hint, String() + "Return type is reference but hint is not '" _STR(PROPERTY_HINT_RESOURCE_TYPE) "'." +
- " Are you returning a reference type by pointer? Method: '" +
- exposed_class.name + "." + method.name + "'.");
+ ClassDB::is_parent_class(return_info.class_name, r_context.names_cache.ref_counted_class);
+ TEST_COND(bad_reference_hint, "Return type is reference but hint is not '" _STR(PROPERTY_HINT_RESOURCE_TYPE) "'.", " Are you returning a reference type by pointer? Method: '",
+ exposed_class.name, ".", method.name, "'.");
} else if (return_info.hint == PROPERTY_HINT_RESOURCE_TYPE) {
method.return_type.name = return_info.hint_string;
} else if (return_info.type == Variant::NIL && return_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT) {
@@ -605,8 +619,9 @@ void add_exposed_classes(Context &r_context) {
ArgumentData arg;
arg.name = orig_arg_name;
+ arg.position = i;
- if (arg_info.type == Variant::INT && arg_info.usage & PROPERTY_USAGE_CLASS_IS_ENUM) {
+ if (arg_info.type == Variant::INT && arg_info.usage & (PROPERTY_USAGE_CLASS_IS_ENUM | PROPERTY_USAGE_CLASS_IS_BITFIELD)) {
arg.type.name = arg_info.class_name;
arg.type.is_enum = true;
} else if (arg_info.class_name != StringName()) {
@@ -636,12 +651,12 @@ void add_exposed_classes(Context &r_context) {
}
TEST_COND(exposed_class.find_property_by_name(method.name),
- "Method name conflicts with property: '" + String(class_name) + "." + String(method.name) + "'.");
+ "Method name conflicts with property: '", String(class_name), ".", String(method.name), "'.");
- // Classes starting with an underscore are ignored unless they're used as a property setter or getter
+ // Methods starting with an underscore are ignored unless they're virtual or used as a property setter or getter.
if (!method.is_virtual && String(method.name)[0] == '_') {
- for (const List<PropertyData>::Element *F = exposed_class.properties.front(); F; F = F->next()) {
- const PropertyData &prop = F->get();
+ for (const PropertyData &F : exposed_class.properties) {
+ const PropertyData &prop = F;
if (prop.setter == method.name || prop.getter == method.name) {
exposed_class.methods.push_back(method);
@@ -651,17 +666,20 @@ void add_exposed_classes(Context &r_context) {
} else {
exposed_class.methods.push_back(method);
}
+
+ if (method.is_virtual) {
+ TEST_COND(String(method.name)[0] != '_', "Virtual method ", String(method.name), " does not start with underscore.");
+ }
}
// Add signals
const HashMap<StringName, MethodInfo> &signal_map = class_info->signal_map;
- const StringName *k = nullptr;
- while ((k = signal_map.next(k))) {
+ for (const KeyValue<StringName, MethodInfo> &K : signal_map) {
SignalData signal;
- const MethodInfo &method_info = signal_map.get(*k);
+ const MethodInfo &method_info = signal_map.get(K.key);
signal.name = method_info.name;
@@ -674,8 +692,9 @@ void add_exposed_classes(Context &r_context) {
ArgumentData arg;
arg.name = orig_arg_name;
+ arg.position = i;
- if (arg_info.type == Variant::INT && arg_info.usage & PROPERTY_USAGE_CLASS_IS_ENUM) {
+ if (arg_info.type == Variant::INT && arg_info.usage & (PROPERTY_USAGE_CLASS_IS_ENUM | PROPERTY_USAGE_CLASS_IS_BITFIELD)) {
arg.type.name = arg_info.class_name;
arg.type.is_enum = true;
} else if (arg_info.class_name != StringName()) {
@@ -713,19 +732,20 @@ void add_exposed_classes(Context &r_context) {
List<String> constants;
ClassDB::get_integer_constant_list(class_name, &constants, true);
- const HashMap<StringName, List<StringName>> &enum_map = class_info->enum_map;
- k = nullptr;
+ const HashMap<StringName, ClassDB::ClassInfo::EnumInfo> &enum_map = class_info->enum_map;
- while ((k = enum_map.next(k))) {
+ for (const KeyValue<StringName, ClassDB::ClassInfo::EnumInfo> &K : enum_map) {
EnumData enum_;
- enum_.name = *k;
-
- const List<StringName> &enum_constants = enum_map.get(*k);
- for (const List<StringName>::Element *E = enum_constants.front(); E; E = E->next()) {
- const StringName &constant_name = E->get();
- int *value = class_info->constant_map.getptr(constant_name);
- TEST_FAIL_COND(!value, "Missing enum constant value: '" +
- String(class_name) + "." + String(enum_.name) + "." + String(constant_name) + "'.");
+ enum_.name = K.key;
+
+ for (const StringName &E : K.value.constants) {
+ const StringName &constant_name = E;
+ TEST_FAIL_COND(String(constant_name).find("::") != -1,
+ "Enum constant contains '::', check bindings to remove the scope: '",
+ String(class_name), ".", String(enum_.name), ".", String(constant_name), "'.");
+ int64_t *value = class_info->constant_map.getptr(constant_name);
+ TEST_FAIL_COND(!value, "Missing enum constant value: '",
+ String(class_name), ".", String(enum_.name), ".", String(constant_name), "'.");
constants.erase(constant_name);
ConstantData constant;
@@ -737,13 +757,16 @@ void add_exposed_classes(Context &r_context) {
exposed_class.enums.push_back(enum_);
- r_context.enum_types.push_back(String(class_name) + "." + String(*k));
+ r_context.enum_types.push_back(String(class_name) + "." + String(K.key));
}
- for (const List<String>::Element *E = constants.front(); E; E = E->next()) {
- const String &constant_name = E->get();
- int *value = class_info->constant_map.getptr(StringName(E->get()));
- TEST_FAIL_COND(!value, "Missing enum constant value: '" + String(class_name) + "." + String(constant_name) + "'.");
+ for (const String &E : constants) {
+ const String &constant_name = E;
+ TEST_FAIL_COND(constant_name.find("::") != -1,
+ "Constant contains '::', check bindings to remove the scope: '",
+ String(class_name), ".", constant_name, "'.");
+ int64_t *value = class_info->constant_map.getptr(StringName(E));
+ TEST_FAIL_COND(!value, "Missing constant value: '", String(class_name), ".", String(constant_name), "'.");
ConstantData constant;
constant.name = constant_name;
@@ -769,16 +792,16 @@ void add_builtin_types(Context &r_context) {
}
void add_global_enums(Context &r_context) {
- int global_constants_count = GlobalConstants::get_global_constant_count();
+ int global_constants_count = CoreConstants::get_global_constant_count();
if (global_constants_count > 0) {
for (int i = 0; i < global_constants_count; i++) {
- StringName enum_name = GlobalConstants::get_global_constant_enum(i);
+ StringName enum_name = CoreConstants::get_global_constant_enum(i);
if (enum_name != StringName()) {
ConstantData constant;
- constant.name = GlobalConstants::get_global_constant_name(i);
- constant.value = GlobalConstants::get_global_constant_value(i);
+ constant.name = CoreConstants::get_global_constant_name(i);
+ constant.value = CoreConstants::get_global_constant_value(i);
EnumData enum_;
enum_.name = enum_name;
@@ -792,8 +815,8 @@ void add_global_enums(Context &r_context) {
}
}
- for (List<EnumData>::Element *E = r_context.global_enums.front(); E; E = E->next()) {
- r_context.enum_types.push_back(E->get().name);
+ for (const EnumData &E : r_context.global_enums) {
+ r_context.enum_types.push_back(E.name);
}
}
@@ -803,10 +826,10 @@ void add_global_enums(Context &r_context) {
hardcoded_enums.push_back("Vector2i.Axis");
hardcoded_enums.push_back("Vector3.Axis");
hardcoded_enums.push_back("Vector3i.Axis");
- for (List<StringName>::Element *E = hardcoded_enums.front(); E; E = E->next()) {
+ for (const StringName &E : hardcoded_enums) {
// These enums are not generated and must be written manually (e.g.: Vector3.Axis)
// Here, we assume core types do not begin with underscore
- r_context.enum_types.push_back(E->get());
+ r_context.enum_types.push_back(E);
}
}
@@ -818,19 +841,18 @@ TEST_SUITE("[ClassDB]") {
add_builtin_types(context);
add_global_enums(context);
- SUBCASE("[ClassDB] Find exposed class") {
+ SUBCASE("[ClassDB] Validate exposed classes") {
const ExposedClass *object_class = context.find_exposed_class(context.names_cache.object_class);
TEST_FAIL_COND(!object_class, "Object class not found.");
TEST_FAIL_COND(object_class->base != StringName(),
- "Object class derives from another class: '" + object_class->base + "'.");
+ "Object class derives from another class: '", object_class->base, "'.");
- for (ExposedClasses::Element E = context.exposed_classes.front(); E; E = E.next()) {
- validate_class(context, E.value());
+ for (const KeyValue<StringName, ExposedClass> &E : context.exposed_classes) {
+ validate_class(context, E.value);
}
}
}
}
-
} // namespace TestClassDB
-#endif //GODOT_TEST_CLASS_DB_H
+#endif // TEST_CLASS_DB_H
diff --git a/tests/core/object/test_method_bind.h b/tests/core/object/test_method_bind.h
new file mode 100644
index 0000000000..2fe0c264a1
--- /dev/null
+++ b/tests/core/object/test_method_bind.h
@@ -0,0 +1,175 @@
+/*************************************************************************/
+/* test_method_bind.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_METHOD_BIND_H
+#define TEST_METHOD_BIND_H
+
+#include "core/object/class_db.h"
+
+#include "tests/test_macros.h"
+
+namespace TestMethodBind {
+
+class MethodBindTester : public Object {
+ GDCLASS(MethodBindTester, Object);
+
+public:
+ enum Test {
+ TEST_METHOD,
+ TEST_METHOD_ARGS,
+ TEST_METHODC,
+ TEST_METHODC_ARGS,
+ TEST_METHODR,
+ TEST_METHODR_ARGS,
+ TEST_METHODRC,
+ TEST_METHODRC_ARGS,
+ TEST_METHOD_DEFARGS,
+ TEST_METHOD_OBJECT_CAST,
+ TEST_MAX
+ };
+
+ class ObjectSubclass : public Object {
+ public:
+ int value = 1;
+ };
+
+ int test_num = 0;
+
+ bool test_valid[TEST_MAX];
+
+ void test_method() {
+ test_valid[TEST_METHOD] = true;
+ }
+
+ void test_method_args(int p_arg) {
+ test_valid[TEST_METHOD_ARGS] = p_arg == test_num;
+ }
+
+ void test_methodc() {
+ test_valid[TEST_METHODC] = true;
+ }
+
+ void test_methodc_args(int p_arg) {
+ test_valid[TEST_METHODC_ARGS] = p_arg == test_num;
+ }
+
+ int test_methodr() {
+ test_valid[TEST_METHODR] = true; //temporary
+ return test_num;
+ }
+
+ int test_methodr_args(int p_arg) {
+ test_valid[TEST_METHODR_ARGS] = true; //temporary
+ return p_arg;
+ }
+
+ int test_methodrc() {
+ test_valid[TEST_METHODRC] = true; //temporary
+ return test_num;
+ }
+
+ int test_methodrc_args(int p_arg) {
+ test_valid[TEST_METHODRC_ARGS] = true; //temporary
+ return p_arg;
+ }
+
+ void test_method_default_args(int p_arg1, int p_arg2, int p_arg3, int p_arg4, int p_arg5) {
+ test_valid[TEST_METHOD_DEFARGS] = p_arg1 == 1 && p_arg2 == 2 && p_arg3 == 3 && p_arg4 == 4 && p_arg5 == 5; //temporary
+ }
+
+ void test_method_object_cast(ObjectSubclass *p_object) {
+ test_valid[TEST_METHOD_OBJECT_CAST] = p_object->value == 1;
+ }
+
+ static void _bind_methods() {
+ ClassDB::bind_method(D_METHOD("test_method"), &MethodBindTester::test_method);
+ ClassDB::bind_method(D_METHOD("test_method_args"), &MethodBindTester::test_method_args);
+ ClassDB::bind_method(D_METHOD("test_methodc"), &MethodBindTester::test_methodc);
+ ClassDB::bind_method(D_METHOD("test_methodc_args"), &MethodBindTester::test_methodc_args);
+ ClassDB::bind_method(D_METHOD("test_methodr"), &MethodBindTester::test_methodr);
+ ClassDB::bind_method(D_METHOD("test_methodr_args"), &MethodBindTester::test_methodr_args);
+ ClassDB::bind_method(D_METHOD("test_methodrc"), &MethodBindTester::test_methodrc);
+ ClassDB::bind_method(D_METHOD("test_methodrc_args"), &MethodBindTester::test_methodrc_args);
+ ClassDB::bind_method(D_METHOD("test_method_default_args"), &MethodBindTester::test_method_default_args, DEFVAL(9) /* wrong on purpose */, DEFVAL(4), DEFVAL(5));
+ ClassDB::bind_method(D_METHOD("test_method_object_cast", "object"), &MethodBindTester::test_method_object_cast);
+ }
+
+ virtual void run_tests() {
+ for (int i = 0; i < TEST_MAX; i++) {
+ test_valid[i] = false;
+ }
+ //regular
+ test_num = Math::rand();
+ call("test_method");
+ test_num = Math::rand();
+ call("test_method_args", test_num);
+ test_num = Math::rand();
+ call("test_methodc");
+ test_num = Math::rand();
+ call("test_methodc_args", test_num);
+ //return
+ test_num = Math::rand();
+ test_valid[TEST_METHODR] = int(call("test_methodr")) == test_num && test_valid[TEST_METHODR];
+ test_num = Math::rand();
+ test_valid[TEST_METHODR_ARGS] = int(call("test_methodr_args", test_num)) == test_num && test_valid[TEST_METHODR_ARGS];
+ test_num = Math::rand();
+ test_valid[TEST_METHODRC] = int(call("test_methodrc")) == test_num && test_valid[TEST_METHODRC];
+ test_num = Math::rand();
+ test_valid[TEST_METHODRC_ARGS] = int(call("test_methodrc_args", test_num)) == test_num && test_valid[TEST_METHODRC_ARGS];
+
+ call("test_method_default_args", 1, 2, 3, 4);
+
+ ObjectSubclass *obj = memnew(ObjectSubclass);
+ call("test_method_object_cast", obj);
+ memdelete(obj);
+ }
+};
+
+TEST_CASE("[MethodBind] check all method binds") {
+ MethodBindTester *mbt = memnew(MethodBindTester);
+
+ mbt->run_tests();
+
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHOD]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHOD_ARGS]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHODC]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHODC_ARGS]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHODR]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHODR_ARGS]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHODRC]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHODRC_ARGS]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHOD_DEFARGS]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHOD_OBJECT_CAST]);
+
+ memdelete(mbt);
+}
+} // namespace TestMethodBind
+
+#endif // TEST_METHOD_BIND_H
diff --git a/tests/core/object/test_object.h b/tests/core/object/test_object.h
new file mode 100644
index 0000000000..f5c5de7fdf
--- /dev/null
+++ b/tests/core/object/test_object.h
@@ -0,0 +1,286 @@
+/*************************************************************************/
+/* test_object.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_OBJECT_H
+#define TEST_OBJECT_H
+
+#include "core/core_string_names.h"
+#include "core/object/class_db.h"
+#include "core/object/object.h"
+#include "core/object/script_language.h"
+
+#include "tests/test_macros.h"
+
+// Declared in global namespace because of GDCLASS macro warning (Windows):
+// "Unqualified friend declaration referring to type outside of the nearest enclosing namespace
+// is a Microsoft extension; add a nested name specifier".
+class _TestDerivedObject : public Object {
+ GDCLASS(_TestDerivedObject, Object);
+
+ int property_value;
+
+protected:
+ static void _bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_property", "property"), &_TestDerivedObject::set_property);
+ ClassDB::bind_method(D_METHOD("get_property"), &_TestDerivedObject::get_property);
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "property"), "set_property", "get_property");
+ }
+
+public:
+ void set_property(int value) { property_value = value; }
+ int get_property() const { return property_value; }
+};
+
+namespace TestObject {
+
+class _MockScriptInstance : public ScriptInstance {
+ StringName property_name = "NO_NAME";
+ Variant property_value;
+
+public:
+ bool set(const StringName &p_name, const Variant &p_value) override {
+ property_name = p_name;
+ property_value = p_value;
+ return true;
+ }
+ bool get(const StringName &p_name, Variant &r_ret) const override {
+ if (property_name == p_name) {
+ r_ret = property_value;
+ return true;
+ }
+ return false;
+ }
+ void get_property_list(List<PropertyInfo> *p_properties) const override {
+ }
+ Variant::Type get_property_type(const StringName &p_name, bool *r_is_valid) const override {
+ return Variant::PACKED_FLOAT32_ARRAY;
+ }
+ bool property_can_revert(const StringName &p_name) const override {
+ return false;
+ };
+ bool property_get_revert(const StringName &p_name, Variant &r_ret) const override {
+ return false;
+ };
+ void get_method_list(List<MethodInfo> *p_list) const override {
+ }
+ bool has_method(const StringName &p_method) const override {
+ return false;
+ }
+ Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override {
+ return Variant();
+ }
+ void notification(int p_notification) override {
+ }
+ Ref<Script> get_script() const override {
+ return Ref<Script>();
+ }
+ const Variant get_rpc_config() const override {
+ return Variant();
+ }
+ ScriptLanguage *get_language() override {
+ return nullptr;
+ }
+};
+
+TEST_CASE("[Object] Core getters") {
+ Object object;
+
+ CHECK_MESSAGE(
+ object.is_class("Object"),
+ "is_class() should return the expected value.");
+ CHECK_MESSAGE(
+ object.get_class() == "Object",
+ "The returned class should match the expected value.");
+ CHECK_MESSAGE(
+ object.get_class_name() == "Object",
+ "The returned class name should match the expected value.");
+ CHECK_MESSAGE(
+ object.get_class_static() == "Object",
+ "The returned static class should match the expected value.");
+ CHECK_MESSAGE(
+ object.get_save_class() == "Object",
+ "The returned save class should match the expected value.");
+
+ List<String> inheritance_list;
+ object.get_inheritance_list_static(&inheritance_list);
+ CHECK_MESSAGE(
+ inheritance_list.size() == 1,
+ "The inheritance list should consist of Object only");
+ CHECK_MESSAGE(
+ inheritance_list[0] == "Object",
+ "The inheritance list should consist of Object only");
+}
+
+TEST_CASE("[Object] Metadata") {
+ const String meta_path = "complex_metadata_path";
+ Object object;
+
+ object.set_meta(meta_path, Color(0, 1, 0));
+ CHECK_MESSAGE(
+ Color(object.get_meta(meta_path)).is_equal_approx(Color(0, 1, 0)),
+ "The returned object metadata after setting should match the expected value.");
+
+ List<StringName> meta_list;
+ object.get_meta_list(&meta_list);
+ CHECK_MESSAGE(
+ meta_list.size() == 1,
+ "The metadata list should only contain 1 item after adding one metadata item.");
+
+ object.remove_meta(meta_path);
+ // Also try removing nonexistent metadata (it should do nothing, without printing an error message).
+ object.remove_meta("I don't exist");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ object.get_meta(meta_path) == Variant(),
+ "The returned object metadata after removing should match the expected value.");
+ ERR_PRINT_ON;
+
+ List<StringName> meta_list2;
+ object.get_meta_list(&meta_list2);
+ CHECK_MESSAGE(
+ meta_list2.size() == 0,
+ "The metadata list should contain 0 items after removing all metadata items.");
+}
+
+TEST_CASE("[Object] Construction") {
+ Object object;
+
+ CHECK_MESSAGE(
+ !object.is_ref_counted(),
+ "Object is not a RefCounted.");
+
+ Object *p_db = ObjectDB::get_instance(object.get_instance_id());
+ CHECK_MESSAGE(
+ p_db == &object,
+ "The database pointer returned by the object id should reference same object.");
+}
+
+TEST_CASE("[Object] Script instance property setter") {
+ Object object;
+ _MockScriptInstance *script_instance = memnew(_MockScriptInstance);
+ object.set_script_instance(script_instance);
+
+ bool valid = false;
+ object.set("some_name", 100, &valid);
+ CHECK(valid);
+ Variant actual_value;
+ CHECK_MESSAGE(
+ script_instance->get("some_name", actual_value),
+ "The assigned script instance should successfully retrieve value by name.");
+ CHECK_MESSAGE(
+ actual_value == Variant(100),
+ "The returned value should equal the one which was set by the object.");
+}
+
+TEST_CASE("[Object] Script instance property getter") {
+ Object object;
+ _MockScriptInstance *script_instance = memnew(_MockScriptInstance);
+ script_instance->set("some_name", 100); // Make sure script instance has the property
+ object.set_script_instance(script_instance);
+
+ bool valid = false;
+ const Variant &actual_value = object.get("some_name", &valid);
+ CHECK(valid);
+ CHECK_MESSAGE(
+ actual_value == Variant(100),
+ "The returned value should equal the one which was set by the script instance.");
+}
+
+TEST_CASE("[Object] Built-in property setter") {
+ GDREGISTER_CLASS(_TestDerivedObject);
+ _TestDerivedObject derived_object;
+
+ bool valid = false;
+ derived_object.set("property", 100, &valid);
+ CHECK(valid);
+ CHECK_MESSAGE(
+ derived_object.get_property() == 100,
+ "The property value should equal the one which was set with built-in setter.");
+}
+
+TEST_CASE("[Object] Built-in property getter") {
+ GDREGISTER_CLASS(_TestDerivedObject);
+ _TestDerivedObject derived_object;
+ derived_object.set_property(100);
+
+ bool valid = false;
+ const Variant &actual_value = derived_object.get("property", &valid);
+ CHECK(valid);
+ CHECK_MESSAGE(
+ actual_value == Variant(100),
+ "The returned value should equal the one which was set with built-in setter.");
+}
+
+TEST_CASE("[Object] Script property setter") {
+ Object object;
+ Variant script;
+
+ bool valid = false;
+ object.set(CoreStringNames::get_singleton()->_script, script, &valid);
+ CHECK(valid);
+ CHECK_MESSAGE(
+ object.get_script() == script,
+ "The object script should be equal to the assigned one.");
+}
+
+TEST_CASE("[Object] Script property getter") {
+ Object object;
+ Variant script;
+ object.set_script(script);
+
+ bool valid = false;
+ const Variant &actual_value = object.get(CoreStringNames::get_singleton()->_script, &valid);
+ CHECK(valid);
+ CHECK_MESSAGE(
+ actual_value == script,
+ "The returned value should be equal to the assigned script.");
+}
+
+TEST_CASE("[Object] Absent name setter") {
+ Object object;
+
+ bool valid = true;
+ object.set("absent_name", 100, &valid);
+ CHECK(!valid);
+}
+
+TEST_CASE("[Object] Absent name getter") {
+ Object object;
+
+ bool valid = true;
+ const Variant &actual_value = object.get("absent_name", &valid);
+ CHECK(!valid);
+ CHECK_MESSAGE(
+ actual_value == Variant(),
+ "The returned value should equal nil variant.");
+}
+} // namespace TestObject
+
+#endif // TEST_OBJECT_H
diff --git a/tests/core/os/test_os.h b/tests/core/os/test_os.h
new file mode 100644
index 0000000000..c46da5e156
--- /dev/null
+++ b/tests/core/os/test_os.h
@@ -0,0 +1,158 @@
+/*************************************************************************/
+/* test_os.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_OS_H
+#define TEST_OS_H
+
+#include "core/os/os.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestOS {
+
+TEST_CASE("[OS] Environment variables") {
+#ifdef WINDOWS_ENABLED
+ CHECK_MESSAGE(
+ OS::get_singleton()->has_environment("USERPROFILE"),
+ "The USERPROFILE environment variable should be present.");
+#else
+ CHECK_MESSAGE(
+ OS::get_singleton()->has_environment("HOME"),
+ "The HOME environment variable should be present.");
+#endif
+
+ OS::get_singleton()->set_environment("HELLO", "world");
+ CHECK_MESSAGE(
+ OS::get_singleton()->get_environment("HELLO") == "world",
+ "The previously-set HELLO environment variable should return the expected value.");
+}
+
+TEST_CASE("[OS] Command line arguments") {
+ List<String> arguments = OS::get_singleton()->get_cmdline_args();
+ bool found = false;
+ for (int i = 0; i < arguments.size(); i++) {
+ if (arguments[i] == "--test") {
+ found = true;
+ break;
+ }
+ }
+ CHECK_MESSAGE(
+ found,
+ "The `--test` option must be present in the list of command line arguments.");
+}
+
+TEST_CASE("[OS] Executable and data paths") {
+ CHECK_MESSAGE(
+ OS::get_singleton()->get_executable_path().is_absolute_path(),
+ "The executable path returned should be an absolute path.");
+ CHECK_MESSAGE(
+ OS::get_singleton()->get_data_path().is_absolute_path(),
+ "The user data path returned should be an absolute path.");
+ CHECK_MESSAGE(
+ OS::get_singleton()->get_config_path().is_absolute_path(),
+ "The user configuration path returned should be an absolute path.");
+ CHECK_MESSAGE(
+ OS::get_singleton()->get_cache_path().is_absolute_path(),
+ "The cache path returned should be an absolute path.");
+}
+
+TEST_CASE("[OS] Ticks") {
+ CHECK_MESSAGE(
+ OS::get_singleton()->get_ticks_usec() > 1000,
+ "The returned ticks (in microseconds) must be greater than 1,000.");
+ CHECK_MESSAGE(
+ OS::get_singleton()->get_ticks_msec() > 1,
+ "The returned ticks (in milliseconds) must be greater than 1.");
+}
+
+TEST_CASE("[OS] Feature tags") {
+ CHECK_MESSAGE(
+ OS::get_singleton()->has_feature("editor"),
+ "The binary has the \"editor\" feature tag.");
+ CHECK_MESSAGE(
+ !OS::get_singleton()->has_feature("standalone"),
+ "The binary does not have the \"standalone\" feature tag.");
+ CHECK_MESSAGE(
+ OS::get_singleton()->has_feature("debug"),
+ "The binary has the \"debug\" feature tag.");
+ CHECK_MESSAGE(
+ !OS::get_singleton()->has_feature("release"),
+ "The binary does not have the \"release\" feature tag.");
+}
+
+TEST_CASE("[OS] Process ID") {
+ CHECK_MESSAGE(
+ OS::get_singleton()->get_process_id() >= 1,
+ "The returned process ID should be greater than zero.");
+}
+
+TEST_CASE("[OS] Processor count and memory information") {
+ CHECK_MESSAGE(
+ OS::get_singleton()->get_processor_count() >= 1,
+ "The returned processor count should be greater than zero.");
+ CHECK_MESSAGE(
+ OS::get_singleton()->get_static_memory_usage() >= 1,
+ "The returned static memory usage should be greater than zero.");
+ CHECK_MESSAGE(
+ OS::get_singleton()->get_static_memory_peak_usage() >= 1,
+ "The returned static memory peak usage should be greater than zero.");
+}
+
+TEST_CASE("[OS] Execute") {
+#ifdef WINDOWS_ENABLED
+ List<String> arguments;
+ arguments.push_back("/C");
+ arguments.push_back("dir > NUL");
+ int exit_code;
+ const Error err = OS::get_singleton()->execute("cmd", arguments, nullptr, &exit_code);
+ CHECK_MESSAGE(
+ err == OK,
+ "(Running the command `cmd /C \"dir > NUL\"` returns the expected Godot error code (OK).");
+ CHECK_MESSAGE(
+ exit_code == 0,
+ "Running the command `cmd /C \"dir > NUL\"` returns a zero (successful) exit code.");
+#else
+ List<String> arguments;
+ arguments.push_back("-c");
+ arguments.push_back("ls > /dev/null");
+ int exit_code;
+ const Error err = OS::get_singleton()->execute("sh", arguments, nullptr, &exit_code);
+ CHECK_MESSAGE(
+ err == OK,
+ "(Running the command `sh -c \"ls > /dev/null\"` returns the expected Godot error code (OK).");
+ CHECK_MESSAGE(
+ exit_code == 0,
+ "Running the command `sh -c \"ls > /dev/null\"` returns a zero (successful) exit code.");
+#endif
+}
+
+} // namespace TestOS
+
+#endif // TEST_OS_H
diff --git a/tests/core/string/test_node_path.h b/tests/core/string/test_node_path.h
new file mode 100644
index 0000000000..d2de766889
--- /dev/null
+++ b/tests/core/string/test_node_path.h
@@ -0,0 +1,172 @@
+/*************************************************************************/
+/* test_node_path.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_NODE_PATH_H
+#define TEST_NODE_PATH_H
+
+#include "core/string/node_path.h"
+
+#include "tests/test_macros.h"
+
+namespace TestNodePath {
+
+TEST_CASE("[NodePath] Relative path") {
+ const NodePath node_path_relative = NodePath("Path2D/PathFollow2D/Sprite2D:position:x");
+
+ CHECK_MESSAGE(
+ node_path_relative.get_as_property_path() == NodePath(":Path2D/PathFollow2D/Sprite2D:position:x"),
+ "The returned property path should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_relative.get_concatenated_subnames() == "position:x",
+ "The returned concatenated subnames should match the expected value.");
+
+ CHECK_MESSAGE(
+ node_path_relative.get_name(0) == "Path2D",
+ "The returned name at index 0 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_relative.get_name(1) == "PathFollow2D",
+ "The returned name at index 1 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_relative.get_name(2) == "Sprite2D",
+ "The returned name at index 2 should match the expected value.");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ node_path_relative.get_name(3) == "",
+ "The returned name at invalid index 3 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_relative.get_name(-1) == "",
+ "The returned name at invalid index -1 should match the expected value.");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ node_path_relative.get_name_count() == 3,
+ "The returned number of names should match the expected value.");
+
+ CHECK_MESSAGE(
+ node_path_relative.get_subname(0) == "position",
+ "The returned subname at index 0 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_relative.get_subname(1) == "x",
+ "The returned subname at index 1 should match the expected value.");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ node_path_relative.get_subname(2) == "",
+ "The returned subname at invalid index 2 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_relative.get_subname(-1) == "",
+ "The returned subname at invalid index -1 should match the expected value.");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ node_path_relative.get_subname_count() == 2,
+ "The returned number of subnames should match the expected value.");
+
+ CHECK_MESSAGE(
+ !node_path_relative.is_absolute(),
+ "The node path should be considered relative.");
+
+ CHECK_MESSAGE(
+ !node_path_relative.is_empty(),
+ "The node path shouldn't be considered empty.");
+}
+
+TEST_CASE("[NodePath] Absolute path") {
+ const NodePath node_path_aboslute = NodePath("/root/Sprite2D");
+
+ CHECK_MESSAGE(
+ node_path_aboslute.get_as_property_path() == NodePath(":root/Sprite2D"),
+ "The returned property path should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_aboslute.get_concatenated_subnames() == "",
+ "The returned concatenated subnames should match the expected value.");
+
+ CHECK_MESSAGE(
+ node_path_aboslute.get_name(0) == "root",
+ "The returned name at index 0 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_aboslute.get_name(1) == "Sprite2D",
+ "The returned name at index 1 should match the expected value.");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ node_path_aboslute.get_name(2) == "",
+ "The returned name at invalid index 2 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_aboslute.get_name(-1) == "",
+ "The returned name at invalid index -1 should match the expected value.");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ node_path_aboslute.get_name_count() == 2,
+ "The returned number of names should match the expected value.");
+
+ CHECK_MESSAGE(
+ node_path_aboslute.get_subname_count() == 0,
+ "The returned number of subnames should match the expected value.");
+
+ CHECK_MESSAGE(
+ node_path_aboslute.is_absolute(),
+ "The node path should be considered absolute.");
+
+ CHECK_MESSAGE(
+ !node_path_aboslute.is_empty(),
+ "The node path shouldn't be considered empty.");
+}
+
+TEST_CASE("[NodePath] Empty path") {
+ const NodePath node_path_empty = NodePath();
+
+ CHECK_MESSAGE(
+ node_path_empty.get_as_property_path() == NodePath(),
+ "The returned property path should match the expected value.");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ node_path_empty.get_concatenated_subnames() == "",
+ "The returned concatenated subnames should match the expected value.");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ node_path_empty.get_name_count() == 0,
+ "The returned number of names should match the expected value.");
+
+ CHECK_MESSAGE(
+ node_path_empty.get_subname_count() == 0,
+ "The returned number of subnames should match the expected value.");
+
+ CHECK_MESSAGE(
+ !node_path_empty.is_absolute(),
+ "The node path shouldn't be considered absolute.");
+
+ CHECK_MESSAGE(
+ node_path_empty.is_empty(),
+ "The node path should be considered empty.");
+}
+} // namespace TestNodePath
+
+#endif // TEST_NODE_PATH_H
diff --git a/tests/test_string.h b/tests/core/string/test_string.h
index b041cb2f49..cd1b421ce8 100644
--- a/tests/test_string.h
+++ b/tests/core/string/test_string.h
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -31,26 +31,16 @@
#ifndef TEST_STRING_H
#define TEST_STRING_H
-#include <inttypes.h>
-#include <stdio.h>
-#include <wchar.h>
-
-#include "core/io/ip_address.h"
-#include "core/os/main_loop.h"
-#include "core/os/os.h"
-#include "core/ustring.h"
-
-#ifdef MODULE_REGEX_ENABLED
-#include "modules/regex/regex.h"
-#endif
+#include "core/string/ustring.h"
#include "tests/test_macros.h"
namespace TestString {
int u32scmp(const char32_t *l, const char32_t *r) {
- for (; *l == *r && *l && *r; l++, r++)
- ;
+ for (; *l == *r && *l && *r; l++, r++) {
+ // Continue.
+ }
return *l - *r;
}
@@ -99,12 +89,12 @@ TEST_CASE("[String] UTF8") {
static const char32_t u32str[] = { 0x0045, 0x0020, 0x304A, 0x360F, 0x3088, 0x3046, 0x1F3A4, 0 };
static const uint8_t u8str[] = { 0x45, 0x20, 0xE3, 0x81, 0x8A, 0xE3, 0x98, 0x8F, 0xE3, 0x82, 0x88, 0xE3, 0x81, 0x86, 0xF0, 0x9F, 0x8E, 0xA4, 0 };
String s = u32str;
- bool err = s.parse_utf8(s.utf8().get_data());
- CHECK(!err);
+ Error err = s.parse_utf8(s.utf8().get_data());
+ CHECK(err == OK);
CHECK(s == u32str);
err = s.parse_utf8((const char *)u8str);
- CHECK(!err);
+ CHECK(err == OK);
CHECK(s == u32str);
CharString cs = (const char *)u8str;
@@ -116,12 +106,12 @@ TEST_CASE("[String] UTF16") {
static const char32_t u32str[] = { 0x0045, 0x0020, 0x304A, 0x360F, 0x3088, 0x3046, 0x1F3A4, 0 };
static const char16_t u16str[] = { 0x0045, 0x0020, 0x304A, 0x360F, 0x3088, 0x3046, 0xD83C, 0xDFA4, 0 };
String s = u32str;
- bool err = s.parse_utf16(s.utf16().get_data());
- CHECK(!err);
+ Error err = s.parse_utf16(s.utf16().get_data());
+ CHECK(err == OK);
CHECK(s == u32str);
err = s.parse_utf16(u16str);
- CHECK(!err);
+ CHECK(err == OK);
CHECK(s == u32str);
Char16String cs = u16str;
@@ -133,8 +123,8 @@ TEST_CASE("[String] UTF8 with BOM") {
static const char32_t u32str[] = { 0x0045, 0x0020, 0x304A, 0x360F, 0x3088, 0x3046, 0x1F3A4, 0 };
static const uint8_t u8str[] = { 0xEF, 0xBB, 0xBF, 0x45, 0x20, 0xE3, 0x81, 0x8A, 0xE3, 0x98, 0x8F, 0xE3, 0x82, 0x88, 0xE3, 0x81, 0x86, 0xF0, 0x9F, 0x8E, 0xA4, 0 };
String s;
- bool err = s.parse_utf8((const char *)u8str);
- CHECK(!err);
+ Error err = s.parse_utf8((const char *)u8str);
+ CHECK(err == OK);
CHECK(s == u32str);
CharString cs = (const char *)u8str;
@@ -147,12 +137,12 @@ TEST_CASE("[String] UTF16 with BOM") {
static const char16_t u16str[] = { 0xFEFF, 0x0020, 0x0045, 0x304A, 0x360F, 0x3088, 0x3046, 0xD83C, 0xDFA4, 0 };
static const char16_t u16str_swap[] = { 0xFFFE, 0x2000, 0x4500, 0x4A30, 0x0F36, 0x8830, 0x4630, 0x3CD8, 0xA4DF, 0 };
String s;
- bool err = s.parse_utf16(u16str);
- CHECK(!err);
+ Error err = s.parse_utf16(u16str);
+ CHECK(err == OK);
CHECK(s == u32str);
err = s.parse_utf16(u16str_swap);
- CHECK(!err);
+ CHECK(err == OK);
CHECK(s == u32str);
Char16String cs = u16str;
@@ -162,29 +152,62 @@ TEST_CASE("[String] UTF16 with BOM") {
CHECK(String::utf16(cs) == s);
}
-TEST_CASE("[String] Invalid UTF8") {
+TEST_CASE("[String] UTF8 with CR") {
+ const String base = U"Hello darkness\r\nMy old friend\nI've come to talk\rWith you again";
+
+ String keep_cr;
+ Error err = keep_cr.parse_utf8(base.utf8().get_data());
+ CHECK(err == OK);
+ CHECK(keep_cr == base);
+
+ String no_cr;
+ err = no_cr.parse_utf8(base.utf8().get_data(), -1, true); // Skip CR.
+ CHECK(err == OK);
+ CHECK(no_cr == base.replace("\r", ""));
+}
+
+TEST_CASE("[String] Invalid UTF8 (non-standard)") {
ERR_PRINT_OFF
- static const uint8_t u8str[] = { 0x45, 0xE3, 0x81, 0x8A, 0x8F, 0xE3, 0xE3, 0x98, 0x8F, 0xE3, 0x82, 0x88, 0xE3, 0x81, 0x86, 0xF0, 0x9F, 0x8E, 0xA4, 0 };
+ static const uint8_t u8str[] = { 0x45, 0xE3, 0x81, 0x8A, 0xE3, 0x82, 0x88, 0xE3, 0x81, 0x86, 0xF0, 0x9F, 0x8E, 0xA4, 0xF0, 0x82, 0x82, 0xAC, 0xED, 0xA0, 0x81, 0 };
+ // + +2 +2 +2 +3 overlong +3 unpaired +2
+ static const char32_t u32str[] = { 0x45, 0x304A, 0x3088, 0x3046, 0x1F3A4, 0x20AC, 0xD801, 0 };
String s;
- bool err = s.parse_utf8((const char *)u8str);
- CHECK(err);
- CHECK(s == String());
+ Error err = s.parse_utf8((const char *)u8str);
+ CHECK(err == ERR_PARSE_ERROR);
+ CHECK(s == u32str);
CharString cs = (const char *)u8str;
- CHECK(String::utf8(cs) == String());
+ CHECK(String::utf8(cs) == s);
ERR_PRINT_ON
}
-TEST_CASE("[String] Invalid UTF16") {
+TEST_CASE("[String] Invalid UTF8 (unrecoverable)") {
+ ERR_PRINT_OFF
+ static const uint8_t u8str[] = { 0x45, 0xE3, 0x81, 0x8A, 0x8F, 0xE3, 0xE3, 0x98, 0x8F, 0xE3, 0x82, 0x88, 0xE3, 0x81, 0x86, 0xC0, 0x80, 0xF0, 0x9F, 0x8E, 0xA4, 0xF0, 0x82, 0x82, 0xAC, 0xED, 0xA0, 0x81, 0 };
+ // + +2 inv +2 inv inv inv +2 +2 ovl NUL +1 +3 overlong +3 unpaired +2
+ static const char32_t u32str[] = { 0x45, 0x304A, 0x20, 0x20, 0x20, 0x20, 0x3088, 0x3046, 0x20, 0x1F3A4, 0x20AC, 0xD801, 0 };
+ String s;
+ Error err = s.parse_utf8((const char *)u8str);
+ CHECK(err == ERR_INVALID_DATA);
+ CHECK(s == u32str);
+
+ CharString cs = (const char *)u8str;
+ CHECK(String::utf8(cs) == s);
+ ERR_PRINT_ON
+}
+
+TEST_CASE("[String] Invalid UTF16 (non-standard)") {
ERR_PRINT_OFF
static const char16_t u16str[] = { 0x0045, 0x304A, 0x3088, 0x3046, 0xDFA4, 0 };
+ // + + + + unpaired
+ static const char32_t u32str[] = { 0x0045, 0x304A, 0x3088, 0x3046, 0xDFA4, 0 };
String s;
- bool err = s.parse_utf16(u16str);
- CHECK(err);
- CHECK(s == String());
+ Error err = s.parse_utf16(u16str);
+ CHECK(err == ERR_PARSE_ERROR);
+ CHECK(s == u32str);
Char16String cs = u16str;
- CHECK(String::utf16(cs) == String());
+ CHECK(String::utf16(cs) == s);
ERR_PRINT_ON
}
@@ -248,19 +271,32 @@ TEST_CASE("[String] Testing size and length of string") {
}
TEST_CASE("[String] Testing for empty string") {
- CHECK(!String("Mellon").empty());
+ CHECK(!String("Mellon").is_empty());
// do this more than once, to check for string corruption
- CHECK(String("").empty());
- CHECK(String("").empty());
- CHECK(String("").empty());
+ CHECK(String("").is_empty());
+ CHECK(String("").is_empty());
+ CHECK(String("").is_empty());
+}
+
+TEST_CASE("[String] Contains") {
+ String s = "C:\\Godot\\project\\string_test.tscn";
+ CHECK(s.contains(":\\"));
+ CHECK(s.contains("Godot"));
+ CHECK(s.contains(String("project\\string_test")));
+ CHECK(s.contains(String("\\string_test.tscn")));
+
+ CHECK(!s.contains("://"));
+ CHECK(!s.contains("Godoh"));
+ CHECK(!s.contains(String("project\\string test")));
+ CHECK(!s.contains(String("\\char_test.tscn")));
}
TEST_CASE("[String] Test chr") {
CHECK(String::chr('H') == "H");
CHECK(String::chr(0x3012)[0] == 0x3012);
ERR_PRINT_OFF
- CHECK(String::chr(0xd812)[0] == 0xfffd); // Unpaired UTF-16 surrogate
- CHECK(String::chr(0x20d812)[0] == 0xfffd); // Outside UTF-32 range
+ CHECK(String::chr(0xd812)[0] == 0xd812); // Unpaired UTF-16 surrogate
+ CHECK(String::chr(0x20d812)[0] == 0x20d812); // Outside UTF-32 range
ERR_PRINT_ON
}
@@ -270,7 +306,7 @@ TEST_CASE("[String] Operator []") {
a[6] = 'C';
CHECK(a == "Sugar Cane");
CHECK(a[1] == 'u');
- CHECK(a.ord_at(1) == 'u');
+ CHECK(a.unicode_at(1) == 'u');
}
TEST_CASE("[String] Case function test") {
@@ -303,6 +339,7 @@ TEST_CASE("[String] hex_encode_buffer") {
TEST_CASE("[String] Substr") {
String s = "Killer Baby";
CHECK(s.substr(3, 4) == "ler ");
+ CHECK(s.substr(3) == "ler Baby");
}
TEST_CASE("[String] Find") {
@@ -353,13 +390,57 @@ TEST_CASE("[String] Insertion") {
}
TEST_CASE("[String] Number to string") {
+ CHECK(String::num(0) == "0");
+ CHECK(String::num(0.0) == "0"); // No trailing zeros.
+ CHECK(String::num(-0.0) == "-0"); // Includes sign even for zero.
CHECK(String::num(3.141593) == "3.141593");
CHECK(String::num(3.141593, 3) == "3.142");
- CHECK(String::num_real(3.141593) == "3.141593");
CHECK(String::num_scientific(30000000) == "3e+07");
CHECK(String::num_int64(3141593) == "3141593");
CHECK(String::num_int64(0xA141593, 16) == "a141593");
CHECK(String::num_int64(0xA141593, 16, true) == "A141593");
+ CHECK(String::num(42.100023, 4) == "42.1"); // No trailing zeros.
+
+ // String::num_real tests.
+ CHECK(String::num_real(1.0) == "1.0");
+ CHECK(String::num_real(1.0, false) == "1");
+ CHECK(String::num_real(9.9) == "9.9");
+ CHECK(String::num_real(9.99) == "9.99");
+ CHECK(String::num_real(9.999) == "9.999");
+ CHECK(String::num_real(9.9999) == "9.9999");
+ CHECK(String::num_real(3.141593) == "3.141593");
+ CHECK(String::num_real(3.141) == "3.141"); // No trailing zeros.
+#ifdef REAL_T_IS_DOUBLE
+ CHECK_MESSAGE(String::num_real(123.456789) == "123.456789", "Prints the appropriate amount of digits for real_t = double.");
+ CHECK_MESSAGE(String::num_real(-123.456789) == "-123.456789", "Prints the appropriate amount of digits for real_t = double.");
+ CHECK_MESSAGE(String::num_real(Math_PI) == "3.14159265358979", "Prints the appropriate amount of digits for real_t = double.");
+ CHECK_MESSAGE(String::num_real(3.1415f) == "3.1414999961853", "Prints more digits of 32-bit float when real_t = double (ones that would be reliable for double) and no trailing zero.");
+#else
+ CHECK_MESSAGE(String::num_real(123.456789) == "123.4568", "Prints the appropriate amount of digits for real_t = float.");
+ CHECK_MESSAGE(String::num_real(-123.456789) == "-123.4568", "Prints the appropriate amount of digits for real_t = float.");
+ CHECK_MESSAGE(String::num_real(Math_PI) == "3.141593", "Prints the appropriate amount of digits for real_t = float.");
+ CHECK_MESSAGE(String::num_real(3.1415f) == "3.1415", "Prints only reliable digits of 32-bit float when real_t = float.");
+#endif // REAL_T_IS_DOUBLE
+
+ // Checks doubles with many decimal places.
+ CHECK(String::num(0.0000012345432123454321, -1) == "0.00000123454321"); // -1 uses 14 as sane default.
+ CHECK(String::num(0.0000012345432123454321) == "0.00000123454321"); // -1 is the default value.
+ CHECK(String::num(-0.0000012345432123454321) == "-0.00000123454321");
+ CHECK(String::num(-10000.0000012345432123454321) == "-10000.0000012345");
+ CHECK(String::num(0.0000000000012345432123454321) == "0.00000000000123");
+ CHECK(String::num(0.0000000000012345432123454321, 3) == "0");
+
+ // Note: When relevant (remainder > 0.5), the last digit gets rounded up,
+ // which can also lead to not include a trailing zero, e.g. "...89" -> "...9".
+ CHECK(String::num(0.0000056789876567898765) == "0.00000567898766"); // Should round last digit.
+ CHECK(String::num(10000.000005678999999999) == "10000.000005679"); // We cut at ...789|99 which is rounded to ...79, so only 13 decimals.
+ CHECK(String::num(42.12999999, 6) == "42.13"); // Also happens with lower decimals count.
+
+ // 32 is MAX_DECIMALS. We can't reliably store that many so we can't compare against a string,
+ // but we can check that the string length is 34 (32 + 2 for "0.").
+ CHECK(String::num(0.00000123456789987654321123456789987654321, 32).length() == 34);
+ CHECK(String::num(0.00000123456789987654321123456789987654321, 42).length() == 34); // Should enforce MAX_DECIMALS.
+ CHECK(String::num(10000.00000123456789987654321123456789987654321, 42).length() == 38); // 32 decimals + "10000.".
}
TEST_CASE("[String] String to integer") {
@@ -374,12 +455,9 @@ TEST_CASE("[String] String to integer") {
TEST_CASE("[String] Hex to integer") {
static const char *nums[4] = { "0xFFAE", "22", "0", "AADDAD" };
static const int64_t num[4] = { 0xFFAE, 0x22, 0, 0xAADDAD };
- static const bool wo_prefix[4] = { false, true, true, true };
- static const bool w_prefix[4] = { true, false, true, false };
for (int i = 0; i < 4; i++) {
- CHECK((String(nums[i]).hex_to_int(true) == num[i]) == w_prefix[i]);
- CHECK((String(nums[i]).hex_to_int(false) == num[i]) == wo_prefix[i]);
+ CHECK(String(nums[i]).hex_to_int() == num[i]);
}
}
@@ -392,11 +470,6 @@ TEST_CASE("[String] String to float") {
}
}
-TEST_CASE("[String] CamelCase to underscore") {
- CHECK(String("TestTestStringGD").camelcase_to_underscore(false) == String("Test_Test_String_GD"));
- CHECK(String("TestTestStringGD").camelcase_to_underscore(true) == String("test_test_string_gd"));
-}
-
TEST_CASE("[String] Slicing") {
String s = "Mars,Jupiter,Saturn,Uranus";
@@ -469,21 +542,6 @@ TEST_CASE("[String] Splitting") {
}
}
-TEST_CASE("[String] Erasing") {
- String s = "Josephine is such a cute girl!";
- s.erase(s.find("cute "), String("cute ").length());
- CHECK(s == "Josephine is such a girl!");
-}
-
-#ifdef MODULE_REGEX_ENABLED
-TEST_CASE("[String] Regex substitution") {
- String s = "Double all the vowels.";
- RegEx re("(?<vowel>[aeiou])");
- s = re.sub(s, "$0$vowel", true);
- CHECK(s == "Doouublee aall thee vooweels.");
-}
-#endif
-
struct test_27_data {
char const *data;
char const *part;
@@ -560,7 +618,7 @@ TEST_CASE("[String] sprintf") {
output = format.sprintf(args, &error);
REQUIRE(error == false);
CHECK(output == String("fish % frog"));
- //////// INTS
+ ///// Ints
// Int
format = "fish %d frog";
@@ -610,6 +668,40 @@ TEST_CASE("[String] sprintf") {
REQUIRE(error == false);
CHECK(output == String("fish -5 frog"));
+ // Negative int left padded with spaces.
+ format = "fish %5d frog";
+ args.clear();
+ args.push_back(-5);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish -5 frog"));
+
+ // Negative int left padded with zeros.
+ format = "fish %05d frog";
+ args.clear();
+ args.push_back(-5);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish -0005 frog"));
+
+ // Negative int right padded with spaces.
+ format = "fish %-5d frog";
+ args.clear();
+ args.push_back(-5);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish -5 frog"));
+
+ // Negative int right padded with zeros. (0 ignored)
+ format = "fish %-05d frog";
+ args.clear();
+ args.push_back(-5);
+ ERR_PRINT_OFF; // Silence warning about 0 ignored.
+ output = format.sprintf(args, &error);
+ ERR_PRINT_ON;
+ REQUIRE(error == false);
+ CHECK(output == String("fish -5 frog"));
+
// Hex (lower)
format = "fish %x frog";
args.clear();
@@ -634,7 +726,7 @@ TEST_CASE("[String] sprintf") {
REQUIRE(error == false);
CHECK(output == String("fish 143 frog"));
- ////// REALS
+ ///// Reals
// Real
format = "fish %f frog";
@@ -644,7 +736,7 @@ TEST_CASE("[String] sprintf") {
REQUIRE(error == false);
CHECK(output == String("fish 99.990000 frog"));
- // Real left-padded
+ // Real left-padded.
format = "fish %11f frog";
args.clear();
args.push_back(99.99);
@@ -652,7 +744,15 @@ TEST_CASE("[String] sprintf") {
REQUIRE(error == false);
CHECK(output == String("fish 99.990000 frog"));
- // Real right-padded
+ // Real (infinity) left-padded
+ format = "fish %11f frog";
+ args.clear();
+ args.push_back(INFINITY);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish inf frog"));
+
+ // Real right-padded.
format = "fish %-11f frog";
args.clear();
args.push_back(99.99);
@@ -676,7 +776,7 @@ TEST_CASE("[String] sprintf") {
REQUIRE(error == false);
CHECK(output == String("fish +99.990000 frog"));
- // Real with 1 decimals.
+ // Real with 1 decimal.
format = "fish %.1f frog";
args.clear();
args.push_back(99.99);
@@ -700,7 +800,115 @@ TEST_CASE("[String] sprintf") {
REQUIRE(error == false);
CHECK(output == String("fish 100 frog"));
- /////// Strings.
+ // Negative real right padded with zeros. (0 ignored)
+ format = "fish %-011f frog";
+ args.clear();
+ args.push_back(-99.99);
+ ERR_PRINT_OFF; // Silence warning about 0 ignored.
+ output = format.sprintf(args, &error);
+ ERR_PRINT_ON;
+ REQUIRE(error == false);
+ CHECK(output == String("fish -99.990000 frog"));
+
+ ///// Vectors
+
+ // Vector2
+ format = "fish %v frog";
+ args.clear();
+ args.push_back(Variant(Vector2(19.99, 1.00)));
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish (19.990000, 1.000000) frog"));
+
+ // Vector3
+ format = "fish %v frog";
+ args.clear();
+ args.push_back(Variant(Vector3(19.99, 1.00, -2.05)));
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish (19.990000, 1.000000, -2.050000) frog"));
+
+ // Vector4
+ format = "fish %v frog";
+ args.clear();
+ args.push_back(Variant(Vector4(19.99, 1.00, -2.05, 5.5)));
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish (19.990000, 1.000000, -2.050000, 5.500000) frog"));
+
+ // Vector with negative values.
+ format = "fish %v frog";
+ args.clear();
+ args.push_back(Variant(Vector2(-19.99, -1.00)));
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish (-19.990000, -1.000000) frog"));
+
+ // Vector left-padded.
+ format = "fish %11v frog";
+ args.clear();
+ args.push_back(Variant(Vector3(19.99, 1.00, -2.05)));
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish ( 19.990000, 1.000000, -2.050000) frog"));
+
+ // Vector left-padded with inf/nan
+ format = "fish %11v frog";
+ args.clear();
+ args.push_back(Variant(Vector2(INFINITY, NAN)));
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish ( inf, nan) frog"));
+
+ // Vector right-padded.
+ format = "fish %-11v frog";
+ args.clear();
+ args.push_back(Variant(Vector3(19.99, 1.00, -2.05)));
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish (19.990000 , 1.000000 , -2.050000 ) frog"));
+
+ // Vector left-padded with zeros.
+ format = "fish %011v frog";
+ args.clear();
+ args.push_back(Variant(Vector3(19.99, 1.00, -2.05)));
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish (0019.990000, 0001.000000, -002.050000) frog"));
+
+ // Vector given Vector3i.
+ format = "fish %v frog";
+ args.clear();
+ args.push_back(Variant(Vector3i(19, 1, -2)));
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish (19.000000, 1.000000, -2.000000) frog"));
+
+ // Vector with 1 decimal.
+ format = "fish %.1v frog";
+ args.clear();
+ args.push_back(Variant(Vector3(19.99, 1.00, -2.05)));
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish (20.0, 1.0, -2.0) frog"));
+
+ // Vector with 12 decimals.
+ format = "fish %.12v frog";
+ args.clear();
+ args.push_back(Variant(Vector3(19.00, 1.00, -2.00)));
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish (19.000000000000, 1.000000000000, -2.000000000000) frog"));
+
+ // Vector with no decimals.
+ format = "fish %.v frog";
+ args.clear();
+ args.push_back(Variant(Vector3(19.99, 1.00, -2.05)));
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish (20, 1, -2) frog"));
+
+ ///// Strings
// String
format = "fish %s frog";
@@ -710,7 +918,7 @@ TEST_CASE("[String] sprintf") {
REQUIRE(error == false);
CHECK(output == String("fish cheese frog"));
- // String left-padded
+ // String left-padded.
format = "fish %10s frog";
args.clear();
args.push_back("cheese");
@@ -718,7 +926,7 @@ TEST_CASE("[String] sprintf") {
REQUIRE(error == false);
CHECK(output == String("fish cheese frog"));
- // String right-padded
+ // String right-padded.
format = "fish %-10s frog";
args.clear();
args.push_back("cheese");
@@ -746,7 +954,7 @@ TEST_CASE("[String] sprintf") {
///// Dynamic width
- // String dynamic width
+ // String dynamic width.
format = "fish %*s frog";
args.clear();
args.push_back(10);
@@ -755,7 +963,7 @@ TEST_CASE("[String] sprintf") {
REQUIRE(error == false);
REQUIRE(output == String("fish cheese frog"));
- // Int dynamic width
+ // Int dynamic width.
format = "fish %*d frog";
args.clear();
args.push_back(10);
@@ -764,7 +972,7 @@ TEST_CASE("[String] sprintf") {
REQUIRE(error == false);
REQUIRE(output == String("fish 99 frog"));
- // Float dynamic width
+ // Float dynamic width.
format = "fish %*.*f frog";
args.clear();
args.push_back(10);
@@ -801,7 +1009,7 @@ TEST_CASE("[String] sprintf") {
REQUIRE(error);
CHECK(output == "incomplete format");
- // Bad character in format string
+ // Bad character in format string.
format = "fish %&f frog";
args.clear();
args.push_back("cheese");
@@ -817,14 +1025,14 @@ TEST_CASE("[String] sprintf") {
REQUIRE(error);
CHECK(output == "too many decimal points in format");
- // * not a number
+ // * not a number or vector.
format = "fish %*f frog";
args.clear();
args.push_back("cheese");
args.push_back(99.99);
output = format.sprintf(args, &error);
REQUIRE(error);
- CHECK(output == "* wants number");
+ CHECK(output == "* wants number or vector");
// Character too long.
format = "fish %c frog";
@@ -865,7 +1073,7 @@ TEST_CASE("[String] is_subsequence_of") {
String a = "is subsequence of";
CHECK(String("sub").is_subsequence_of(a));
CHECK(!String("Sub").is_subsequence_of(a));
- CHECK(String("Sub").is_subsequence_ofi(a));
+ CHECK(String("Sub").is_subsequence_ofn(a));
}
TEST_CASE("[String] match") {
@@ -876,10 +1084,10 @@ TEST_CASE("[String] match") {
}
TEST_CASE("[String] IPVX address to string") {
- IP_Address ip0("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
- IP_Address ip(0x0123, 0x4567, 0x89ab, 0xcdef, true);
- IP_Address ip2("fe80::52e5:49ff:fe93:1baf");
- IP_Address ip3("::ffff:192.168.0.1");
+ IPAddress ip0("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
+ IPAddress ip(0x0123, 0x4567, 0x89ab, 0xcdef, true);
+ IPAddress ip2("fe80::52e5:49ff:fe93:1baf");
+ IPAddress ip3("::ffff:192.168.0.1");
String ip4 = "192.168.0.1";
CHECK(ip4.is_valid_ip_address());
@@ -903,8 +1111,36 @@ TEST_CASE("[String] IPVX address to string") {
}
TEST_CASE("[String] Capitalize against many strings") {
- String input = "bytes2var";
- String output = "Bytes 2 Var";
+ String input = "2D";
+ String output = "2d";
+ CHECK(input.capitalize() == output);
+
+ input = "2d";
+ output = "2d";
+ CHECK(input.capitalize() == output);
+
+ input = "2db";
+ output = "2 Db";
+ CHECK(input.capitalize() == output);
+
+ input = "HTML5 Html5 html5 html_5";
+ output = "Html 5 Html 5 Html 5 Html 5";
+ CHECK(input.capitalize() == output);
+
+ input = "Node2D Node2d NODE2D NODE_2D node_2d";
+ output = "Node 2d Node 2d Node 2d Node 2d Node 2d";
+ CHECK(input.capitalize() == output);
+
+ input = "Node2DPosition";
+ output = "Node 2d Position";
+ CHECK(input.capitalize() == output);
+
+ input = "Number2Digits";
+ output = "Number 2 Digits";
+ CHECK(input.capitalize() == output);
+
+ input = "bytes2var";
+ output = "Bytes 2 Var";
CHECK(input.capitalize() == output);
input = "linear2db";
@@ -919,10 +1155,6 @@ TEST_CASE("[String] Capitalize against many strings") {
output = "Sha 256";
CHECK(input.capitalize() == output);
- input = "2db";
- output = "2 Db";
- CHECK(input.capitalize() == output);
-
input = "PascalCase";
output = "Pascal Case";
CHECK(input.capitalize() == output);
@@ -960,6 +1192,50 @@ TEST_CASE("[String] Capitalize against many strings") {
CHECK(input.capitalize() == output);
}
+struct StringCasesTestCase {
+ const char *input;
+ const char *camel_case;
+ const char *pascal_case;
+ const char *snake_case;
+};
+
+TEST_CASE("[String] Checking case conversion methods") {
+ StringCasesTestCase test_cases[] = {
+ /* clang-format off */
+ { "2D", "2d", "2d", "2d" },
+ { "2d", "2d", "2d", "2d" },
+ { "2db", "2Db", "2Db", "2_db" },
+ { "Vector3", "vector3", "Vector3", "vector_3" },
+ { "sha256", "sha256", "Sha256", "sha_256" },
+ { "Node2D", "node2d", "Node2d", "node_2d" },
+ { "RichTextLabel", "richTextLabel", "RichTextLabel", "rich_text_label" },
+ { "HTML5", "html5", "Html5", "html_5" },
+ { "Node2DPosition", "node2dPosition", "Node2dPosition", "node_2d_position" },
+ { "Number2Digits", "number2Digits", "Number2Digits", "number_2_digits" },
+ { "get_property_list", "getPropertyList", "GetPropertyList", "get_property_list" },
+ { "get_camera_2d", "getCamera2d", "GetCamera2d", "get_camera_2d" },
+ { "_physics_process", "physicsProcess", "PhysicsProcess", "_physics_process" },
+ { "bytes2var", "bytes2Var", "Bytes2Var", "bytes_2_var" },
+ { "linear2db", "linear2Db", "Linear2Db", "linear_2_db" },
+ { "sha256sum", "sha256Sum", "Sha256Sum", "sha_256_sum" },
+ { "camelCase", "camelCase", "CamelCase", "camel_case" },
+ { "PascalCase", "pascalCase", "PascalCase", "pascal_case" },
+ { "snake_case", "snakeCase", "SnakeCase", "snake_case" },
+ { "Test TEST test", "testTestTest", "TestTestTest", "test_test_test" },
+ { nullptr, nullptr, nullptr, nullptr },
+ /* clang-format on */
+ };
+
+ int idx = 0;
+ while (test_cases[idx].input != nullptr) {
+ String input = test_cases[idx].input;
+ CHECK(input.to_camel_case() == test_cases[idx].camel_case);
+ CHECK(input.to_pascal_case() == test_cases[idx].pascal_case);
+ CHECK(input.to_snake_case() == test_cases[idx].snake_case);
+ idx++;
+ }
+}
+
TEST_CASE("[String] Checking string is empty when it should be") {
bool state = true;
bool success;
@@ -1059,9 +1335,9 @@ TEST_CASE("[String] lstrip and rstrip") {
#undef STRIP_TEST
}
-TEST_CASE("[String] ensuring empty string into parse_utf8 passes empty string") {
+TEST_CASE("[String] Ensuring empty string into parse_utf8 passes empty string") {
String empty;
- CHECK(empty.parse_utf8(NULL, -1));
+ CHECK(empty.parse_utf8(nullptr, -1) == ERR_INVALID_DATA);
}
TEST_CASE("[String] Cyrillic to_lower()") {
@@ -1126,6 +1402,25 @@ TEST_CASE("[String] c-escape/unescape") {
CHECK(s.c_escape().c_unescape() == s);
}
+TEST_CASE("[String] indent") {
+ static const char *input[] = {
+ "",
+ "aaa\nbbb",
+ "\tcontains\n\tindent",
+ "empty\n\nline",
+ };
+ static const char *expected[] = {
+ "",
+ "\taaa\n\tbbb",
+ "\t\tcontains\n\t\tindent",
+ "\tempty\n\n\tline",
+ };
+
+ for (int i = 0; i < 3; i++) {
+ CHECK(String(input[i]).indent("\t") == expected[i]);
+ }
+}
+
TEST_CASE("[String] dedent") {
String s = " aaa\n bbb";
String t = "aaa\nbbb";
@@ -1133,21 +1428,23 @@ TEST_CASE("[String] dedent") {
}
TEST_CASE("[String] Path functions") {
- static const char *path[4] = { "C:\\Godot\\project\\test.tscn", "/Godot/project/test.xscn", "../Godot/project/test.scn", "Godot\\test.doc" };
- static const char *base_dir[4] = { "C:\\Godot\\project", "/Godot/project", "../Godot/project", "Godot" };
- static const char *base_name[4] = { "C:\\Godot\\project\\test", "/Godot/project/test", "../Godot/project/test", "Godot\\test" };
- static const char *ext[4] = { "tscn", "xscn", "scn", "doc" };
- static const char *file[4] = { "test.tscn", "test.xscn", "test.scn", "test.doc" };
- static const bool abs[4] = { true, true, false, false };
-
- for (int i = 0; i < 4; i++) {
+ static const char *path[8] = { "C:\\Godot\\project\\test.tscn", "/Godot/project/test.xscn", "../Godot/project/test.scn", "Godot\\test.doc", "C:\\test.", "res://test", "user://test", "/.test" };
+ static const char *base_dir[8] = { "C:\\Godot\\project", "/Godot/project", "../Godot/project", "Godot", "C:\\", "res://", "user://", "/" };
+ static const char *base_name[8] = { "C:\\Godot\\project\\test", "/Godot/project/test", "../Godot/project/test", "Godot\\test", "C:\\test", "res://test", "user://test", "/" };
+ static const char *ext[8] = { "tscn", "xscn", "scn", "doc", "", "", "", "test" };
+ static const char *file[8] = { "test.tscn", "test.xscn", "test.scn", "test.doc", "test.", "test", "test", ".test" };
+ static const char *simplified[8] = { "C:/Godot/project/test.tscn", "/Godot/project/test.xscn", "Godot/project/test.scn", "Godot/test.doc", "C:/test.", "res://test", "user://test", "/.test" };
+ static const bool abs[8] = { true, true, false, false, true, true, true, true };
+
+ for (int i = 0; i < 8; i++) {
CHECK(String(path[i]).get_base_dir() == base_dir[i]);
CHECK(String(path[i]).get_basename() == base_name[i]);
CHECK(String(path[i]).get_extension() == ext[i]);
CHECK(String(path[i]).get_file() == file[i]);
- CHECK(String(path[i]).is_abs_path() == abs[i]);
- CHECK(String(path[i]).is_rel_path() != abs[i]);
- CHECK(String(path[i]).simplify_path().get_base_dir().plus_file(file[i]) == String(path[i]).simplify_path());
+ CHECK(String(path[i]).is_absolute_path() == abs[i]);
+ CHECK(String(path[i]).is_relative_path() != abs[i]);
+ CHECK(String(path[i]).simplify_path() == String(simplified[i]));
+ CHECK(String(path[i]).simplify_path().get_base_dir().path_join(file[i]) == String(path[i]).simplify_path());
}
static const char *file_name[3] = { "test.tscn", "test://.xscn", "?tes*t.scn" };
@@ -1168,20 +1465,23 @@ TEST_CASE("[String] hash") {
CHECK(a.hash64() != c.hash64());
}
-TEST_CASE("[String] http_escape/unescape") {
+TEST_CASE("[String] uri_encode/unescape") {
String s = "Godot Engine:'docs'";
String t = "Godot%20Engine%3A%27docs%27";
- CHECK(s.http_escape() == t);
- CHECK(t.http_unescape() == s);
-}
+ String x1 = "T%C4%93%C5%A1t";
+ static const uint8_t u8str[] = { 0x54, 0xC4, 0x93, 0xC5, 0xA1, 0x74, 0x00 };
+ String x2 = String::utf8((const char *)u8str);
+ String x3 = U"Tēšt";
-TEST_CASE("[String] percent_encode/decode") { // Note: is it redundant? Seems to be same as http_escape/unescape but in lower case.
- String s = "Godot Engine:'docs'";
- String t = "Godot%20Engine%3a%27docs%27";
+ CHECK(x1.uri_decode() == x2);
+ CHECK(x1.uri_decode() == x3);
+ CHECK((x1 + x3).uri_decode() == (x2 + x3)); // Mixed unicode and URL encoded string, e.g. GTK+ bookmark.
+ CHECK(x2.uri_encode() == x1);
+ CHECK(x3.uri_encode() == x1);
- CHECK(s.percent_encode() == t);
- CHECK(t.percent_decode() == s);
+ CHECK(s.uri_encode() == t);
+ CHECK(t.uri_decode() == s);
}
TEST_CASE("[String] xml_escape/unescape") {
@@ -1190,6 +1490,52 @@ TEST_CASE("[String] xml_escape/unescape") {
CHECK(s.xml_escape(false).xml_unescape() == s);
}
+TEST_CASE("[String] xml_unescape") {
+ // Named entities
+ String input = "&quot;&amp;&apos;&lt;&gt;";
+ CHECK(input.xml_unescape() == "\"&\'<>");
+
+ // Numeric entities
+ input = "&#x41;&#66;";
+ CHECK(input.xml_unescape() == "AB");
+
+ input = "&#0;&x#0;More text";
+ String result = input.xml_unescape();
+ // Didn't put in a leading NUL and terminate the string
+ CHECK(input.length() > 0);
+ CHECK(input[0] != '\0');
+ // Entity should be left as-is if invalid
+ CHECK(input.xml_unescape() == input);
+
+ // Check near char32_t range
+ input = "&#xFFFFFFFF;";
+ result = input.xml_unescape();
+ CHECK(result.length() == 1);
+ CHECK(result[0] == 0xFFFFFFFF);
+ input = "&#4294967295;";
+ result = input.xml_unescape();
+ CHECK(result.length() == 1);
+ CHECK(result[0] == 0xFFFFFFFF);
+
+ // Check out of range of char32_t
+ input = "&#xFFFFFFFFF;";
+ CHECK(input.xml_unescape() == input);
+ input = "&#4294967296;";
+ CHECK(input.xml_unescape() == input);
+
+ // Shouldn't consume without ending in a ';'
+ input = "&#66";
+ CHECK(input.xml_unescape() == input);
+ input = "&#x41";
+ CHECK(input.xml_unescape() == input);
+
+ // Invalid characters should make the entity ignored
+ input = "&#x41SomeIrrelevantText;";
+ CHECK(input.xml_unescape() == input);
+ input = "&#66SomeIrrelevantText;";
+ CHECK(input.xml_unescape() == input);
+}
+
TEST_CASE("[String] Strip escapes") {
String s = "\t\tTest Test\r\n Test";
CHECK(s.strip_escapes() == "Test Test Test");
@@ -1219,8 +1565,10 @@ TEST_CASE("[String] Trim") {
TEST_CASE("[String] Right/Left") {
String s = "aaaTestbbb";
// ^
- CHECK(s.right(6) == "tbbb");
+ CHECK(s.right(6) == "estbbb");
+ CHECK(s.right(-6) == "tbbb");
CHECK(s.left(6) == "aaaTes");
+ CHECK(s.left(-6) == "aaaT");
}
TEST_CASE("[String] Repeat") {
@@ -1281,7 +1629,7 @@ TEST_CASE("[String] Is_*") {
for (int i = 0; i < 12; i++) {
String s = String(data[i]);
CHECK(s.is_numeric() == isnum[i]);
- CHECK(s.is_valid_integer() == isint[i]);
+ CHECK(s.is_valid_int() == isint[i]);
CHECK(s.is_valid_hex_number(false) == ishex[i]);
CHECK(s.is_valid_hex_number(true) == ishex_p[i]);
CHECK(s.is_valid_float() == isflt[i]);
@@ -1297,6 +1645,123 @@ TEST_CASE("[String] humanize_size") {
CHECK(String::humanize_size(5345555000) == "4.97 GiB");
}
+TEST_CASE("[String] validate_node_name") {
+ String numeric_only = "12345";
+ CHECK(numeric_only.validate_node_name() == "12345");
+
+ String name_with_spaces = "Name with spaces";
+ CHECK(name_with_spaces.validate_node_name() == "Name with spaces");
+
+ String name_with_kana = U"Name with kana ゴドツ";
+ CHECK(name_with_kana.validate_node_name() == U"Name with kana ゴドツ");
+
+ String name_with_invalid_chars = "Name with invalid characters :.@removed!";
+ CHECK(name_with_invalid_chars.validate_node_name() == "Name with invalid characters removed!");
+}
+
+TEST_CASE("[String] validate_identifier") {
+ String empty_string;
+ CHECK(empty_string.validate_identifier() == "_");
+
+ String numeric_only = "12345";
+ CHECK(numeric_only.validate_identifier() == "_2345");
+
+ String name_with_spaces = "Name with spaces";
+ CHECK(name_with_spaces.validate_identifier() == "Name_with_spaces");
+
+ String name_with_invalid_chars = String::utf8("Invalid characters:@*#&世界");
+ CHECK(name_with_invalid_chars.validate_identifier() == "Invalid_characters_______");
+}
+
+TEST_CASE("[String] Variant indexed get") {
+ Variant s = String("abcd");
+ bool valid = false;
+ bool oob = true;
+
+ String r = s.get_indexed(1, valid, oob);
+
+ CHECK(valid);
+ CHECK_FALSE(oob);
+ CHECK_EQ(r, String("b"));
+}
+
+TEST_CASE("[String] Variant validated indexed get") {
+ Variant s = String("abcd");
+
+ Variant::ValidatedIndexedGetter getter = Variant::get_member_validated_indexed_getter(Variant::STRING);
+
+ Variant r;
+ bool oob = true;
+ getter(&s, 1, &r, &oob);
+
+ CHECK_FALSE(oob);
+ CHECK_EQ(r, String("b"));
+}
+
+TEST_CASE("[String] Variant ptr indexed get") {
+ String s("abcd");
+
+ Variant::PTRIndexedGetter getter = Variant::get_member_ptr_indexed_getter(Variant::STRING);
+
+ String r;
+ getter(&s, 1, &r);
+
+ CHECK_EQ(r, String("b"));
+}
+
+TEST_CASE("[String] Variant indexed set") {
+ Variant s = String("abcd");
+ bool valid = false;
+ bool oob = true;
+
+ s.set_indexed(1, String("z"), valid, oob);
+
+ CHECK(valid);
+ CHECK_FALSE(oob);
+ CHECK_EQ(s, String("azcd"));
+}
+
+TEST_CASE("[String] Variant validated indexed set") {
+ Variant s = String("abcd");
+
+ Variant::ValidatedIndexedSetter setter = Variant::get_member_validated_indexed_setter(Variant::STRING);
+
+ Variant v = String("z");
+ bool oob = true;
+ setter(&s, 1, &v, &oob);
+
+ CHECK_FALSE(oob);
+ CHECK_EQ(s, String("azcd"));
+}
+
+TEST_CASE("[String] Variant ptr indexed set") {
+ String s("abcd");
+
+ Variant::PTRIndexedSetter setter = Variant::get_member_ptr_indexed_setter(Variant::STRING);
+
+ String v("z");
+ setter(&s, 1, &v);
+
+ CHECK_EQ(s, String("azcd"));
+}
+
+TEST_CASE("[Stress][String] Empty via ' == String()'") {
+ for (int i = 0; i < 100000; ++i) {
+ String str = "Hello World!";
+ if (str == String()) {
+ continue;
+ }
+ }
+}
+
+TEST_CASE("[Stress][String] Empty via `is_empty()`") {
+ for (int i = 0; i < 100000; ++i) {
+ String str = "Hello World!";
+ if (str.is_empty()) {
+ continue;
+ }
+ }
+}
} // namespace TestString
#endif // TEST_STRING_H
diff --git a/tests/core/string/test_translation.h b/tests/core/string/test_translation.h
new file mode 100644
index 0000000000..0a1903ccbf
--- /dev/null
+++ b/tests/core/string/test_translation.h
@@ -0,0 +1,182 @@
+/*************************************************************************/
+/* test_translation.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_TRANSLATION_H
+#define TEST_TRANSLATION_H
+
+#include "core/string/optimized_translation.h"
+#include "core/string/translation.h"
+#include "core/string/translation_po.h"
+
+#ifdef TOOLS_ENABLED
+#include "editor/import/resource_importer_csv_translation.h"
+#endif
+
+#include "tests/test_macros.h"
+#include "tests/test_utils.h"
+
+namespace TestTranslation {
+
+TEST_CASE("[Translation] Messages") {
+ Ref<Translation> translation = memnew(Translation);
+ translation->set_locale("fr");
+ translation->add_message("Hello", "Bonjour");
+ CHECK(translation->get_message("Hello") == "Bonjour");
+
+ translation->erase_message("Hello");
+ // The message no longer exists, so it returns an empty string instead.
+ CHECK(translation->get_message("Hello") == "");
+
+ List<StringName> messages;
+ translation->get_message_list(&messages);
+ CHECK(translation->get_message_count() == 0);
+ CHECK(messages.size() == 0);
+
+ translation->add_message("Hello2", "Bonjour2");
+ translation->add_message("Hello3", "Bonjour3");
+ messages.clear();
+ translation->get_message_list(&messages);
+ CHECK(translation->get_message_count() == 2);
+ CHECK(messages.size() == 2);
+ // Messages are stored in a Map, don't assume ordering.
+ CHECK(messages.find("Hello2"));
+ CHECK(messages.find("Hello3"));
+}
+
+TEST_CASE("[TranslationPO] Messages with context") {
+ Ref<TranslationPO> translation = memnew(TranslationPO);
+ translation->set_locale("fr");
+ translation->add_message("Hello", "Bonjour");
+ translation->add_message("Hello", "Salut", "friendly");
+ CHECK(translation->get_message("Hello") == "Bonjour");
+ CHECK(translation->get_message("Hello", "friendly") == "Salut");
+ CHECK(translation->get_message("Hello", "nonexistent_context") == "");
+
+ // Only remove the message for the default context, not the "friendly" context.
+ translation->erase_message("Hello");
+ // The message no longer exists, so it returns an empty string instead.
+ CHECK(translation->get_message("Hello") == "");
+ CHECK(translation->get_message("Hello", "friendly") == "Salut");
+ CHECK(translation->get_message("Hello", "nonexistent_context") == "");
+
+ List<StringName> messages;
+ translation->get_message_list(&messages);
+
+ // `get_message_count()` takes all contexts into account.
+ CHECK(translation->get_message_count() == 1);
+ // Only the default context is taken into account.
+ // Since "Hello" is now only present in a non-default context, it is not counted in the list of messages.
+ CHECK(messages.size() == 0);
+
+ translation->add_message("Hello2", "Bonjour2");
+ translation->add_message("Hello2", "Salut2", "friendly");
+ translation->add_message("Hello3", "Bonjour3");
+ messages.clear();
+ translation->get_message_list(&messages);
+
+ // `get_message_count()` takes all contexts into account.
+ CHECK(translation->get_message_count() == 4);
+ // Only the default context is taken into account.
+ CHECK(messages.size() == 2);
+ // Messages are stored in a Map, don't assume ordering.
+ CHECK(messages.find("Hello2"));
+ CHECK(messages.find("Hello3"));
+}
+
+TEST_CASE("[TranslationPO] Plural messages") {
+ Ref<TranslationPO> translation = memnew(TranslationPO);
+ translation->set_locale("fr");
+ translation->set_plural_rule("Plural-Forms: nplurals=2; plural=(n >= 2);");
+ CHECK(translation->get_plural_forms() == 2);
+
+ PackedStringArray plurals;
+ plurals.push_back("Il y a %d pomme");
+ plurals.push_back("Il y a %d pommes");
+ translation->add_plural_message("There are %d apples", plurals);
+ ERR_PRINT_OFF;
+ // This is invalid, as the number passed to `get_plural_message()` may not be negative.
+ CHECK(vformat(translation->get_plural_message("There are %d apples", "", -1), -1) == "");
+ ERR_PRINT_ON;
+ CHECK(vformat(translation->get_plural_message("There are %d apples", "", 0), 0) == "Il y a 0 pomme");
+ CHECK(vformat(translation->get_plural_message("There are %d apples", "", 1), 1) == "Il y a 1 pomme");
+ CHECK(vformat(translation->get_plural_message("There are %d apples", "", 2), 2) == "Il y a 2 pommes");
+}
+
+TEST_CASE("[OptimizedTranslation] Generate from Translation and read messages") {
+ Ref<Translation> translation = memnew(Translation);
+ translation->set_locale("fr");
+ translation->add_message("Hello", "Bonjour");
+ translation->add_message("Hello2", "Bonjour2");
+ translation->add_message("Hello3", "Bonjour3");
+
+ Ref<OptimizedTranslation> optimized_translation = memnew(OptimizedTranslation);
+ optimized_translation->generate(translation);
+ CHECK(optimized_translation->get_message("Hello") == "Bonjour");
+ CHECK(optimized_translation->get_message("Hello2") == "Bonjour2");
+ CHECK(optimized_translation->get_message("Hello3") == "Bonjour3");
+ CHECK(optimized_translation->get_message("DoesNotExist") == "");
+
+ List<StringName> messages;
+ // `get_message_list()` can't return the list of messages stored in an OptimizedTranslation.
+ optimized_translation->get_message_list(&messages);
+ CHECK(optimized_translation->get_message_count() == 0);
+ CHECK(messages.size() == 0);
+}
+
+#ifdef TOOLS_ENABLED
+TEST_CASE("[Translation] CSV import") {
+ Ref<ResourceImporterCSVTranslation> import_csv_translation = memnew(ResourceImporterCSVTranslation);
+
+ HashMap<StringName, Variant> options;
+ options["compress"] = false;
+ options["delimiter"] = 0;
+
+ List<String> gen_files;
+
+ Error result = import_csv_translation->import(TestUtils::get_data_path("translations.csv"),
+ "", options, nullptr, &gen_files);
+ CHECK(result == OK);
+ CHECK(gen_files.size() == 2);
+
+ for (const String &file : gen_files) {
+ Ref<Translation> translation = ResourceLoader::load(file);
+ CHECK(translation.is_valid());
+ TranslationServer::get_singleton()->add_translation(translation);
+ }
+
+ TranslationServer::get_singleton()->set_locale("de");
+
+ CHECK(Object().tr("GOOD_MORNING", "") == "Guten Morgen");
+}
+#endif // TOOLS_ENABLED
+
+} // namespace TestTranslation
+
+#endif // TEST_TRANSLATION_H
diff --git a/tests/core/templates/test_command_queue.h b/tests/core/templates/test_command_queue.h
new file mode 100644
index 0000000000..db1e436ed9
--- /dev/null
+++ b/tests/core/templates/test_command_queue.h
@@ -0,0 +1,427 @@
+/*************************************************************************/
+/* test_command_queue.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_COMMAND_QUEUE_H
+#define TEST_COMMAND_QUEUE_H
+
+#include "core/config/project_settings.h"
+#include "core/math/random_number_generator.h"
+#include "core/os/os.h"
+#include "core/os/thread.h"
+#include "core/templates/command_queue_mt.h"
+#include "tests/test_macros.h"
+
+namespace TestCommandQueue {
+
+class ThreadWork {
+ Semaphore thread_sem;
+ Semaphore main_sem;
+ Mutex mut;
+ int threading_errors = 0;
+ enum State {
+ MAIN_START,
+ MAIN_DONE,
+ THREAD_START,
+ THREAD_DONE,
+ } state;
+
+public:
+ ThreadWork() {
+ mut.lock();
+ state = MAIN_START;
+ }
+ ~ThreadWork() {
+ CHECK_MESSAGE(threading_errors == 0, "threads did not lock/unlock correctly");
+ }
+ void thread_wait_for_work() {
+ thread_sem.wait();
+ mut.lock();
+ if (state != MAIN_DONE) {
+ threading_errors++;
+ }
+ state = THREAD_START;
+ }
+ void thread_done_work() {
+ if (state != THREAD_START) {
+ threading_errors++;
+ }
+ state = THREAD_DONE;
+ mut.unlock();
+ main_sem.post();
+ }
+
+ void main_wait_for_done() {
+ main_sem.wait();
+ mut.lock();
+ if (state != THREAD_DONE) {
+ threading_errors++;
+ }
+ state = MAIN_START;
+ }
+ void main_start_work() {
+ if (state != MAIN_START) {
+ threading_errors++;
+ }
+ state = MAIN_DONE;
+ mut.unlock();
+ thread_sem.post();
+ }
+};
+
+class SharedThreadState {
+public:
+ ThreadWork reader_threadwork;
+ ThreadWork writer_threadwork;
+
+ CommandQueueMT command_queue = CommandQueueMT(true);
+
+ enum TestMsgType {
+ TEST_MSG_FUNC1_TRANSFORM,
+ TEST_MSG_FUNC2_TRANSFORM_FLOAT,
+ TEST_MSG_FUNC3_TRANSFORMx6,
+ TEST_MSGSYNC_FUNC1_TRANSFORM,
+ TEST_MSGSYNC_FUNC2_TRANSFORM_FLOAT,
+ TEST_MSGRET_FUNC1_TRANSFORM,
+ TEST_MSGRET_FUNC2_TRANSFORM_FLOAT,
+ TEST_MSG_MAX
+ };
+
+ Vector<TestMsgType> message_types_to_write;
+ bool during_writing = false;
+ int message_count_to_read = 0;
+ bool exit_threads = false;
+
+ Thread reader_thread;
+ Thread writer_thread;
+
+ int func1_count = 0;
+
+ void func1(Transform3D t) {
+ func1_count++;
+ }
+ void func2(Transform3D t, float f) {
+ func1_count++;
+ }
+ void func3(Transform3D t1, Transform3D t2, Transform3D t3, Transform3D t4, Transform3D t5, Transform3D t6) {
+ func1_count++;
+ }
+ Transform3D func1r(Transform3D t) {
+ func1_count++;
+ return t;
+ }
+ Transform3D func2r(Transform3D t, float f) {
+ func1_count++;
+ return t;
+ }
+
+ void add_msg_to_write(TestMsgType type) {
+ message_types_to_write.push_back(type);
+ }
+
+ void reader_thread_loop() {
+ reader_threadwork.thread_wait_for_work();
+ while (!exit_threads) {
+ if (message_count_to_read < 0) {
+ command_queue.flush_all();
+ }
+ for (int i = 0; i < message_count_to_read; i++) {
+ command_queue.wait_and_flush();
+ }
+ message_count_to_read = 0;
+
+ reader_threadwork.thread_done_work();
+ reader_threadwork.thread_wait_for_work();
+ }
+ command_queue.flush_all();
+ reader_threadwork.thread_done_work();
+ }
+ static void static_reader_thread_loop(void *stsvoid) {
+ SharedThreadState *sts = static_cast<SharedThreadState *>(stsvoid);
+ sts->reader_thread_loop();
+ }
+
+ void writer_thread_loop() {
+ during_writing = false;
+ writer_threadwork.thread_wait_for_work();
+ while (!exit_threads) {
+ Transform3D tr;
+ Transform3D otr;
+ float f = 1;
+ during_writing = true;
+ for (int i = 0; i < message_types_to_write.size(); i++) {
+ TestMsgType msg_type = message_types_to_write[i];
+ switch (msg_type) {
+ case TEST_MSG_FUNC1_TRANSFORM:
+ command_queue.push(this, &SharedThreadState::func1, tr);
+ break;
+ case TEST_MSG_FUNC2_TRANSFORM_FLOAT:
+ command_queue.push(this, &SharedThreadState::func2, tr, f);
+ break;
+ case TEST_MSG_FUNC3_TRANSFORMx6:
+ command_queue.push(this, &SharedThreadState::func3, tr, tr, tr, tr, tr, tr);
+ break;
+ case TEST_MSGSYNC_FUNC1_TRANSFORM:
+ command_queue.push_and_sync(this, &SharedThreadState::func1, tr);
+ break;
+ case TEST_MSGSYNC_FUNC2_TRANSFORM_FLOAT:
+ command_queue.push_and_sync(this, &SharedThreadState::func2, tr, f);
+ break;
+ case TEST_MSGRET_FUNC1_TRANSFORM:
+ command_queue.push_and_ret(this, &SharedThreadState::func1r, tr, &otr);
+ break;
+ case TEST_MSGRET_FUNC2_TRANSFORM_FLOAT:
+ command_queue.push_and_ret(this, &SharedThreadState::func2r, tr, f, &otr);
+ break;
+ default:
+ break;
+ }
+ }
+ message_types_to_write.clear();
+ during_writing = false;
+
+ writer_threadwork.thread_done_work();
+ writer_threadwork.thread_wait_for_work();
+ }
+ writer_threadwork.thread_done_work();
+ }
+ static void static_writer_thread_loop(void *stsvoid) {
+ SharedThreadState *sts = static_cast<SharedThreadState *>(stsvoid);
+ sts->writer_thread_loop();
+ }
+
+ void init_threads() {
+ reader_thread.start(&SharedThreadState::static_reader_thread_loop, this);
+ writer_thread.start(&SharedThreadState::static_writer_thread_loop, this);
+ }
+ void destroy_threads() {
+ exit_threads = true;
+ reader_threadwork.main_start_work();
+ writer_threadwork.main_start_work();
+
+ reader_thread.wait_to_finish();
+ writer_thread.wait_to_finish();
+ }
+};
+
+TEST_CASE("[CommandQueue] Test Queue Basics") {
+ const char *COMMAND_QUEUE_SETTING = "memory/limits/command_queue/multithreading_queue_size_kb";
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING, 1);
+ SharedThreadState sts;
+ sts.init_threads();
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC1_TRANSFORM);
+ sts.writer_threadwork.main_start_work();
+ sts.writer_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 0,
+ "Control: no messages read before reader has run.");
+
+ sts.message_count_to_read = 1;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 1,
+ "Reader should have read one message");
+
+ sts.message_count_to_read = -1;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 1,
+ "Reader should have read no additional messages from flush_all");
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC1_TRANSFORM);
+ sts.writer_threadwork.main_start_work();
+ sts.writer_threadwork.main_wait_for_done();
+
+ sts.message_count_to_read = -1;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 2,
+ "Reader should have read one additional message from flush_all");
+
+ sts.destroy_threads();
+
+ CHECK_MESSAGE(sts.func1_count == 2,
+ "Reader should have read no additional messages after join");
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING,
+ ProjectSettings::get_singleton()->property_get_revert(COMMAND_QUEUE_SETTING));
+}
+
+TEST_CASE("[CommandQueue] Test Queue Wrapping to same spot.") {
+ const char *COMMAND_QUEUE_SETTING = "memory/limits/command_queue/multithreading_queue_size_kb";
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING, 1);
+ SharedThreadState sts;
+ sts.init_threads();
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC1_TRANSFORM);
+ sts.writer_threadwork.main_start_work();
+ sts.writer_threadwork.main_wait_for_done();
+
+ sts.message_count_to_read = -1;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 3,
+ "Reader should have read at least three messages");
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.writer_threadwork.main_start_work();
+ sts.writer_threadwork.main_wait_for_done();
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC1_TRANSFORM);
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.writer_threadwork.main_start_work();
+ OS::get_singleton()->delay_usec(1000);
+
+ sts.message_count_to_read = -1;
+ sts.reader_threadwork.main_start_work();
+ OS::get_singleton()->delay_usec(1000);
+
+ sts.writer_threadwork.main_wait_for_done();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count >= 3,
+ "Reader should have read at least three messages");
+
+ sts.message_count_to_read = 6 - sts.func1_count;
+ sts.reader_threadwork.main_start_work();
+
+ // The following will fail immediately.
+ // The reason it hangs indefinitely in engine, is all subsequent calls to
+ // CommandQueue.wait_and_flush_one will also fail.
+ sts.reader_threadwork.main_wait_for_done();
+
+ // Because looping around uses an extra message, easiest to consume all.
+ sts.message_count_to_read = -1;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 6,
+ "Reader should have read both message sets");
+
+ sts.destroy_threads();
+
+ CHECK_MESSAGE(sts.func1_count == 6,
+ "Reader should have read no additional messages after join");
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING,
+ ProjectSettings::get_singleton()->property_get_revert(COMMAND_QUEUE_SETTING));
+}
+
+TEST_CASE("[CommandQueue] Test Queue Lapping") {
+ const char *COMMAND_QUEUE_SETTING = "memory/limits/command_queue/multithreading_queue_size_kb";
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING, 1);
+ SharedThreadState sts;
+ sts.init_threads();
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC1_TRANSFORM);
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.writer_threadwork.main_start_work();
+ sts.writer_threadwork.main_wait_for_done();
+
+ // We need to read an extra message so that it triggers the dealloc logic once.
+ // Otherwise, the queue will be considered full.
+ sts.message_count_to_read = 3;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 3,
+ "Reader should have read first set of messages");
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.writer_threadwork.main_start_work();
+ // Don't wait for these, because the queue isn't big enough.
+ sts.writer_threadwork.main_wait_for_done();
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC2_TRANSFORM_FLOAT);
+ sts.writer_threadwork.main_start_work();
+ OS::get_singleton()->delay_usec(1000);
+
+ sts.message_count_to_read = 3;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+
+ sts.writer_threadwork.main_wait_for_done();
+
+ sts.message_count_to_read = -1;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+
+ CHECK_MESSAGE(sts.func1_count == 6,
+ "Reader should have read rest of the messages after lapping writers.");
+
+ sts.destroy_threads();
+
+ CHECK_MESSAGE(sts.func1_count == 6,
+ "Reader should have read no additional messages after join");
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING,
+ ProjectSettings::get_singleton()->property_get_revert(COMMAND_QUEUE_SETTING));
+}
+
+TEST_CASE("[Stress][CommandQueue] Stress test command queue") {
+ const char *COMMAND_QUEUE_SETTING = "memory/limits/command_queue/multithreading_queue_size_kb";
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING, 1);
+ SharedThreadState sts;
+ sts.init_threads();
+
+ RandomNumberGenerator rng;
+
+ rng.set_seed(1837267);
+
+ int msgs_to_add = 2048;
+
+ for (int i = 0; i < msgs_to_add; i++) {
+ // randi_range is inclusive, so allow any enum value except MAX.
+ sts.add_msg_to_write((SharedThreadState::TestMsgType)rng.randi_range(0, SharedThreadState::TEST_MSG_MAX - 1));
+ }
+ sts.writer_threadwork.main_start_work();
+
+ int max_loop_iters = msgs_to_add * 2;
+ int loop_iters = 0;
+ while (sts.func1_count < msgs_to_add && loop_iters < max_loop_iters) {
+ int remaining = (msgs_to_add - sts.func1_count);
+ sts.message_count_to_read = rng.randi_range(1, remaining < 128 ? remaining : 128);
+ if (loop_iters % 3 == 0) {
+ sts.message_count_to_read = -1;
+ }
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ loop_iters++;
+ }
+ CHECK_MESSAGE(loop_iters < max_loop_iters,
+ "Reader needed too many iterations to read messages!");
+ sts.writer_threadwork.main_wait_for_done();
+
+ sts.destroy_threads();
+
+ CHECK_MESSAGE(sts.func1_count == msgs_to_add,
+ "Reader should have read no additional messages after join");
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING,
+ ProjectSettings::get_singleton()->property_get_revert(COMMAND_QUEUE_SETTING));
+}
+} // namespace TestCommandQueue
+
+#endif // TEST_COMMAND_QUEUE_H
diff --git a/tests/test_ordered_hash_map.h b/tests/core/templates/test_hash_map.h
index 3182c391cb..7a3d5f5d47 100644
--- a/tests/test_ordered_hash_map.h
+++ b/tests/core/templates/test_hash_map.h
@@ -1,12 +1,12 @@
/*************************************************************************/
-/* test_ordered_hash_map.h */
+/* test_hash_map.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -28,59 +28,53 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-#ifndef TEST_ORDERED_HASH_MAP_H
-#define TEST_ORDERED_HASH_MAP_H
+#ifndef TEST_HASH_MAP_H
+#define TEST_HASH_MAP_H
-#include "core/ordered_hash_map.h"
-#include "core/os/os.h"
-#include "core/pair.h"
-#include "core/vector.h"
+#include "core/templates/hash_map.h"
#include "tests/test_macros.h"
-namespace TestOrderedHashMap {
+namespace TestHashMap {
-TEST_CASE("[OrderedHashMap] Insert element") {
- OrderedHashMap<int, int> map;
- OrderedHashMap<int, int>::Element e = map.insert(42, 84);
+TEST_CASE("[HashMap] Insert element") {
+ HashMap<int, int> map;
+ HashMap<int, int>::Iterator e = map.insert(42, 84);
CHECK(e);
- CHECK(e.key() == 42);
- CHECK(e.get() == 84);
- CHECK(e.value() == 84);
+ CHECK(e->key == 42);
+ CHECK(e->value == 84);
CHECK(map[42] == 84);
CHECK(map.has(42));
CHECK(map.find(42));
}
-TEST_CASE("[OrderedHashMap] Overwrite element") {
- OrderedHashMap<int, int> map;
+TEST_CASE("[HashMap] Overwrite element") {
+ HashMap<int, int> map;
map.insert(42, 84);
map.insert(42, 1234);
CHECK(map[42] == 1234);
}
-TEST_CASE("[OrderedHashMap] Erase via element") {
- OrderedHashMap<int, int> map;
- OrderedHashMap<int, int>::Element e = map.insert(42, 84);
-
- map.erase(e);
- CHECK(!e);
+TEST_CASE("[HashMap] Erase via element") {
+ HashMap<int, int> map;
+ HashMap<int, int>::Iterator e = map.insert(42, 84);
+ map.remove(e);
CHECK(!map.has(42));
CHECK(!map.find(42));
}
-TEST_CASE("[OrderedHashMap] Erase via key") {
- OrderedHashMap<int, int> map;
+TEST_CASE("[HashMap] Erase via key") {
+ HashMap<int, int> map;
map.insert(42, 84);
map.erase(42);
CHECK(!map.has(42));
CHECK(!map.find(42));
}
-TEST_CASE("[OrderedHashMap] Size") {
- OrderedHashMap<int, int> map;
+TEST_CASE("[HashMap] Size") {
+ HashMap<int, int> map;
map.insert(42, 84);
map.insert(123, 84);
map.insert(123, 84);
@@ -90,8 +84,8 @@ TEST_CASE("[OrderedHashMap] Size") {
CHECK(map.size() == 4);
}
-TEST_CASE("[OrderedHashMap] Iteration") {
- OrderedHashMap<int, int> map;
+TEST_CASE("[HashMap] Iteration") {
+ HashMap<int, int> map;
map.insert(42, 84);
map.insert(123, 12385);
map.insert(0, 12934);
@@ -105,35 +99,35 @@ TEST_CASE("[OrderedHashMap] Iteration") {
expected.push_back(Pair<int, int>(123485, 1238888));
int idx = 0;
- for (OrderedHashMap<int, int>::Element E = map.front(); E; E = E.next()) {
- CHECK(expected[idx] == Pair<int, int>(E.key(), E.value()));
+ for (const KeyValue<int, int> &E : map) {
+ CHECK(expected[idx] == Pair<int, int>(E.key, E.value));
++idx;
}
}
-TEST_CASE("[OrderedHashMap] Const iteration") {
- OrderedHashMap<int, int> map;
+TEST_CASE("[HashMap] Const iteration") {
+ HashMap<int, int> map;
map.insert(42, 84);
map.insert(123, 12385);
map.insert(0, 12934);
map.insert(123485, 1238888);
map.insert(123, 111111);
- const OrderedHashMap<int, int> const_map = map;
+ const HashMap<int, int> const_map = map;
Vector<Pair<int, int>> expected;
expected.push_back(Pair<int, int>(42, 84));
expected.push_back(Pair<int, int>(123, 111111));
expected.push_back(Pair<int, int>(0, 12934));
expected.push_back(Pair<int, int>(123485, 1238888));
+ expected.push_back(Pair<int, int>(123, 111111));
int idx = 0;
- for (OrderedHashMap<int, int>::ConstElement E = const_map.front(); E; E = E.next()) {
- CHECK(expected[idx] == Pair<int, int>(E.key(), E.value()));
+ for (const KeyValue<int, int> &E : const_map) {
+ CHECK(expected[idx] == Pair<int, int>(E.key, E.value));
++idx;
}
}
+} // namespace TestHashMap
-} // namespace TestOrderedHashMap
-
-#endif // TEST_ORDERED_HASH_MAP_H
+#endif // TEST_HASH_MAP_H
diff --git a/tests/core/templates/test_hash_set.h b/tests/core/templates/test_hash_set.h
new file mode 100644
index 0000000000..dad149604b
--- /dev/null
+++ b/tests/core/templates/test_hash_set.h
@@ -0,0 +1,226 @@
+/*************************************************************************/
+/* test_hash_set.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_HASH_SET_H
+#define TEST_HASH_SET_H
+
+#include "core/templates/hash_set.h"
+
+#include "tests/test_macros.h"
+
+namespace TestHashSet {
+
+TEST_CASE("[HashSet] Insert element") {
+ HashSet<int> set;
+ HashSet<int>::Iterator e = set.insert(42);
+
+ CHECK(e);
+ CHECK(*e == 42);
+ CHECK(set.has(42));
+ CHECK(set.find(42));
+ set.reset();
+}
+
+TEST_CASE("[HashSet] Insert existing element") {
+ HashSet<int> set;
+ set.insert(42);
+ set.insert(42);
+
+ CHECK(set.has(42));
+ CHECK(set.size() == 1);
+}
+
+TEST_CASE("[HashSet] Insert, iterate and remove many elements") {
+ const int elem_max = 12343;
+ HashSet<int> set;
+ for (int i = 0; i < elem_max; i++) {
+ set.insert(i);
+ }
+
+ //insert order should have been kept
+ int idx = 0;
+ for (const int &K : set) {
+ CHECK(idx == K);
+ CHECK(set.has(idx));
+ idx++;
+ }
+
+ Vector<int> elems_still_valid;
+
+ for (int i = 0; i < elem_max; i++) {
+ if ((i % 5) == 0) {
+ set.erase(i);
+ } else {
+ elems_still_valid.push_back(i);
+ }
+ }
+
+ CHECK(elems_still_valid.size() == set.size());
+
+ for (int i = 0; i < elems_still_valid.size(); i++) {
+ CHECK(set.has(elems_still_valid[i]));
+ }
+}
+
+TEST_CASE("[HashSet] Insert, iterate and remove many strings") {
+ // This tests a key that uses allocation, to see if any leaks occur
+
+ uint64_t pre_mem = Memory::get_mem_usage();
+ const int elem_max = 4018;
+ HashSet<String> set;
+ for (int i = 0; i < elem_max; i++) {
+ set.insert(itos(i));
+ }
+
+ //insert order should have been kept
+ int idx = 0;
+ for (const String &K : set) {
+ CHECK(itos(idx) == K);
+ CHECK(set.has(itos(idx)));
+ idx++;
+ }
+
+ Vector<String> elems_still_valid;
+
+ for (int i = 0; i < elem_max; i++) {
+ if ((i % 5) == 0) {
+ set.erase(itos(i));
+ } else {
+ elems_still_valid.push_back(itos(i));
+ }
+ }
+
+ CHECK(elems_still_valid.size() == set.size());
+
+ for (int i = 0; i < elems_still_valid.size(); i++) {
+ CHECK(set.has(elems_still_valid[i]));
+ }
+
+ elems_still_valid.clear();
+ set.reset();
+
+ CHECK(Memory::get_mem_usage() == pre_mem);
+}
+
+TEST_CASE("[HashSet] Erase via element") {
+ HashSet<int> set;
+ HashSet<int>::Iterator e = set.insert(42);
+ set.remove(e);
+ CHECK(!set.has(42));
+ CHECK(!set.find(42));
+}
+
+TEST_CASE("[HashSet] Erase via key") {
+ HashSet<int> set;
+ set.insert(42);
+ set.insert(49);
+ set.erase(42);
+ CHECK(!set.has(42));
+ CHECK(!set.find(42));
+}
+
+TEST_CASE("[HashSet] Insert and erase half elements") {
+ HashSet<int> set;
+ set.insert(1);
+ set.insert(2);
+ set.insert(3);
+ set.insert(4);
+ set.erase(1);
+ set.erase(3);
+
+ CHECK(set.size() == 2);
+ CHECK(set.has(2));
+ CHECK(set.has(4));
+}
+
+TEST_CASE("[HashSet] Size") {
+ HashSet<int> set;
+ set.insert(42);
+ set.insert(123);
+ set.insert(123);
+ set.insert(0);
+ set.insert(123485);
+
+ CHECK(set.size() == 4);
+}
+
+TEST_CASE("[HashSet] Iteration") {
+ HashSet<int> set;
+ set.insert(42);
+ set.insert(123);
+ set.insert(0);
+ set.insert(123485);
+
+ Vector<int> expected;
+ expected.push_back(42);
+ expected.push_back(123);
+ expected.push_back(0);
+ expected.push_back(123485);
+
+ int idx = 0;
+ for (const int &E : set) {
+ CHECK(expected[idx] == E);
+ ++idx;
+ }
+}
+
+TEST_CASE("[HashSet] Copy") {
+ HashSet<int> set;
+ set.insert(42);
+ set.insert(123);
+ set.insert(0);
+ set.insert(123485);
+
+ Vector<int> expected;
+ expected.push_back(42);
+ expected.push_back(123);
+ expected.push_back(0);
+ expected.push_back(123485);
+
+ HashSet<int> copy_assign = set;
+
+ int idx = 0;
+ for (const int &E : copy_assign) {
+ CHECK(expected[idx] == E);
+ ++idx;
+ }
+
+ HashSet<int> copy_construct(set);
+
+ idx = 0;
+ for (const int &E : copy_construct) {
+ CHECK(expected[idx] == E);
+ ++idx;
+ }
+}
+
+} // namespace TestHashSet
+
+#endif // TEST_HASH_SET_H
diff --git a/tests/test_list.h b/tests/core/templates/test_list.h
index 7412fbc3c8..49da0b8aad 100644
--- a/tests/test_list.h
+++ b/tests/core/templates/test_list.h
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -31,7 +31,7 @@
#ifndef TEST_LIST_H
#define TEST_LIST_H
-#include "core/list.h"
+#include "core/templates/list.h"
#include "tests/test_macros.h"
@@ -45,6 +45,276 @@ static void populate_integers(List<int> &p_list, List<int>::Element *r_elements[
}
}
+TEST_CASE("[List] Push/pop back") {
+ List<String> list;
+
+ List<String>::Element *n;
+ n = list.push_back("A");
+ CHECK(n->get() == "A");
+ n = list.push_back("B");
+ CHECK(n->get() == "B");
+ n = list.push_back("C");
+ CHECK(n->get() == "C");
+
+ CHECK(list.size() == 3);
+ CHECK(!list.is_empty());
+
+ String v;
+ v = list.back()->get();
+ list.pop_back();
+ CHECK(v == "C");
+ v = list.back()->get();
+ list.pop_back();
+ CHECK(v == "B");
+ v = list.back()->get();
+ list.pop_back();
+ CHECK(v == "A");
+
+ CHECK(list.size() == 0);
+ CHECK(list.is_empty());
+
+ CHECK(list.back() == nullptr);
+ CHECK(list.front() == nullptr);
+}
+
+TEST_CASE("[List] Push/pop front") {
+ List<String> list;
+
+ List<String>::Element *n;
+ n = list.push_front("A");
+ CHECK(n->get() == "A");
+ n = list.push_front("B");
+ CHECK(n->get() == "B");
+ n = list.push_front("C");
+ CHECK(n->get() == "C");
+
+ CHECK(list.size() == 3);
+ CHECK(!list.is_empty());
+
+ String v;
+ v = list.front()->get();
+ list.pop_front();
+ CHECK(v == "C");
+ v = list.front()->get();
+ list.pop_front();
+ CHECK(v == "B");
+ v = list.front()->get();
+ list.pop_front();
+ CHECK(v == "A");
+
+ CHECK(list.size() == 0);
+ CHECK(list.is_empty());
+
+ CHECK(list.back() == nullptr);
+ CHECK(list.front() == nullptr);
+}
+
+TEST_CASE("[List] Set and get") {
+ List<String> list;
+ list.push_back("A");
+
+ List<String>::Element *n = list.front();
+ CHECK(n->get() == "A");
+
+ n->set("X");
+ CHECK(n->get() == "X");
+}
+
+TEST_CASE("[List] Insert before") {
+ List<String> list;
+ List<String>::Element *a = list.push_back("A");
+ List<String>::Element *b = list.push_back("B");
+ List<String>::Element *c = list.push_back("C");
+
+ list.insert_before(b, "I");
+
+ CHECK(a->next()->get() == "I");
+ CHECK(c->prev()->prev()->get() == "I");
+ CHECK(list.front()->next()->get() == "I");
+ CHECK(list.back()->prev()->prev()->get() == "I");
+}
+
+TEST_CASE("[List] Insert after") {
+ List<String> list;
+ List<String>::Element *a = list.push_back("A");
+ List<String>::Element *b = list.push_back("B");
+ List<String>::Element *c = list.push_back("C");
+
+ list.insert_after(b, "I");
+
+ CHECK(a->next()->next()->get() == "I");
+ CHECK(c->prev()->get() == "I");
+ CHECK(list.front()->next()->next()->get() == "I");
+ CHECK(list.back()->prev()->get() == "I");
+}
+
+TEST_CASE("[List] Insert before null") {
+ List<String> list;
+ List<String>::Element *a = list.push_back("A");
+ List<String>::Element *b = list.push_back("B");
+ List<String>::Element *c = list.push_back("C");
+
+ list.insert_before(nullptr, "I");
+
+ CHECK(a->next()->get() == "B");
+ CHECK(b->get() == "B");
+ CHECK(c->prev()->prev()->get() == "A");
+ CHECK(list.front()->next()->get() == "B");
+ CHECK(list.back()->prev()->prev()->get() == "B");
+ CHECK(list.back()->get() == "I");
+}
+
+TEST_CASE("[List] Insert after null") {
+ List<String> list;
+ List<String>::Element *a = list.push_back("A");
+ List<String>::Element *b = list.push_back("B");
+ List<String>::Element *c = list.push_back("C");
+
+ list.insert_after(nullptr, "I");
+
+ CHECK(a->next()->get() == "B");
+ CHECK(b->get() == "B");
+ CHECK(c->prev()->prev()->get() == "A");
+ CHECK(list.front()->next()->get() == "B");
+ CHECK(list.back()->prev()->prev()->get() == "B");
+ CHECK(list.back()->get() == "I");
+}
+
+TEST_CASE("[List] Find") {
+ List<int> list;
+ List<int>::Element *n[10];
+ // Indices match values.
+ populate_integers(list, n, 10);
+
+ for (int i = 0; i < 10; ++i) {
+ CHECK(n[i]->get() == list.find(i)->get());
+ }
+}
+
+TEST_CASE("[List] Erase (by value)") {
+ List<int> list;
+ List<int>::Element *n[4];
+ // Indices match values.
+ populate_integers(list, n, 4);
+
+ CHECK(list.front()->next()->next()->get() == 2);
+ bool erased = list.erase(2); // 0, 1, 3.
+ CHECK(erased);
+ CHECK(list.size() == 3);
+
+ // The pointer n[2] points to the freed memory which is not reset to zero,
+ // so the below assertion may pass, but this relies on undefined behavior.
+ // CHECK(n[2]->get() == 2);
+
+ CHECK(list.front()->get() == 0);
+ CHECK(list.front()->next()->next()->get() == 3);
+ CHECK(list.back()->get() == 3);
+ CHECK(list.back()->prev()->get() == 1);
+
+ CHECK(n[1]->next()->get() == 3);
+ CHECK(n[3]->prev()->get() == 1);
+
+ erased = list.erase(9000); // Doesn't exist.
+ CHECK(!erased);
+}
+
+TEST_CASE("[List] Erase (by element)") {
+ List<int> list;
+ List<int>::Element *n[4];
+ // Indices match values.
+ populate_integers(list, n, 4);
+
+ bool erased = list.erase(n[2]);
+ CHECK(erased);
+ CHECK(list.size() == 3);
+ CHECK(n[1]->next()->get() == 3);
+ CHECK(n[3]->prev()->get() == 1);
+}
+
+TEST_CASE("[List] Element erase") {
+ List<int> list;
+ List<int>::Element *n[4];
+ // Indices match values.
+ populate_integers(list, n, 4);
+
+ n[2]->erase();
+
+ CHECK(list.size() == 3);
+ CHECK(n[1]->next()->get() == 3);
+ CHECK(n[3]->prev()->get() == 1);
+}
+
+TEST_CASE("[List] Clear") {
+ List<int> list;
+ List<int>::Element *n[100];
+ populate_integers(list, n, 100);
+
+ list.clear();
+
+ CHECK(list.size() == 0);
+ CHECK(list.is_empty());
+}
+
+TEST_CASE("[List] Invert") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.reverse();
+
+ CHECK(list.front()->get() == 3);
+ CHECK(list.front()->next()->get() == 2);
+ CHECK(list.back()->prev()->get() == 1);
+ CHECK(list.back()->get() == 0);
+}
+
+TEST_CASE("[List] Move to front") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.move_to_front(n[3]);
+
+ CHECK(list.front()->get() == 3);
+ CHECK(list.back()->get() == 2);
+}
+
+TEST_CASE("[List] Move to back") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.move_to_back(n[0]);
+
+ CHECK(list.back()->get() == 0);
+ CHECK(list.front()->get() == 1);
+}
+
+TEST_CASE("[List] Move before") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.move_before(n[3], n[1]);
+
+ CHECK(list.front()->next()->get() == n[3]->get());
+}
+
+TEST_CASE("[List] Sort") {
+ List<String> list;
+ list.push_back("D");
+ list.push_back("B");
+ list.push_back("A");
+ list.push_back("C");
+
+ list.sort();
+
+ CHECK(list.front()->get() == "A");
+ CHECK(list.front()->next()->get() == "B");
+ CHECK(list.back()->prev()->get() == "C");
+ CHECK(list.back()->get() == "D");
+}
+
TEST_CASE("[List] Swap adjacent front and back") {
List<int> list;
List<int>::Element *n[2];
@@ -273,7 +543,6 @@ TEST_CASE("[Stress][List] Swap random 10 elements, 1000 iterations.") {
populate_integers(list, n, 10);
swap_random(list, n, 10, 1000);
}
-
} // namespace TestList
#endif // TEST_LIST_H
diff --git a/tests/core/templates/test_local_vector.h b/tests/core/templates/test_local_vector.h
new file mode 100644
index 0000000000..b2464c3914
--- /dev/null
+++ b/tests/core/templates/test_local_vector.h
@@ -0,0 +1,240 @@
+/*************************************************************************/
+/* test_local_vector.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_LOCAL_VECTOR_H
+#define TEST_LOCAL_VECTOR_H
+
+#include "core/templates/local_vector.h"
+
+#include "tests/test_macros.h"
+
+namespace TestLocalVector {
+
+TEST_CASE("[LocalVector] List Initialization.") {
+ LocalVector<int> vector{ 0, 1, 2, 3, 4 };
+
+ CHECK(vector.size() == 5);
+ CHECK(vector[0] == 0);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 3);
+ CHECK(vector[4] == 4);
+}
+
+TEST_CASE("[LocalVector] Push Back.") {
+ LocalVector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ CHECK(vector[0] == 0);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 3);
+ CHECK(vector[4] == 4);
+}
+
+TEST_CASE("[LocalVector] Find.") {
+ LocalVector<int> vector;
+ vector.push_back(3);
+ vector.push_back(1);
+ vector.push_back(4);
+ vector.push_back(0);
+ vector.push_back(2);
+
+ CHECK(vector[0] == 3);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 4);
+ CHECK(vector[3] == 0);
+ CHECK(vector[4] == 2);
+
+ CHECK(vector.find(0) == 3);
+ CHECK(vector.find(1) == 1);
+ CHECK(vector.find(2) == 4);
+ CHECK(vector.find(3) == 0);
+ CHECK(vector.find(4) == 2);
+
+ CHECK(vector.find(-1) == -1);
+ CHECK(vector.find(5) == -1);
+}
+
+TEST_CASE("[LocalVector] Remove.") {
+ LocalVector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ vector.remove_at(0);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 3);
+ CHECK(vector[3] == 4);
+
+ vector.remove_at(2);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 4);
+
+ vector.remove_at(1);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 4);
+
+ vector.remove_at(0);
+
+ CHECK(vector[0] == 4);
+}
+
+TEST_CASE("[LocalVector] Remove Unordered.") {
+ LocalVector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ CHECK(vector.size() == 5);
+
+ vector.remove_at_unordered(0);
+
+ CHECK(vector.size() == 4);
+
+ CHECK(vector.find(0) == -1);
+ CHECK(vector.find(1) != -1);
+ CHECK(vector.find(2) != -1);
+ CHECK(vector.find(3) != -1);
+ CHECK(vector.find(4) != -1);
+
+ // Now the vector is no more ordered.
+ vector.remove_at_unordered(vector.find(3));
+
+ CHECK(vector.size() == 3);
+
+ CHECK(vector.find(3) == -1);
+ CHECK(vector.find(1) != -1);
+ CHECK(vector.find(2) != -1);
+ CHECK(vector.find(4) != -1);
+
+ vector.remove_at_unordered(vector.find(2));
+
+ CHECK(vector.size() == 2);
+
+ CHECK(vector.find(2) == -1);
+ CHECK(vector.find(1) != -1);
+ CHECK(vector.find(4) != -1);
+
+ vector.remove_at_unordered(vector.find(4));
+
+ CHECK(vector.size() == 1);
+
+ CHECK(vector.find(4) == -1);
+ CHECK(vector.find(1) != -1);
+
+ // Remove the last one.
+ vector.remove_at_unordered(0);
+
+ CHECK(vector.is_empty());
+ CHECK(vector.size() == 0);
+}
+
+TEST_CASE("[LocalVector] Erase.") {
+ LocalVector<int> vector;
+ vector.push_back(1);
+ vector.push_back(3);
+ vector.push_back(0);
+ vector.push_back(2);
+ vector.push_back(4);
+
+ CHECK(vector.find(2) == 3);
+
+ vector.erase(2);
+
+ CHECK(vector.find(2) == -1);
+ CHECK(vector.size() == 4);
+}
+
+TEST_CASE("[LocalVector] Size / Resize / Reserve.") {
+ LocalVector<int> vector;
+
+ CHECK(vector.is_empty());
+ CHECK(vector.size() == 0);
+ CHECK(vector.get_capacity() == 0);
+
+ vector.resize(10);
+
+ CHECK(vector.size() == 10);
+ CHECK(vector.get_capacity() >= 10);
+
+ vector.resize(5);
+
+ CHECK(vector.size() == 5);
+ // Capacity is supposed to change only when the size increase.
+ CHECK(vector.get_capacity() >= 10);
+
+ vector.remove_at(0);
+ vector.remove_at(0);
+ vector.remove_at(0);
+
+ CHECK(vector.size() == 2);
+ // Capacity is supposed to change only when the size increase.
+ CHECK(vector.get_capacity() >= 10);
+
+ vector.reset();
+
+ CHECK(vector.size() == 0);
+ CHECK(vector.get_capacity() == 0);
+
+ vector.reserve(3);
+
+ CHECK(vector.is_empty());
+ CHECK(vector.size() == 0);
+ CHECK(vector.get_capacity() >= 3);
+
+ vector.push_back(0);
+ vector.push_back(0);
+ vector.push_back(0);
+
+ CHECK(vector.size() == 3);
+ CHECK(vector.get_capacity() >= 3);
+
+ vector.push_back(0);
+
+ CHECK(vector.size() == 4);
+ CHECK(vector.get_capacity() >= 4);
+}
+} // namespace TestLocalVector
+
+#endif // TEST_LOCAL_VECTOR_H
diff --git a/tests/test_physics_2d.h b/tests/core/templates/test_lru.h
index 517d324f3b..354f53e164 100644
--- a/tests/test_physics_2d.h
+++ b/tests/core/templates/test_lru.h
@@ -1,12 +1,12 @@
/*************************************************************************/
-/* test_physics_2d.h */
+/* test_lru.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -28,14 +28,72 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-#ifndef TEST_PHYSICS_2D_H
-#define TEST_PHYSICS_2D_H
+#ifndef TEST_LRU_H
+#define TEST_LRU_H
-#include "core/os/main_loop.h"
+#include "core/templates/lru.h"
-namespace TestPhysics2D {
+#include "tests/test_macros.h"
-MainLoop *test();
+namespace TestLRU {
+
+TEST_CASE("[LRU] Store and read") {
+ LRUCache<int, int> lru;
+
+ lru.set_capacity(3);
+ lru.insert(1, 1);
+ lru.insert(50, 2);
+ lru.insert(100, 5);
+
+ CHECK(lru.has(1));
+ CHECK(lru.has(50));
+ CHECK(lru.has(100));
+ CHECK(!lru.has(200));
+
+ CHECK(lru.get(1) == 1);
+ CHECK(lru.get(50) == 2);
+ CHECK(lru.get(100) == 5);
+
+ CHECK(lru.getptr(1) != nullptr);
+ CHECK(lru.getptr(1000) == nullptr);
+
+ lru.insert(600, 600); // Erase <50>
+ CHECK(lru.has(600));
+ CHECK(!lru.has(50));
+}
+
+TEST_CASE("[LRU] Resize and clear") {
+ LRUCache<int, int> lru;
+
+ lru.set_capacity(3);
+ lru.insert(1, 1);
+ lru.insert(2, 2);
+ lru.insert(3, 3);
+
+ CHECK(lru.get_capacity() == 3);
+
+ lru.set_capacity(5);
+ CHECK(lru.get_capacity() == 5);
+
+ CHECK(lru.has(1));
+ CHECK(lru.has(2));
+ CHECK(lru.has(3));
+ CHECK(!lru.has(4));
+
+ lru.set_capacity(2);
+ CHECK(lru.get_capacity() == 2);
+
+ CHECK(!lru.has(1));
+ CHECK(lru.has(2));
+ CHECK(lru.has(3));
+ CHECK(!lru.has(4));
+
+ lru.clear();
+ CHECK(!lru.has(1));
+ CHECK(!lru.has(2));
+ CHECK(!lru.has(3));
+ CHECK(!lru.has(4));
}
+} // namespace TestLRU
-#endif // TEST_PHYSICS_2D_H
+#endif // TEST_LRU_H
diff --git a/tests/core/templates/test_paged_array.h b/tests/core/templates/test_paged_array.h
new file mode 100644
index 0000000000..86cf3a2dfc
--- /dev/null
+++ b/tests/core/templates/test_paged_array.h
@@ -0,0 +1,153 @@
+/*************************************************************************/
+/* test_paged_array.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_PAGED_ARRAY_H
+#define TEST_PAGED_ARRAY_H
+
+#include "core/templates/paged_array.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestPagedArray {
+
+// PagedArray
+
+TEST_CASE("[PagedArray] Simple fill and refill") {
+ PagedArrayPool<uint32_t> pool;
+ PagedArray<uint32_t> array;
+ array.set_page_pool(&pool);
+
+ for (uint32_t i = 0; i < 123456; i++) {
+ array.push_back(i);
+ }
+ CHECK_MESSAGE(
+ array.size() == 123456,
+ "PagedArray should have 123456 elements.");
+
+ bool all_match = true;
+ for (uint32_t i = 0; i < 123456; i++) {
+ if (array[i] != i) {
+ all_match = false;
+ break;
+ }
+ }
+
+ CHECK_MESSAGE(
+ all_match,
+ "PagedArray elements should match from 0 to 123455.");
+
+ array.clear();
+
+ CHECK_MESSAGE(
+ array.size() == 0,
+ "PagedArray elements should be 0 after clear.");
+
+ for (uint32_t i = 0; i < 999; i++) {
+ array.push_back(i);
+ }
+ CHECK_MESSAGE(
+ array.size() == 999,
+ "PagedArray should have 999 elements.");
+
+ all_match = true;
+ for (uint32_t i = 0; i < 999; i++) {
+ if (array[i] != i) {
+ all_match = false;
+ }
+ }
+
+ CHECK_MESSAGE(
+ all_match,
+ "PagedArray elements should match from 0 to 998.");
+
+ array.reset(); //reset so pagepool can be reset
+ pool.reset();
+}
+
+TEST_CASE("[PagedArray] Shared pool fill, including merging") {
+ PagedArrayPool<uint32_t> pool;
+ PagedArray<uint32_t> array1;
+ PagedArray<uint32_t> array2;
+ array1.set_page_pool(&pool);
+ array2.set_page_pool(&pool);
+
+ for (uint32_t i = 0; i < 123456; i++) {
+ array1.push_back(i);
+ }
+ CHECK_MESSAGE(
+ array1.size() == 123456,
+ "PagedArray #1 should have 123456 elements.");
+
+ bool all_match = true;
+ for (uint32_t i = 0; i < 123456; i++) {
+ if (array1[i] != i) {
+ all_match = false;
+ }
+ }
+
+ CHECK_MESSAGE(
+ all_match,
+ "PagedArray #1 elements should match from 0 to 123455.");
+
+ for (uint32_t i = 0; i < 999; i++) {
+ array2.push_back(i);
+ }
+ CHECK_MESSAGE(
+ array2.size() == 999,
+ "PagedArray #2 should have 999 elements.");
+
+ all_match = true;
+ for (uint32_t i = 0; i < 999; i++) {
+ if (array2[i] != i) {
+ all_match = false;
+ }
+ }
+
+ CHECK_MESSAGE(
+ all_match,
+ "PagedArray #2 elements should match from 0 to 998.");
+
+ array1.merge_unordered(array2);
+
+ CHECK_MESSAGE(
+ array1.size() == 123456 + 999,
+ "PagedArray #1 should now be 123456 + 999 elements.");
+
+ CHECK_MESSAGE(
+ array2.size() == 0,
+ "PagedArray #2 should now be 0 elements.");
+
+ array1.reset(); //reset so pagepool can be reset
+ array2.reset(); //reset so pagepool can be reset
+ pool.reset();
+}
+} // namespace TestPagedArray
+
+#endif // TEST_PAGED_ARRAY_H
diff --git a/tests/core/templates/test_rid.h b/tests/core/templates/test_rid.h
new file mode 100644
index 0000000000..8d4dd0703b
--- /dev/null
+++ b/tests/core/templates/test_rid.h
@@ -0,0 +1,101 @@
+/*************************************************************************/
+/* test_rid.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_RID_H
+#define TEST_RID_H
+
+#include "core/templates/rid.h"
+
+#include "tests/test_macros.h"
+
+namespace TestRID {
+TEST_CASE("[RID] Default Constructor") {
+ RID rid;
+
+ CHECK(rid.get_id() == 0);
+}
+
+TEST_CASE("[RID] Factory method") {
+ RID rid = RID::from_uint64(1);
+
+ CHECK(rid.get_id() == 1);
+}
+
+TEST_CASE("[RID] Operators") {
+ RID rid = RID::from_uint64(1);
+
+ RID rid_zero = RID::from_uint64(0);
+ RID rid_one = RID::from_uint64(1);
+ RID rid_two = RID::from_uint64(2);
+
+ CHECK_FALSE(rid == rid_zero);
+ CHECK(rid == rid_one);
+ CHECK_FALSE(rid == rid_two);
+
+ CHECK_FALSE(rid < rid_zero);
+ CHECK_FALSE(rid < rid_one);
+ CHECK(rid < rid_two);
+
+ CHECK_FALSE(rid <= rid_zero);
+ CHECK(rid <= rid_one);
+ CHECK(rid <= rid_two);
+
+ CHECK(rid > rid_zero);
+ CHECK_FALSE(rid > rid_one);
+ CHECK_FALSE(rid > rid_two);
+
+ CHECK(rid >= rid_zero);
+ CHECK(rid >= rid_one);
+ CHECK_FALSE(rid >= rid_two);
+
+ CHECK(rid != rid_zero);
+ CHECK_FALSE(rid != rid_one);
+ CHECK(rid != rid_two);
+}
+
+TEST_CASE("[RID] 'is_valid' & 'is_null'") {
+ RID rid_zero = RID::from_uint64(0);
+ RID rid_one = RID::from_uint64(1);
+
+ CHECK_FALSE(rid_zero.is_valid());
+ CHECK(rid_zero.is_null());
+
+ CHECK(rid_one.is_valid());
+ CHECK_FALSE(rid_one.is_null());
+}
+
+TEST_CASE("[RID] 'get_local_index'") {
+ CHECK(RID::from_uint64(1).get_local_index() == 1);
+ CHECK(RID::from_uint64(4'294'967'295).get_local_index() == 4'294'967'295);
+ CHECK(RID::from_uint64(4'294'967'297).get_local_index() == 1);
+}
+} // namespace TestRID
+
+#endif // TEST_RID_H
diff --git a/tests/core/templates/test_vector.h b/tests/core/templates/test_vector.h
new file mode 100644
index 0000000000..3fc9264f5a
--- /dev/null
+++ b/tests/core/templates/test_vector.h
@@ -0,0 +1,537 @@
+/*************************************************************************/
+/* test_vector.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_VECTOR_H
+#define TEST_VECTOR_H
+
+#include "core/templates/vector.h"
+
+#include "tests/test_macros.h"
+
+namespace TestVector {
+
+TEST_CASE("[Vector] List initialization") {
+ Vector<int> vector{ 0, 1, 2, 3, 4 };
+
+ CHECK(vector.size() == 5);
+ CHECK(vector[0] == 0);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 3);
+ CHECK(vector[4] == 4);
+}
+
+TEST_CASE("[Vector] Push back and append") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ // Alias for `push_back`.
+ vector.append(4);
+
+ CHECK(vector[0] == 0);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 3);
+ CHECK(vector[4] == 4);
+}
+
+TEST_CASE("[Vector] Append array") {
+ Vector<int> vector;
+ vector.push_back(1);
+ vector.push_back(2);
+
+ Vector<int> vector_other;
+ vector_other.push_back(128);
+ vector_other.push_back(129);
+ vector.append_array(vector_other);
+
+ CHECK(vector.size() == 4);
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 128);
+ CHECK(vector[3] == 129);
+}
+
+TEST_CASE("[Vector] Insert") {
+ Vector<int> vector;
+ vector.insert(0, 2);
+ vector.insert(0, 8);
+ vector.insert(2, 5);
+ vector.insert(1, 5);
+ vector.insert(0, -2);
+
+ CHECK(vector.size() == 5);
+ CHECK(vector[0] == -2);
+ CHECK(vector[1] == 8);
+ CHECK(vector[2] == 5);
+ CHECK(vector[3] == 2);
+ CHECK(vector[4] == 5);
+}
+
+TEST_CASE("[Vector] Ordered insert") {
+ Vector<int> vector;
+ vector.ordered_insert(2);
+ vector.ordered_insert(8);
+ vector.ordered_insert(5);
+ vector.ordered_insert(5);
+ vector.ordered_insert(-2);
+
+ CHECK(vector.size() == 5);
+ CHECK(vector[0] == -2);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 5);
+ CHECK(vector[3] == 5);
+ CHECK(vector[4] == 8);
+}
+
+TEST_CASE("[Vector] Insert + Ordered insert") {
+ Vector<int> vector;
+ vector.ordered_insert(2);
+ vector.ordered_insert(8);
+ vector.insert(0, 5);
+ vector.ordered_insert(5);
+ vector.insert(1, -2);
+
+ CHECK(vector.size() == 5);
+ CHECK(vector[0] == 5);
+ CHECK(vector[1] == -2);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 5);
+ CHECK(vector[4] == 8);
+}
+
+TEST_CASE("[Vector] Fill large array and modify it") {
+ Vector<int> vector;
+ vector.resize(1'000'000);
+ vector.fill(0x60d07);
+
+ vector.write[200] = 0;
+ CHECK(vector.size() == 1'000'000);
+ CHECK(vector[0] == 0x60d07);
+ CHECK(vector[200] == 0);
+ CHECK(vector[499'999] == 0x60d07);
+ CHECK(vector[999'999] == 0x60d07);
+ vector.remove_at(200);
+ CHECK(vector[200] == 0x60d07);
+
+ vector.clear();
+ CHECK(vector.size() == 0);
+}
+
+TEST_CASE("[Vector] Copy creation") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ Vector<int> vector_other = Vector<int>(vector);
+ vector_other.remove_at(0);
+ CHECK(vector_other[0] == 1);
+ CHECK(vector_other[1] == 2);
+ CHECK(vector_other[2] == 3);
+ CHECK(vector_other[3] == 4);
+
+ // Make sure the original vector isn't modified.
+ CHECK(vector[0] == 0);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 3);
+ CHECK(vector[4] == 4);
+}
+
+TEST_CASE("[Vector] Duplicate") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ Vector<int> vector_other = vector.duplicate();
+ vector_other.remove_at(0);
+ CHECK(vector_other[0] == 1);
+ CHECK(vector_other[1] == 2);
+ CHECK(vector_other[2] == 3);
+ CHECK(vector_other[3] == 4);
+
+ // Make sure the original vector isn't modified.
+ CHECK(vector[0] == 0);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 3);
+ CHECK(vector[4] == 4);
+}
+
+TEST_CASE("[Vector] Get, set") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ CHECK(vector.get(0) == 0);
+ CHECK(vector.get(1) == 1);
+ vector.set(2, 256);
+ CHECK(vector.get(2) == 256);
+ CHECK(vector.get(3) == 3);
+
+ ERR_PRINT_OFF;
+ // Invalid (but should not crash): setting out of bounds.
+ vector.set(6, 500);
+ ERR_PRINT_ON;
+
+ CHECK(vector.get(4) == 4);
+}
+
+TEST_CASE("[Vector] To byte array") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(-1);
+ vector.push_back(2008);
+ vector.push_back(999999999);
+
+ Vector<uint8_t> byte_array = vector.to_byte_array();
+ CHECK(byte_array.size() == 16);
+ // vector[0]
+ CHECK(byte_array[0] == 0);
+ CHECK(byte_array[1] == 0);
+ CHECK(byte_array[2] == 0);
+ CHECK(byte_array[3] == 0);
+
+ // vector[1]
+ CHECK(byte_array[4] == 255);
+ CHECK(byte_array[5] == 255);
+ CHECK(byte_array[6] == 255);
+ CHECK(byte_array[7] == 255);
+
+ // vector[2]
+ CHECK(byte_array[8] == 216);
+ CHECK(byte_array[9] == 7);
+ CHECK(byte_array[10] == 0);
+ CHECK(byte_array[11] == 0);
+
+ // vector[3]
+ CHECK(byte_array[12] == 255);
+ CHECK(byte_array[13] == 201);
+ CHECK(byte_array[14] == 154);
+ CHECK(byte_array[15] == 59);
+}
+
+TEST_CASE("[Vector] Slice") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ Vector<int> slice0 = vector.slice(0, 0);
+ CHECK(slice0.size() == 0);
+
+ Vector<int> slice1 = vector.slice(1, 3);
+ CHECK(slice1.size() == 2);
+ CHECK(slice1[0] == 1);
+ CHECK(slice1[1] == 2);
+
+ Vector<int> slice2 = vector.slice(1, -1);
+ CHECK(slice2.size() == 3);
+ CHECK(slice2[0] == 1);
+ CHECK(slice2[1] == 2);
+ CHECK(slice2[2] == 3);
+
+ Vector<int> slice3 = vector.slice(3);
+ CHECK(slice3.size() == 2);
+ CHECK(slice3[0] == 3);
+ CHECK(slice3[1] == 4);
+
+ Vector<int> slice4 = vector.slice(2, -2);
+ CHECK(slice4.size() == 1);
+ CHECK(slice4[0] == 2);
+
+ Vector<int> slice5 = vector.slice(-2);
+ CHECK(slice5.size() == 2);
+ CHECK(slice5[0] == 3);
+ CHECK(slice5[1] == 4);
+
+ Vector<int> slice6 = vector.slice(2, 42);
+ CHECK(slice6.size() == 3);
+ CHECK(slice6[0] == 2);
+ CHECK(slice6[1] == 3);
+ CHECK(slice6[2] == 4);
+
+ ERR_PRINT_OFF;
+ Vector<int> slice7 = vector.slice(5, 1);
+ CHECK(slice7.size() == 0); // Expected to fail.
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[Vector] Find, has") {
+ Vector<int> vector;
+ vector.push_back(3);
+ vector.push_back(1);
+ vector.push_back(4);
+ vector.push_back(0);
+ vector.push_back(2);
+
+ CHECK(vector[0] == 3);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 4);
+ CHECK(vector[3] == 0);
+ CHECK(vector[4] == 2);
+
+ CHECK(vector.find(0) == 3);
+ CHECK(vector.find(1) == 1);
+ CHECK(vector.find(2) == 4);
+ CHECK(vector.find(3) == 0);
+ CHECK(vector.find(4) == 2);
+
+ CHECK(vector.find(-1) == -1);
+ CHECK(vector.find(5) == -1);
+
+ CHECK(vector.has(0));
+ CHECK(vector.has(1));
+ CHECK(vector.has(2));
+ CHECK(vector.has(3));
+ CHECK(vector.has(4));
+
+ CHECK(!vector.has(-1));
+ CHECK(!vector.has(5));
+}
+
+TEST_CASE("[Vector] Remove at") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ vector.remove_at(0);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 3);
+ CHECK(vector[3] == 4);
+
+ vector.remove_at(2);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 4);
+
+ vector.remove_at(1);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 4);
+
+ vector.remove_at(0);
+
+ CHECK(vector[0] == 4);
+}
+
+TEST_CASE("[Vector] Remove at and find") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ CHECK(vector.size() == 5);
+
+ vector.remove_at(0);
+
+ CHECK(vector.size() == 4);
+
+ CHECK(vector.find(0) == -1);
+ CHECK(vector.find(1) != -1);
+ CHECK(vector.find(2) != -1);
+ CHECK(vector.find(3) != -1);
+ CHECK(vector.find(4) != -1);
+
+ vector.remove_at(vector.find(3));
+
+ CHECK(vector.size() == 3);
+
+ CHECK(vector.find(3) == -1);
+ CHECK(vector.find(1) != -1);
+ CHECK(vector.find(2) != -1);
+ CHECK(vector.find(4) != -1);
+
+ vector.remove_at(vector.find(2));
+
+ CHECK(vector.size() == 2);
+
+ CHECK(vector.find(2) == -1);
+ CHECK(vector.find(1) != -1);
+ CHECK(vector.find(4) != -1);
+
+ vector.remove_at(vector.find(4));
+
+ CHECK(vector.size() == 1);
+
+ CHECK(vector.find(4) == -1);
+ CHECK(vector.find(1) != -1);
+
+ vector.remove_at(0);
+
+ CHECK(vector.is_empty());
+ CHECK(vector.size() == 0);
+}
+
+TEST_CASE("[Vector] Erase") {
+ Vector<int> vector;
+ vector.push_back(1);
+ vector.push_back(3);
+ vector.push_back(0);
+ vector.push_back(2);
+ vector.push_back(4);
+
+ CHECK(vector.find(2) == 3);
+
+ vector.erase(2);
+
+ CHECK(vector.find(2) == -1);
+ CHECK(vector.size() == 4);
+}
+
+TEST_CASE("[Vector] Size, resize, reserve") {
+ Vector<int> vector;
+ CHECK(vector.is_empty());
+ CHECK(vector.size() == 0);
+
+ vector.resize(10);
+
+ CHECK(vector.size() == 10);
+
+ vector.resize(5);
+
+ CHECK(vector.size() == 5);
+
+ vector.remove_at(0);
+ vector.remove_at(0);
+ vector.remove_at(0);
+
+ CHECK(vector.size() == 2);
+
+ vector.clear();
+
+ CHECK(vector.size() == 0);
+ CHECK(vector.is_empty());
+
+ vector.push_back(0);
+ vector.push_back(0);
+ vector.push_back(0);
+
+ CHECK(vector.size() == 3);
+
+ vector.push_back(0);
+
+ CHECK(vector.size() == 4);
+}
+
+TEST_CASE("[Vector] Sort") {
+ Vector<int> vector;
+ vector.push_back(2);
+ vector.push_back(8);
+ vector.push_back(-4);
+ vector.push_back(5);
+ vector.sort();
+
+ CHECK(vector.size() == 4);
+ CHECK(vector[0] == -4);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 5);
+ CHECK(vector[3] == 8);
+}
+
+TEST_CASE("[Vector] Sort custom") {
+ Vector<String> vector;
+ vector.push_back("world");
+ vector.push_back("World");
+ vector.push_back("Hello");
+ vector.push_back("10Hello");
+ vector.push_back("12Hello");
+ vector.push_back("01Hello");
+ vector.push_back("1Hello");
+ vector.push_back(".Hello");
+ vector.sort_custom<NaturalNoCaseComparator>();
+
+ CHECK(vector.size() == 8);
+ CHECK(vector[0] == ".Hello");
+ CHECK(vector[1] == "01Hello");
+ CHECK(vector[2] == "1Hello");
+ CHECK(vector[3] == "10Hello");
+ CHECK(vector[4] == "12Hello");
+ CHECK(vector[5] == "Hello");
+ CHECK(vector[6] == "world");
+ CHECK(vector[7] == "World");
+}
+
+TEST_CASE("[Vector] Search") {
+ Vector<int> vector;
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(5);
+ vector.push_back(8);
+ CHECK(vector.bsearch(2, true) == 1);
+ CHECK(vector.bsearch(2, false) == 2);
+ CHECK(vector.bsearch(5, true) == 3);
+ CHECK(vector.bsearch(5, false) == 4);
+}
+
+TEST_CASE("[Vector] Operators") {
+ Vector<int> vector;
+ vector.push_back(2);
+ vector.push_back(8);
+ vector.push_back(-4);
+ vector.push_back(5);
+
+ Vector<int> vector_other;
+ vector_other.push_back(2);
+ vector_other.push_back(8);
+ vector_other.push_back(-4);
+ vector_other.push_back(5);
+
+ CHECK(vector == vector_other);
+
+ vector_other.push_back(10);
+ CHECK(vector != vector_other);
+}
+
+} // namespace TestVector
+
+#endif // TEST_VECTOR_H
diff --git a/tests/core/test_crypto.h b/tests/core/test_crypto.h
new file mode 100644
index 0000000000..ce4edc71ae
--- /dev/null
+++ b/tests/core/test_crypto.h
@@ -0,0 +1,73 @@
+/*************************************************************************/
+/* test_crypto.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_CRYPTO_H
+#define TEST_CRYPTO_H
+
+#include "core/crypto/crypto.h"
+#include "tests/test_macros.h"
+
+namespace TestCrypto {
+
+class _MockCrypto : public Crypto {
+ virtual PackedByteArray generate_random_bytes(int p_bytes) { return PackedByteArray(); }
+ virtual Ref<CryptoKey> generate_rsa(int p_bytes) { return nullptr; }
+ virtual Ref<X509Certificate> generate_self_signed_certificate(Ref<CryptoKey> p_key, String p_issuer_name, String p_not_before, String p_not_after) { return nullptr; }
+
+ virtual Vector<uint8_t> sign(HashingContext::HashType p_hash_type, Vector<uint8_t> p_hash, Ref<CryptoKey> p_key) { return Vector<uint8_t>(); }
+ virtual bool verify(HashingContext::HashType p_hash_type, Vector<uint8_t> p_hash, Vector<uint8_t> p_signature, Ref<CryptoKey> p_key) { return false; }
+ virtual Vector<uint8_t> encrypt(Ref<CryptoKey> p_key, Vector<uint8_t> p_plaintext) { return Vector<uint8_t>(); }
+ virtual Vector<uint8_t> decrypt(Ref<CryptoKey> p_key, Vector<uint8_t> p_ciphertext) { return Vector<uint8_t>(); }
+ virtual PackedByteArray hmac_digest(HashingContext::HashType p_hash_type, PackedByteArray p_key, PackedByteArray p_msg) { return PackedByteArray(); }
+};
+
+PackedByteArray raw_to_pba(const uint8_t *arr, size_t len) {
+ PackedByteArray pba;
+ pba.resize(len);
+ for (size_t i = 0; i < len; i++) {
+ pba.set(i, arr[i]);
+ }
+ return pba;
+}
+
+TEST_CASE("[Crypto] PackedByteArray constant time compare") {
+ const uint8_t hm1[] = { 144, 140, 176, 38, 88, 113, 101, 45, 71, 105, 10, 91, 248, 16, 117, 244, 189, 30, 238, 29, 219, 134, 82, 130, 212, 114, 161, 166, 188, 169, 200, 106 };
+ const uint8_t hm2[] = { 80, 30, 144, 228, 108, 38, 188, 125, 150, 64, 165, 127, 221, 118, 144, 232, 45, 100, 15, 248, 193, 244, 245, 34, 116, 147, 132, 200, 110, 27, 38, 75 };
+ PackedByteArray p1 = raw_to_pba(hm1, sizeof(hm1) / sizeof(hm1[0]));
+ PackedByteArray p2 = raw_to_pba(hm2, sizeof(hm2) / sizeof(hm2[0]));
+ _MockCrypto crypto;
+ bool equal = crypto.constant_time_compare(p1, p1);
+ CHECK(equal);
+ equal = crypto.constant_time_compare(p1, p2);
+ CHECK(!equal);
+}
+} // namespace TestCrypto
+
+#endif // TEST_CRYPTO_H
diff --git a/tests/core/test_hashing_context.h b/tests/core/test_hashing_context.h
new file mode 100644
index 0000000000..4795d24103
--- /dev/null
+++ b/tests/core/test_hashing_context.h
@@ -0,0 +1,165 @@
+/*************************************************************************/
+/* test_hashing_context.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_HASHING_CONTEXT_H
+#define TEST_HASHING_CONTEXT_H
+
+#include "core/crypto/hashing_context.h"
+
+#include "tests/test_macros.h"
+
+namespace TestHashingContext {
+
+TEST_CASE("[HashingContext] Default - MD5/SHA1/SHA256") {
+ HashingContext ctx;
+
+ static const uint8_t md5_expected[] = {
+ 0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8, 0x42, 0x7e
+ };
+ static const uint8_t sha1_expected[] = {
+ 0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60, 0x18, 0x90,
+ 0xaf, 0xd8, 0x07, 0x09
+ };
+ static const uint8_t sha256_expected[] = {
+ 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
+ 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55
+ };
+
+ CHECK(ctx.start(HashingContext::HASH_MD5) == OK);
+ PackedByteArray result = ctx.finish();
+ REQUIRE(result.size() == 16);
+ CHECK(memcmp(result.ptr(), md5_expected, 16) == 0);
+
+ CHECK(ctx.start(HashingContext::HASH_SHA1) == OK);
+ result = ctx.finish();
+ REQUIRE(result.size() == 20);
+ CHECK(memcmp(result.ptr(), sha1_expected, 20) == 0);
+
+ CHECK(ctx.start(HashingContext::HASH_SHA256) == OK);
+ result = ctx.finish();
+ REQUIRE(result.size() == 32);
+ CHECK(memcmp(result.ptr(), sha256_expected, 32) == 0);
+}
+
+TEST_CASE("[HashingContext] Multiple updates - MD5/SHA1/SHA256") {
+ HashingContext ctx;
+ const String s = "xyz";
+
+ const PackedByteArray s_byte_parts[] = {
+ String("x").to_ascii_buffer(),
+ String("y").to_ascii_buffer(),
+ String("z").to_ascii_buffer()
+ };
+
+ static const uint8_t md5_expected[] = {
+ 0xd1, 0x6f, 0xb3, 0x6f, 0x09, 0x11, 0xf8, 0x78, 0x99, 0x8c, 0x13, 0x61, 0x91, 0xaf, 0x70, 0x5e
+ };
+ static const uint8_t sha1_expected[] = {
+ 0x66, 0xb2, 0x74, 0x17, 0xd3, 0x7e, 0x02, 0x4c, 0x46, 0x52, 0x6c, 0x2f, 0x6d, 0x35, 0x8a, 0x75,
+ 0x4f, 0xc5, 0x52, 0xf3
+ };
+ static const uint8_t sha256_expected[] = {
+ 0x36, 0x08, 0xbc, 0xa1, 0xe4, 0x4e, 0xa6, 0xc4, 0xd2, 0x68, 0xeb, 0x6d, 0xb0, 0x22, 0x60, 0x26,
+ 0x98, 0x92, 0xc0, 0xb4, 0x2b, 0x86, 0xbb, 0xf1, 0xe7, 0x7a, 0x6f, 0xa1, 0x6c, 0x3c, 0x92, 0x82
+ };
+
+ CHECK(ctx.start(HashingContext::HASH_MD5) == OK);
+ CHECK(ctx.update(s_byte_parts[0]) == OK);
+ CHECK(ctx.update(s_byte_parts[1]) == OK);
+ CHECK(ctx.update(s_byte_parts[2]) == OK);
+ PackedByteArray result = ctx.finish();
+ REQUIRE(result.size() == 16);
+ CHECK(memcmp(result.ptr(), md5_expected, 16) == 0);
+
+ CHECK(ctx.start(HashingContext::HASH_SHA1) == OK);
+ CHECK(ctx.update(s_byte_parts[0]) == OK);
+ CHECK(ctx.update(s_byte_parts[1]) == OK);
+ CHECK(ctx.update(s_byte_parts[2]) == OK);
+ result = ctx.finish();
+ REQUIRE(result.size() == 20);
+ CHECK(memcmp(result.ptr(), sha1_expected, 20) == 0);
+
+ CHECK(ctx.start(HashingContext::HASH_SHA256) == OK);
+ CHECK(ctx.update(s_byte_parts[0]) == OK);
+ CHECK(ctx.update(s_byte_parts[1]) == OK);
+ CHECK(ctx.update(s_byte_parts[2]) == OK);
+ result = ctx.finish();
+ REQUIRE(result.size() == 32);
+ CHECK(memcmp(result.ptr(), sha256_expected, 32) == 0);
+}
+
+TEST_CASE("[HashingContext] Invalid use of start") {
+ HashingContext ctx;
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ ctx.start(static_cast<HashingContext::HashType>(-1)) == ERR_UNAVAILABLE,
+ "Using invalid hash types should fail.");
+ ERR_PRINT_ON;
+
+ REQUIRE(ctx.start(HashingContext::HASH_MD5) == OK);
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ ctx.start(HashingContext::HASH_MD5) == ERR_ALREADY_IN_USE,
+ "Calling 'start' twice before 'finish' should fail.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[HashingContext] Invalid use of update") {
+ HashingContext ctx;
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ ctx.update(PackedByteArray()) == ERR_UNCONFIGURED,
+ "Calling 'update' before 'start' should fail.");
+ ERR_PRINT_ON;
+
+ REQUIRE(ctx.start(HashingContext::HASH_MD5) == OK);
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ ctx.update(PackedByteArray()) == FAILED,
+ "Calling 'update' with an empty byte array should fail.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[HashingContext] Invalid use of finish") {
+ HashingContext ctx;
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ ctx.finish() == PackedByteArray(),
+ "Calling 'finish' before 'start' should return an empty byte array.");
+ ERR_PRINT_ON;
+}
+} // namespace TestHashingContext
+
+#endif // TEST_HASHING_CONTEXT_H
diff --git a/tests/core/test_time.h b/tests/core/test_time.h
new file mode 100644
index 0000000000..177512c832
--- /dev/null
+++ b/tests/core/test_time.h
@@ -0,0 +1,148 @@
+/*************************************************************************/
+/* test_time.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_TIME_H
+#define TEST_TIME_H
+
+#include "core/os/time.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+#define YEAR_KEY "year"
+#define MONTH_KEY "month"
+#define DAY_KEY "day"
+#define WEEKDAY_KEY "weekday"
+#define HOUR_KEY "hour"
+#define MINUTE_KEY "minute"
+#define SECOND_KEY "second"
+#define DST_KEY "dst"
+
+namespace TestTime {
+
+TEST_CASE("[Time] Unix time conversion to/from datetime string") {
+ const Time *time = Time::get_singleton();
+
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1970-01-01T00:00:00") == 0, "Time get_unix_time_from_datetime_string: The timestamp for Unix epoch is zero.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1970-01-01 00:00:00") == 0, "Time get_unix_time_from_datetime_string: The timestamp for Unix epoch with space is zero.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1970-01-01") == 0, "Time get_unix_time_from_datetime_string: The timestamp for Unix epoch without time is zero.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("00:00:00") == 0, "Time get_unix_time_from_datetime_string: The timestamp for zero time without date is zero.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1969-12-31T23:59:59") == -1, "Time get_unix_time_from_datetime_string: The timestamp for just before Unix epoch is negative one.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1234-05-06T07:08:09") == -23215049511, "Time get_unix_time_from_datetime_string: The timestamp for an arbitrary datetime is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1234-05-06 07:08:09") == -23215049511, "Time get_unix_time_from_datetime_string: The timestamp for an arbitrary datetime with space is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1234-05-06") == -23215075200, "Time get_unix_time_from_datetime_string: The timestamp for an arbitrary date without time is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("07:08:09") == 25689, "Time get_unix_time_from_datetime_string: The timestamp for an arbitrary time without date is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("2014-02-09T22:10:30") == 1391983830, "Time get_unix_time_from_datetime_string: The timestamp for GODOT IS OPEN SOURCE is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("2014-02-09 22:10:30") == 1391983830, "Time get_unix_time_from_datetime_string: The timestamp for GODOT IS OPEN SOURCE with space is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("2014-02-09") == 1391904000, "Time get_unix_time_from_datetime_string: The date for GODOT IS OPEN SOURCE without time is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("22:10:30") == 79830, "Time get_unix_time_from_datetime_string: The time for GODOT IS OPEN SOURCE without date is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("-1000000000-01-01T00:00:00") == -31557014167219200, "Time get_unix_time_from_datetime_string: In the year negative a billion, Japan might not have been here.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1000000-01-01T00:00:00") == 31494784780800, "Time get_unix_time_from_datetime_string: The timestamp for the year a million is as expected.");
+
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(0) == "1970-01-01T00:00:00", "Time get_datetime_string_from_unix_time: The timestamp string for Unix epoch is zero.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(0, true) == "1970-01-01 00:00:00", "Time get_datetime_string_from_unix_time: The timestamp string for Unix epoch with space is zero.");
+ CHECK_MESSAGE(time->get_date_string_from_unix_time(0) == "1970-01-01", "Time get_date_string_from_unix_time: The date string for zero is Unix epoch date.");
+ CHECK_MESSAGE(time->get_time_string_from_unix_time(0) == "00:00:00", "Time get_time_string_from_unix_time: The date for zero zero is Unix epoch date.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(-1) == "1969-12-31T23:59:59", "Time get_time_string_from_unix_time: The timestamp string for just before Unix epoch is as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(-23215049511) == "1234-05-06T07:08:09", "Time get_datetime_string_from_unix_time: The timestamp for an arbitrary datetime is as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(-23215049511, true) == "1234-05-06 07:08:09", "Time get_datetime_string_from_unix_time: The timestamp for an arbitrary datetime with space is as expected.");
+ CHECK_MESSAGE(time->get_date_string_from_unix_time(-23215075200) == "1234-05-06", "Time get_date_string_from_unix_time: The timestamp for an arbitrary date without time is as expected.");
+ CHECK_MESSAGE(time->get_time_string_from_unix_time(25689) == "07:08:09", "Time get_time_string_from_unix_time: The timestamp for an arbitrary time without date is as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(1391983830) == "2014-02-09T22:10:30", "Time get_datetime_string_from_unix_time: The timestamp for GODOT IS OPEN SOURCE is as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(1391983830, true) == "2014-02-09 22:10:30", "Time get_datetime_string_from_unix_time: The timestamp for GODOT IS OPEN SOURCE with space is as expected.");
+ CHECK_MESSAGE(time->get_date_string_from_unix_time(1391904000) == "2014-02-09", "Time get_date_string_from_unix_time: The date for GODOT IS OPEN SOURCE without time is as expected.");
+ CHECK_MESSAGE(time->get_time_string_from_unix_time(79830) == "22:10:30", "Time get_time_string_from_unix_time: The time for GODOT IS OPEN SOURCE without date is as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(31494784780800) == "1000000-01-01T00:00:00", "Time get_datetime_string_from_unix_time: The timestamp for the year a million is as expected.");
+ CHECK_MESSAGE(time->get_offset_string_from_offset_minutes(0) == "+00:00", "Time get_offset_string_from_offset_minutes: The offset string is as expected.");
+ CHECK_MESSAGE(time->get_offset_string_from_offset_minutes(-600) == "-10:00", "Time get_offset_string_from_offset_minutes: The offset string is as expected.");
+ CHECK_MESSAGE(time->get_offset_string_from_offset_minutes(345) == "+05:45", "Time get_offset_string_from_offset_minutes: The offset string is as expected.");
+}
+
+TEST_CASE("[Time] Datetime dictionary conversion methods") {
+ const Time *time = Time::get_singleton();
+
+ Dictionary datetime;
+ datetime[YEAR_KEY] = 2014;
+ datetime[MONTH_KEY] = 2;
+ datetime[DAY_KEY] = 9;
+ datetime[WEEKDAY_KEY] = Time::Weekday::WEEKDAY_SUNDAY;
+ datetime[HOUR_KEY] = 22;
+ datetime[MINUTE_KEY] = 10;
+ datetime[SECOND_KEY] = 30;
+
+ Dictionary date_only;
+ date_only[YEAR_KEY] = 2014;
+ date_only[MONTH_KEY] = 2;
+ date_only[DAY_KEY] = 9;
+ date_only[WEEKDAY_KEY] = Time::Weekday::WEEKDAY_SUNDAY;
+
+ Dictionary time_only;
+ time_only[HOUR_KEY] = 22;
+ time_only[MINUTE_KEY] = 10;
+ time_only[SECOND_KEY] = 30;
+
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(datetime) == 1391983830, "Time get_unix_time_from_datetime_dict: The datetime dictionary for GODOT IS OPEN SOURCE is converted to a timestamp as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(date_only) == 1391904000, "Time get_unix_time_from_datetime_dict: The date dictionary for GODOT IS OPEN SOURCE is converted to a timestamp as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(time_only) == 79830, "Time get_unix_time_from_datetime_dict: The time dictionary for GODOT IS OPEN SOURCE is converted to a timestamp as expected.");
+
+ CHECK_MESSAGE(time->get_datetime_dict_from_unix_time(1391983830).hash() == datetime.hash(), "Time get_datetime_dict_from_unix_time: The datetime timestamp for GODOT IS OPEN SOURCE is converted to a dictionary as expected.");
+ CHECK_MESSAGE(time->get_date_dict_from_unix_time(1391904000).hash() == date_only.hash(), "Time get_date_dict_from_unix_time: The date timestamp for GODOT IS OPEN SOURCE is converted to a dictionary as expected.");
+ CHECK_MESSAGE(time->get_time_dict_from_unix_time(79830).hash() == time_only.hash(), "Time get_time_dict_from_unix_time: The time timestamp for GODOT IS OPEN SOURCE is converted to a dictionary as expected.");
+
+ CHECK_MESSAGE((Time::Weekday)(int)time->get_datetime_dict_from_unix_time(0)[WEEKDAY_KEY] == Time::Weekday::WEEKDAY_THURSDAY, "Time get_datetime_dict_from_unix_time: The weekday for the Unix epoch is a Thursday as expected.");
+ CHECK_MESSAGE((Time::Weekday)(int)time->get_datetime_dict_from_unix_time(1391983830)[WEEKDAY_KEY] == Time::Weekday::WEEKDAY_SUNDAY, "Time get_datetime_dict_from_unix_time: The weekday for GODOT IS OPEN SOURCE is a Sunday as expected.");
+
+ CHECK_MESSAGE(time->get_datetime_dict_from_datetime_string("2014-02-09T22:10:30").hash() == datetime.hash(), "Time get_datetime_dict_from_string: The dictionary from string for GODOT IS OPEN SOURCE works as expected.");
+ CHECK_MESSAGE(!time->get_datetime_dict_from_datetime_string("2014-02-09T22:10:30", false).has(WEEKDAY_KEY), "Time get_datetime_dict_from_string: The dictionary from string for GODOT IS OPEN SOURCE without weekday doesn't contain the weekday key as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_datetime_dict(datetime) == "2014-02-09T22:10:30", "Time get_datetime_string_from_dict: The string from dictionary for GODOT IS OPEN SOURCE works as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_datetime_dict(time->get_datetime_dict_from_datetime_string("2014-02-09T22:10:30")) == "2014-02-09T22:10:30", "Time get_datetime_string_from_dict: The round-trip string to dict to string GODOT IS OPEN SOURCE works as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_datetime_dict(time->get_datetime_dict_from_datetime_string("2014-02-09 22:10:30"), true) == "2014-02-09 22:10:30", "Time get_datetime_string_from_dict: The round-trip string to dict to string GODOT IS OPEN SOURCE with spaces works as expected.");
+}
+
+TEST_CASE("[Time] System time methods") {
+ const Time *time = Time::get_singleton();
+
+ const uint64_t ticks_msec = time->get_ticks_msec();
+ const uint64_t ticks_usec = time->get_ticks_usec();
+
+ CHECK_MESSAGE(time->get_unix_time_from_system() > 1000000000, "Time get_unix_time_from_system: The timestamp from system time doesn't fail and is very positive.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(time->get_datetime_dict_from_system()) > 1000000000, "Time get_datetime_string_from_system: The timestamp from system time doesn't fail and is very positive.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(time->get_date_dict_from_system()) > 1000000000, "Time get_datetime_string_from_system: The date from system time doesn't fail and is very positive.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(time->get_time_dict_from_system()) < 86400, "Time get_datetime_string_from_system: The time from system time doesn't fail and is within the acceptable range.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string(time->get_datetime_string_from_system()) > 1000000000, "Time get_datetime_string_from_system: The timestamp from system time doesn't fail and is very positive.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string(time->get_date_string_from_system()) > 1000000000, "Time get_datetime_string_from_system: The date from system time doesn't fail and is very positive.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string(time->get_time_string_from_system()) < 86400, "Time get_datetime_string_from_system: The time from system time doesn't fail and is within the acceptable range.");
+
+ CHECK_MESSAGE(time->get_ticks_msec() >= ticks_msec, "Time get_ticks_msec: The value has not decreased.");
+ CHECK_MESSAGE(time->get_ticks_usec() > ticks_usec, "Time get_ticks_usec: The value has increased.");
+}
+
+} // namespace TestTime
+
+#endif // TEST_TIME_H
diff --git a/tests/core/threads/test_worker_thread_pool.h b/tests/core/threads/test_worker_thread_pool.h
new file mode 100644
index 0000000000..641b293c8a
--- /dev/null
+++ b/tests/core/threads/test_worker_thread_pool.h
@@ -0,0 +1,158 @@
+/*************************************************************************/
+/* test_worker_thread_pool.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_WORKER_THREAD_POOL_H
+#define TEST_WORKER_THREAD_POOL_H
+
+#include "core/object/worker_thread_pool.h"
+
+#include "tests/test_macros.h"
+
+namespace TestWorkerThreadPool {
+
+int u32scmp(const char32_t *l, const char32_t *r) {
+ for (; *l == *r && *l && *r; l++, r++) {
+ // Continue.
+ }
+ return *l - *r;
+}
+
+static void static_test(void *p_arg) {
+ SafeNumeric<uint32_t> *counter = (SafeNumeric<uint32_t> *)p_arg;
+ counter->increment();
+}
+
+static SafeNumeric<uint32_t> callable_counter;
+
+static void static_callable_test() {
+ callable_counter.increment();
+}
+
+TEST_CASE("[WorkerThreadPool] Process 256 threads using native task") {
+ const int count = 256;
+ SafeNumeric<uint32_t> counter;
+ WorkerThreadPool::TaskID tasks[count];
+ for (int i = 0; i < count; i++) {
+ tasks[i] = WorkerThreadPool::get_singleton()->add_native_task(static_test, &counter, true);
+ }
+ for (int i = 0; i < count; i++) {
+ WorkerThreadPool::get_singleton()->wait_for_task_completion(tasks[i]);
+ }
+
+ CHECK(counter.get() == count);
+}
+
+TEST_CASE("[WorkerThreadPool] Process 256 threads using native low priority") {
+ const int count = 256;
+ SafeNumeric<uint32_t> counter = SafeNumeric<uint32_t>(0);
+ WorkerThreadPool::TaskID tasks[count];
+ for (int i = 0; i < count; i++) {
+ tasks[i] = WorkerThreadPool::get_singleton()->add_native_task(static_test, &counter, false);
+ }
+ for (int i = 0; i < count; i++) {
+ WorkerThreadPool::get_singleton()->wait_for_task_completion(tasks[i]);
+ }
+
+ CHECK(counter.get() == count);
+}
+
+TEST_CASE("[WorkerThreadPool] Process 256 threads using callable") {
+ const int count = 256;
+ WorkerThreadPool::TaskID tasks[count];
+ callable_counter.set(0);
+ for (int i = 0; i < count; i++) {
+ tasks[i] = WorkerThreadPool::get_singleton()->add_task(callable_mp_static(static_callable_test), true);
+ }
+ for (int i = 0; i < count; i++) {
+ WorkerThreadPool::get_singleton()->wait_for_task_completion(tasks[i]);
+ }
+
+ CHECK(callable_counter.get() == count);
+}
+
+TEST_CASE("[WorkerThreadPool] Process 256 threads using callable low priority") {
+ const int count = 256;
+ WorkerThreadPool::TaskID tasks[count];
+ callable_counter.set(0);
+ for (int i = 0; i < count; i++) {
+ tasks[i] = WorkerThreadPool::get_singleton()->add_task(callable_mp_static(static_callable_test), false);
+ }
+ for (int i = 0; i < count; i++) {
+ WorkerThreadPool::get_singleton()->wait_for_task_completion(tasks[i]);
+ }
+
+ CHECK(callable_counter.get() == count);
+}
+
+static void static_group_test(void *p_arg, uint32_t p_index) {
+ SafeNumeric<uint32_t> *counter = (SafeNumeric<uint32_t> *)p_arg;
+ counter->exchange_if_greater(p_index);
+}
+
+TEST_CASE("[WorkerThreadPool] Process 256 elements on native task group") {
+ const int count = 256;
+ SafeNumeric<uint32_t> counter;
+ WorkerThreadPool::GroupID group = WorkerThreadPool::get_singleton()->add_native_group_task(static_group_test, &counter, count, -1, true);
+ WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group);
+ CHECK(counter.get() == count - 1);
+}
+
+TEST_CASE("[WorkerThreadPool] Process 256 elements on native task group low priority") {
+ const int count = 256;
+ SafeNumeric<uint32_t> counter;
+ WorkerThreadPool::GroupID group = WorkerThreadPool::get_singleton()->add_native_group_task(static_group_test, &counter, count, -1, false);
+ WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group);
+ CHECK(counter.get() == count - 1);
+}
+
+static SafeNumeric<uint32_t> callable_group_counter;
+
+static void static_callable_group_test(uint32_t p_index) {
+ callable_group_counter.exchange_if_greater(p_index);
+}
+
+TEST_CASE("[WorkerThreadPool] Process 256 elements on native task group") {
+ const int count = 256;
+ WorkerThreadPool::GroupID group = WorkerThreadPool::get_singleton()->add_group_task(callable_mp_static(static_callable_group_test), count, -1, true);
+ WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group);
+ CHECK(callable_group_counter.get() == count - 1);
+}
+
+TEST_CASE("[WorkerThreadPool] Process 256 elements on native task group low priority") {
+ const int count = 256;
+ callable_group_counter.set(0);
+ WorkerThreadPool::GroupID group = WorkerThreadPool::get_singleton()->add_group_task(callable_mp_static(static_callable_group_test), count, -1, false);
+ WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group);
+ CHECK(callable_group_counter.get() == count - 1);
+}
+
+} // namespace TestWorkerThreadPool
+
+#endif // TEST_WORKER_THREAD_POOL_H
diff --git a/tests/core/variant/test_array.h b/tests/core/variant/test_array.h
new file mode 100644
index 0000000000..6093048307
--- /dev/null
+++ b/tests/core/variant/test_array.h
@@ -0,0 +1,521 @@
+/*************************************************************************/
+/* test_array.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_ARRAY_H
+#define TEST_ARRAY_H
+
+#include "core/variant/array.h"
+#include "tests/test_macros.h"
+#include "tests/test_tools.h"
+
+namespace TestArray {
+
+static inline Array build_array() {
+ return Array();
+}
+template <typename... Targs>
+static inline Array build_array(Variant item, Targs... Fargs) {
+ Array a = build_array(Fargs...);
+ a.push_front(item);
+ return a;
+}
+static inline Dictionary build_dictionary() {
+ return Dictionary();
+}
+template <typename... Targs>
+static inline Dictionary build_dictionary(Variant key, Variant item, Targs... Fargs) {
+ Dictionary d = build_dictionary(Fargs...);
+ d[key] = item;
+ return d;
+}
+
+TEST_CASE("[Array] size(), clear(), and is_empty()") {
+ Array arr;
+ CHECK(arr.size() == 0);
+ CHECK(arr.is_empty());
+ arr.push_back(1);
+ CHECK(arr.size() == 1);
+ arr.clear();
+ CHECK(arr.is_empty());
+ CHECK(arr.size() == 0);
+}
+
+TEST_CASE("[Array] Assignment and comparison operators") {
+ Array arr1;
+ Array arr2;
+ arr1.push_back(1);
+ CHECK(arr1 != arr2);
+ CHECK(arr1 > arr2);
+ CHECK(arr1 >= arr2);
+ arr2.push_back(2);
+ CHECK(arr1 != arr2);
+ CHECK(arr1 < arr2);
+ CHECK(arr1 <= arr2);
+ CHECK(arr2 > arr1);
+ CHECK(arr2 >= arr1);
+ Array arr3 = arr2;
+ CHECK(arr3 == arr2);
+}
+
+TEST_CASE("[Array] append_array()") {
+ Array arr1;
+ Array arr2;
+ arr1.push_back(1);
+ arr1.append_array(arr2);
+ CHECK(arr1.size() == 1);
+ arr2.push_back(2);
+ arr1.append_array(arr2);
+ CHECK(arr1.size() == 2);
+ CHECK(int(arr1[0]) == 1);
+ CHECK(int(arr1[1]) == 2);
+}
+
+TEST_CASE("[Array] resize(), insert(), and erase()") {
+ Array arr;
+ arr.resize(2);
+ CHECK(arr.size() == 2);
+ arr.insert(0, 1);
+ CHECK(int(arr[0]) == 1);
+ arr.insert(0, 2);
+ CHECK(int(arr[0]) == 2);
+ arr.erase(2);
+ CHECK(int(arr[0]) == 1);
+}
+
+TEST_CASE("[Array] front() and back()") {
+ Array arr;
+ arr.push_back(1);
+ CHECK(int(arr.front()) == 1);
+ CHECK(int(arr.back()) == 1);
+ arr.push_back(3);
+ CHECK(int(arr.front()) == 1);
+ CHECK(int(arr.back()) == 3);
+}
+
+TEST_CASE("[Array] has() and count()") {
+ Array arr;
+ arr.push_back(1);
+ arr.push_back(1);
+ CHECK(arr.has(1));
+ CHECK(!arr.has(2));
+ CHECK(arr.count(1) == 2);
+ CHECK(arr.count(2) == 0);
+}
+
+TEST_CASE("[Array] remove_at()") {
+ Array arr;
+ arr.push_back(1);
+ arr.push_back(2);
+ arr.remove_at(0);
+ CHECK(arr.size() == 1);
+ CHECK(int(arr[0]) == 2);
+ arr.remove_at(0);
+ CHECK(arr.size() == 0);
+
+ // The array is now empty; try to use `remove_at()` again.
+ // Normally, this prints an error message so we silence it.
+ ERR_PRINT_OFF;
+ arr.remove_at(0);
+ ERR_PRINT_ON;
+
+ CHECK(arr.size() == 0);
+}
+
+TEST_CASE("[Array] get()") {
+ Array arr;
+ arr.push_back(1);
+ CHECK(int(arr.get(0)) == 1);
+}
+
+TEST_CASE("[Array] sort()") {
+ Array arr;
+
+ arr.push_back(3);
+ arr.push_back(4);
+ arr.push_back(2);
+ arr.push_back(1);
+ arr.sort();
+ int val = 1;
+ for (int i = 0; i < arr.size(); i++) {
+ CHECK(int(arr[i]) == val);
+ val++;
+ }
+}
+
+TEST_CASE("[Array] push_front(), pop_front(), pop_back()") {
+ Array arr;
+ arr.push_front(1);
+ arr.push_front(2);
+ CHECK(int(arr[0]) == 2);
+ arr.pop_front();
+ CHECK(int(arr[0]) == 1);
+ CHECK(arr.size() == 1);
+ arr.push_front(2);
+ arr.push_front(3);
+ arr.pop_back();
+ CHECK(int(arr[1]) == 2);
+ CHECK(arr.size() == 2);
+}
+
+TEST_CASE("[Array] pop_at()") {
+ ErrorDetector ed;
+
+ Array arr;
+ arr.push_back(2);
+ arr.push_back(4);
+ arr.push_back(6);
+ arr.push_back(8);
+ arr.push_back(10);
+
+ REQUIRE(int(arr.pop_at(2)) == 6);
+ REQUIRE(arr.size() == 4);
+ CHECK(int(arr[0]) == 2);
+ CHECK(int(arr[1]) == 4);
+ CHECK(int(arr[2]) == 8);
+ CHECK(int(arr[3]) == 10);
+
+ REQUIRE(int(arr.pop_at(2)) == 8);
+ REQUIRE(arr.size() == 3);
+ CHECK(int(arr[0]) == 2);
+ CHECK(int(arr[1]) == 4);
+ CHECK(int(arr[2]) == 10);
+
+ // Negative index.
+ REQUIRE(int(arr.pop_at(-1)) == 10);
+ REQUIRE(arr.size() == 2);
+ CHECK(int(arr[0]) == 2);
+ CHECK(int(arr[1]) == 4);
+
+ // Invalid pop.
+ ed.clear();
+ ERR_PRINT_OFF;
+ const Variant ret = arr.pop_at(-15);
+ ERR_PRINT_ON;
+ REQUIRE(ret.is_null());
+ CHECK(ed.has_error);
+
+ REQUIRE(int(arr.pop_at(0)) == 2);
+ REQUIRE(arr.size() == 1);
+ CHECK(int(arr[0]) == 4);
+
+ REQUIRE(int(arr.pop_at(0)) == 4);
+ REQUIRE(arr.is_empty());
+
+ // Pop from empty array.
+ ed.clear();
+ REQUIRE(arr.pop_at(24).is_null());
+ CHECK_FALSE(ed.has_error);
+}
+
+TEST_CASE("[Array] max() and min()") {
+ Array arr;
+ arr.push_back(3);
+ arr.push_front(4);
+ arr.push_back(5);
+ arr.push_back(2);
+ int max = int(arr.max());
+ int min = int(arr.min());
+ CHECK(max == 5);
+ CHECK(min == 2);
+}
+
+TEST_CASE("[Array] slice()") {
+ Array array;
+ array.push_back(0);
+ array.push_back(1);
+ array.push_back(2);
+ array.push_back(3);
+ array.push_back(4);
+
+ Array slice0 = array.slice(0, 0);
+ CHECK(slice0.size() == 0);
+
+ Array slice1 = array.slice(1, 3);
+ CHECK(slice1.size() == 2);
+ CHECK(slice1[0] == Variant(1));
+ CHECK(slice1[1] == Variant(2));
+
+ Array slice2 = array.slice(1, -1);
+ CHECK(slice2.size() == 3);
+ CHECK(slice2[0] == Variant(1));
+ CHECK(slice2[1] == Variant(2));
+ CHECK(slice2[2] == Variant(3));
+
+ Array slice3 = array.slice(3);
+ CHECK(slice3.size() == 2);
+ CHECK(slice3[0] == Variant(3));
+ CHECK(slice3[1] == Variant(4));
+
+ Array slice4 = array.slice(2, -2);
+ CHECK(slice4.size() == 1);
+ CHECK(slice4[0] == Variant(2));
+
+ Array slice5 = array.slice(-2);
+ CHECK(slice5.size() == 2);
+ CHECK(slice5[0] == Variant(3));
+ CHECK(slice5[1] == Variant(4));
+
+ Array slice6 = array.slice(2, 42);
+ CHECK(slice6.size() == 3);
+ CHECK(slice6[0] == Variant(2));
+ CHECK(slice6[1] == Variant(3));
+ CHECK(slice6[2] == Variant(4));
+
+ Array slice7 = array.slice(4, 0, -2);
+ CHECK(slice7.size() == 2);
+ CHECK(slice7[0] == Variant(4));
+ CHECK(slice7[1] == Variant(2));
+
+ ERR_PRINT_OFF;
+ Array slice8 = array.slice(4, 1);
+ CHECK(slice8.size() == 0);
+
+ Array slice9 = array.slice(3, -4);
+ CHECK(slice9.size() == 0);
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[Array] Duplicate array") {
+ // a = [1, [2, 2], {3: 3}]
+ Array a = build_array(1, build_array(2, 2), build_dictionary(3, 3));
+
+ // Deep copy
+ Array deep_a = a.duplicate(true);
+ CHECK_MESSAGE(deep_a.id() != a.id(), "Should create a new array");
+ CHECK_MESSAGE(Array(deep_a[1]).id() != Array(a[1]).id(), "Should clone nested array");
+ CHECK_MESSAGE(Dictionary(deep_a[2]).id() != Dictionary(a[2]).id(), "Should clone nested dictionary");
+ CHECK_EQ(deep_a, a);
+ deep_a.push_back(1);
+ CHECK_NE(deep_a, a);
+ deep_a.pop_back();
+ Array(deep_a[1]).push_back(1);
+ CHECK_NE(deep_a, a);
+ Array(deep_a[1]).pop_back();
+ CHECK_EQ(deep_a, a);
+
+ // Shallow copy
+ Array shallow_a = a.duplicate(false);
+ CHECK_MESSAGE(shallow_a.id() != a.id(), "Should create a new array");
+ CHECK_MESSAGE(Array(shallow_a[1]).id() == Array(a[1]).id(), "Should keep nested array");
+ CHECK_MESSAGE(Dictionary(shallow_a[2]).id() == Dictionary(a[2]).id(), "Should keep nested dictionary");
+ CHECK_EQ(shallow_a, a);
+ Array(shallow_a).push_back(1);
+ CHECK_NE(shallow_a, a);
+}
+
+TEST_CASE("[Array] Duplicate recursive array") {
+ // Self recursive
+ Array a;
+ a.push_back(a);
+
+ Array a_shallow = a.duplicate(false);
+ CHECK_EQ(a, a_shallow);
+
+ // Deep copy of recursive array endup with recursion limit and return
+ // an invalid result (multiple nested arrays), the point is we should
+ // not end up with a segfault and an error log should be printed
+ ERR_PRINT_OFF;
+ a.duplicate(true);
+ ERR_PRINT_ON;
+
+ // Nested recursive
+ Array a1;
+ Array a2;
+ a2.push_back(a1);
+ a1.push_back(a2);
+
+ Array a1_shallow = a1.duplicate(false);
+ CHECK_EQ(a1, a1_shallow);
+
+ // Same deep copy issue as above
+ ERR_PRINT_OFF;
+ a1.duplicate(true);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Array teardown will leak memory
+ a.clear();
+ a1.clear();
+ a2.clear();
+}
+
+TEST_CASE("[Array] Hash array") {
+ // a = [1, [2, 2], {3: 3}]
+ Array a = build_array(1, build_array(2, 2), build_dictionary(3, 3));
+ uint32_t original_hash = a.hash();
+
+ a.push_back(1);
+ CHECK_NE(a.hash(), original_hash);
+
+ a.pop_back();
+ CHECK_EQ(a.hash(), original_hash);
+
+ Array(a[1]).push_back(1);
+ CHECK_NE(a.hash(), original_hash);
+ Array(a[1]).pop_back();
+ CHECK_EQ(a.hash(), original_hash);
+
+ (Dictionary(a[2]))[1] = 1;
+ CHECK_NE(a.hash(), original_hash);
+ Dictionary(a[2]).erase(1);
+ CHECK_EQ(a.hash(), original_hash);
+
+ Array a2 = a.duplicate(true);
+ CHECK_EQ(a2.hash(), a.hash());
+}
+
+TEST_CASE("[Array] Hash recursive array") {
+ Array a1;
+ a1.push_back(a1);
+
+ Array a2;
+ a2.push_back(a2);
+
+ // Hash should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(a1.hash(), a2.hash());
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Array teardown will leak memory
+ a1.clear();
+ a2.clear();
+}
+
+TEST_CASE("[Array] Empty comparison") {
+ Array a1;
+ Array a2;
+
+ // test both operator== and operator!=
+ CHECK_EQ(a1, a2);
+ CHECK_FALSE(a1 != a2);
+}
+
+TEST_CASE("[Array] Flat comparison") {
+ Array a1 = build_array(1);
+ Array a2 = build_array(1);
+ Array other_a = build_array(2);
+
+ // test both operator== and operator!=
+ CHECK_EQ(a1, a1); // compare self
+ CHECK_FALSE(a1 != a1);
+ CHECK_EQ(a1, a2); // different equivalent arrays
+ CHECK_FALSE(a1 != a2);
+ CHECK_NE(a1, other_a); // different arrays with different content
+ CHECK_FALSE(a1 == other_a);
+}
+
+TEST_CASE("[Array] Nested array comparison") {
+ // a1 = [[[1], 2], 3]
+ Array a1 = build_array(build_array(build_array(1), 2), 3);
+
+ Array a2 = a1.duplicate(true);
+
+ // other_a = [[[1, 0], 2], 3]
+ Array other_a = build_array(build_array(build_array(1, 0), 2), 3);
+
+ // test both operator== and operator!=
+ CHECK_EQ(a1, a1); // compare self
+ CHECK_FALSE(a1 != a1);
+ CHECK_EQ(a1, a2); // different equivalent arrays
+ CHECK_FALSE(a1 != a2);
+ CHECK_NE(a1, other_a); // different arrays with different content
+ CHECK_FALSE(a1 == other_a);
+}
+
+TEST_CASE("[Array] Nested dictionary comparison") {
+ // a1 = [{1: 2}, 3]
+ Array a1 = build_array(build_dictionary(1, 2), 3);
+
+ Array a2 = a1.duplicate(true);
+
+ // other_a = [{1: 0}, 3]
+ Array other_a = build_array(build_dictionary(1, 0), 3);
+
+ // test both operator== and operator!=
+ CHECK_EQ(a1, a1); // compare self
+ CHECK_FALSE(a1 != a1);
+ CHECK_EQ(a1, a2); // different equivalent arrays
+ CHECK_FALSE(a1 != a2);
+ CHECK_NE(a1, other_a); // different arrays with different content
+ CHECK_FALSE(a1 == other_a);
+}
+
+TEST_CASE("[Array] Recursive comparison") {
+ Array a1;
+ a1.push_back(a1);
+
+ Array a2;
+ a2.push_back(a2);
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(a1, a2);
+ CHECK_FALSE(a1 != a2);
+ ERR_PRINT_ON;
+
+ a1.push_back(1);
+ a2.push_back(1);
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(a1, a2);
+ CHECK_FALSE(a1 != a2);
+ ERR_PRINT_ON;
+
+ a1.push_back(1);
+ a2.push_back(2);
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_NE(a1, a2);
+ CHECK_FALSE(a1 == a2);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Array tearndown will leak memory
+ a1.clear();
+ a2.clear();
+}
+
+TEST_CASE("[Array] Recursive self comparison") {
+ Array a1;
+ Array a2;
+ a2.push_back(a1);
+ a1.push_back(a2);
+
+ CHECK_EQ(a1, a1);
+ CHECK_FALSE(a1 != a1);
+
+ // Break the recursivity otherwise Array tearndown will leak memory
+ a1.clear();
+ a2.clear();
+}
+
+} // namespace TestArray
+
+#endif // TEST_ARRAY_H
diff --git a/tests/core/variant/test_dictionary.h b/tests/core/variant/test_dictionary.h
new file mode 100644
index 0000000000..c98434d42c
--- /dev/null
+++ b/tests/core/variant/test_dictionary.h
@@ -0,0 +1,523 @@
+/*************************************************************************/
+/* test_dictionary.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_DICTIONARY_H
+#define TEST_DICTIONARY_H
+
+#include "core/variant/dictionary.h"
+#include "tests/test_macros.h"
+
+namespace TestDictionary {
+
+static inline Array build_array() {
+ return Array();
+}
+template <typename... Targs>
+static inline Array build_array(Variant item, Targs... Fargs) {
+ Array a = build_array(Fargs...);
+ a.push_front(item);
+ return a;
+}
+static inline Dictionary build_dictionary() {
+ return Dictionary();
+}
+template <typename... Targs>
+static inline Dictionary build_dictionary(Variant key, Variant item, Targs... Fargs) {
+ Dictionary d = build_dictionary(Fargs...);
+ d[key] = item;
+ return d;
+}
+
+TEST_CASE("[Dictionary] Assignment using bracket notation ([])") {
+ Dictionary map;
+ map["Hello"] = 0;
+ CHECK(int(map["Hello"]) == 0);
+ map["Hello"] = 3;
+ CHECK(int(map["Hello"]) == 3);
+ map["World!"] = 4;
+ CHECK(int(map["World!"]) == 4);
+
+ // Test non-string keys, since keys can be of any Variant type.
+ map[12345] = -5;
+ CHECK(int(map[12345]) == -5);
+ map[false] = 128;
+ CHECK(int(map[false]) == 128);
+ map[Vector2(10, 20)] = 30;
+ CHECK(int(map[Vector2(10, 20)]) == 30);
+ map[0] = 400;
+ CHECK(int(map[0]) == 400);
+ // Check that assigning 0 doesn't overwrite the value for `false`.
+ CHECK(int(map[false]) == 128);
+}
+
+TEST_CASE("[Dictionary] get_key_lists()") {
+ Dictionary map;
+ List<Variant> keys;
+ List<Variant> *ptr = &keys;
+ map.get_key_list(ptr);
+ CHECK(keys.is_empty());
+ map[1] = 3;
+ map.get_key_list(ptr);
+ CHECK(keys.size() == 1);
+ CHECK(int(keys[0]) == 1);
+ map[2] = 4;
+ map.get_key_list(ptr);
+ CHECK(keys.size() == 3);
+}
+
+TEST_CASE("[Dictionary] get_key_at_index()") {
+ Dictionary map;
+ map[4] = 3;
+ Variant val = map.get_key_at_index(0);
+ CHECK(int(val) == 4);
+ map[3] = 1;
+ val = map.get_key_at_index(0);
+ CHECK(int(val) == 4);
+ val = map.get_key_at_index(1);
+ CHECK(int(val) == 3);
+}
+
+TEST_CASE("[Dictionary] getptr()") {
+ Dictionary map;
+ map[1] = 3;
+ Variant *key = map.getptr(1);
+ CHECK(int(*key) == 3);
+ key = map.getptr(2);
+ CHECK(key == nullptr);
+}
+
+TEST_CASE("[Dictionary] get_valid()") {
+ Dictionary map;
+ map[1] = 3;
+ Variant val = map.get_valid(1);
+ CHECK(int(val) == 3);
+}
+TEST_CASE("[Dictionary] get()") {
+ Dictionary map;
+ map[1] = 3;
+ Variant val = map.get(1, -1);
+ CHECK(int(val) == 3);
+}
+
+TEST_CASE("[Dictionary] size(), empty() and clear()") {
+ Dictionary map;
+ CHECK(map.size() == 0);
+ CHECK(map.is_empty());
+ map[1] = 3;
+ CHECK(map.size() == 1);
+ CHECK(!map.is_empty());
+ map.clear();
+ CHECK(map.size() == 0);
+ CHECK(map.is_empty());
+}
+
+TEST_CASE("[Dictionary] has() and has_all()") {
+ Dictionary map;
+ CHECK(map.has(1) == false);
+ map[1] = 3;
+ CHECK(map.has(1));
+ Array keys;
+ keys.push_back(1);
+ CHECK(map.has_all(keys));
+ keys.push_back(2);
+ CHECK(map.has_all(keys) == false);
+}
+
+TEST_CASE("[Dictionary] keys() and values()") {
+ Dictionary map;
+ Array keys = map.keys();
+ Array values = map.values();
+ CHECK(keys.is_empty());
+ CHECK(values.is_empty());
+ map[1] = 3;
+ keys = map.keys();
+ values = map.values();
+ CHECK(int(keys[0]) == 1);
+ CHECK(int(values[0]) == 3);
+}
+
+TEST_CASE("[Dictionary] Duplicate dictionary") {
+ // d = {1: {1: 1}, {2: 2}: [2], [3]: 3}
+ Dictionary k2 = build_dictionary(2, 2);
+ Array k3 = build_array(3);
+ Dictionary d = build_dictionary(1, build_dictionary(1, 1), k2, build_array(2), k3, 3);
+
+ // Deep copy
+ Dictionary deep_d = d.duplicate(true);
+ CHECK_MESSAGE(deep_d.id() != d.id(), "Should create a new dictionary");
+ CHECK_MESSAGE(Dictionary(deep_d[1]).id() != Dictionary(d[1]).id(), "Should clone nested dictionary");
+ CHECK_MESSAGE(Array(deep_d[k2]).id() != Array(d[k2]).id(), "Should clone nested array");
+ CHECK_EQ(deep_d, d);
+ deep_d[0] = 0;
+ CHECK_NE(deep_d, d);
+ deep_d.erase(0);
+ Dictionary(deep_d[1]).operator[](0) = 0;
+ CHECK_NE(deep_d, d);
+ Dictionary(deep_d[1]).erase(0);
+ CHECK_EQ(deep_d, d);
+ // Keys should also be copied
+ k2[0] = 0;
+ CHECK_NE(deep_d, d);
+ k2.erase(0);
+ CHECK_EQ(deep_d, d);
+ k3.push_back(0);
+ CHECK_NE(deep_d, d);
+ k3.pop_back();
+ CHECK_EQ(deep_d, d);
+
+ // Shallow copy
+ Dictionary shallow_d = d.duplicate(false);
+ CHECK_MESSAGE(shallow_d.id() != d.id(), "Should create a new array");
+ CHECK_MESSAGE(Dictionary(shallow_d[1]).id() == Dictionary(d[1]).id(), "Should keep nested dictionary");
+ CHECK_MESSAGE(Array(shallow_d[k2]).id() == Array(d[k2]).id(), "Should keep nested array");
+ CHECK_EQ(shallow_d, d);
+ shallow_d[0] = 0;
+ CHECK_NE(shallow_d, d);
+ shallow_d.erase(0);
+#if 0 // TODO: recursion in dict key currently is buggy
+ // Keys should also be shallowed
+ k2[0] = 0;
+ CHECK_EQ(shallow_d, d);
+ k2.erase(0);
+ k3.push_back(0);
+ CHECK_EQ(shallow_d, d);
+#endif
+}
+
+TEST_CASE("[Dictionary] Duplicate recursive dictionary") {
+ // Self recursive
+ Dictionary d;
+ d[1] = d;
+
+ Dictionary d_shallow = d.duplicate(false);
+ CHECK_EQ(d, d_shallow);
+
+ // Deep copy of recursive dictionary endup with recursion limit and return
+ // an invalid result (multiple nested dictionaries), the point is we should
+ // not end up with a segfault and an error log should be printed
+ ERR_PRINT_OFF;
+ d.duplicate(true);
+ ERR_PRINT_ON;
+
+ // Nested recursive
+ Dictionary d1;
+ Dictionary d2;
+ d1[2] = d2;
+ d2[1] = d1;
+
+ Dictionary d1_shallow = d1.duplicate(false);
+ CHECK_EQ(d1, d1_shallow);
+
+ // Same deep copy issue as above
+ ERR_PRINT_OFF;
+ d1.duplicate(true);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d.clear();
+ d1.clear();
+ d2.clear();
+}
+
+#if 0 // TODO: duplicate recursion in dict key is currently buggy
+TEST_CASE("[Dictionary] Duplicate recursive dictionary on keys") {
+ // Self recursive
+ Dictionary d;
+ d[d] = d;
+
+ Dictionary d_shallow = d.duplicate(false);
+ CHECK_EQ(d, d_shallow);
+
+ // Deep copy of recursive dictionary endup with recursion limit and return
+ // an invalid result (multiple nested dictionaries), the point is we should
+ // not end up with a segfault and an error log should be printed
+ ERR_PRINT_OFF;
+ d.duplicate(true);
+ ERR_PRINT_ON;
+
+ // Nested recursive
+ Dictionary d1;
+ Dictionary d2;
+ d1[d2] = d2;
+ d2[d1] = d1;
+
+ Dictionary d1_shallow = d1.duplicate(false);
+ CHECK_EQ(d1, d1_shallow);
+
+ // Same deep copy issue as above
+ ERR_PRINT_OFF;
+ d1.duplicate(true);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d.clear();
+ d1.clear();
+ d2.clear();
+}
+#endif
+
+TEST_CASE("[Dictionary] Hash dictionary") {
+ // d = {1: {1: 1}, {2: 2}: [2], [3]: 3}
+ Dictionary k2 = build_dictionary(2, 2);
+ Array k3 = build_array(3);
+ Dictionary d = build_dictionary(1, build_dictionary(1, 1), k2, build_array(2), k3, 3);
+ uint32_t original_hash = d.hash();
+
+ // Modify dict change the hash
+ d[0] = 0;
+ CHECK_NE(d.hash(), original_hash);
+ d.erase(0);
+ CHECK_EQ(d.hash(), original_hash);
+
+ // Modify nested item change the hash
+ Dictionary(d[1]).operator[](0) = 0;
+ CHECK_NE(d.hash(), original_hash);
+ Dictionary(d[1]).erase(0);
+ Array(d[k2]).push_back(0);
+ CHECK_NE(d.hash(), original_hash);
+ Array(d[k2]).pop_back();
+
+ // Modify a key change the hash
+ k2[0] = 0;
+ CHECK_NE(d.hash(), original_hash);
+ k2.erase(0);
+ CHECK_EQ(d.hash(), original_hash);
+ k3.push_back(0);
+ CHECK_NE(d.hash(), original_hash);
+ k3.pop_back();
+ CHECK_EQ(d.hash(), original_hash);
+
+ // Duplication doesn't change the hash
+ Dictionary d2 = d.duplicate(true);
+ CHECK_EQ(d2.hash(), original_hash);
+}
+
+TEST_CASE("[Dictionary] Hash recursive dictionary") {
+ Dictionary d;
+ d[1] = d;
+
+ // Hash should reach recursion limit, we just make sure this doesn't blow up
+ ERR_PRINT_OFF;
+ d.hash();
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d.clear();
+}
+
+#if 0 // TODO: recursion in dict key is currently buggy
+TEST_CASE("[Dictionary] Hash recursive dictionary on keys") {
+ Dictionary d;
+ d[d] = 1;
+
+ // Hash should reach recursion limit, we just make sure this doesn't blow up
+ ERR_PRINT_OFF;
+ d.hash();
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d.clear();
+}
+#endif
+
+TEST_CASE("[Dictionary] Empty comparison") {
+ Dictionary d1;
+ Dictionary d2;
+
+ // test both operator== and operator!=
+ CHECK_EQ(d1, d2);
+ CHECK_FALSE(d1 != d2);
+}
+
+TEST_CASE("[Dictionary] Flat comparison") {
+ Dictionary d1 = build_dictionary(1, 1);
+ Dictionary d2 = build_dictionary(1, 1);
+ Dictionary other_d = build_dictionary(2, 1);
+
+ // test both operator== and operator!=
+ CHECK_EQ(d1, d1); // compare self
+ CHECK_FALSE(d1 != d1);
+ CHECK_EQ(d1, d2); // different equivalent arrays
+ CHECK_FALSE(d1 != d2);
+ CHECK_NE(d1, other_d); // different arrays with different content
+ CHECK_FALSE(d1 == other_d);
+}
+
+TEST_CASE("[Dictionary] Nested dictionary comparison") {
+ // d1 = {1: {2: {3: 4}}}
+ Dictionary d1 = build_dictionary(1, build_dictionary(2, build_dictionary(3, 4)));
+
+ Dictionary d2 = d1.duplicate(true);
+
+ // other_d = {1: {2: {3: 0}}}
+ Dictionary other_d = build_dictionary(1, build_dictionary(2, build_dictionary(3, 0)));
+
+ // test both operator== and operator!=
+ CHECK_EQ(d1, d1); // compare self
+ CHECK_FALSE(d1 != d1);
+ CHECK_EQ(d1, d2); // different equivalent arrays
+ CHECK_FALSE(d1 != d2);
+ CHECK_NE(d1, other_d); // different arrays with different content
+ CHECK_FALSE(d1 == other_d);
+}
+
+TEST_CASE("[Dictionary] Nested array comparison") {
+ // d1 = {1: [2, 3]}
+ Dictionary d1 = build_dictionary(1, build_array(2, 3));
+
+ Dictionary d2 = d1.duplicate(true);
+
+ // other_d = {1: [2, 0]}
+ Dictionary other_d = build_dictionary(1, build_array(2, 0));
+
+ // test both operator== and operator!=
+ CHECK_EQ(d1, d1); // compare self
+ CHECK_FALSE(d1 != d1);
+ CHECK_EQ(d1, d2); // different equivalent arrays
+ CHECK_FALSE(d1 != d2);
+ CHECK_NE(d1, other_d); // different arrays with different content
+ CHECK_FALSE(d1 == other_d);
+}
+
+TEST_CASE("[Dictionary] Recursive comparison") {
+ Dictionary d1;
+ d1[1] = d1;
+
+ Dictionary d2;
+ d2[1] = d2;
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(d1, d2);
+ CHECK_FALSE(d1 != d2);
+ ERR_PRINT_ON;
+
+ d1[2] = 2;
+ d2[2] = 2;
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(d1, d2);
+ CHECK_FALSE(d1 != d2);
+ ERR_PRINT_ON;
+
+ d1[3] = 3;
+ d2[3] = 0;
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_NE(d1, d2);
+ CHECK_FALSE(d1 == d2);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d1.clear();
+ d2.clear();
+}
+
+#if 0 // TODO: recursion in dict key is currently buggy
+TEST_CASE("[Dictionary] Recursive comparison on keys") {
+ Dictionary d1;
+ // Hash computation should reach recursion limit
+ ERR_PRINT_OFF;
+ d1[d1] = 1;
+ ERR_PRINT_ON;
+
+ Dictionary d2;
+ // Hash computation should reach recursion limit
+ ERR_PRINT_OFF;
+ d2[d2] = 1;
+ ERR_PRINT_ON;
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(d1, d2);
+ CHECK_FALSE(d1 != d2);
+ ERR_PRINT_ON;
+
+ d1[2] = 2;
+ d2[2] = 2;
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(d1, d2);
+ CHECK_FALSE(d1 != d2);
+ ERR_PRINT_ON;
+
+ d1[3] = 3;
+ d2[3] = 0;
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_NE(d1, d2);
+ CHECK_FALSE(d1 == d2);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d1.clear();
+ d2.clear();
+}
+#endif
+
+TEST_CASE("[Dictionary] Recursive self comparison") {
+ Dictionary d1;
+ Dictionary d2;
+ d1[1] = d2;
+ d2[1] = d1;
+
+ CHECK_EQ(d1, d1);
+ CHECK_FALSE(d1 != d1);
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d1.clear();
+ d2.clear();
+}
+
+TEST_CASE("[Dictionary] Order and find") {
+ Dictionary d;
+ d[4] = "four";
+ d[8] = "eight";
+ d[12] = "twelve";
+ d["4"] = "four";
+
+ Array keys;
+ keys.append(4);
+ keys.append(8);
+ keys.append(12);
+ keys.append("4");
+
+ CHECK_EQ(d.keys(), keys);
+ CHECK_EQ(d.find_key("four"), Variant(4));
+ CHECK_EQ(d.find_key("does not exist"), Variant());
+}
+
+} // namespace TestDictionary
+
+#endif // TEST_DICTIONARY_H
diff --git a/tests/core/variant/test_variant.h b/tests/core/variant/test_variant.h
new file mode 100644
index 0000000000..d6799928b4
--- /dev/null
+++ b/tests/core/variant/test_variant.h
@@ -0,0 +1,978 @@
+/*************************************************************************/
+/* test_variant.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_VARIANT_H
+#define TEST_VARIANT_H
+
+#include "core/variant/variant.h"
+#include "core/variant/variant_parser.h"
+
+#include "tests/test_macros.h"
+
+namespace TestVariant {
+
+static inline Array build_array() {
+ return Array();
+}
+template <typename... Targs>
+static inline Array build_array(Variant item, Targs... Fargs) {
+ Array a = build_array(Fargs...);
+ a.push_front(item);
+ return a;
+}
+static inline Dictionary build_dictionary() {
+ return Dictionary();
+}
+template <typename... Targs>
+static inline Dictionary build_dictionary(Variant key, Variant item, Targs... Fargs) {
+ Dictionary d = build_dictionary(Fargs...);
+ d[key] = item;
+ return d;
+}
+
+TEST_CASE("[Variant] Writer and parser integer") {
+ int64_t a32 = 2147483648; // 2^31, so out of bounds for 32-bit signed int [-2^31, +2^31-1].
+ String a32_str;
+ VariantWriter::write_to_string(a32, a32_str);
+
+ CHECK_MESSAGE(a32_str != "-2147483648", "Should not wrap around");
+
+ int64_t b64 = 9223372036854775807; // 2^63-1, upper bound for signed 64-bit int.
+ String b64_str;
+ VariantWriter::write_to_string(b64, b64_str);
+
+ CHECK_MESSAGE(b64_str == "9223372036854775807", "Should not wrap around.");
+
+ VariantParser::StreamString ss;
+ String errs;
+ int line;
+ Variant b64_parsed;
+ int64_t b64_int_parsed;
+
+ ss.s = b64_str;
+ VariantParser::parse(&ss, b64_parsed, errs, line);
+ b64_int_parsed = b64_parsed;
+
+ CHECK_MESSAGE(b64_int_parsed == 9223372036854775807, "Should parse back.");
+
+ ss.s = "9223372036854775808"; // Overflowed by one.
+ VariantParser::parse(&ss, b64_parsed, errs, line);
+ b64_int_parsed = b64_parsed;
+
+ CHECK_MESSAGE(b64_int_parsed == 9223372036854775807, "The result should be clamped to max value.");
+
+ ss.s = "1e100"; // Googol! Scientific notation.
+ VariantParser::parse(&ss, b64_parsed, errs, line);
+ b64_int_parsed = b64_parsed;
+
+ CHECK_MESSAGE(b64_int_parsed == 9223372036854775807, "The result should be clamped to max value.");
+}
+
+TEST_CASE("[Variant] Writer and parser Variant::FLOAT") {
+ // Variant::FLOAT is always 64-bit (C++ double).
+ // This is the maximum non-infinity double-precision float.
+ double a64 = 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0;
+ String a64_str;
+ VariantWriter::write_to_string(a64, a64_str);
+
+ CHECK_MESSAGE(a64_str == "1.79769e+308", "Writes in scientific notation.");
+ CHECK_MESSAGE(a64_str != "inf", "Should not overflow.");
+ CHECK_MESSAGE(a64_str != "nan", "The result should be defined.");
+
+ String errs;
+ int line;
+ Variant variant_parsed;
+ double float_parsed;
+
+ VariantParser::StreamString bss;
+ bss.s = a64_str;
+ VariantParser::parse(&bss, variant_parsed, errs, line);
+ float_parsed = variant_parsed;
+ // Loses precision, but that's alright.
+ CHECK_MESSAGE(float_parsed == 1.79769e+308, "Should parse back.");
+
+ // Approximation of Googol with a double-precision float.
+ VariantParser::StreamString css;
+ css.s = "1.0e+100";
+ VariantParser::parse(&css, variant_parsed, errs, line);
+ float_parsed = variant_parsed;
+ CHECK_MESSAGE(float_parsed == 1.0e+100, "Should match the double literal.");
+}
+
+TEST_CASE("[Variant] Assignment To Bool from Int,Float,String,Vec2,Vec2i,Vec3,Vec3i and Color") {
+ Variant int_v = 0;
+ Variant bool_v = true;
+ int_v = bool_v; // int_v is now a bool
+ CHECK(int_v == Variant(true));
+ bool_v = false;
+ int_v = bool_v;
+ CHECK(int_v.get_type() == Variant::BOOL);
+
+ Variant float_v = 0.0f;
+ bool_v = true;
+ float_v = bool_v;
+ CHECK(float_v == Variant(true));
+ bool_v = false;
+ float_v = bool_v;
+ CHECK(float_v.get_type() == Variant::BOOL);
+
+ Variant string_v = "";
+ bool_v = true;
+ string_v = bool_v;
+ CHECK(string_v == Variant(true));
+ bool_v = false;
+ string_v = bool_v;
+ CHECK(string_v.get_type() == Variant::BOOL);
+
+ Variant vec2_v = Vector2(0, 0);
+ bool_v = true;
+ vec2_v = bool_v;
+ CHECK(vec2_v == Variant(true));
+ bool_v = false;
+ vec2_v = bool_v;
+ CHECK(vec2_v.get_type() == Variant::BOOL);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ bool_v = true;
+ vec2i_v = bool_v;
+ CHECK(vec2i_v == Variant(true));
+ bool_v = false;
+ vec2i_v = bool_v;
+ CHECK(vec2i_v.get_type() == Variant::BOOL);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ bool_v = true;
+ vec3_v = bool_v;
+ CHECK(vec3_v == Variant(true));
+ bool_v = false;
+ vec3_v = bool_v;
+ CHECK(vec3_v.get_type() == Variant::BOOL);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ bool_v = true;
+ vec3i_v = bool_v;
+ CHECK(vec3i_v == Variant(true));
+ bool_v = false;
+ vec3i_v = bool_v;
+ CHECK(vec3i_v.get_type() == Variant::BOOL);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ bool_v = true;
+ col_v = bool_v;
+ CHECK(col_v == Variant(true));
+ bool_v = false;
+ col_v = bool_v;
+ CHECK(col_v.get_type() == Variant::BOOL);
+}
+
+TEST_CASE("[Variant] Assignment To Int from Bool,Float,String,Vec2,Vec2i,Vec3,Vec3i and Color") {
+ Variant bool_v = false;
+ Variant int_v = 2;
+ bool_v = int_v; // Now bool_v is int
+ CHECK(bool_v == Variant(2));
+ int_v = -3;
+ bool_v = int_v;
+ CHECK(bool_v.get_type() == Variant::INT);
+
+ Variant float_v = 0.0f;
+ int_v = 2;
+ float_v = int_v;
+ CHECK(float_v == Variant(2));
+ int_v = -3;
+ float_v = int_v;
+ CHECK(float_v.get_type() == Variant::INT);
+
+ Variant string_v = "";
+ int_v = 2;
+ string_v = int_v;
+ CHECK(string_v == Variant(2));
+ int_v = -3;
+ string_v = int_v;
+ CHECK(string_v.get_type() == Variant::INT);
+
+ Variant vec2_v = Vector2(0, 0);
+ int_v = 2;
+ vec2_v = int_v;
+ CHECK(vec2_v == Variant(2));
+ int_v = -3;
+ vec2_v = int_v;
+ CHECK(vec2_v.get_type() == Variant::INT);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ int_v = 2;
+ vec2i_v = int_v;
+ CHECK(vec2i_v == Variant(2));
+ int_v = -3;
+ vec2i_v = int_v;
+ CHECK(vec2i_v.get_type() == Variant::INT);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ int_v = 2;
+ vec3_v = int_v;
+ CHECK(vec3_v == Variant(2));
+ int_v = -3;
+ vec3_v = int_v;
+ CHECK(vec3_v.get_type() == Variant::INT);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ int_v = 2;
+ vec3i_v = int_v;
+ CHECK(vec3i_v == Variant(2));
+ int_v = -3;
+ vec3i_v = int_v;
+ CHECK(vec3i_v.get_type() == Variant::INT);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ int_v = 2;
+ col_v = int_v;
+ CHECK(col_v == Variant(2));
+ int_v = -3;
+ col_v = int_v;
+ CHECK(col_v.get_type() == Variant::INT);
+}
+
+TEST_CASE("[Variant] Assignment To Float from Bool,Int,String,Vec2,Vec2i,Vec3,Vec3i and Color") {
+ Variant bool_v = false;
+ Variant float_v = 1.5f;
+ bool_v = float_v; // Now bool_v is float
+ CHECK(bool_v == Variant(1.5f));
+ float_v = -4.6f;
+ bool_v = float_v;
+ CHECK(bool_v.get_type() == Variant::FLOAT);
+
+ Variant int_v = 1;
+ float_v = 1.5f;
+ int_v = float_v;
+ CHECK(int_v == Variant(1.5f));
+ float_v = -4.6f;
+ int_v = float_v;
+ CHECK(int_v.get_type() == Variant::FLOAT);
+
+ Variant string_v = "";
+ float_v = 1.5f;
+ string_v = float_v;
+ CHECK(string_v == Variant(1.5f));
+ float_v = -4.6f;
+ string_v = float_v;
+ CHECK(string_v.get_type() == Variant::FLOAT);
+
+ Variant vec2_v = Vector2(0, 0);
+ float_v = 1.5f;
+ vec2_v = float_v;
+ CHECK(vec2_v == Variant(1.5f));
+ float_v = -4.6f;
+ vec2_v = float_v;
+ CHECK(vec2_v.get_type() == Variant::FLOAT);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ float_v = 1.5f;
+ vec2i_v = float_v;
+ CHECK(vec2i_v == Variant(1.5f));
+ float_v = -4.6f;
+ vec2i_v = float_v;
+ CHECK(vec2i_v.get_type() == Variant::FLOAT);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ float_v = 1.5f;
+ vec3_v = float_v;
+ CHECK(vec3_v == Variant(1.5f));
+ float_v = -4.6f;
+ vec3_v = float_v;
+ CHECK(vec3_v.get_type() == Variant::FLOAT);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ float_v = 1.5f;
+ vec3i_v = float_v;
+ CHECK(vec3i_v == Variant(1.5f));
+ float_v = -4.6f;
+ vec3i_v = float_v;
+ CHECK(vec3i_v.get_type() == Variant::FLOAT);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ float_v = 1.5f;
+ col_v = float_v;
+ CHECK(col_v == Variant(1.5f));
+ float_v = -4.6f;
+ col_v = float_v;
+ CHECK(col_v.get_type() == Variant::FLOAT);
+}
+
+TEST_CASE("[Variant] Assignment To String from Bool,Int,Float,Vec2,Vec2i,Vec3,Vec3i and Color") {
+ Variant bool_v = false;
+ Variant string_v = "Hello";
+ bool_v = string_v; // Now bool_v is string
+ CHECK(bool_v == Variant("Hello"));
+ string_v = "Hello there";
+ bool_v = string_v;
+ CHECK(bool_v.get_type() == Variant::STRING);
+
+ Variant int_v = 0;
+ string_v = "Hello";
+ int_v = string_v;
+ CHECK(int_v == Variant("Hello"));
+ string_v = "Hello there";
+ int_v = string_v;
+ CHECK(int_v.get_type() == Variant::STRING);
+
+ Variant float_v = 0.0f;
+ string_v = "Hello";
+ float_v = string_v;
+ CHECK(float_v == Variant("Hello"));
+ string_v = "Hello there";
+ float_v = string_v;
+ CHECK(float_v.get_type() == Variant::STRING);
+
+ Variant vec2_v = Vector2(0, 0);
+ string_v = "Hello";
+ vec2_v = string_v;
+ CHECK(vec2_v == Variant("Hello"));
+ string_v = "Hello there";
+ vec2_v = string_v;
+ CHECK(vec2_v.get_type() == Variant::STRING);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ string_v = "Hello";
+ vec2i_v = string_v;
+ CHECK(vec2i_v == Variant("Hello"));
+ string_v = "Hello there";
+ vec2i_v = string_v;
+ CHECK(vec2i_v.get_type() == Variant::STRING);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ string_v = "Hello";
+ vec3_v = string_v;
+ CHECK(vec3_v == Variant("Hello"));
+ string_v = "Hello there";
+ vec3_v = string_v;
+ CHECK(vec3_v.get_type() == Variant::STRING);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ string_v = "Hello";
+ vec3i_v = string_v;
+ CHECK(vec3i_v == Variant("Hello"));
+ string_v = "Hello there";
+ vec3i_v = string_v;
+ CHECK(vec3i_v.get_type() == Variant::STRING);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ string_v = "Hello";
+ col_v = string_v;
+ CHECK(col_v == Variant("Hello"));
+ string_v = "Hello there";
+ col_v = string_v;
+ CHECK(col_v.get_type() == Variant::STRING);
+}
+
+TEST_CASE("[Variant] Assignment To Vec2 from Bool,Int,Float,String,Vec2i,Vec3,Vec3i and Color") {
+ Variant bool_v = false;
+ Variant vec2_v = Vector2(2.2f, 3.5f);
+ bool_v = vec2_v; // Now bool_v is Vector2
+ CHECK(bool_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ bool_v = vec2_v;
+ CHECK(bool_v.get_type() == Variant::VECTOR2);
+
+ Variant int_v = 0;
+ vec2_v = Vector2(2.2f, 3.5f);
+ int_v = vec2_v;
+ CHECK(int_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ int_v = vec2_v;
+ CHECK(int_v.get_type() == Variant::VECTOR2);
+
+ Variant float_v = 0.0f;
+ vec2_v = Vector2(2.2f, 3.5f);
+ float_v = vec2_v;
+ CHECK(float_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ float_v = vec2_v;
+ CHECK(float_v.get_type() == Variant::VECTOR2);
+
+ Variant string_v = "";
+ vec2_v = Vector2(2.2f, 3.5f);
+ string_v = vec2_v;
+ CHECK(string_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ string_v = vec2_v;
+ CHECK(string_v.get_type() == Variant::VECTOR2);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ vec2_v = Vector2(2.2f, 3.5f);
+ vec2i_v = vec2_v;
+ CHECK(vec2i_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ vec2i_v = vec2_v;
+ CHECK(vec2i_v.get_type() == Variant::VECTOR2);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ vec2_v = Vector2(2.2f, 3.5f);
+ vec3_v = vec2_v;
+ CHECK(vec3_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ vec3_v = vec2_v;
+ CHECK(vec3_v.get_type() == Variant::VECTOR2);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ vec2_v = Vector2(2.2f, 3.5f);
+ vec3i_v = vec2_v;
+ CHECK(vec3i_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ vec3i_v = vec2_v;
+ CHECK(vec3i_v.get_type() == Variant::VECTOR2);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ vec2_v = Vector2(2.2f, 3.5f);
+ col_v = vec2_v;
+ CHECK(col_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ col_v = vec2_v;
+ CHECK(col_v.get_type() == Variant::VECTOR2);
+}
+
+TEST_CASE("[Variant] Assignment To Vec2i from Bool,Int,Float,String,Vec2,Vec3,Vec3i and Color") {
+ Variant bool_v = false;
+ Variant vec2i_v = Vector2i(2, 3);
+ bool_v = vec2i_v; // Now bool_v is Vector2i
+ CHECK(bool_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ bool_v = vec2i_v;
+ CHECK(bool_v.get_type() == Variant::VECTOR2I);
+
+ Variant int_v = 0;
+ vec2i_v = Vector2i(2, 3);
+ int_v = vec2i_v;
+ CHECK(int_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ int_v = vec2i_v;
+ CHECK(int_v.get_type() == Variant::VECTOR2I);
+
+ Variant float_v = 0.0f;
+ vec2i_v = Vector2i(2, 3);
+ float_v = vec2i_v;
+ CHECK(float_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ float_v = vec2i_v;
+ CHECK(float_v.get_type() == Variant::VECTOR2I);
+
+ Variant string_v = "";
+ vec2i_v = Vector2i(2, 3);
+ string_v = vec2i_v;
+ CHECK(string_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ string_v = vec2i_v;
+ CHECK(string_v.get_type() == Variant::VECTOR2I);
+
+ Variant vec2_v = Vector2(0, 0);
+ vec2i_v = Vector2i(2, 3);
+ vec2_v = vec2i_v;
+ CHECK(vec2_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ vec2_v = vec2i_v;
+ CHECK(vec2i_v.get_type() == Variant::VECTOR2I);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ vec2i_v = Vector2i(2, 3);
+ vec3_v = vec2i_v;
+ CHECK(vec3_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ vec3_v = vec2i_v;
+ CHECK(vec3_v.get_type() == Variant::VECTOR2I);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ vec2i_v = Vector2i(2, 3);
+ vec3i_v = vec2i_v;
+ CHECK(vec3i_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ vec3i_v = vec2i_v;
+ CHECK(vec3i_v.get_type() == Variant::VECTOR2I);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ vec2i_v = Vector2i(2, 3);
+ col_v = vec2i_v;
+ CHECK(col_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ col_v = vec2i_v;
+ CHECK(col_v.get_type() == Variant::VECTOR2I);
+}
+
+TEST_CASE("[Variant] Assignment To Vec3 from Bool,Int,Float,String,Vec2,Vec2i,Vec3i and Color") {
+ Variant bool_v = false;
+ Variant vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ bool_v = vec3_v; // Now bool_v is Vector3
+ CHECK(bool_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ bool_v = vec3_v;
+ CHECK(bool_v.get_type() == Variant::VECTOR3);
+
+ Variant int_v = 0;
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ int_v = vec3_v;
+ CHECK(int_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ int_v = vec3_v;
+ CHECK(int_v.get_type() == Variant::VECTOR3);
+
+ Variant float_v = 0.0f;
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ float_v = vec3_v;
+ CHECK(float_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ float_v = vec3_v;
+ CHECK(float_v.get_type() == Variant::VECTOR3);
+
+ Variant string_v = "";
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ string_v = vec3_v;
+ CHECK(string_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ string_v = vec3_v;
+ CHECK(string_v.get_type() == Variant::VECTOR3);
+
+ Variant vec2_v = Vector2(0, 0);
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ vec2_v = vec3_v;
+ CHECK(vec2_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ vec2_v = vec3_v;
+ CHECK(vec2_v.get_type() == Variant::VECTOR3);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ vec2i_v = vec3_v;
+ CHECK(vec2i_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ vec2i_v = vec3_v;
+ CHECK(vec2i_v.get_type() == Variant::VECTOR3);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ vec3i_v = vec3_v;
+ CHECK(vec3i_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ vec3i_v = vec3_v;
+ CHECK(vec3i_v.get_type() == Variant::VECTOR3);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ col_v = vec3_v;
+ CHECK(col_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ col_v = vec3_v;
+ CHECK(col_v.get_type() == Variant::VECTOR3);
+}
+
+TEST_CASE("[Variant] Assignment To Vec3i from Bool,Int,Float,String,Vec2,Vec2i,Vec3 and Color") {
+ Variant bool_v = false;
+ Variant vec3i_v = Vector3i(2, 3, 5);
+ bool_v = vec3i_v; // Now bool_v is Vector3i
+ CHECK(bool_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ bool_v = vec3i_v;
+ CHECK(bool_v.get_type() == Variant::VECTOR3I);
+
+ Variant int_v = 0;
+ vec3i_v = Vector3i(2, 3, 5);
+ int_v = vec3i_v;
+ CHECK(int_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ int_v = vec3i_v;
+ CHECK(int_v.get_type() == Variant::VECTOR3I);
+
+ Variant float_v = 0.0f;
+ vec3i_v = Vector3i(2, 3, 5);
+ float_v = vec3i_v;
+ CHECK(float_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ float_v = vec3i_v;
+ CHECK(float_v.get_type() == Variant::VECTOR3I);
+
+ Variant string_v = "";
+ vec3i_v = Vector3i(2, 3, 5);
+ string_v = vec3i_v;
+ CHECK(string_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ string_v = vec3i_v;
+ CHECK(string_v.get_type() == Variant::VECTOR3I);
+
+ Variant vec2_v = Vector2(0, 0);
+ vec3i_v = Vector3i(2, 3, 5);
+ vec2_v = vec3i_v;
+ CHECK(vec2_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ vec2_v = vec3i_v;
+ CHECK(vec2_v.get_type() == Variant::VECTOR3I);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ vec3i_v = Vector3i(2, 3, 5);
+ vec2i_v = vec3i_v;
+ CHECK(vec2i_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ vec2i_v = vec3i_v;
+ CHECK(vec2i_v.get_type() == Variant::VECTOR3I);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ vec3i_v = Vector3i(2, 3, 5);
+ vec3_v = vec3i_v;
+ CHECK(vec3_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ vec3_v = vec3i_v;
+ CHECK(vec3_v.get_type() == Variant::VECTOR3I);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ vec3i_v = Vector3i(2, 3, 5);
+ col_v = vec3i_v;
+ CHECK(col_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ col_v = vec3i_v;
+ CHECK(col_v.get_type() == Variant::VECTOR3I);
+}
+
+TEST_CASE("[Variant] Assignment To Color from Bool,Int,Float,String,Vec2,Vec2i,Vec3 and Vec3i") {
+ Variant bool_v = false;
+ Variant col_v = Color(0.25f, 0.4f, 0.78f);
+ bool_v = col_v; // Now bool_v is Color
+ CHECK(bool_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ bool_v = col_v;
+ CHECK(bool_v.get_type() == Variant::COLOR);
+
+ Variant int_v = 0;
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ int_v = col_v;
+ CHECK(int_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ int_v = col_v;
+ CHECK(int_v.get_type() == Variant::COLOR);
+
+ Variant float_v = 0.0f;
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ float_v = col_v;
+ CHECK(float_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ float_v = col_v;
+ CHECK(float_v.get_type() == Variant::COLOR);
+
+ Variant string_v = "";
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ string_v = col_v;
+ CHECK(string_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ string_v = col_v;
+ CHECK(string_v.get_type() == Variant::COLOR);
+
+ Variant vec2_v = Vector2(0, 0);
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ vec2_v = col_v;
+ CHECK(vec2_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ vec2_v = col_v;
+ CHECK(vec2_v.get_type() == Variant::COLOR);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ vec2i_v = col_v;
+ CHECK(vec2i_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ vec2i_v = col_v;
+ CHECK(vec2i_v.get_type() == Variant::COLOR);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ vec3_v = col_v;
+ CHECK(vec3_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ vec3_v = col_v;
+ CHECK(vec3_v.get_type() == Variant::COLOR);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ vec3i_v = col_v;
+ CHECK(vec3i_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ vec3i_v = col_v;
+ CHECK(vec3i_v.get_type() == Variant::COLOR);
+}
+
+TEST_CASE("[Variant] Writer and parser array") {
+ Array a = build_array(1, String("hello"), build_array(Variant()));
+ String a_str;
+ VariantWriter::write_to_string(a, a_str);
+
+ CHECK_EQ(a_str, "[1, \"hello\", [null]]");
+
+ VariantParser::StreamString ss;
+ String errs;
+ int line;
+ Variant a_parsed;
+
+ ss.s = a_str;
+ VariantParser::parse(&ss, a_parsed, errs, line);
+
+ CHECK_MESSAGE(a_parsed == Variant(a), "Should parse back.");
+}
+
+TEST_CASE("[Variant] Writer recursive array") {
+ // There is no way to accurately represent a recursive array,
+ // the only thing we can do is make sure the writer doesn't blow up
+
+ // Self recursive
+ Array a;
+ a.push_back(a);
+
+ // Writer should it recursion limit while visiting the array
+ ERR_PRINT_OFF;
+ String a_str;
+ VariantWriter::write_to_string(a, a_str);
+ ERR_PRINT_ON;
+
+ // Nested recursive
+ Array a1;
+ Array a2;
+ a1.push_back(a2);
+ a2.push_back(a1);
+
+ // Writer should it recursion limit while visiting the array
+ ERR_PRINT_OFF;
+ String a1_str;
+ VariantWriter::write_to_string(a1, a1_str);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary tearndown will leak memory
+ a.clear();
+ a1.clear();
+ a2.clear();
+}
+
+TEST_CASE("[Variant] Writer and parser dictionary") {
+ // d = {{1: 2}: 3, 4: "hello", 5: {null: []}}
+ Dictionary d = build_dictionary(build_dictionary(1, 2), 3, 4, String("hello"), 5, build_dictionary(Variant(), build_array()));
+ String d_str;
+ VariantWriter::write_to_string(d, d_str);
+
+ CHECK_EQ(d_str, "{\n4: \"hello\",\n5: {\nnull: []\n},\n{\n1: 2\n}: 3\n}");
+
+ VariantParser::StreamString ss;
+ String errs;
+ int line;
+ Variant d_parsed;
+
+ ss.s = d_str;
+ VariantParser::parse(&ss, d_parsed, errs, line);
+
+ CHECK_MESSAGE(d_parsed == Variant(d), "Should parse back.");
+}
+
+TEST_CASE("[Variant] Writer recursive dictionary") {
+ // There is no way to accurately represent a recursive dictionary,
+ // the only thing we can do is make sure the writer doesn't blow up
+
+ // Self recursive
+ Dictionary d;
+ d[1] = d;
+
+ // Writer should it recursion limit while visiting the dictionary
+ ERR_PRINT_OFF;
+ String d_str;
+ VariantWriter::write_to_string(d, d_str);
+ ERR_PRINT_ON;
+
+ // Nested recursive
+ Dictionary d1;
+ Dictionary d2;
+ d1[2] = d2;
+ d2[1] = d1;
+
+ // Writer should it recursion limit while visiting the dictionary
+ ERR_PRINT_OFF;
+ String d1_str;
+ VariantWriter::write_to_string(d1, d1_str);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary tearndown will leak memory
+ d.clear();
+ d1.clear();
+ d2.clear();
+}
+
+#if 0 // TODO: recursion in dict key is currently buggy
+TEST_CASE("[Variant] Writer recursive dictionary on keys") {
+ // There is no way to accurately represent a recursive dictionary,
+ // the only thing we can do is make sure the writer doesn't blow up
+
+ // Self recursive
+ Dictionary d;
+ d[d] = 1;
+
+ // Writer should it recursion limit while visiting the dictionary
+ ERR_PRINT_OFF;
+ String d_str;
+ VariantWriter::write_to_string(d, d_str);
+ ERR_PRINT_ON;
+
+ // Nested recursive
+ Dictionary d1;
+ Dictionary d2;
+ d1[d2] = 2;
+ d2[d1] = 1;
+
+ // Writer should it recursion limit while visiting the dictionary
+ ERR_PRINT_OFF;
+ String d1_str;
+ VariantWriter::write_to_string(d1, d1_str);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary tearndown will leak memory
+ d.clear();
+ d1.clear();
+ d2.clear();
+}
+#endif
+
+TEST_CASE("[Variant] Basic comparison") {
+ CHECK_EQ(Variant(1), Variant(1));
+ CHECK_FALSE(Variant(1) != Variant(1));
+ CHECK_NE(Variant(1), Variant(2));
+ CHECK_EQ(Variant(String("foo")), Variant(String("foo")));
+ CHECK_NE(Variant(String("foo")), Variant(String("bar")));
+ // Check "empty" version of different types are not equivalents
+ CHECK_NE(Variant(0), Variant());
+ CHECK_NE(Variant(String()), Variant());
+ CHECK_NE(Variant(Array()), Variant());
+ CHECK_NE(Variant(Dictionary()), Variant());
+}
+
+TEST_CASE("[Variant] Nested array comparison") {
+ Array a1 = build_array(1, build_array(2, 3));
+ Array a2 = build_array(1, build_array(2, 3));
+ Array a_other = build_array(1, build_array(2, 4));
+ Variant v_a1 = a1;
+ Variant v_a1_ref2 = a1;
+ Variant v_a2 = a2;
+ Variant v_a_other = a_other;
+
+ // test both operator== and operator!=
+ CHECK_EQ(v_a1, v_a1);
+ CHECK_FALSE(v_a1 != v_a1);
+ CHECK_EQ(v_a1, v_a1_ref2);
+ CHECK_FALSE(v_a1 != v_a1_ref2);
+ CHECK_EQ(v_a1, v_a2);
+ CHECK_FALSE(v_a1 != v_a2);
+ CHECK_NE(v_a1, v_a_other);
+ CHECK_FALSE(v_a1 == v_a_other);
+}
+
+TEST_CASE("[Variant] Nested dictionary comparison") {
+ Dictionary d1 = build_dictionary(build_dictionary(1, 2), build_dictionary(3, 4));
+ Dictionary d2 = build_dictionary(build_dictionary(1, 2), build_dictionary(3, 4));
+ Dictionary d_other_key = build_dictionary(build_dictionary(1, 0), build_dictionary(3, 4));
+ Dictionary d_other_val = build_dictionary(build_dictionary(1, 2), build_dictionary(3, 0));
+ Variant v_d1 = d1;
+ Variant v_d1_ref2 = d1;
+ Variant v_d2 = d2;
+ Variant v_d_other_key = d_other_key;
+ Variant v_d_other_val = d_other_val;
+
+ // test both operator== and operator!=
+ CHECK_EQ(v_d1, v_d1);
+ CHECK_FALSE(v_d1 != v_d1);
+ CHECK_EQ(v_d1, v_d1_ref2);
+ CHECK_FALSE(v_d1 != v_d1_ref2);
+ CHECK_EQ(v_d1, v_d2);
+ CHECK_FALSE(v_d1 != v_d2);
+ CHECK_NE(v_d1, v_d_other_key);
+ CHECK_FALSE(v_d1 == v_d_other_key);
+ CHECK_NE(v_d1, v_d_other_val);
+ CHECK_FALSE(v_d1 == v_d_other_val);
+}
+
+struct ArgumentData {
+ Variant::Type type;
+ String name;
+ bool has_defval = false;
+ Variant defval;
+ int position;
+};
+
+struct MethodData {
+ StringName name;
+ Variant::Type return_type;
+ List<ArgumentData> arguments;
+ bool is_virtual = false;
+ bool is_vararg = false;
+};
+
+TEST_CASE("[Variant] Utility functions") {
+ List<MethodData> functions;
+
+ List<StringName> function_names;
+ Variant::get_utility_function_list(&function_names);
+ function_names.sort_custom<StringName::AlphCompare>();
+
+ for (const StringName &E : function_names) {
+ MethodData md;
+ md.name = E;
+
+ // Utility function's return type.
+ if (Variant::has_utility_function_return_value(E)) {
+ md.return_type = Variant::get_utility_function_return_type(E);
+ }
+
+ // Utility function's arguments.
+ if (Variant::is_utility_function_vararg(E)) {
+ md.is_vararg = true;
+ } else {
+ for (int i = 0; i < Variant::get_utility_function_argument_count(E); i++) {
+ ArgumentData arg;
+ arg.type = Variant::get_utility_function_argument_type(E, i);
+ arg.name = Variant::get_utility_function_argument_name(E, i);
+ arg.position = i;
+
+ md.arguments.push_back(arg);
+ }
+ }
+
+ functions.push_back(md);
+ }
+
+ SUBCASE("[Variant] Validate utility functions") {
+ for (const MethodData &E : functions) {
+ for (const ArgumentData &F : E.arguments) {
+ const ArgumentData &arg = F;
+
+ TEST_COND((arg.name.is_empty() || arg.name.begins_with("_unnamed_arg")),
+ vformat("Unnamed argument in position %d of function '%s'.", arg.position, E.name));
+ }
+ }
+ }
+}
+
+} // namespace TestVariant
+
+#endif // TEST_VARIANT_H
diff --git a/tests/create_test.py b/tests/create_test.py
new file mode 100644
index 0000000000..5a0439084f
--- /dev/null
+++ b/tests/create_test.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+
+import argparse
+import os
+import re
+import sys
+from subprocess import call
+
+
+def main():
+ # Change to the directory where the script is located,
+ # so that the script can be run from any location.
+ os.chdir(os.path.dirname(os.path.realpath(__file__)))
+
+ parser = argparse.ArgumentParser(description="Creates a new unit test file.")
+ parser.add_argument("name", type=str, help="The unit test name in PascalCase notation")
+ parser.add_argument(
+ "path",
+ type=str,
+ nargs="?",
+ help="The path to the unit test file relative to the tests folder (default: .)",
+ default=".",
+ )
+ parser.add_argument(
+ "-i",
+ "--invasive",
+ action="store_true",
+ help="if set, the script will automatically insert the include directive in test_main.cpp. Use with caution!",
+ )
+ args = parser.parse_args()
+
+ snake_case_regex = re.compile(r"(?<!^)(?=[A-Z])")
+ name_snake_case = snake_case_regex.sub("_", args.name).lower()
+
+ file_path = os.path.normpath(os.path.join(args.path, f"test_{name_snake_case}.h"))
+
+ print(file_path)
+ if os.path.isfile(file_path):
+ print(f'ERROR: The file "{file_path}" already exists.')
+ sys.exit(1)
+ with open(file_path, "w") as file:
+ file.write(
+ """/*************************************************************************/
+/* test_{name_snake_case}.h {padding} */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_{name_upper_snake_case}_H
+#define TEST_{name_upper_snake_case}_H
+
+#include "tests/test_macros.h"
+
+namespace Test{name_pascal_case} {{
+
+TEST_CASE("[{name_pascal_case}] Example test case") {{
+ // TODO: Remove this comment and write your test code here.
+}}
+
+}} // namespace Test{name_pascal_case}
+
+#endif // TEST_{name_upper_snake_case}_H
+""".format(
+ name_snake_case=name_snake_case,
+ # Capitalize the first letter but keep capitalization for the rest of the string.
+ # This is done in case the user passes a camelCase string instead of PascalCase.
+ name_pascal_case=args.name[0].upper() + args.name[1:],
+ name_upper_snake_case=name_snake_case.upper(),
+ # The padding length depends on the test name length.
+ padding=" " * (60 - len(name_snake_case)),
+ )
+ )
+
+ # Print an absolute path so it can be Ctrl + clicked in some IDEs and terminal emulators.
+ print("Test header file created:")
+ print(os.path.abspath(file_path))
+
+ if args.invasive:
+ print("Trying to insert include directive in test_main.cpp...")
+ with open("test_main.cpp", "r") as file:
+ contents = file.read()
+ match = re.search(r'#include "tests.*\n', contents)
+
+ if match:
+ new_string = contents[: match.start()] + f'#include "tests/{file_path}"\n' + contents[match.start() :]
+
+ with open("test_main.cpp", "w") as file:
+ file.write(new_string)
+ print("Done.")
+ # Use clang format to sort include directives afster insertion.
+ clang_format_args = ["clang-format", "test_main.cpp", "-i"]
+ retcode = call(clang_format_args)
+ if retcode != 0:
+ print(
+ "Include directives in test_main.cpp could not be sorted automatically using clang-format. Please sort them manually."
+ )
+ else:
+ print("Could not find a valid position in test_main.cpp to insert the include directive.")
+
+ else:
+ print("\nRemember to #include the new test header in this file (following alphabetical order):")
+ print(os.path.abspath("test_main.cpp"))
+ print("Insert the following line in the appropriate place:")
+ print(f'#include "tests/{file_path}"')
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/data/images/icon.bmp b/tests/data/images/icon.bmp
new file mode 100644
index 0000000000..e006f7ebdd
--- /dev/null
+++ b/tests/data/images/icon.bmp
Binary files differ
diff --git a/tests/data/images/icon.jpg b/tests/data/images/icon.jpg
new file mode 100644
index 0000000000..b45bfa8d9b
--- /dev/null
+++ b/tests/data/images/icon.jpg
Binary files differ
diff --git a/tests/data/images/icon.png b/tests/data/images/icon.png
new file mode 100644
index 0000000000..45aaaf584f
--- /dev/null
+++ b/tests/data/images/icon.png
Binary files differ
diff --git a/tests/data/images/icon.tga b/tests/data/images/icon.tga
new file mode 100644
index 0000000000..dcacdc5c67
--- /dev/null
+++ b/tests/data/images/icon.tga
Binary files differ
diff --git a/tests/data/images/icon.webp b/tests/data/images/icon.webp
new file mode 100644
index 0000000000..6c4707e858
--- /dev/null
+++ b/tests/data/images/icon.webp
Binary files differ
diff --git a/tests/data/line_endings_cr.test.txt b/tests/data/line_endings_cr.test.txt
new file mode 100644
index 0000000000..556154bb25
--- /dev/null
+++ b/tests/data/line_endings_cr.test.txt
@@ -0,0 +1 @@
+Hello darkness My old friend I've come to talk With you again \ No newline at end of file
diff --git a/tests/data/line_endings_crlf.test.txt b/tests/data/line_endings_crlf.test.txt
new file mode 100644
index 0000000000..a3cbe55b7f
--- /dev/null
+++ b/tests/data/line_endings_crlf.test.txt
@@ -0,0 +1,4 @@
+Hello darkness
+My old friend
+I've come to talk
+With you again
diff --git a/tests/data/line_endings_lf.test.txt b/tests/data/line_endings_lf.test.txt
new file mode 100644
index 0000000000..0aabcd911e
--- /dev/null
+++ b/tests/data/line_endings_lf.test.txt
@@ -0,0 +1,4 @@
+Hello darkness
+My old friend
+I've come to talk
+With you again
diff --git a/tests/data/translations.csv b/tests/data/translations.csv
new file mode 100644
index 0000000000..8cb7b800c5
--- /dev/null
+++ b/tests/data/translations.csv
@@ -0,0 +1,8 @@
+keys,en,de
+GOOD_MORNING,"Good Morning","Guten Morgen"
+GOOD_EVENING,"Good Evening",""
+Without quotes,"With, comma","With ""inner"" quotes","With ""inner"", quotes"","" and comma","With ""inner
+split"" quotes and
+line breaks","With \nnewline chars"
+Some other~delimiter~should still work, shouldn't it?
+What about tab separated lines, good?
diff --git a/tests/python_build/conftest.py b/tests/python_build/conftest.py
new file mode 100644
index 0000000000..617230926a
--- /dev/null
+++ b/tests/python_build/conftest.py
@@ -0,0 +1,26 @@
+import os
+import sys
+from pathlib import Path
+
+import pytest
+
+CWD = Path(__file__).parent
+ROOT = CWD.parent.parent
+# append directory with build files to sys.path to import them
+sys.path.append(str(ROOT))
+
+
+@pytest.fixture
+def shader_files(request):
+ shader_path = request.param
+
+ res = {
+ "path_input": str(CWD / "fixtures" / f"{shader_path}.glsl"),
+ "path_output": str(CWD / "fixtures" / f"{shader_path}.glsl.gen.h"),
+ "path_expected_full": str(CWD / "fixtures" / f"{shader_path}_expected_full.glsl"),
+ "path_expected_parts": str(CWD / "fixtures" / f"{shader_path}_expected_parts.json"),
+ }
+ yield res
+
+ if not os.getenv("PYTEST_KEEP_GENERATED_FILES"):
+ os.remove(res["path_output"])
diff --git a/tests/python_build/fixtures/gles3/_included.glsl b/tests/python_build/fixtures/gles3/_included.glsl
new file mode 100644
index 0000000000..adf5f702d3
--- /dev/null
+++ b/tests/python_build/fixtures/gles3/_included.glsl
@@ -0,0 +1 @@
+#define M_PI 3.14159265359
diff --git a/tests/python_build/fixtures/gles3/vertex_fragment.glsl b/tests/python_build/fixtures/gles3/vertex_fragment.glsl
new file mode 100644
index 0000000000..3004e22f25
--- /dev/null
+++ b/tests/python_build/fixtures/gles3/vertex_fragment.glsl
@@ -0,0 +1,34 @@
+#include "_included.glsl"
+
+#[modes]
+
+mode_ninepatch = #define USE_NINEPATCH
+
+#[specializations]
+
+DISABLE_LIGHTING = false
+
+#[vertex]
+
+precision highp float;
+precision highp int;
+
+layout(location = 0) in highp vec3 vertex;
+
+out highp vec4 position_interp;
+
+void main() {
+ position_interp = vec4(vertex.x,1,0,1);
+}
+
+#[fragment]
+
+precision highp float;
+precision highp int;
+
+in highp vec4 position_interp;
+
+void main() {
+ highp float depth = ((position_interp.z / position_interp.w) + 1.0);
+ frag_color = vec4(depth);
+}
diff --git a/tests/python_build/fixtures/gles3/vertex_fragment_expected_full.glsl b/tests/python_build/fixtures/gles3/vertex_fragment_expected_full.glsl
new file mode 100644
index 0000000000..7bf56e73cd
--- /dev/null
+++ b/tests/python_build/fixtures/gles3/vertex_fragment_expected_full.glsl
@@ -0,0 +1,50 @@
+/* WARNING, THIS FILE WAS GENERATED, DO NOT EDIT */
+#ifndef VERTEX_FRAGMENT_GLSL_GEN_HGLES3_GLES3
+#define VERTEX_FRAGMENT_GLSL_GEN_HGLES3_GLES3
+
+
+#include "drivers/gles3/shader_gles3.h"
+
+
+class VertexFragmentShaderGLES3 : public ShaderGLES3 {
+
+public:
+
+ enum ShaderVariant {
+ MODE_NINEPATCH,
+ };
+
+ enum Specializations {
+ DISABLE_LIGHTING=1,
+ };
+
+ _FORCE_INLINE_ void version_bind_shader(RID p_version,ShaderVariant p_variant,uint64_t p_specialization=0) { _version_bind_shader(p_version,p_variant,p_specialization); }
+
+protected:
+
+ virtual void _init() override {
+
+ static const char **_uniform_strings=nullptr;
+ static const char* _variant_defines[]={
+ "#define USE_NINEPATCH",
+ };
+
+ static TexUnitPair *_texunit_pairs=nullptr;
+ static UBOPair *_ubo_pairs=nullptr;
+ static Specialization _spec_pairs[]={
+ {"DISABLE_LIGHTING",false},
+ };
+
+ static const char _vertex_code[]={
+10,112,114,101,99,105,115,105,111,110,32,104,105,103,104,112,32,102,108,111,97,116,59,10,112,114,101,99,105,115,105,111,110,32,104,105,103,104,112,32,105,110,116,59,10,10,108,97,121,111,117,116,40,108,111,99,97,116,105,111,110,32,61,32,48,41,32,105,110,32,104,105,103,104,112,32,118,101,99,51,32,118,101,114,116,101,120,59,10,10,111,117,116,32,104,105,103,104,112,32,118,101,99,52,32,112,111,115,105,116,105,111,110,95,105,110,116,101,114,112,59,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,112,111,115,105,116,105,111,110,95,105,110,116,101,114,112,32,61,32,118,101,99,52,40,118,101,114,116,101,120,46,120,44,49,44,48,44,49,41,59,10,125,10,10, 0};
+
+ static const char _fragment_code[]={
+10,112,114,101,99,105,115,105,111,110,32,104,105,103,104,112,32,102,108,111,97,116,59,10,112,114,101,99,105,115,105,111,110,32,104,105,103,104,112,32,105,110,116,59,10,10,105,110,32,104,105,103,104,112,32,118,101,99,52,32,112,111,115,105,116,105,111,110,95,105,110,116,101,114,112,59,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,104,105,103,104,112,32,102,108,111,97,116,32,100,101,112,116,104,32,61,32,40,40,112,111,115,105,116,105,111,110,95,105,110,116,101,114,112,46,122,32,47,32,112,111,115,105,116,105,111,110,95,105,110,116,101,114,112,46,119,41,32,43,32,49,46,48,41,59,10,9,102,114,97,103,95,99,111,108,111,114,32,61,32,118,101,99,52,40,100,101,112,116,104,41,59,10,125,10, 0};
+
+ _setup(_vertex_code,_fragment_code,"VertexFragmentShaderGLES3",0,_uniform_strings,0,_ubo_pairs,0,_texunit_pairs,1,_spec_pairs,1,_variant_defines);
+ }
+
+};
+
+#endif
+
diff --git a/tests/python_build/fixtures/gles3/vertex_fragment_expected_parts.json b/tests/python_build/fixtures/gles3/vertex_fragment_expected_parts.json
new file mode 100644
index 0000000000..eaeb5981c0
--- /dev/null
+++ b/tests/python_build/fixtures/gles3/vertex_fragment_expected_parts.json
@@ -0,0 +1,52 @@
+{
+ "vertex_lines": [
+ "",
+ "precision highp float;",
+ "precision highp int;",
+ "",
+ "layout(location = 0) in highp vec3 vertex;",
+ "",
+ "out highp vec4 position_interp;",
+ "",
+ "void main() {",
+ "\tposition_interp = vec4(vertex.x,1,0,1);",
+ "}",
+ ""
+ ],
+ "fragment_lines": [
+ "",
+ "precision highp float;",
+ "precision highp int;",
+ "",
+ "in highp vec4 position_interp;",
+ "",
+ "void main() {",
+ "\thighp float depth = ((position_interp.z / position_interp.w) + 1.0);",
+ "\tfrag_color = vec4(depth);",
+ "}"
+ ],
+ "uniforms": [],
+ "fbos": [],
+ "texunits": [],
+ "texunit_names": [],
+ "ubos": [],
+ "ubo_names": [],
+ "vertex_included_files": [],
+ "fragment_included_files": [],
+ "reading": "fragment",
+ "line_offset": 33,
+ "vertex_offset": 10,
+ "fragment_offset": 23,
+ "variant_defines": [
+ "#define USE_NINEPATCH"
+ ],
+ "variant_names": [
+ "MODE_NINEPATCH"
+ ],
+ "specialization_names": [
+ "DISABLE_LIGHTING"
+ ],
+ "specialization_values": [
+ " false\n"
+ ]
+}
diff --git a/tests/python_build/fixtures/glsl/_included.glsl b/tests/python_build/fixtures/glsl/_included.glsl
new file mode 100644
index 0000000000..adf5f702d3
--- /dev/null
+++ b/tests/python_build/fixtures/glsl/_included.glsl
@@ -0,0 +1 @@
+#define M_PI 3.14159265359
diff --git a/tests/python_build/fixtures/glsl/compute.glsl b/tests/python_build/fixtures/glsl/compute.glsl
new file mode 100644
index 0000000000..e81f48d463
--- /dev/null
+++ b/tests/python_build/fixtures/glsl/compute.glsl
@@ -0,0 +1,12 @@
+#[compute]
+
+#version 450
+
+#VERSION_DEFINES
+
+
+#include "_included.glsl"
+
+void main() {
+ vec3 static_light = vec3(0, 1, 0);
+}
diff --git a/tests/python_build/fixtures/glsl/compute_expected_full.glsl b/tests/python_build/fixtures/glsl/compute_expected_full.glsl
new file mode 100644
index 0000000000..b937d732c8
--- /dev/null
+++ b/tests/python_build/fixtures/glsl/compute_expected_full.glsl
@@ -0,0 +1,8 @@
+/* WARNING, THIS FILE WAS GENERATED, DO NOT EDIT */
+#ifndef COMPUTE_SHADER_GLSL_RAW_H
+#define COMPUTE_SHADER_GLSL_RAW_H
+
+static const char compute_shader_glsl[] = {
+ 35,91,99,111,109,112,117,116,101,93,10,10,35,118,101,114,115,105,111,110,32,52,53,48,10,10,35,86,69,82,83,73,79,78,95,68,69,70,73,78,69,83,10,10,10,35,100,101,102,105,110,101,32,77,95,80,73,32,51,46,49,52,49,53,57,50,54,53,51,53,57,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,118,101,99,51,32,115,116,97,116,105,99,95,108,105,103,104,116,32,61,32,118,101,99,51,40,48,44,32,49,44,32,48,41,59,10,125,10,0
+};
+#endif
diff --git a/tests/python_build/fixtures/glsl/compute_expected_parts.json b/tests/python_build/fixtures/glsl/compute_expected_parts.json
new file mode 100644
index 0000000000..025c568ae0
--- /dev/null
+++ b/tests/python_build/fixtures/glsl/compute_expected_parts.json
@@ -0,0 +1,3 @@
+{
+ "code": "#[compute]\n\n#version 450\n\n#VERSION_DEFINES\n\n\n#define M_PI 3.14159265359\n\nvoid main() {\n\tvec3 static_light = vec3(0, 1, 0);\n}\n"
+}
diff --git a/tests/python_build/fixtures/glsl/vertex_fragment.glsl b/tests/python_build/fixtures/glsl/vertex_fragment.glsl
new file mode 100644
index 0000000000..0bdce783d7
--- /dev/null
+++ b/tests/python_build/fixtures/glsl/vertex_fragment.glsl
@@ -0,0 +1,32 @@
+#[versions]
+
+lines = "#define MODE_LINES";
+
+#[vertex]
+
+#version 450
+
+#VERSION_DEFINES
+
+layout(location = 0) out vec3 uv_interp;
+
+void main() {
+
+#ifdef MODE_LINES
+ uv_interp = vec3(0,0,1);
+#endif
+}
+
+#[fragment]
+
+#version 450
+
+#VERSION_DEFINES
+
+#include "_included.glsl"
+
+layout(location = 0) out vec4 dst_color;
+
+void main() {
+ dst_color = vec4(1,1,0,0);
+}
diff --git a/tests/python_build/fixtures/glsl/vertex_fragment_expected_full.glsl b/tests/python_build/fixtures/glsl/vertex_fragment_expected_full.glsl
new file mode 100644
index 0000000000..3f53a17fac
--- /dev/null
+++ b/tests/python_build/fixtures/glsl/vertex_fragment_expected_full.glsl
@@ -0,0 +1,8 @@
+/* WARNING, THIS FILE WAS GENERATED, DO NOT EDIT */
+#ifndef VERTEX_FRAGMENT_SHADER_GLSL_RAW_H
+#define VERTEX_FRAGMENT_SHADER_GLSL_RAW_H
+
+static const char vertex_fragment_shader_glsl[] = {
+ 35,91,118,101,114,115,105,111,110,115,93,10,10,108,105,110,101,115,32,61,32,34,35,100,101,102,105,110,101,32,77,79,68,69,95,76,73,78,69,83,34,59,10,10,35,91,118,101,114,116,101,120,93,10,10,35,118,101,114,115,105,111,110,32,52,53,48,10,10,35,86,69,82,83,73,79,78,95,68,69,70,73,78,69,83,10,10,108,97,121,111,117,116,40,108,111,99,97,116,105,111,110,32,61,32,48,41,32,111,117,116,32,118,101,99,51,32,117,118,95,105,110,116,101,114,112,59,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,10,35,105,102,100,101,102,32,77,79,68,69,95,76,73,78,69,83,10,9,117,118,95,105,110,116,101,114,112,32,61,32,118,101,99,51,40,48,44,48,44,49,41,59,10,35,101,110,100,105,102,10,125,10,10,35,91,102,114,97,103,109,101,110,116,93,10,10,35,118,101,114,115,105,111,110,32,52,53,48,10,10,35,86,69,82,83,73,79,78,95,68,69,70,73,78,69,83,10,10,35,100,101,102,105,110,101,32,77,95,80,73,32,51,46,49,52,49,53,57,50,54,53,51,53,57,10,10,108,97,121,111,117,116,40,108,111,99,97,116,105,111,110,32,61,32,48,41,32,111,117,116,32,118,101,99,52,32,100,115,116,95,99,111,108,111,114,59,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,100,115,116,95,99,111,108,111,114,32,61,32,118,101,99,52,40,49,44,49,44,48,44,48,41,59,10,125,10,0
+};
+#endif
diff --git a/tests/python_build/fixtures/glsl/vertex_fragment_expected_parts.json b/tests/python_build/fixtures/glsl/vertex_fragment_expected_parts.json
new file mode 100644
index 0000000000..38312367a0
--- /dev/null
+++ b/tests/python_build/fixtures/glsl/vertex_fragment_expected_parts.json
@@ -0,0 +1,3 @@
+{
+ "code": "#[versions]\n\nlines = \"#define MODE_LINES\";\n\n#[vertex]\n\n#version 450\n\n#VERSION_DEFINES\n\nlayout(location = 0) out vec3 uv_interp;\n\nvoid main() {\n\n#ifdef MODE_LINES\n\tuv_interp = vec3(0,0,1);\n#endif\n}\n\n#[fragment]\n\n#version 450\n\n#VERSION_DEFINES\n\n#define M_PI 3.14159265359\n\nlayout(location = 0) out vec4 dst_color;\n\nvoid main() {\n\tdst_color = vec4(1,1,0,0);\n}\n"
+}
diff --git a/tests/python_build/fixtures/rd_glsl/_included.glsl b/tests/python_build/fixtures/rd_glsl/_included.glsl
new file mode 100644
index 0000000000..adf5f702d3
--- /dev/null
+++ b/tests/python_build/fixtures/rd_glsl/_included.glsl
@@ -0,0 +1 @@
+#define M_PI 3.14159265359
diff --git a/tests/python_build/fixtures/rd_glsl/compute.glsl b/tests/python_build/fixtures/rd_glsl/compute.glsl
new file mode 100644
index 0000000000..66fbbeb401
--- /dev/null
+++ b/tests/python_build/fixtures/rd_glsl/compute.glsl
@@ -0,0 +1,13 @@
+#[compute]
+
+#version 450
+
+#VERSION_DEFINES
+
+#define BLOCK_SIZE 8
+
+#include "_included.glsl"
+
+void main() {
+ uint t = BLOCK_SIZE + 1;
+}
diff --git a/tests/python_build/fixtures/rd_glsl/compute_expected_full.glsl b/tests/python_build/fixtures/rd_glsl/compute_expected_full.glsl
new file mode 100644
index 0000000000..b59923e28a
--- /dev/null
+++ b/tests/python_build/fixtures/rd_glsl/compute_expected_full.glsl
@@ -0,0 +1,20 @@
+/* WARNING, THIS FILE WAS GENERATED, DO NOT EDIT */
+#ifndef COMPUTE_GLSL_GEN_H_RD
+#define COMPUTE_GLSL_GEN_H_RD
+
+#include "servers/rendering/renderer_rd/shader_rd.h"
+
+class ComputeShaderRD : public ShaderRD {
+
+public:
+
+ ComputeShaderRD() {
+
+ static const char _compute_code[] = {
+10,35,118,101,114,115,105,111,110,32,52,53,48,10,10,35,86,69,82,83,73,79,78,95,68,69,70,73,78,69,83,10,10,35,100,101,102,105,110,101,32,66,76,79,67,75,95,83,73,90,69,32,56,10,10,35,100,101,102,105,110,101,32,77,95,80,73,32,51,46,49,52,49,53,57,50,54,53,51,53,57,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,117,105,110,116,32,116,32,61,32,66,76,79,67,75,95,83,73,90,69,32,43,32,49,59,10,125,10,0
+ };
+ setup(nullptr, nullptr, _compute_code, "ComputeShaderRD");
+ }
+};
+
+#endif
diff --git a/tests/python_build/fixtures/rd_glsl/compute_expected_parts.json b/tests/python_build/fixtures/rd_glsl/compute_expected_parts.json
new file mode 100644
index 0000000000..26ba9e4fc4
--- /dev/null
+++ b/tests/python_build/fixtures/rd_glsl/compute_expected_parts.json
@@ -0,0 +1,28 @@
+{
+ "vertex_lines": [],
+ "fragment_lines": [],
+ "compute_lines": [
+ "",
+ "#version 450",
+ "",
+ "#VERSION_DEFINES",
+ "",
+ "#define BLOCK_SIZE 8",
+ "",
+ "#define M_PI 3.14159265359",
+ "",
+ "void main() {",
+ "\tuint t = BLOCK_SIZE + 1;",
+ "}"
+ ],
+ "vertex_included_files": [],
+ "fragment_included_files": [],
+ "compute_included_files": [
+ "tests/python_build/fixtures/rd_glsl/_included.glsl"
+ ],
+ "reading": "compute",
+ "line_offset": 13,
+ "vertex_offset": 0,
+ "fragment_offset": 0,
+ "compute_offset": 1
+}
diff --git a/tests/python_build/fixtures/rd_glsl/vertex_fragment.glsl b/tests/python_build/fixtures/rd_glsl/vertex_fragment.glsl
new file mode 100644
index 0000000000..27be08a857
--- /dev/null
+++ b/tests/python_build/fixtures/rd_glsl/vertex_fragment.glsl
@@ -0,0 +1,25 @@
+#[vertex]
+
+#version 450
+
+#VERSION_DEFINES
+
+#include "_included.glsl"
+
+layout(location = 0) out vec2 uv_interp;
+
+void main() {
+ uv_interp = vec2(0, 1);
+}
+
+#[fragment]
+
+#version 450
+
+#VERSION_DEFINES
+
+layout(location = 0) in vec2 uv_interp;
+
+void main() {
+ uv_interp = vec2(1, 0);
+}
diff --git a/tests/python_build/fixtures/rd_glsl/vertex_fragment_expected_full.glsl b/tests/python_build/fixtures/rd_glsl/vertex_fragment_expected_full.glsl
new file mode 100644
index 0000000000..ff804dbf89
--- /dev/null
+++ b/tests/python_build/fixtures/rd_glsl/vertex_fragment_expected_full.glsl
@@ -0,0 +1,23 @@
+/* WARNING, THIS FILE WAS GENERATED, DO NOT EDIT */
+#ifndef VERTEX_FRAGMENT_GLSL_GEN_H_RD
+#define VERTEX_FRAGMENT_GLSL_GEN_H_RD
+
+#include "servers/rendering/renderer_rd/shader_rd.h"
+
+class VertexFragmentShaderRD : public ShaderRD {
+
+public:
+
+ VertexFragmentShaderRD() {
+
+ static const char _vertex_code[] = {
+10,35,118,101,114,115,105,111,110,32,52,53,48,10,10,35,86,69,82,83,73,79,78,95,68,69,70,73,78,69,83,10,10,35,100,101,102,105,110,101,32,77,95,80,73,32,51,46,49,52,49,53,57,50,54,53,51,53,57,10,10,108,97,121,111,117,116,40,108,111,99,97,116,105,111,110,32,61,32,48,41,32,111,117,116,32,118,101,99,50,32,117,118,95,105,110,116,101,114,112,59,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,117,118,95,105,110,116,101,114,112,32,61,32,118,101,99,50,40,48,44,32,49,41,59,10,125,10,10,0
+ };
+ static const char _fragment_code[] = {
+10,35,118,101,114,115,105,111,110,32,52,53,48,10,10,35,86,69,82,83,73,79,78,95,68,69,70,73,78,69,83,10,10,108,97,121,111,117,116,40,108,111,99,97,116,105,111,110,32,61,32,48,41,32,105,110,32,118,101,99,50,32,117,118,95,105,110,116,101,114,112,59,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,117,118,95,105,110,116,101,114,112,32,61,32,118,101,99,50,40,49,44,32,48,41,59,10,125,10,0
+ };
+ setup(_vertex_code, _fragment_code, nullptr, "VertexFragmentShaderRD");
+ }
+};
+
+#endif
diff --git a/tests/python_build/fixtures/rd_glsl/vertex_fragment_expected_parts.json b/tests/python_build/fixtures/rd_glsl/vertex_fragment_expected_parts.json
new file mode 100644
index 0000000000..dbf833edea
--- /dev/null
+++ b/tests/python_build/fixtures/rd_glsl/vertex_fragment_expected_parts.json
@@ -0,0 +1,40 @@
+{
+ "vertex_lines": [
+ "",
+ "#version 450",
+ "",
+ "#VERSION_DEFINES",
+ "",
+ "#define M_PI 3.14159265359",
+ "",
+ "layout(location = 0) out vec2 uv_interp;",
+ "",
+ "void main() {",
+ "\tuv_interp = vec2(0, 1);",
+ "}",
+ ""
+ ],
+ "fragment_lines": [
+ "",
+ "#version 450",
+ "",
+ "#VERSION_DEFINES",
+ "",
+ "layout(location = 0) in vec2 uv_interp;",
+ "",
+ "void main() {",
+ "\tuv_interp = vec2(1, 0);",
+ "}"
+ ],
+ "compute_lines": [],
+ "vertex_included_files": [
+ "tests/python_build/fixtures/rd_glsl/_included.glsl"
+ ],
+ "fragment_included_files": [],
+ "compute_included_files": [],
+ "reading": "fragment",
+ "line_offset": 25,
+ "vertex_offset": 1,
+ "fragment_offset": 15,
+ "compute_offset": 0
+}
diff --git a/tests/python_build/test_gles3_builder.py b/tests/python_build/test_gles3_builder.py
new file mode 100644
index 0000000000..861e0b84c4
--- /dev/null
+++ b/tests/python_build/test_gles3_builder.py
@@ -0,0 +1,31 @@
+import json
+
+import pytest
+
+from gles3_builders import build_gles3_header, GLES3HeaderStruct
+
+
+@pytest.mark.parametrize(
+ ["shader_files", "builder", "header_struct"],
+ [
+ ("gles3/vertex_fragment", build_gles3_header, GLES3HeaderStruct),
+ ],
+ indirect=["shader_files"],
+)
+def test_gles3_builder(shader_files, builder, header_struct):
+ header = header_struct()
+
+ builder(shader_files["path_input"], "drivers/gles3/shader_gles3.h", "GLES3", header_data=header)
+
+ with open(shader_files["path_expected_parts"], "r") as f:
+ expected_parts = json.load(f)
+ assert expected_parts == header.__dict__
+
+ with open(shader_files["path_output"], "r") as f:
+ actual_output = f.read()
+ assert actual_output
+
+ with open(shader_files["path_expected_full"], "r") as f:
+ expected_output = f.read()
+
+ assert actual_output == expected_output
diff --git a/tests/python_build/test_glsl_builder.py b/tests/python_build/test_glsl_builder.py
new file mode 100644
index 0000000000..b9dcef48ac
--- /dev/null
+++ b/tests/python_build/test_glsl_builder.py
@@ -0,0 +1,37 @@
+import json
+
+import pytest
+
+from glsl_builders import build_raw_header, RAWHeaderStruct, build_rd_header, RDHeaderStruct
+
+
+@pytest.mark.parametrize(
+ [
+ "shader_files",
+ "builder",
+ "header_struct",
+ ],
+ [
+ ("glsl/vertex_fragment", build_raw_header, RAWHeaderStruct),
+ ("glsl/compute", build_raw_header, RAWHeaderStruct),
+ ("rd_glsl/vertex_fragment", build_rd_header, RDHeaderStruct),
+ ("rd_glsl/compute", build_rd_header, RDHeaderStruct),
+ ],
+ indirect=["shader_files"],
+)
+def test_glsl_builder(shader_files, builder, header_struct):
+ header = header_struct()
+ builder(shader_files["path_input"], header_data=header)
+
+ with open(shader_files["path_expected_parts"], "r") as f:
+ expected_parts = json.load(f)
+ assert expected_parts == header.__dict__
+
+ with open(shader_files["path_output"], "r") as f:
+ actual_output = f.read()
+ assert actual_output
+
+ with open(shader_files["path_expected_full"], "r") as f:
+ expected_output = f.read()
+
+ assert actual_output == expected_output
diff --git a/tests/scene/test_animation.h b/tests/scene/test_animation.h
new file mode 100644
index 0000000000..9199713fd9
--- /dev/null
+++ b/tests/scene/test_animation.h
@@ -0,0 +1,314 @@
+/*************************************************************************/
+/* test_animation.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_ANIMATION_H
+#define TEST_ANIMATION_H
+
+#include "scene/resources/animation.h"
+
+#include "tests/test_macros.h"
+
+namespace TestAnimation {
+
+TEST_CASE("[Animation] Empty animation getters") {
+ const Ref<Animation> animation = memnew(Animation);
+
+ CHECK(Math::is_equal_approx(animation->get_length(), real_t(1.0)));
+ CHECK(Math::is_equal_approx(animation->get_step(), real_t(0.1)));
+}
+
+TEST_CASE("[Animation] Create value track") {
+ // This creates an animation that makes the node "Enemy" move to the right by
+ // 100 pixels in 0.5 seconds.
+ Ref<Animation> animation = memnew(Animation);
+ const int track_index = animation->add_track(Animation::TYPE_VALUE);
+ CHECK(track_index == 0);
+ animation->track_set_path(track_index, NodePath("Enemy:position:x"));
+ animation->track_insert_key(track_index, 0.0, 0);
+ animation->track_insert_key(track_index, 0.5, 100);
+
+ CHECK(animation->get_track_count() == 1);
+ CHECK(!animation->track_is_compressed(0));
+ CHECK(int(animation->track_get_key_value(0, 0)) == 0);
+ CHECK(int(animation->track_get_key_value(0, 1)) == 100);
+
+ CHECK(Math::is_equal_approx(animation->value_track_interpolate(0, -0.2), 0.0));
+ CHECK(Math::is_equal_approx(animation->value_track_interpolate(0, 0.0), 0.0));
+ CHECK(Math::is_equal_approx(animation->value_track_interpolate(0, 0.2), 40.0));
+ CHECK(Math::is_equal_approx(animation->value_track_interpolate(0, 0.4), 80.0));
+ CHECK(Math::is_equal_approx(animation->value_track_interpolate(0, 0.5), 100.0));
+ CHECK(Math::is_equal_approx(animation->value_track_interpolate(0, 0.6), 100.0));
+
+ CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 0), real_t(1.0)));
+ CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 1), real_t(1.0)));
+
+ ERR_PRINT_OFF;
+ // Nonexistent keys.
+ CHECK(animation->track_get_key_value(0, 2).is_null());
+ CHECK(animation->track_get_key_value(0, -1).is_null());
+ CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 2), real_t(-1.0)));
+ // Nonexistent track (and keys).
+ CHECK(animation->track_get_key_value(1, 0).is_null());
+ CHECK(animation->track_get_key_value(1, 1).is_null());
+ CHECK(animation->track_get_key_value(1, 2).is_null());
+ CHECK(animation->track_get_key_value(1, -1).is_null());
+ CHECK(Math::is_equal_approx(animation->track_get_key_transition(1, 0), real_t(-1.0)));
+
+ // This is a value track, so the methods below should return errors.
+ CHECK(animation->position_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(animation->rotation_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(animation->scale_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(Math::is_zero_approx(animation->bezier_track_interpolate(0, 0.0)));
+ CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[Animation] Create 3D position track") {
+ Ref<Animation> animation = memnew(Animation);
+ const int track_index = animation->add_track(Animation::TYPE_POSITION_3D);
+ animation->track_set_path(track_index, NodePath("Enemy:position"));
+ animation->position_track_insert_key(track_index, 0.0, Vector3(0, 1, 2));
+ animation->position_track_insert_key(track_index, 0.5, Vector3(3.5, 4, 5));
+
+ CHECK(animation->get_track_count() == 1);
+ CHECK(!animation->track_is_compressed(0));
+ CHECK(Vector3(animation->track_get_key_value(0, 0)).is_equal_approx(Vector3(0, 1, 2)));
+ CHECK(Vector3(animation->track_get_key_value(0, 1)).is_equal_approx(Vector3(3.5, 4, 5)));
+
+ Vector3 r_interpolation;
+
+ CHECK(animation->position_track_interpolate(0, -0.2, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Vector3(0, 1, 2)));
+
+ CHECK(animation->position_track_interpolate(0, 0.0, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Vector3(0, 1, 2)));
+
+ CHECK(animation->position_track_interpolate(0, 0.2, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Vector3(1.4, 2.2, 3.2)));
+
+ CHECK(animation->position_track_interpolate(0, 0.4, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Vector3(2.8, 3.4, 4.4)));
+
+ CHECK(animation->position_track_interpolate(0, 0.5, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Vector3(3.5, 4, 5)));
+
+ CHECK(animation->position_track_interpolate(0, 0.6, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Vector3(3.5, 4, 5)));
+
+ // 3D position tracks always use linear interpolation for performance reasons.
+ CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 0), real_t(1.0)));
+ CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 1), real_t(1.0)));
+
+ // This is a 3D position track, so the methods below should return errors.
+ ERR_PRINT_OFF;
+ CHECK(animation->value_track_interpolate(0, 0.0).is_null());
+ CHECK(animation->rotation_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(animation->scale_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(Math::is_zero_approx(animation->bezier_track_interpolate(0, 0.0)));
+ CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[Animation] Create 3D rotation track") {
+ Ref<Animation> animation = memnew(Animation);
+ const int track_index = animation->add_track(Animation::TYPE_ROTATION_3D);
+ animation->track_set_path(track_index, NodePath("Enemy:rotation"));
+ animation->rotation_track_insert_key(track_index, 0.0, Quaternion(Vector3(0, 1, 2)));
+ animation->rotation_track_insert_key(track_index, 0.5, Quaternion(Vector3(3.5, 4, 5)));
+
+ CHECK(animation->get_track_count() == 1);
+ CHECK(!animation->track_is_compressed(0));
+ CHECK(Quaternion(animation->track_get_key_value(0, 0)).is_equal_approx(Quaternion(Vector3(0, 1, 2))));
+ CHECK(Quaternion(animation->track_get_key_value(0, 1)).is_equal_approx(Quaternion(Vector3(3.5, 4, 5))));
+
+ Quaternion r_interpolation;
+
+ CHECK(animation->rotation_track_interpolate(0, -0.2, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Quaternion(0.403423, 0.259035, 0.73846, 0.47416)));
+
+ CHECK(animation->rotation_track_interpolate(0, 0.0, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Quaternion(0.403423, 0.259035, 0.73846, 0.47416)));
+
+ CHECK(animation->rotation_track_interpolate(0, 0.2, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Quaternion(0.336182, 0.30704, 0.751515, 0.477425)));
+
+ CHECK(animation->rotation_track_interpolate(0, 0.4, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Quaternion(0.266585, 0.352893, 0.759303, 0.477344)));
+
+ CHECK(animation->rotation_track_interpolate(0, 0.5, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Quaternion(0.231055, 0.374912, 0.761204, 0.476048)));
+
+ CHECK(animation->rotation_track_interpolate(0, 0.6, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Quaternion(0.231055, 0.374912, 0.761204, 0.476048)));
+
+ // 3D rotation tracks always use linear interpolation for performance reasons.
+ CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 0), real_t(1.0)));
+ CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 1), real_t(1.0)));
+
+ // This is a 3D rotation track, so the methods below should return errors.
+ ERR_PRINT_OFF;
+ CHECK(animation->value_track_interpolate(0, 0.0).is_null());
+ CHECK(animation->position_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(animation->scale_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(Math::is_zero_approx(animation->bezier_track_interpolate(0, 0.0)));
+ CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[Animation] Create 3D scale track") {
+ Ref<Animation> animation = memnew(Animation);
+ const int track_index = animation->add_track(Animation::TYPE_SCALE_3D);
+ animation->track_set_path(track_index, NodePath("Enemy:scale"));
+ animation->scale_track_insert_key(track_index, 0.0, Vector3(0, 1, 2));
+ animation->scale_track_insert_key(track_index, 0.5, Vector3(3.5, 4, 5));
+
+ CHECK(animation->get_track_count() == 1);
+ CHECK(!animation->track_is_compressed(0));
+ CHECK(Vector3(animation->track_get_key_value(0, 0)).is_equal_approx(Vector3(0, 1, 2)));
+ CHECK(Vector3(animation->track_get_key_value(0, 1)).is_equal_approx(Vector3(3.5, 4, 5)));
+
+ Vector3 r_interpolation;
+
+ CHECK(animation->scale_track_interpolate(0, -0.2, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Vector3(0, 1, 2)));
+
+ CHECK(animation->scale_track_interpolate(0, 0.0, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Vector3(0, 1, 2)));
+
+ CHECK(animation->scale_track_interpolate(0, 0.2, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Vector3(1.4, 2.2, 3.2)));
+
+ CHECK(animation->scale_track_interpolate(0, 0.4, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Vector3(2.8, 3.4, 4.4)));
+
+ CHECK(animation->scale_track_interpolate(0, 0.5, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Vector3(3.5, 4, 5)));
+
+ CHECK(animation->scale_track_interpolate(0, 0.6, &r_interpolation) == OK);
+ CHECK(r_interpolation.is_equal_approx(Vector3(3.5, 4, 5)));
+
+ // 3D scale tracks always use linear interpolation for performance reasons.
+ CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 0), real_t(1.0)));
+ CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 1), real_t(1.0)));
+
+ // This is a 3D scale track, so the methods below should return errors.
+ ERR_PRINT_OFF;
+ CHECK(animation->value_track_interpolate(0, 0.0).is_null());
+ CHECK(animation->position_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(animation->rotation_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(Math::is_zero_approx(animation->bezier_track_interpolate(0, 0.0)));
+ CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[Animation] Create blend shape track") {
+ Ref<Animation> animation = memnew(Animation);
+ const int track_index = animation->add_track(Animation::TYPE_BLEND_SHAPE);
+ animation->track_set_path(track_index, NodePath("Enemy:scale"));
+ // Negative values for blend shapes should work as expected.
+ animation->blend_shape_track_insert_key(track_index, 0.0, -1.0);
+ animation->blend_shape_track_insert_key(track_index, 0.5, 1.0);
+
+ CHECK(animation->get_track_count() == 1);
+ CHECK(!animation->track_is_compressed(0));
+
+ float r_blend = 0.0f;
+
+ CHECK(animation->blend_shape_track_get_key(0, 0, &r_blend) == OK);
+ CHECK(Math::is_equal_approx(r_blend, -1.0f));
+
+ CHECK(animation->blend_shape_track_get_key(0, 1, &r_blend) == OK);
+ CHECK(Math::is_equal_approx(r_blend, 1.0f));
+
+ CHECK(animation->blend_shape_track_interpolate(0, -0.2, &r_blend) == OK);
+ CHECK(Math::is_equal_approx(r_blend, -1.0f));
+
+ CHECK(animation->blend_shape_track_interpolate(0, 0.0, &r_blend) == OK);
+ CHECK(Math::is_equal_approx(r_blend, -1.0f));
+
+ CHECK(animation->blend_shape_track_interpolate(0, 0.2, &r_blend) == OK);
+ CHECK(Math::is_equal_approx(r_blend, -0.2f));
+
+ CHECK(animation->blend_shape_track_interpolate(0, 0.4, &r_blend) == OK);
+ CHECK(Math::is_equal_approx(r_blend, 0.6f));
+
+ CHECK(animation->blend_shape_track_interpolate(0, 0.5, &r_blend) == OK);
+ CHECK(Math::is_equal_approx(r_blend, 1.0f));
+
+ CHECK(animation->blend_shape_track_interpolate(0, 0.6, &r_blend) == OK);
+ CHECK(Math::is_equal_approx(r_blend, 1.0f));
+
+ // Blend shape tracks always use linear interpolation for performance reasons.
+ CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 0), real_t(1.0)));
+ CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 1), real_t(1.0)));
+
+ // This is a blend shape track, so the methods below should return errors.
+ ERR_PRINT_OFF;
+ CHECK(animation->value_track_interpolate(0, 0.0).is_null());
+ CHECK(animation->position_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(animation->rotation_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(animation->scale_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(Math::is_zero_approx(animation->bezier_track_interpolate(0, 0.0)));
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[Animation] Create Bezier track") {
+ Ref<Animation> animation = memnew(Animation);
+ const int track_index = animation->add_track(Animation::TYPE_BEZIER);
+ animation->track_set_path(track_index, NodePath("Enemy:scale"));
+ animation->bezier_track_insert_key(track_index, 0.0, -1.0, Vector2(-1, -1), Vector2(1, 1));
+ animation->bezier_track_insert_key(track_index, 0.5, 1.0, Vector2(0, 1), Vector2(1, 0.5));
+
+ CHECK(animation->get_track_count() == 1);
+ CHECK(!animation->track_is_compressed(0));
+
+ CHECK(Math::is_equal_approx(animation->bezier_track_get_key_value(0, 0), real_t(-1.0)));
+ CHECK(Math::is_equal_approx(animation->bezier_track_get_key_value(0, 1), real_t(1.0)));
+
+ CHECK(Math::is_equal_approx(animation->bezier_track_interpolate(0, -0.2), real_t(-1.0)));
+ CHECK(Math::is_equal_approx(animation->bezier_track_interpolate(0, 0.0), real_t(-1.0)));
+ CHECK(Math::is_equal_approx(animation->bezier_track_interpolate(0, 0.2), real_t(-0.76057207584381)));
+ CHECK(Math::is_equal_approx(animation->bezier_track_interpolate(0, 0.4), real_t(-0.39975279569626)));
+ CHECK(Math::is_equal_approx(animation->bezier_track_interpolate(0, 0.5), real_t(1.0)));
+ CHECK(Math::is_equal_approx(animation->bezier_track_interpolate(0, 0.6), real_t(1.0)));
+
+ // This is a bezier track, so the methods below should return errors.
+ ERR_PRINT_OFF;
+ CHECK(animation->value_track_interpolate(0, 0.0).is_null());
+ CHECK(animation->position_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(animation->rotation_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(animation->scale_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER);
+ ERR_PRINT_ON;
+}
+
+} // namespace TestAnimation
+
+#endif // TEST_ANIMATION_H
diff --git a/tests/scene/test_arraymesh.h b/tests/scene/test_arraymesh.h
new file mode 100644
index 0000000000..fc23adcd06
--- /dev/null
+++ b/tests/scene/test_arraymesh.h
@@ -0,0 +1,321 @@
+/*************************************************************************/
+/* test_arraymesh.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_ARRAYMESH_H
+#define TEST_ARRAYMESH_H
+
+#include "scene/resources/mesh.h"
+#include "scene/resources/primitive_meshes.h"
+
+#include "tests/test_macros.h"
+
+namespace TestArrayMesh {
+
+TEST_CASE("[SceneTree][ArrayMesh] Adding and modifying blendshapes.") {
+ Ref<ArrayMesh> mesh = memnew(ArrayMesh);
+ StringName name_a{ "ShapeA" };
+ StringName name_b{ "ShapeB" };
+
+ SUBCASE("Adding a blend shape to the mesh before a surface is added.") {
+ mesh->add_blend_shape(name_a);
+ mesh->add_blend_shape(name_b);
+
+ CHECK(mesh->get_blend_shape_name(0) == name_a);
+ CHECK(mesh->get_blend_shape_name(1) == name_b);
+ }
+
+ SUBCASE("Add same blend shape multiple times appends name with number.") {
+ mesh->add_blend_shape(name_a);
+ mesh->add_blend_shape(name_a);
+ mesh->add_blend_shape(name_a);
+
+ CHECK(mesh->get_blend_shape_name(0) == "ShapeA");
+ bool all_different = (static_cast<String>(mesh->get_blend_shape_name(0)) != static_cast<String>(mesh->get_blend_shape_name(1))) &&
+ (static_cast<String>(mesh->get_blend_shape_name(1)) != static_cast<String>(mesh->get_blend_shape_name(2))) &&
+ (static_cast<String>(mesh->get_blend_shape_name(0)) != static_cast<String>(mesh->get_blend_shape_name(2)));
+ bool all_have_name = static_cast<String>(mesh->get_blend_shape_name(1)).contains("ShapeA") &&
+ static_cast<String>(mesh->get_blend_shape_name(2)).contains("ShapeA");
+ CHECK((all_different && all_have_name));
+ }
+
+ SUBCASE("ArrayMesh keeps correct count of number of blend shapes") {
+ mesh->add_blend_shape(name_a);
+ mesh->add_blend_shape(name_a);
+ mesh->add_blend_shape(name_b);
+ mesh->add_blend_shape(name_b);
+ mesh->add_blend_shape(name_b);
+
+ REQUIRE(mesh->get_blend_shape_count() == 5);
+ }
+
+ SUBCASE("Adding blend shape after surface is added causes error") {
+ Ref<CylinderMesh> cylinder = memnew(CylinderMesh);
+ Array cylinder_array{};
+ cylinder_array.resize(Mesh::ARRAY_MAX);
+ cylinder->create_mesh_array(cylinder_array, 3.f, 3.f, 5.f);
+ mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, cylinder_array);
+
+ mesh->add_blend_shape(name_a);
+ CHECK(mesh->get_blend_shape_count() == 0);
+ }
+
+ SUBCASE("Change blend shape name after adding.") {
+ mesh->add_blend_shape(name_a);
+ mesh->set_blend_shape_name(0, name_b);
+
+ CHECK(mesh->get_blend_shape_name(0) == name_b);
+ }
+
+ SUBCASE("Change blend shape name to the name of one already there, should append number to end") {
+ mesh->add_blend_shape(name_a);
+ mesh->add_blend_shape(name_b);
+ mesh->set_blend_shape_name(0, name_b);
+
+ String name_string = mesh->get_blend_shape_name(0);
+ CHECK(name_string.contains("ShapeB"));
+ CHECK(name_string.length() > static_cast<String>(name_b).size());
+ }
+
+ SUBCASE("Clear all blend shapes before surface has been added.") {
+ mesh->add_blend_shape(name_a);
+ mesh->add_blend_shape(name_b);
+ CHECK(mesh->get_blend_shape_count() == 2);
+
+ mesh->clear_blend_shapes();
+ CHECK(mesh->get_blend_shape_count() == 0);
+ }
+
+ SUBCASE("Can't clear blend shapes after surface had been added.") {
+ mesh->add_blend_shape(name_a);
+ mesh->add_blend_shape(name_b);
+ Ref<CylinderMesh> cylinder = memnew(CylinderMesh);
+ Array cylinder_array{};
+ cylinder_array.resize(Mesh::ARRAY_MAX);
+ cylinder->create_mesh_array(cylinder_array, 3.f, 3.f, 5.f);
+ mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, cylinder_array);
+
+ mesh->clear_blend_shapes();
+ CHECK(mesh->get_blend_shape_count() == 2);
+ }
+
+ SUBCASE("Set the blend shape mode of ArrayMesh and underlying mesh RID.") {
+ mesh->set_blend_shape_mode(Mesh::BLEND_SHAPE_MODE_RELATIVE);
+ CHECK(mesh->get_blend_shape_mode() == Mesh::BLEND_SHAPE_MODE_RELATIVE);
+ }
+}
+
+TEST_CASE("[SceneTree][ArrayMesh] Surface meta data tests.") {
+ Ref<ArrayMesh> mesh = memnew(ArrayMesh);
+ Ref<CylinderMesh> cylinder = memnew(CylinderMesh);
+ Array cylinder_array{};
+ cylinder_array.resize(Mesh::ARRAY_MAX);
+ cylinder->create_mesh_array(cylinder_array, 3.f, 3.f, 5.f);
+ mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, cylinder_array);
+
+ Ref<BoxMesh> box = memnew(BoxMesh);
+ Array box_array{};
+ box_array.resize(Mesh::ARRAY_MAX);
+ box->create_mesh_array(box_array, Vector3(2.f, 1.2f, 1.6f));
+ mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, box_array);
+
+ SUBCASE("Add 2 surfaces and count the number of surfaces in the mesh.") {
+ REQUIRE(mesh->get_surface_count() == 2);
+ }
+
+ SUBCASE("Get the surface array from mesh.") {
+ REQUIRE(mesh->surface_get_arrays(0)[0] == cylinder_array[0]);
+ REQUIRE(mesh->surface_get_arrays(1)[0] == box_array[0]);
+ }
+
+ SUBCASE("Get the array length of a particular surface.") {
+ CHECK(mesh->surface_get_array_len(0) == static_cast<Vector<Vector3>>(cylinder_array[RenderingServer::ARRAY_VERTEX]).size());
+ CHECK(mesh->surface_get_array_len(1) == static_cast<Vector<Vector3>>(box_array[RenderingServer::ARRAY_VERTEX]).size());
+ }
+
+ SUBCASE("Get the index array length of a particular surface.") {
+ CHECK(mesh->surface_get_array_index_len(0) == static_cast<Vector<Vector3>>(cylinder_array[RenderingServer::ARRAY_INDEX]).size());
+ CHECK(mesh->surface_get_array_index_len(1) == static_cast<Vector<Vector3>>(box_array[RenderingServer::ARRAY_INDEX]).size());
+ }
+
+ SUBCASE("Get correct primitive type") {
+ CHECK(mesh->surface_get_primitive_type(0) == Mesh::PRIMITIVE_TRIANGLES);
+ CHECK(mesh->surface_get_primitive_type(1) == Mesh::PRIMITIVE_TRIANGLES);
+ mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLE_STRIP, box_array);
+ CHECK(mesh->surface_get_primitive_type(2) == Mesh::PRIMITIVE_TRIANGLE_STRIP);
+ }
+
+ SUBCASE("Returns correct format for the mesh") {
+ auto format = RS::ARRAY_FORMAT_BLEND_SHAPE_MASK | RS::ARRAY_FORMAT_TEX_UV | RS::ARRAY_FORMAT_INDEX;
+ CHECK((mesh->surface_get_format(0) & format) != 0);
+ CHECK((mesh->surface_get_format(1) & format) != 0);
+ }
+
+ SUBCASE("Set a surface name and retrieve it by name.") {
+ mesh->surface_set_name(0, "surf1");
+ CHECK(mesh->surface_find_by_name("surf1") == 0);
+ CHECK(mesh->surface_get_name(0) == "surf1");
+ }
+
+ SUBCASE("Set material to two different surfaces.") {
+ Ref<Material> mat = memnew(Material);
+ mesh->surface_set_material(0, mat);
+ CHECK(mesh->surface_get_material(0) == mat);
+ mesh->surface_set_material(1, mat);
+ CHECK(mesh->surface_get_material(1) == mat);
+ }
+
+ SUBCASE("Set same material multiple times doesn't change material of surface.") {
+ Ref<Material> mat = memnew(Material);
+ mesh->surface_set_material(0, mat);
+ mesh->surface_set_material(0, mat);
+ mesh->surface_set_material(0, mat);
+ CHECK(mesh->surface_get_material(0) == mat);
+ }
+
+ SUBCASE("Set material of surface then change to different material.") {
+ Ref<Material> mat1 = memnew(Material);
+ Ref<Material> mat2 = memnew(Material);
+ mesh->surface_set_material(1, mat1);
+ CHECK(mesh->surface_get_material(1) == mat1);
+ mesh->surface_set_material(1, mat2);
+ CHECK(mesh->surface_get_material(1) == mat2);
+ }
+
+ SUBCASE("Get the LOD of the mesh.") {
+ Dictionary lod{};
+ mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, cylinder_array, TypedArray<Array>{}, lod);
+ CHECK(mesh->surface_get_lods(2) == lod);
+ }
+
+ SUBCASE("Get the blend shape arrays from the mesh.") {
+ TypedArray<Array> blend{};
+ mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, cylinder_array, blend);
+ CHECK(mesh->surface_get_blend_shape_arrays(2) == blend);
+ }
+}
+
+TEST_CASE("[SceneTree][ArrayMesh] Get/Set mesh metadata and actions") {
+ Ref<ArrayMesh> mesh = memnew(ArrayMesh);
+ Ref<CylinderMesh> cylinder = memnew(CylinderMesh);
+ Array cylinder_array{};
+ cylinder_array.resize(Mesh::ARRAY_MAX);
+ cylinder->create_mesh_array(cylinder_array, 3.f, 3.f, 5.f);
+ mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, cylinder_array);
+
+ Ref<BoxMesh> box = memnew(BoxMesh);
+ Array box_array{};
+ box_array.resize(Mesh::ARRAY_MAX);
+ box->create_mesh_array(box_array, Vector3(2.f, 1.2f, 1.6f));
+ mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, box_array);
+
+ SUBCASE("Set the shadow mesh.") {
+ Ref<ArrayMesh> shadow = memnew(ArrayMesh);
+ mesh->set_shadow_mesh(shadow);
+ CHECK(mesh->get_shadow_mesh() == shadow);
+ }
+
+ SUBCASE("Set the shadow mesh multiple times.") {
+ Ref<ArrayMesh> shadow = memnew(ArrayMesh);
+ mesh->set_shadow_mesh(shadow);
+ mesh->set_shadow_mesh(shadow);
+ mesh->set_shadow_mesh(shadow);
+ mesh->set_shadow_mesh(shadow);
+ CHECK(mesh->get_shadow_mesh() == shadow);
+ }
+
+ SUBCASE("Set the same shadow mesh on multiple meshes.") {
+ Ref<ArrayMesh> shadow = memnew(ArrayMesh);
+ Ref<ArrayMesh> mesh2 = memnew(ArrayMesh);
+ mesh->set_shadow_mesh(shadow);
+ mesh2->set_shadow_mesh(shadow);
+
+ CHECK(mesh->get_shadow_mesh() == shadow);
+ CHECK(mesh2->get_shadow_mesh() == shadow);
+ }
+
+ SUBCASE("Set the shadow mesh and then change it.") {
+ Ref<ArrayMesh> shadow = memnew(ArrayMesh);
+ mesh->set_shadow_mesh(shadow);
+ CHECK(mesh->get_shadow_mesh() == shadow);
+ Ref<ArrayMesh> shadow2 = memnew(ArrayMesh);
+ mesh->set_shadow_mesh(shadow2);
+ CHECK(mesh->get_shadow_mesh() == shadow2);
+ }
+
+ SUBCASE("Set custom AABB.") {
+ AABB bound{};
+ mesh->set_custom_aabb(bound);
+ CHECK(mesh->get_custom_aabb() == bound);
+ }
+
+ SUBCASE("Set custom AABB multiple times.") {
+ AABB bound{};
+ mesh->set_custom_aabb(bound);
+ mesh->set_custom_aabb(bound);
+ mesh->set_custom_aabb(bound);
+ mesh->set_custom_aabb(bound);
+ CHECK(mesh->get_custom_aabb() == bound);
+ }
+
+ SUBCASE("Set custom AABB then change to another AABB.") {
+ AABB bound{};
+ AABB bound2{};
+ mesh->set_custom_aabb(bound);
+ CHECK(mesh->get_custom_aabb() == bound);
+ mesh->set_custom_aabb(bound2);
+ CHECK(mesh->get_custom_aabb() == bound2);
+ }
+
+ SUBCASE("Clear all surfaces should leave zero count.") {
+ mesh->clear_surfaces();
+ CHECK(mesh->get_surface_count() == 0);
+ }
+
+ SUBCASE("Able to get correct mesh RID.") {
+ RID rid = mesh->get_rid();
+ CHECK(RS::get_singleton()->mesh_get_surface_count(rid) == 2);
+ }
+
+ SUBCASE("Create surface from raw SurfaceData data.") {
+ RID mesh_rid = mesh->get_rid();
+ RS::SurfaceData surface_data = RS::get_singleton()->mesh_get_surface(mesh_rid, 0);
+ Ref<ArrayMesh> mesh2 = memnew(ArrayMesh);
+ mesh2->add_surface(surface_data.format, Mesh::PRIMITIVE_TRIANGLES, surface_data.vertex_data, surface_data.attribute_data,
+ surface_data.skin_data, surface_data.vertex_count, surface_data.index_data, surface_data.index_count, surface_data.aabb);
+ CHECK(mesh2->get_surface_count() == 1);
+ CHECK(mesh2->surface_get_primitive_type(0) == Mesh::PRIMITIVE_TRIANGLES);
+ CHECK((mesh2->surface_get_format(0) & surface_data.format) != 0);
+ CHECK(mesh2->get_aabb().is_equal_approx(surface_data.aabb));
+ }
+}
+
+} // namespace TestArrayMesh
+
+#endif // TEST_ARRAYMESH_H
diff --git a/tests/scene/test_audio_stream_wav.h b/tests/scene/test_audio_stream_wav.h
new file mode 100644
index 0000000000..4ba431dfc2
--- /dev/null
+++ b/tests/scene/test_audio_stream_wav.h
@@ -0,0 +1,243 @@
+/*************************************************************************/
+/* test_audio_stream_wav.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_AUDIO_STREAM_WAV_H
+#define TEST_AUDIO_STREAM_WAV_H
+
+#include "core/math/math_defs.h"
+#include "core/math/math_funcs.h"
+#include "scene/resources/audio_stream_wav.h"
+
+#include "tests/test_macros.h"
+
+#ifdef TOOLS_ENABLED
+#include "core/io/resource_loader.h"
+#include "editor/import/resource_importer_wav.h"
+#endif
+
+namespace TestAudioStreamWAV {
+
+// Default wav rate for test cases.
+constexpr float WAV_RATE = 44100;
+/* Default wav count for test cases. 1 second of audio is used so that the file can be listened
+to manually if needed. */
+constexpr int WAV_COUNT = WAV_RATE;
+
+float gen_wav(float frequency, float wav_rate, int wav_number) {
+ // formula for generating a sin wave with given frequency.
+ return Math::sin((Math_TAU * frequency / wav_rate) * wav_number);
+}
+
+/* Generates a 440Hz sin wave in channel 0 (mono channel or left stereo channel)
+ * and a 261.63Hz wave in channel 1 (right stereo channel).
+ * These waves correspond to the music notes A4 and C4 respectively.
+ */
+Vector<uint8_t> gen_pcm8_test(float wav_rate, int wav_count, bool stereo) {
+ Vector<uint8_t> buffer;
+ buffer.resize(stereo ? wav_count * 2 : wav_count);
+
+ uint8_t *write_ptr = buffer.ptrw();
+ for (int i = 0; i < buffer.size(); i++) {
+ float wav;
+ if (stereo) {
+ if (i % 2 == 0) {
+ wav = gen_wav(440, wav_rate, i / 2);
+ } else {
+ wav = gen_wav(261.63, wav_rate, i / 2);
+ }
+ } else {
+ wav = gen_wav(440, wav_rate, i);
+ }
+
+ // Map sin wave to full range of 8-bit values.
+ uint8_t wav_8bit = Math::fast_ftoi(((wav + 1) / 2) * UINT8_MAX);
+ // Unlike the .wav format, AudioStreamWAV expects signed 8-bit wavs.
+ uint8_t wav_8bit_signed = wav_8bit - (INT8_MAX + 1);
+ write_ptr[i] = wav_8bit_signed;
+ }
+
+ return buffer;
+}
+
+// Same as gen_pcm8_test but with 16-bit wavs.
+Vector<uint8_t> gen_pcm16_test(float wav_rate, int wav_count, bool stereo) {
+ Vector<uint8_t> buffer;
+ buffer.resize(stereo ? wav_count * 4 : wav_count * 2);
+
+ uint8_t *write_ptr = buffer.ptrw();
+ for (int i = 0; i < buffer.size() / 2; i++) {
+ float wav;
+ if (stereo) {
+ if (i % 2 == 0) {
+ wav = gen_wav(440, wav_rate, i / 2);
+ } else {
+ wav = gen_wav(261.63, wav_rate, i / 2);
+ }
+ } else {
+ wav = gen_wav(440, wav_rate, i);
+ }
+
+ // Map sin wave to full range of 16-bit values.
+ uint16_t wav_16bit = Math::fast_ftoi(((wav + 1) / 2) * UINT16_MAX);
+ // The .wav format expects wavs larger than 8 bits to be signed.
+ uint16_t wav_16bit_signed = wav_16bit - (INT16_MAX + 1);
+ encode_uint16(wav_16bit_signed, write_ptr + (i * 2));
+ }
+
+ return buffer;
+}
+
+void run_test(String file_name, AudioStreamWAV::Format data_format, bool stereo, float wav_rate, float wav_count) {
+ String save_path = OS::get_singleton()->get_cache_path().path_join(file_name);
+
+ Vector<uint8_t> test_data;
+ if (data_format == AudioStreamWAV::FORMAT_8_BITS) {
+ test_data = gen_pcm8_test(wav_rate, wav_count, stereo);
+ } else {
+ test_data = gen_pcm16_test(wav_rate, wav_count, stereo);
+ }
+
+ Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV);
+ stream->set_mix_rate(wav_rate);
+ CHECK(stream->get_mix_rate() == wav_rate);
+
+ stream->set_format(data_format);
+ CHECK(stream->get_format() == data_format);
+
+ stream->set_stereo(stereo);
+ CHECK(stream->is_stereo() == stereo);
+
+ stream->set_data(test_data);
+ CHECK(stream->get_data() == test_data);
+
+ SUBCASE("Stream length is computed properly") {
+ CHECK(Math::is_equal_approx(stream->get_length(), double(wav_count / wav_rate)));
+ }
+
+ SUBCASE("Stream can be saved as .wav") {
+ REQUIRE(stream->save_to_wav(save_path) == OK);
+
+ Error error;
+ Ref<FileAccess> wav_file = FileAccess::open(save_path, FileAccess::READ, &error);
+ REQUIRE(error == OK);
+
+#if TOOLS_ENABLED
+ // The WAV importer can be used if enabled to check that the saved file is valid.
+ Ref<ResourceImporterWAV> wav_importer = memnew(ResourceImporterWAV);
+
+ List<ResourceImporter::ImportOption> options_list;
+ wav_importer->get_import_options("", &options_list);
+
+ HashMap<StringName, Variant> options_map;
+ for (const ResourceImporter::ImportOption &E : options_list) {
+ options_map[E.option.name] = E.default_value;
+ }
+
+ REQUIRE(wav_importer->import(save_path, save_path, options_map, nullptr) == OK);
+
+ String load_path = save_path + "." + wav_importer->get_save_extension();
+ Ref<AudioStreamWAV> loaded_stream = ResourceLoader::load(load_path, "AudioStreamWAV", ResourceFormatImporter::CACHE_MODE_IGNORE, &error);
+ REQUIRE(error == OK);
+
+ CHECK(loaded_stream->get_format() == stream->get_format());
+ CHECK(loaded_stream->get_loop_mode() == stream->get_loop_mode());
+ CHECK(loaded_stream->get_loop_begin() == stream->get_loop_begin());
+ CHECK(loaded_stream->get_loop_end() == stream->get_loop_end());
+ CHECK(loaded_stream->get_mix_rate() == stream->get_mix_rate());
+ CHECK(loaded_stream->is_stereo() == stream->is_stereo());
+ CHECK(loaded_stream->get_length() == stream->get_length());
+ CHECK(loaded_stream->is_monophonic() == stream->is_monophonic());
+ CHECK(loaded_stream->get_data() == stream->get_data());
+#endif
+ }
+}
+
+TEST_CASE("[AudioStreamWAV] Mono PCM8 format") {
+ run_test("test_pcm8_mono.wav", AudioStreamWAV::FORMAT_8_BITS, false, WAV_RATE, WAV_COUNT);
+}
+
+TEST_CASE("[AudioStreamWAV] Mono PCM16 format") {
+ run_test("test_pcm16_mono.wav", AudioStreamWAV::FORMAT_16_BITS, false, WAV_RATE, WAV_COUNT);
+}
+
+TEST_CASE("[AudioStreamWAV] Stereo PCM8 format") {
+ run_test("test_pcm8_stereo.wav", AudioStreamWAV::FORMAT_8_BITS, true, WAV_RATE, WAV_COUNT);
+}
+
+TEST_CASE("[AudioStreamWAV] Stereo PCM16 format") {
+ run_test("test_pcm16_stereo.wav", AudioStreamWAV::FORMAT_16_BITS, true, WAV_RATE, WAV_COUNT);
+}
+
+TEST_CASE("[AudioStreamWAV] Alternate mix rate") {
+ run_test("test_pcm16_stereo_38000Hz.wav", AudioStreamWAV::FORMAT_16_BITS, true, 38000, 38000);
+}
+
+TEST_CASE("[AudioStreamWAV] save_to_wav() adds '.wav' file extension automatically") {
+ String save_path = OS::get_singleton()->get_cache_path().path_join("test_wav_extension");
+ Vector<uint8_t> test_data = gen_pcm8_test(WAV_RATE, WAV_COUNT, false);
+ Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV);
+ stream->set_data(test_data);
+
+ REQUIRE(stream->save_to_wav(save_path) == OK);
+ Error error;
+ Ref<FileAccess> wav_file = FileAccess::open(save_path + ".wav", FileAccess::READ, &error);
+ CHECK(error == OK);
+}
+
+TEST_CASE("[AudioStreamWAV] Default values") {
+ Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV);
+ CHECK(stream->get_format() == AudioStreamWAV::FORMAT_8_BITS);
+ CHECK(stream->get_loop_mode() == AudioStreamWAV::LOOP_DISABLED);
+ CHECK(stream->get_loop_begin() == 0);
+ CHECK(stream->get_loop_end() == 0);
+ CHECK(stream->get_mix_rate() == 44100);
+ CHECK(stream->is_stereo() == false);
+ CHECK(stream->get_length() == 0);
+ CHECK(stream->is_monophonic() == false);
+ CHECK(stream->get_data() == Vector<uint8_t>{});
+ CHECK(stream->get_stream_name() == "");
+}
+
+TEST_CASE("[AudioStreamWAV] Save empty file") {
+ run_test("test_empty.wav", AudioStreamWAV::FORMAT_8_BITS, false, WAV_RATE, 0);
+}
+
+TEST_CASE("[AudioStreamWAV] Saving IMA ADPCM is not supported") {
+ String save_path = OS::get_singleton()->get_cache_path().path_join("test_adpcm.wav");
+ Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV);
+ stream->set_format(AudioStreamWAV::FORMAT_IMA_ADPCM);
+ ERR_PRINT_OFF;
+ CHECK(stream->save_to_wav(save_path) == ERR_UNAVAILABLE);
+ ERR_PRINT_ON;
+}
+
+} // namespace TestAudioStreamWAV
+
+#endif // TEST_AUDIO_STREAM_WAV_H
diff --git a/tests/scene/test_bit_map.h b/tests/scene/test_bit_map.h
new file mode 100644
index 0000000000..a102f40725
--- /dev/null
+++ b/tests/scene/test_bit_map.h
@@ -0,0 +1,441 @@
+/*************************************************************************/
+/* test_bit_map.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_BIT_MAP_H
+#define TEST_BIT_MAP_H
+
+#include "core/os/memory.h"
+#include "scene/resources/bit_map.h"
+#include "tests/test_macros.h"
+
+namespace TestBitmap {
+
+void reset_bit_map(BitMap &p_bm) {
+ Size2i size = p_bm.get_size();
+ p_bm.set_bit_rect(Rect2i(0, 0, size.width, size.height), false);
+}
+
+TEST_CASE("[BitMap] Create bit map") {
+ Size2i dim{ 256, 512 };
+ BitMap bit_map{};
+ bit_map.create(dim);
+ CHECK(bit_map.get_size() == Size2i(256, 512));
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "This will go through the entire bitmask inside of bitmap, thus hopefully checking if the bitmask was correctly set up.");
+
+ dim = Size2i(0, 256);
+ bit_map.create(dim);
+ CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 512), "We should still have the same dimensions as before, because the new dimension is invalid.");
+
+ dim = Size2i(512, 0);
+ bit_map.create(dim);
+ CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 512), "We should still have the same dimensions as before, because the new dimension is invalid.");
+
+ dim = Size2i(46341, 46341);
+ bit_map.create(dim);
+ CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 512), "We should still have the same dimensions as before, because the new dimension is too large (46341*46341=2147488281).");
+}
+
+TEST_CASE("[BitMap] Create bit map from image alpha") {
+ const Size2i dim{ 256, 256 };
+ BitMap bit_map{};
+ bit_map.create(dim);
+
+ const Ref<Image> null_img = nullptr;
+ bit_map.create_from_image_alpha(null_img);
+ CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 256), "Bitmap should have its old values because bitmap creation from a nullptr should fail.");
+
+ Ref<Image> empty_img;
+ empty_img.instantiate();
+ bit_map.create_from_image_alpha(empty_img);
+ CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 256), "Bitmap should have its old values because bitmap creation from an empty image should fail.");
+
+ Ref<Image> wrong_format_img = Image::create_empty(3, 3, false, Image::Format::FORMAT_DXT1);
+ bit_map.create_from_image_alpha(wrong_format_img);
+ CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 256), "Bitmap should have its old values because converting from a compressed image should fail.");
+
+ Ref<Image> img = Image::create_empty(3, 3, false, Image::Format::FORMAT_RGBA8);
+ img->set_pixel(0, 0, Color(0, 0, 0, 0));
+ img->set_pixel(0, 1, Color(0, 0, 0, 0.09f));
+ img->set_pixel(0, 2, Color(0, 0, 0, 0.25f));
+ img->set_pixel(1, 0, Color(0, 0, 0, 0.5f));
+ img->set_pixel(1, 1, Color(0, 0, 0, 0.75f));
+ img->set_pixel(1, 2, Color(0, 0, 0, 0.99f));
+ img->set_pixel(2, 0, Color(0, 0, 0, 1.f));
+
+ // Check different threshold values.
+ bit_map.create_from_image_alpha(img);
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 5, "There are 5 values in the image that are smaller than the default threshold of 0.1.");
+
+ bit_map.create_from_image_alpha(img, 0.08f);
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 6, "There are 6 values in the image that are smaller than the threshold of 0.08.");
+
+ bit_map.create_from_image_alpha(img, 1);
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "There are no values in the image that are smaller than the threshold of 1, there is one value equal to 1, but we check for inequality only.");
+}
+
+TEST_CASE("[BitMap] Set bit") {
+ Size2i dim{ 256, 256 };
+ BitMap bit_map{};
+
+ // Setting a point before a bit map is created should not crash, because there are checks to see if we are out of bounds.
+ bit_map.set_bitv(Point2i(128, 128), true);
+
+ bit_map.create(dim);
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "All values should be initialized to false.");
+ bit_map.set_bitv(Point2i(128, 128), true);
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 1, "One bit should be set to true.");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 128)) == true, "The bit at (128,128) should be set to true");
+
+ bit_map.set_bitv(Point2i(128, 128), false);
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "The bit should now be set to false again");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 128)) == false, "The bit at (128,128) should now be set to false again");
+
+ bit_map.create(dim);
+ bit_map.set_bitv(Point2i(512, 512), true);
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "Nothing should change as we were trying to edit a bit outside of the correct range.");
+}
+
+TEST_CASE("[BitMap] Get bit") {
+ const Size2i dim{ 256, 256 };
+ BitMap bit_map{};
+
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 128)) == false, "Trying to access a bit outside of the BitMap's range should always return false");
+
+ bit_map.create(dim);
+ CHECK(bit_map.get_bitv(Point2i(128, 128)) == false);
+
+ bit_map.set_bit_rect(Rect2i(-1, -1, 257, 257), true);
+
+ // Checking that range is [0, 256).
+ CHECK(bit_map.get_bitv(Point2i(-1, 0)) == false);
+ CHECK(bit_map.get_bitv(Point2i(0, 0)) == true);
+ CHECK(bit_map.get_bitv(Point2i(128, 128)) == true);
+ CHECK(bit_map.get_bitv(Point2i(255, 255)) == true);
+ CHECK(bit_map.get_bitv(Point2i(256, 256)) == false);
+ CHECK(bit_map.get_bitv(Point2i(257, 257)) == false);
+}
+
+TEST_CASE("[BitMap] Set bit rect") {
+ const Size2i dim{ 256, 256 };
+ BitMap bit_map{};
+
+ // Although we have not setup the BitMap yet, this should not crash because we get an empty intersection inside of the method.
+ bit_map.set_bit_rect(Rect2i{ 0, 0, 128, 128 }, true);
+
+ bit_map.create(dim);
+ CHECK(bit_map.get_true_bit_count() == 0);
+
+ bit_map.set_bit_rect(Rect2i{ 0, 0, 256, 256 }, true);
+ CHECK(bit_map.get_true_bit_count() == 65536);
+
+ reset_bit_map(bit_map);
+
+ // Checking out of bounds handling.
+ bit_map.set_bit_rect(Rect2i{ 128, 128, 256, 256 }, true);
+ CHECK(bit_map.get_true_bit_count() == 16384);
+
+ reset_bit_map(bit_map);
+
+ bit_map.set_bit_rect(Rect2i{ -128, -128, 256, 256 }, true);
+ CHECK(bit_map.get_true_bit_count() == 16384);
+
+ reset_bit_map(bit_map);
+
+ bit_map.set_bit_rect(Rect2i{ -128, -128, 512, 512 }, true);
+ CHECK(bit_map.get_true_bit_count() == 65536);
+}
+
+TEST_CASE("[BitMap] Get true bit count") {
+ const Size2i dim{ 256, 256 };
+ BitMap bit_map{};
+
+ CHECK(bit_map.get_true_bit_count() == 0);
+
+ bit_map.create(dim);
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "Uninitialized bit map should have no true bits");
+ bit_map.set_bit_rect(Rect2i{ 0, 0, 256, 256 }, true);
+ CHECK(bit_map.get_true_bit_count() == 65536);
+ bit_map.set_bitv(Point2i{ 0, 0 }, false);
+ CHECK(bit_map.get_true_bit_count() == 65535);
+ bit_map.set_bit_rect(Rect2i{ 0, 0, 256, 256 }, false);
+ CHECK(bit_map.get_true_bit_count() == 0);
+}
+
+TEST_CASE("[BitMap] Get size") {
+ const Size2i dim{ 256, 256 };
+ BitMap bit_map{};
+
+ CHECK_MESSAGE(bit_map.get_size() == Size2i(0, 0), "Uninitialized bit map should have a size of 0x0");
+
+ bit_map.create(dim);
+ CHECK(bit_map.get_size() == Size2i(256, 256));
+
+ bit_map.create(Size2i(-1, 0));
+ CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 256), "Invalid size should not be accepted by create");
+
+ bit_map.create(Size2i(256, 128));
+ CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 128), "Bitmap should have updated size");
+}
+
+TEST_CASE("[BitMap] Resize") {
+ const Size2i dim{ 128, 128 };
+ BitMap bit_map{};
+
+ bit_map.resize(dim);
+ CHECK(bit_map.get_size() == dim);
+
+ bit_map.create(dim);
+ bit_map.set_bit_rect(Rect2i(0, 0, 10, 10), true);
+ bit_map.set_bit_rect(Rect2i(118, 118, 10, 10), true);
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 200, "There should be 100 bits in the top left corner, and 100 bits in the bottom right corner");
+ bit_map.resize(Size2i(64, 64));
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 50, "There should be 25 bits in the top left corner, and 25 bits in the bottom right corner");
+
+ bit_map.create(dim);
+ bit_map.resize(Size2i(-1, 128));
+ CHECK_MESSAGE(bit_map.get_size() == Size2i(128, 128), "When an invalid size is given the bit map will keep its size");
+
+ bit_map.create(dim);
+ bit_map.set_bit_rect(Rect2i(0, 0, 10, 10), true);
+ bit_map.set_bit_rect(Rect2i(118, 118, 10, 10), true);
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 200, "There should be 100 bits in the top left corner, and 100 bits in the bottom right corner");
+ bit_map.resize(Size2i(256, 256));
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 800, "There should still be 100 bits in the bottom right corner, and all new bits should be initialized to false");
+ CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 256), "The bitmap should now be 256x256");
+}
+
+TEST_CASE("[BitMap] Grow and shrink mask") {
+ const Size2i dim{ 256, 256 };
+ BitMap bit_map{};
+ bit_map.grow_mask(100, Rect2i(0, 0, 128, 128)); // Check if method does not crash when working with an uninitialised bit map.
+ CHECK_MESSAGE(bit_map.get_size() == Size2i(0, 0), "Size should still be equal to 0x0");
+
+ bit_map.create(dim);
+
+ bit_map.set_bit_rect(Rect2i(96, 96, 64, 64), true);
+
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 4096, "Creating a square of 64x64 should be 4096 bits");
+ bit_map.grow_mask(0, Rect2i(0, 0, 256, 256));
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 4096, "Growing with size of 0 should not change any bits");
+
+ reset_bit_map(bit_map);
+
+ bit_map.set_bit_rect(Rect2i(96, 96, 64, 64), true);
+
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(95, 128)) == false, "Bits just outside of the square should not be set");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(160, 128)) == false, "Bits just outside of the square should not be set");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 95)) == false, "Bits just outside of the square should not be set");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 160)) == false, "Bits just outside of the square should not be set");
+ bit_map.grow_mask(1, Rect2i(0, 0, 256, 256));
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 4352, "We should have 4*64 (perimeter of square) more bits set to true");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(95, 128)) == true, "Bits that were just outside of the square should now be set to true");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(160, 128)) == true, "Bits that were just outside of the square should now be set to true");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 95)) == true, "Bits that were just outside of the square should now be set to true");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 160)) == true, "Bits that were just outside of the square should now be set to true");
+
+ reset_bit_map(bit_map);
+
+ bit_map.set_bit_rect(Rect2i(127, 127, 1, 1), true);
+
+ CHECK(bit_map.get_true_bit_count() == 1);
+ bit_map.grow_mask(32, Rect2i(0, 0, 256, 256));
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 3209, "Creates a circle around the initial bit with a radius of 32 bits. Any bit that has a distance within this radius will be set to true");
+
+ reset_bit_map(bit_map);
+
+ bit_map.set_bit_rect(Rect2i(127, 127, 1, 1), true);
+ for (int i = 0; i < 32; i++) {
+ bit_map.grow_mask(1, Rect2i(0, 0, 256, 256));
+ }
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 2113, "Creates a diamond around the initial bit with diagonals that are 65 bits long.");
+
+ reset_bit_map(bit_map);
+
+ bit_map.set_bit_rect(Rect2i(123, 123, 10, 10), true);
+
+ CHECK(bit_map.get_true_bit_count() == 100);
+ bit_map.grow_mask(-11, Rect2i(0, 0, 256, 256));
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "Shrinking by more than the width of the square should totally remove it.");
+
+ reset_bit_map(bit_map);
+ bit_map.set_bit_rect(Rect2i(96, 96, 64, 64), true);
+
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(96, 129)) == true, "Bits on the edge of the square should be true");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(159, 129)) == true, "Bits on the edge of the square should be true");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(129, 96)) == true, "Bits on the edge of the square should be true");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(129, 159)) == true, "Bits on the edge of the square should be true");
+ bit_map.grow_mask(-1, Rect2i(0, 0, 256, 256));
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 3844, "Shrinking by 1 should set 4*63=252 bits to false");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(96, 129)) == false, "Bits that were on the edge of the square should now be set to false");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(159, 129)) == false, "Bits that were on the edge of the square should now be set to false");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(129, 96)) == false, "Bits that were on the edge of the square should now be set to false");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(129, 159)) == false, "Bits that were on the edge of the square should now be set to false");
+
+ reset_bit_map(bit_map);
+
+ bit_map.set_bit_rect(Rect2i(125, 125, 1, 6), true);
+ bit_map.set_bit_rect(Rect2i(130, 125, 1, 6), true);
+ bit_map.set_bit_rect(Rect2i(125, 130, 6, 1), true);
+
+ CHECK(bit_map.get_true_bit_count() == 16);
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(125, 131)) == false, "Bits that are on the edge of the shape should be set to false");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(131, 131)) == false, "Bits that are on the edge of the shape should be set to false");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(125, 124)) == false, "Bits that are on the edge of the shape should be set to false");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(130, 124)) == false, "Bits that are on the edge of the shape should be set to false");
+ bit_map.grow_mask(1, Rect2i(0, 0, 256, 256));
+ CHECK(bit_map.get_true_bit_count() == 48);
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(125, 131)) == true, "Bits that were on the edge of the shape should now be set to true");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(131, 130)) == true, "Bits that were on the edge of the shape should now be set to true");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(125, 124)) == true, "Bits that were on the edge of the shape should now be set to true");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(130, 124)) == true, "Bits that were on the edge of the shape should now be set to true");
+
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(124, 124)) == false, "Bits that are on the edge of the shape should be set to false");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(126, 124)) == false, "Bits that are on the edge of the shape should be set to false");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(124, 131)) == false, "Bits that are on the edge of the shape should be set to false");
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(131, 131)) == false, "Bits that are on the edge of the shape should be set to false");
+}
+
+TEST_CASE("[BitMap] Blit") {
+ Point2i blit_pos{ 128, 128 };
+ Point2i bit_map_size{ 256, 256 };
+ Point2i blit_size{ 32, 32 };
+
+ BitMap bit_map{};
+ Ref<BitMap> blit_bit_map{};
+
+ // Testing null reference to blit bit map.
+ bit_map.blit(blit_pos, blit_bit_map);
+
+ blit_bit_map.instantiate();
+
+ // Testing if uninitialised blit bit map and uninitialised bit map does not crash
+ bit_map.blit(blit_pos, blit_bit_map);
+
+ // Testing if uninitialised bit map does not crash
+ blit_bit_map->create(blit_size);
+ bit_map.blit(blit_pos, blit_bit_map);
+
+ // Testing if uninitialised bit map does not crash
+ blit_bit_map.unref();
+ blit_bit_map.instantiate();
+ CHECK_MESSAGE(blit_bit_map->get_size() == Point2i(0, 0), "Size should be cleared by unref and instance calls.");
+ bit_map.create(bit_map_size);
+ bit_map.blit(Point2i(128, 128), blit_bit_map);
+
+ // Testing if both initialised does not crash.
+ blit_bit_map->create(blit_size);
+ bit_map.blit(blit_pos, blit_bit_map);
+
+ bit_map.set_bit_rect(Rect2i{ 127, 127, 3, 3 }, true);
+ CHECK(bit_map.get_true_bit_count() == 9);
+ bit_map.blit(Point2i(112, 112), blit_bit_map);
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 9, "No bits should have been changed, as the blit bit map only contains falses");
+
+ bit_map.create(bit_map_size);
+ blit_bit_map->create(blit_size);
+ blit_bit_map->set_bit_rect(Rect2i(15, 15, 3, 3), true);
+ CHECK(blit_bit_map->get_true_bit_count() == 9);
+
+ CHECK(bit_map.get_true_bit_count() == 0);
+ bit_map.blit(Point2i(112, 112), blit_bit_map);
+ CHECK_MESSAGE(bit_map.get_true_bit_count() == 9, "All true bits should have been moved to the bit map");
+ for (int x = 127; x < 129; ++x) {
+ for (int y = 127; y < 129; ++y) {
+ CHECK_MESSAGE(bit_map.get_bitv(Point2i(x, y)) == true, "All true bits should have been moved to the bit map");
+ }
+ }
+}
+
+TEST_CASE("[BitMap] Convert to image") {
+ const Size2i dim{ 256, 256 };
+ BitMap bit_map{};
+ Ref<Image> img;
+
+ img = bit_map.convert_to_image();
+ CHECK_MESSAGE(img.is_valid(), "We should receive a valid Image Object even if BitMap is not created yet");
+ CHECK_MESSAGE(img->get_format() == Image::FORMAT_L8, "We should receive a valid Image Object even if BitMap is not created yet");
+ CHECK_MESSAGE(img->get_size() == (Size2i(0, 0)), "Image should have no width or height, because BitMap has not yet been created");
+
+ bit_map.create(dim);
+ img = bit_map.convert_to_image();
+ CHECK_MESSAGE(img->get_size() == dim, "Image should have the same dimensions as the BitMap");
+ CHECK_MESSAGE(img->get_pixel(0, 0).is_equal_approx(Color(0, 0, 0)), "BitMap is initialized to all 0's, so Image should be all black");
+
+ reset_bit_map(bit_map);
+ bit_map.set_bit_rect(Rect2i(0, 0, 128, 128), true);
+ img = bit_map.convert_to_image();
+ CHECK_MESSAGE(img->get_pixel(0, 0).is_equal_approx(Color(1, 1, 1)), "BitMap's top-left quadrant is all 1's, so Image should be white");
+ CHECK_MESSAGE(img->get_pixel(256, 256).is_equal_approx(Color(0, 0, 0)), "All other quadrants were 0's, so these should be black");
+}
+
+TEST_CASE("[BitMap] Clip to polygon") {
+ const Size2i dim{ 256, 256 };
+ BitMap bit_map{};
+ Vector<Vector<Vector2>> polygons;
+
+ polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 128, 128));
+ CHECK_MESSAGE(polygons.size() == 0, "We should have no polygons, because the BitMap was not initialized");
+
+ bit_map.create(dim);
+ polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 128, 128));
+ CHECK_MESSAGE(polygons.size() == 0, "We should have no polygons, because the BitMap was all 0's");
+
+ reset_bit_map(bit_map);
+ bit_map.set_bit_rect(Rect2i(0, 0, 64, 64), true);
+ polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 128, 128));
+ CHECK_MESSAGE(polygons.size() == 1, "We should have exactly 1 polygon");
+ CHECK_MESSAGE(polygons[0].size() == 4, "The polygon should have exactly 4 points");
+
+ reset_bit_map(bit_map);
+ bit_map.set_bit_rect(Rect2i(0, 0, 32, 32), true);
+ bit_map.set_bit_rect(Rect2i(64, 64, 32, 32), true);
+ polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 128, 128));
+ CHECK_MESSAGE(polygons.size() == 2, "We should have exactly 2 polygons");
+ CHECK_MESSAGE(polygons[0].size() == 4, "The polygon should have exactly 4 points");
+ CHECK_MESSAGE(polygons[1].size() == 4, "The polygon should have exactly 4 points");
+
+ reset_bit_map(bit_map);
+ bit_map.set_bit_rect(Rect2i(124, 112, 8, 32), true);
+ bit_map.set_bit_rect(Rect2i(112, 124, 32, 8), true);
+ polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 256, 256));
+ CHECK_MESSAGE(polygons.size() == 1, "We should have exactly 1 polygon");
+ CHECK_MESSAGE(polygons[0].size() == 12, "The polygon should have exactly 12 points");
+
+ reset_bit_map(bit_map);
+ bit_map.set_bit_rect(Rect2i(124, 112, 8, 32), true);
+ bit_map.set_bit_rect(Rect2i(112, 124, 32, 8), true);
+ polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 128, 128));
+ CHECK_MESSAGE(polygons.size() == 1, "We should have exactly 1 polygon");
+ CHECK_MESSAGE(polygons[0].size() == 6, "The polygon should have exactly 6 points");
+}
+
+} // namespace TestBitmap
+
+#endif // TEST_BIT_MAP_H
diff --git a/tests/scene/test_code_edit.h b/tests/scene/test_code_edit.h
new file mode 100644
index 0000000000..3940bdb37a
--- /dev/null
+++ b/tests/scene/test_code_edit.h
@@ -0,0 +1,3393 @@
+/*************************************************************************/
+/* test_code_edit.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_CODE_EDIT_H
+#define TEST_CODE_EDIT_H
+
+#include "scene/gui/code_edit.h"
+
+#include "tests/test_macros.h"
+
+namespace TestCodeEdit {
+
+TEST_CASE("[SceneTree][CodeEdit] line gutters") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+ code_edit->grab_focus();
+
+ SUBCASE("[CodeEdit] breakpoints") {
+ SIGNAL_WATCH(code_edit, "breakpoint_toggled");
+
+ SUBCASE("[CodeEdit] draw breakpoints gutter") {
+ code_edit->set_draw_breakpoints_gutter(false);
+ CHECK_FALSE(code_edit->is_drawing_breakpoints_gutter());
+
+ code_edit->set_draw_breakpoints_gutter(true);
+ CHECK(code_edit->is_drawing_breakpoints_gutter());
+ }
+
+ SUBCASE("[CodeEdit] set line as breakpoint") {
+ /* Out of bounds. */
+ ERR_PRINT_OFF;
+
+ code_edit->set_line_as_breakpoint(-1, true);
+ CHECK_FALSE(code_edit->is_line_breakpointed(-1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ ERR_PRINT_ON;
+
+ Array arg1;
+ arg1.push_back(0);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_line_as_breakpoint(0, true);
+ CHECK(code_edit->is_line_breakpointed(0));
+ CHECK(code_edit->get_breakpointed_lines()[0] == 0);
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->set_line_as_breakpoint(0, false);
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] clear breakpointed lines") {
+ code_edit->clear_breakpointed_lines();
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ Array arg1;
+ arg1.push_back(0);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_line_as_breakpoint(0, true);
+ CHECK(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->clear_breakpointed_lines();
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and set text") {
+ Array arg1;
+ arg1.push_back(0);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_breakpoint(0, true);
+ CHECK(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* breakpoint on lines that still exist are kept. */
+ code_edit->set_text("");
+ MessageQueue::get_singleton()->flush();
+ CHECK(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ /* breakpoint on lines that are removed should also be removed. */
+ code_edit->clear_breakpointed_lines();
+ SIGNAL_DISCARD("breakpoint_toggled")
+
+ ((Array)args[0])[0] = 1;
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->set_text("");
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ ERR_PRINT_ON;
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and clear") {
+ Array arg1;
+ arg1.push_back(0);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_breakpoint(0, true);
+ CHECK(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* breakpoint on lines that still exist are removed. */
+ code_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* breakpoint on lines that are removed should also be removed. */
+ code_edit->clear_breakpointed_lines();
+ SIGNAL_DISCARD("breakpoint_toggled")
+
+ ((Array)args[0])[0] = 1;
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ ERR_PRINT_ON;
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and new lines no text") {
+ Array arg1;
+ arg1.push_back(0);
+ Array args;
+ args.push_back(arg1);
+
+ /* No text moves breakpoint. */
+ code_edit->set_line_as_breakpoint(0, true);
+ CHECK(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Normal. */
+ ((Array)args[0])[0] = 0;
+ Array arg2;
+ arg2.push_back(1);
+ args.push_back(arg2);
+
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Non-Breaking. */
+ ((Array)args[0])[0] = 1;
+ ((Array)args[1])[0] = 2;
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line_count() == 3);
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ CHECK(code_edit->is_line_breakpointed(2));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Above. */
+ ((Array)args[0])[0] = 2;
+ ((Array)args[1])[0] = 3;
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line_count() == 4);
+ CHECK_FALSE(code_edit->is_line_breakpointed(2));
+ CHECK(code_edit->is_line_breakpointed(3));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and new lines with text") {
+ Array arg1;
+ arg1.push_back(0);
+ Array args;
+ args.push_back(arg1);
+
+ /* Having text does not move breakpoint. */
+ code_edit->insert_text_at_caret("text");
+ code_edit->set_line_as_breakpoint(0, true);
+ CHECK(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Normal. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK(code_edit->is_line_breakpointed(0));
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ /* Non-Breaking. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line_count() == 3);
+ CHECK(code_edit->is_line_breakpointed(0));
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ /* Above does move. */
+ ((Array)args[0])[0] = 0;
+ Array arg2;
+ arg2.push_back(1);
+ args.push_back(arg2);
+
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line_count() == 4);
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and backspace") {
+ Array arg1;
+ arg1.push_back(1);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->set_caret_line(2);
+
+ /* backspace onto line does not remove breakpoint */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ /* backspace on breakpointed line removes it */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ ERR_PRINT_ON;
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Backspace above breakpointed line moves it. */
+ ((Array)args[0])[0] = 2;
+
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_breakpoint(2, true);
+ CHECK(code_edit->is_line_breakpointed(2));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->set_caret_line(1);
+
+ Array arg2;
+ arg2.push_back(1);
+ args.push_back(arg2);
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(2));
+ ERR_PRINT_ON;
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and delete") {
+ Array arg1;
+ arg1.push_back(1);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ code_edit->set_caret_line(1);
+
+ /* Delete onto breakpointed lines does not remove it. */
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ /* Delete moving breakpointed line up removes it. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ CHECK(code_edit->get_line_count() == 1);
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ ERR_PRINT_ON;
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Delete above breakpointed line moves it. */
+ ((Array)args[0])[0] = 2;
+
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_breakpoint(2, true);
+ CHECK(code_edit->is_line_breakpointed(2));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->set_caret_line(0);
+
+ Array arg2;
+ arg2.push_back(1);
+ args.push_back(arg2);
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(2));
+ ERR_PRINT_ON;
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and delete selection") {
+ Array arg1;
+ arg1.push_back(1);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->select(0, 0, 2, 0);
+ code_edit->delete_selection();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Should handle breakpoint move when deleting selection by adding less text then removed. */
+ ((Array)args[0])[0] = 9;
+
+ code_edit->set_text("\n\n\n\n\n\n\n\n\n");
+ code_edit->set_line_as_breakpoint(9, true);
+ CHECK(code_edit->is_line_breakpointed(9));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->select(0, 0, 6, 0);
+
+ Array arg2;
+ arg2.push_back(4);
+ args.push_back(arg2);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(9));
+ ERR_PRINT_ON;
+ CHECK(code_edit->is_line_breakpointed(4));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Should handle breakpoint move when deleting selection by adding more text then removed. */
+ ((Array)args[0])[0] = 9;
+ ((Array)args[1])[0] = 14;
+
+ code_edit->insert_text_at_caret("\n\n\n\n\n");
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("breakpoint_toggled")
+ CHECK(code_edit->is_line_breakpointed(9));
+
+ code_edit->select(0, 0, 6, 0);
+ code_edit->insert_text_at_caret("\n\n\n\n\n\n\n\n\n\n\n");
+ MessageQueue::get_singleton()->flush();
+ CHECK(code_edit->is_line_breakpointed(14));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and undo") {
+ Array arg1;
+ arg1.push_back(1);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->select(0, 0, 2, 0);
+ code_edit->delete_selection();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Undo does not restore breakpoint. */
+ code_edit->undo();
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+ }
+
+ SIGNAL_UNWATCH(code_edit, "breakpoint_toggled");
+ }
+
+ SUBCASE("[CodeEdit] bookmarks") {
+ SUBCASE("[CodeEdit] draw bookmarks gutter") {
+ code_edit->set_draw_bookmarks_gutter(false);
+ CHECK_FALSE(code_edit->is_drawing_bookmarks_gutter());
+
+ code_edit->set_draw_bookmarks_gutter(true);
+ CHECK(code_edit->is_drawing_bookmarks_gutter());
+ }
+
+ SUBCASE("[CodeEdit] set line as bookmarks") {
+ /* Out of bounds. */
+ ERR_PRINT_OFF;
+
+ code_edit->set_line_as_bookmarked(-1, true);
+ CHECK_FALSE(code_edit->is_line_bookmarked(-1));
+
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+
+ ERR_PRINT_ON;
+
+ code_edit->set_line_as_bookmarked(0, true);
+ CHECK(code_edit->get_bookmarked_lines()[0] == 0);
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ code_edit->set_line_as_bookmarked(0, false);
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ }
+
+ SUBCASE("[CodeEdit] clear bookmarked lines") {
+ code_edit->clear_bookmarked_lines();
+
+ code_edit->set_line_as_bookmarked(0, true);
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ code_edit->clear_bookmarked_lines();
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and set text") {
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_bookmarked(0, true);
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ /* bookmarks on lines that still exist are kept. */
+ code_edit->set_text("");
+ MessageQueue::get_singleton()->flush();
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ /* bookmarks on lines that are removed should also be removed. */
+ code_edit->clear_bookmarked_lines();
+
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ code_edit->set_text("");
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and clear") {
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_bookmarked(0, true);
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ /* bookmarks on lines that still exist are removed. */
+ code_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+
+ /* bookmarks on lines that are removed should also be removed. */
+ code_edit->clear_bookmarked_lines();
+
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ code_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and new lines no text") {
+ /* No text moves bookmarks. */
+ code_edit->set_line_as_bookmarked(0, true);
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ /* Normal. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ /* Non-Breaking. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line_count() == 3);
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+ CHECK(code_edit->is_line_bookmarked(2));
+
+ /* Above. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line_count() == 4);
+ CHECK_FALSE(code_edit->is_line_bookmarked(2));
+ CHECK(code_edit->is_line_bookmarked(3));
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and new lines with text") {
+ /* Having text does not move bookmark. */
+ code_edit->insert_text_at_caret("text");
+ code_edit->set_line_as_bookmarked(0, true);
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ /* Normal. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK(code_edit->is_line_bookmarked(0));
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+
+ /* Non-Breaking. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line_count() == 3);
+ CHECK(code_edit->is_line_bookmarked(0));
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+
+ /* Above does move. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line_count() == 4);
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ CHECK(code_edit->is_line_bookmarked(1));
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and backspace") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ code_edit->set_caret_line(2);
+
+ /* backspace onto line does not remove bookmark */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ /* backspace on bookmarked line removes it */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and delete") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK(code_edit->is_line_bookmarked(1));
+ code_edit->set_caret_line(1);
+
+ /* Delete onto bookmarked lines does not remove it. */
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ /* Delete moving bookmarked line up removes it. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ CHECK(code_edit->get_line_count() == 1);
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and delete selection") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ code_edit->select(0, 0, 2, 0);
+ code_edit->delete_selection();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and undo") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ code_edit->select(0, 0, 2, 0);
+ code_edit->delete_selection();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+
+ /* Undo does not restore bookmark. */
+ code_edit->undo();
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+ }
+ }
+
+ SUBCASE("[CodeEdit] executing lines") {
+ SUBCASE("[CodeEdit] draw executing lines gutter") {
+ code_edit->set_draw_executing_lines_gutter(false);
+ CHECK_FALSE(code_edit->is_drawing_executing_lines_gutter());
+
+ code_edit->set_draw_executing_lines_gutter(true);
+ CHECK(code_edit->is_drawing_executing_lines_gutter());
+ }
+
+ SUBCASE("[CodeEdit] set line as executing lines") {
+ /* Out of bounds. */
+ ERR_PRINT_OFF;
+
+ code_edit->set_line_as_executing(-1, true);
+ CHECK_FALSE(code_edit->is_line_executing(-1));
+
+ code_edit->set_line_as_executing(1, true);
+ CHECK_FALSE(code_edit->is_line_executing(1));
+
+ ERR_PRINT_ON;
+
+ code_edit->set_line_as_executing(0, true);
+ CHECK(code_edit->get_executing_lines()[0] == 0);
+ CHECK(code_edit->is_line_executing(0));
+
+ code_edit->set_line_as_executing(0, false);
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ }
+
+ SUBCASE("[CodeEdit] clear executing lines lines") {
+ code_edit->clear_executing_lines();
+
+ code_edit->set_line_as_executing(0, true);
+ CHECK(code_edit->is_line_executing(0));
+
+ code_edit->clear_executing_lines();
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ }
+
+ SUBCASE("[CodeEdit] executing lines and set text") {
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_executing(0, true);
+ CHECK(code_edit->is_line_executing(0));
+
+ /* executing on lines that still exist are kept. */
+ code_edit->set_text("");
+ MessageQueue::get_singleton()->flush();
+ CHECK(code_edit->is_line_executing(0));
+
+ /* executing on lines that are removed should also be removed. */
+ code_edit->clear_executing_lines();
+
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_executing(1, true);
+ CHECK(code_edit->is_line_executing(1));
+
+ code_edit->set_text("");
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_executing(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] executing lines and clear") {
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_executing(0, true);
+ CHECK(code_edit->is_line_executing(0));
+
+ /* executing on lines that still exist are removed. */
+ code_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_executing(0));
+
+ /* executing on lines that are removed should also be removed. */
+ code_edit->clear_executing_lines();
+
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_executing(1, true);
+ CHECK(code_edit->is_line_executing(1));
+
+ code_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_executing(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] executing lines and new lines no text") {
+ /* No text moves executing lines. */
+ code_edit->set_line_as_executing(0, true);
+ CHECK(code_edit->is_line_executing(0));
+
+ /* Normal. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ CHECK(code_edit->is_line_executing(1));
+
+ /* Non-Breaking. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line_count() == 3);
+ CHECK_FALSE(code_edit->is_line_executing(1));
+ CHECK(code_edit->is_line_executing(2));
+
+ /* Above. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line_count() == 4);
+ CHECK_FALSE(code_edit->is_line_executing(2));
+ CHECK(code_edit->is_line_executing(3));
+ }
+
+ SUBCASE("[CodeEdit] executing lines and new lines with text") {
+ /* Having text does not move executing lines. */
+ code_edit->insert_text_at_caret("text");
+ code_edit->set_line_as_executing(0, true);
+ CHECK(code_edit->is_line_executing(0));
+
+ /* Normal. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK(code_edit->is_line_executing(0));
+ CHECK_FALSE(code_edit->is_line_executing(1));
+
+ /* Non-Breaking. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line_count() == 3);
+ CHECK(code_edit->is_line_executing(0));
+ CHECK_FALSE(code_edit->is_line_executing(1));
+
+ /* Above does move. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line_count() == 4);
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ CHECK(code_edit->is_line_executing(1));
+ }
+
+ SUBCASE("[CodeEdit] executing lines and backspace") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_executing(1, true);
+ CHECK(code_edit->is_line_executing(1));
+
+ code_edit->set_caret_line(2);
+
+ /* backspace onto line does not remove executing lines. */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK(code_edit->is_line_executing(1));
+
+ /* backspace on executing line removes it */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_executing(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] executing lines and delete") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_executing(1, true);
+ CHECK(code_edit->is_line_executing(1));
+ code_edit->set_caret_line(1);
+
+ /* Delete onto executing lines does not remove it. */
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK(code_edit->is_line_executing(1));
+
+ /* Delete moving executing line up removes it. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ CHECK(code_edit->get_line_count() == 1);
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_executing(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] executing lines and delete selection") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_executing(1, true);
+ CHECK(code_edit->is_line_executing(1));
+
+ code_edit->select(0, 0, 2, 0);
+ code_edit->delete_selection();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ }
+
+ SUBCASE("[CodeEdit] executing lines and undo") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_executing(1, true);
+ CHECK(code_edit->is_line_executing(1));
+
+ code_edit->select(0, 0, 2, 0);
+ code_edit->delete_selection();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_executing(0));
+
+ /* Undo does not restore executing lines. */
+ code_edit->undo();
+ CHECK_FALSE(code_edit->is_line_executing(1));
+ }
+ }
+
+ SUBCASE("[CodeEdit] line numbers") {
+ SUBCASE("[CodeEdit] draw line numbers gutter and padding") {
+ code_edit->set_draw_line_numbers(false);
+ CHECK_FALSE(code_edit->is_draw_line_numbers_enabled());
+
+ code_edit->set_draw_line_numbers(true);
+ CHECK(code_edit->is_draw_line_numbers_enabled());
+
+ code_edit->set_line_numbers_zero_padded(false);
+ CHECK_FALSE(code_edit->is_line_numbers_zero_padded());
+
+ code_edit->set_line_numbers_zero_padded(true);
+ CHECK(code_edit->is_line_numbers_zero_padded());
+
+ code_edit->set_line_numbers_zero_padded(false);
+ CHECK_FALSE(code_edit->is_line_numbers_zero_padded());
+
+ code_edit->set_draw_line_numbers(false);
+ CHECK_FALSE(code_edit->is_draw_line_numbers_enabled());
+
+ code_edit->set_line_numbers_zero_padded(true);
+ CHECK(code_edit->is_line_numbers_zero_padded());
+ }
+ }
+
+ SUBCASE("[CodeEdit] line folding") {
+ SUBCASE("[CodeEdit] draw line folding gutter") {
+ code_edit->set_draw_fold_gutter(false);
+ CHECK_FALSE(code_edit->is_drawing_fold_gutter());
+
+ code_edit->set_draw_fold_gutter(true);
+ CHECK(code_edit->is_drawing_fold_gutter());
+ }
+ }
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] delimiters") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+ code_edit->grab_focus();
+
+ const Point2 OUTSIDE_DELIMETER = Point2(-1, -1);
+
+ code_edit->clear_string_delimiters();
+ code_edit->clear_comment_delimiters();
+
+ SUBCASE("[CodeEdit] add and remove delimiters") {
+ SUBCASE("[CodeEdit] add and remove string delimiters") {
+ /* Add a delimiter.*/
+ code_edit->add_string_delimiter("\"", "\"", false);
+ CHECK(code_edit->has_string_delimiter("\""));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ ERR_PRINT_OFF;
+
+ /* Adding a duplicate start key is not allowed. */
+ code_edit->add_string_delimiter("\"", "\'", false);
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* Adding a duplicate end key is allowed. */
+ code_edit->add_string_delimiter("'", "\"", false);
+ CHECK(code_edit->has_string_delimiter("'"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ /* Both start and end keys have to be symbols. */
+ code_edit->add_string_delimiter("f", "\"", false);
+ CHECK_FALSE(code_edit->has_string_delimiter("f"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ code_edit->add_string_delimiter("f", "\"", false);
+ CHECK_FALSE(code_edit->has_string_delimiter("f"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ code_edit->add_string_delimiter("@", "f", false);
+ CHECK_FALSE(code_edit->has_string_delimiter("@"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ code_edit->add_string_delimiter("f", "f", false);
+ CHECK_FALSE(code_edit->has_string_delimiter("f"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ /* Blank start keys are not allowed */
+ code_edit->add_string_delimiter("", "#", false);
+ CHECK_FALSE(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ ERR_PRINT_ON;
+
+ /* Blank end keys are allowed. */
+ code_edit->add_string_delimiter("#", "", false);
+ CHECK(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 3);
+
+ /* Remove a delimiter. */
+ code_edit->remove_string_delimiter("#");
+ CHECK_FALSE(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ /* Set should override existing, and test multiline */
+ TypedArray<String> delimiters;
+ delimiters.push_back("^^ ^^");
+
+ code_edit->set_string_delimiters(delimiters);
+ CHECK_FALSE(code_edit->has_string_delimiter("\""));
+ CHECK(code_edit->has_string_delimiter("^^"));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* clear should remove all. */
+ code_edit->clear_string_delimiters();
+ CHECK_FALSE(code_edit->has_string_delimiter("^^"));
+ CHECK(code_edit->get_string_delimiters().size() == 0);
+ }
+
+ SUBCASE("[CodeEdit] add and remove comment delimiters") {
+ /* Add a delimiter.*/
+ code_edit->add_comment_delimiter("\"", "\"", false);
+ CHECK(code_edit->has_comment_delimiter("\""));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ ERR_PRINT_OFF;
+
+ /* Adding a duplicate start key is not allowed. */
+ code_edit->add_comment_delimiter("\"", "\'", false);
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* Adding a duplicate end key is allowed. */
+ code_edit->add_comment_delimiter("'", "\"", false);
+ CHECK(code_edit->has_comment_delimiter("'"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ /* Both start and end keys have to be symbols. */
+ code_edit->add_comment_delimiter("f", "\"", false);
+ CHECK_FALSE(code_edit->has_comment_delimiter("f"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ code_edit->add_comment_delimiter("f", "\"", false);
+ CHECK_FALSE(code_edit->has_comment_delimiter("f"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ code_edit->add_comment_delimiter("@", "f", false);
+ CHECK_FALSE(code_edit->has_comment_delimiter("@"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ code_edit->add_comment_delimiter("f", "f", false);
+ CHECK_FALSE(code_edit->has_comment_delimiter("f"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ /* Blank start keys are not allowed. */
+ code_edit->add_comment_delimiter("", "#", false);
+ CHECK_FALSE(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ ERR_PRINT_ON;
+
+ /* Blank end keys are allowed. */
+ code_edit->add_comment_delimiter("#", "", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 3);
+
+ /* Remove a delimiter. */
+ code_edit->remove_comment_delimiter("#");
+ CHECK_FALSE(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ /* Set should override existing, and test multiline. */
+ TypedArray<String> delimiters;
+ delimiters.push_back("^^ ^^");
+
+ code_edit->set_comment_delimiters(delimiters);
+ CHECK_FALSE(code_edit->has_comment_delimiter("\""));
+ CHECK(code_edit->has_comment_delimiter("^^"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* clear should remove all. */
+ code_edit->clear_comment_delimiters();
+ CHECK_FALSE(code_edit->has_comment_delimiter("^^"));
+ CHECK(code_edit->get_comment_delimiters().size() == 0);
+ }
+
+ SUBCASE("[CodeEdit] add and remove mixed delimiters") {
+ code_edit->add_comment_delimiter("#", "", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ ERR_PRINT_OFF;
+
+ /* Disallow adding a string with the same start key as comment. */
+ code_edit->add_string_delimiter("#", "", false);
+ CHECK_FALSE(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 0);
+
+ code_edit->add_string_delimiter("\"", "\"", false);
+ CHECK(code_edit->has_string_delimiter("\""));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* Disallow adding a comment with the same start key as string. */
+ code_edit->add_comment_delimiter("\"", "", false);
+ CHECK_FALSE(code_edit->has_comment_delimiter("\""));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ ERR_PRINT_ON;
+
+ /* Cannot remove string with remove comment. */
+ code_edit->remove_comment_delimiter("\"");
+ CHECK(code_edit->has_string_delimiter("\""));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* Cannot remove comment with remove string. */
+ code_edit->remove_string_delimiter("#");
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* Clear comments leave strings. */
+ code_edit->clear_comment_delimiters();
+ CHECK(code_edit->has_string_delimiter("\""));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* Clear string leave comments. */
+ code_edit->add_comment_delimiter("#", "", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ code_edit->clear_string_delimiters();
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+ }
+ }
+
+ SUBCASE("[CodeEdit] single line delimiters") {
+ SUBCASE("[CodeEdit] single line string delimiters") {
+ /* Blank end key should set lineonly to true. */
+ code_edit->add_string_delimiter("#", "", false);
+ CHECK(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* Insert line above, line with string then line below. */
+ code_edit->insert_text_at_caret(" \n#\n ");
+
+ /* Check line above is not in string. */
+ CHECK(code_edit->is_in_string(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in string. */
+ CHECK(code_edit->is_in_string(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column after start key is in string and start / end positions are correct. */
+ CHECK(code_edit->is_in_string(1, 1) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(2, 1));
+
+ /* Check line after is not in string. */
+ CHECK(code_edit->is_in_string(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Check region metadata. */
+ int idx = code_edit->is_in_string(1, 1);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "");
+
+ /* Check nested strings are handled correctly. */
+ code_edit->set_text(" \n# # \n ");
+
+ /* Check line above is not in string. */
+ CHECK(code_edit->is_in_string(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before first start key is not in string. */
+ CHECK(code_edit->is_in_string(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column after the first start key is in string and start / end positions are correct. */
+ CHECK(code_edit->is_in_string(1, 1) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1));
+
+ /* Check column after the second start key returns data for the first. */
+ CHECK(code_edit->is_in_string(1, 5) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1));
+
+ /* Check line after is not in string. */
+ CHECK(code_edit->is_in_string(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Check is in string with no column returns true if entire line is comment excluding whitespace. */
+ code_edit->set_text(" \n # # \n ");
+ CHECK(code_edit->is_in_string(1) != -1);
+
+ code_edit->set_text(" \n text # # \n ");
+ CHECK(code_edit->is_in_string(1) == -1);
+
+ /* Removing delimiter should update. */
+ code_edit->set_text(" \n # # \n ");
+
+ code_edit->remove_string_delimiter("#");
+ CHECK_FALSE(code_edit->has_string_delimiter("$"));
+ CHECK(code_edit->get_string_delimiters().size() == 0);
+
+ CHECK(code_edit->is_in_string(1) == -1);
+
+ /* Adding and clear should update. */
+ code_edit->add_string_delimiter("#", "", false);
+ CHECK(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+ CHECK(code_edit->is_in_string(1) != -1);
+
+ code_edit->clear_string_delimiters();
+ CHECK_FALSE(code_edit->has_string_delimiter("$"));
+ CHECK(code_edit->get_string_delimiters().size() == 0);
+
+ CHECK(code_edit->is_in_string(1) == -1);
+ }
+
+ SUBCASE("[CodeEdit] single line comment delimiters") {
+ /* Blank end key should set lineonly to true. */
+ code_edit->add_comment_delimiter("#", "", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* Insert line above, line with comment then line below. */
+ code_edit->insert_text_at_caret(" \n#\n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column after start key is in comment and start / end positions are correct. */
+ CHECK(code_edit->is_in_comment(1, 1) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(2, 1));
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Check region metadata. */
+ int idx = code_edit->is_in_comment(1, 1);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "");
+
+ /* Check nested comments are handled correctly. */
+ code_edit->set_text(" \n# # \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before first start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column after the first start key is in comment and start / end positions are correct. */
+ CHECK(code_edit->is_in_comment(1, 1) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1));
+
+ /* Check column after the second start key returns data for the first. */
+ CHECK(code_edit->is_in_comment(1, 5) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1));
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Check is in comment with no column returns true if entire line is comment excluding whitespace. */
+ code_edit->set_text(" \n # # \n ");
+ CHECK(code_edit->is_in_comment(1) != -1);
+
+ code_edit->set_text(" \n text # # \n ");
+ CHECK(code_edit->is_in_comment(1) == -1);
+
+ /* Removing delimiter should update. */
+ code_edit->set_text(" \n # # \n ");
+
+ code_edit->remove_comment_delimiter("#");
+ CHECK_FALSE(code_edit->has_comment_delimiter("$"));
+ CHECK(code_edit->get_comment_delimiters().size() == 0);
+
+ CHECK(code_edit->is_in_comment(1) == -1);
+
+ /* Adding and clear should update. */
+ code_edit->add_comment_delimiter("#", "", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+ CHECK(code_edit->is_in_comment(1) != -1);
+
+ code_edit->clear_comment_delimiters();
+ CHECK_FALSE(code_edit->has_comment_delimiter("$"));
+ CHECK(code_edit->get_comment_delimiters().size() == 0);
+
+ CHECK(code_edit->is_in_comment(1) == -1);
+ }
+
+ SUBCASE("[CodeEdit] single line mixed delimiters") {
+ /* Blank end key should set lineonly to true. */
+ /* Add string delimiter. */
+ code_edit->add_string_delimiter("&", "", false);
+ CHECK(code_edit->has_string_delimiter("&"));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* Add comment delimiter. */
+ code_edit->add_comment_delimiter("#", "", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* Nest a string delimiter inside a comment. */
+ code_edit->set_text(" \n# & \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before first start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column after the first start key is in comment and start / end positions are correct. */
+ CHECK(code_edit->is_in_comment(1, 1) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1));
+
+ /* Check column after the second start key returns data for the first, and does not state string. */
+ CHECK(code_edit->is_in_comment(1, 5) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1));
+ CHECK(code_edit->is_in_string(1, 5) == -1);
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Remove the comment delimiter. */
+ code_edit->remove_comment_delimiter("#");
+ CHECK_FALSE(code_edit->has_comment_delimiter("$"));
+ CHECK(code_edit->get_comment_delimiters().size() == 0);
+
+ /* The "first" comment region is no longer valid. */
+ CHECK(code_edit->is_in_comment(1, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 1) == OUTSIDE_DELIMETER);
+
+ /* The "second" region as string is now valid. */
+ CHECK(code_edit->is_in_string(1, 5) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(4, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1));
+ }
+ }
+
+ SUBCASE("[CodeEdit] multiline delimiters") {
+ SUBCASE("[CodeEdit] multiline string delimiters") {
+ code_edit->clear_string_delimiters();
+ code_edit->clear_comment_delimiters();
+
+ /* Add string delimiter. */
+ code_edit->add_string_delimiter("#", "#", false);
+ CHECK(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* First test over a single line. */
+ code_edit->set_text(" \n # # \n ");
+
+ /* Check line above is not in string. */
+ CHECK(code_edit->is_in_string(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in string. */
+ CHECK(code_edit->is_in_string(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column before closing delimiter is in string. */
+ CHECK(code_edit->is_in_string(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(5, 1));
+
+ /* Check column after end key is not in string. */
+ CHECK(code_edit->is_in_string(1, 6) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 6) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 6) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in string. */
+ CHECK(code_edit->is_in_string(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Check the region metadata. */
+ int idx = code_edit->is_in_string(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Next test over a multiple blank lines. */
+ code_edit->set_text(" \n # \n\n # \n ");
+
+ /* Check line above is not in string. */
+ CHECK(code_edit->is_in_string(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in string. */
+ CHECK(code_edit->is_in_string(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in string. */
+ CHECK(code_edit->is_in_string(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3));
+
+ /* Check blank middle line. */
+ CHECK(code_edit->is_in_string(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3));
+
+ /* Check column just before end key is in string. */
+ CHECK(code_edit->is_in_string(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3));
+
+ /* Check column after end key is not in string. */
+ CHECK(code_edit->is_in_string(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in string. */
+ CHECK(code_edit->is_in_string(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* Next test over a multiple non-blank lines. */
+ code_edit->set_text(" \n # \n \n # \n ");
+
+ /* Check line above is not in string. */
+ CHECK(code_edit->is_in_string(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in string. */
+ CHECK(code_edit->is_in_string(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in string. */
+ CHECK(code_edit->is_in_string(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3));
+
+ /* Check middle line. */
+ CHECK(code_edit->is_in_string(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3));
+
+ /* Check column just before end key is in string. */
+ CHECK(code_edit->is_in_string(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3));
+
+ /* Check column after end key is not in string. */
+ CHECK(code_edit->is_in_string(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in string. */
+ CHECK(code_edit->is_in_string(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* check the region metadata. */
+ idx = code_edit->is_in_string(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Next test nested strings. */
+ code_edit->add_string_delimiter("^", "^", false);
+ CHECK(code_edit->has_string_delimiter("^"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ code_edit->set_text(" \n # ^\n \n^ # \n ");
+
+ /* Check line above is not in string. */
+ CHECK(code_edit->is_in_string(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in string. */
+ CHECK(code_edit->is_in_string(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in string. */
+ CHECK(code_edit->is_in_string(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3));
+
+ /* Check middle line. */
+ CHECK(code_edit->is_in_string(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3));
+
+ /* Check column just before end key is in string. */
+ CHECK(code_edit->is_in_string(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3));
+
+ /* Check column after end key is not in string. */
+ CHECK(code_edit->is_in_string(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in string. */
+ CHECK(code_edit->is_in_string(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* check the region metadata. */
+ idx = code_edit->is_in_string(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Next test no end key. */
+ code_edit->set_text(" \n # \n ");
+
+ /* check the region metadata. */
+ idx = code_edit->is_in_string(1, 2);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(-1, -1));
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Check is in string with no column returns true if entire line is string excluding whitespace. */
+ code_edit->set_text(" \n # \n\n #\n ");
+ CHECK(code_edit->is_in_string(1) != -1);
+ CHECK(code_edit->is_in_string(2) != -1);
+ CHECK(code_edit->is_in_string(3) != -1);
+
+ code_edit->set_text(" \n test # \n\n # test \n ");
+ CHECK(code_edit->is_in_string(1) == -1);
+ CHECK(code_edit->is_in_string(2) != -1);
+ CHECK(code_edit->is_in_string(3) == -1);
+ }
+
+ SUBCASE("[CodeEdit] multiline comment delimiters") {
+ /* Add comment delimiter. */
+ code_edit->add_comment_delimiter("#", "#", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* First test over a single line. */
+ code_edit->set_text(" \n # # \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column before closing delimiter is in comment. */
+ CHECK(code_edit->is_in_comment(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(5, 1));
+
+ /* Check column after end key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 6) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 6) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 6) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Check the region metadata. */
+ int idx = code_edit->is_in_comment(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Next test over a multiple blank lines. */
+ code_edit->set_text(" \n # \n\n # \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in comment. */
+ CHECK(code_edit->is_in_comment(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3));
+
+ /* Check blank middle line. */
+ CHECK(code_edit->is_in_comment(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3));
+
+ /* Check column just before end key is in comment. */
+ CHECK(code_edit->is_in_comment(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3));
+
+ /* Check column after end key is not in comment. */
+ CHECK(code_edit->is_in_comment(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* Next test over a multiple non-blank lines. */
+ code_edit->set_text(" \n # \n \n # \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in comment. */
+ CHECK(code_edit->is_in_comment(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3));
+
+ /* Check middle line. */
+ CHECK(code_edit->is_in_comment(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3));
+
+ /* Check column just before end key is in comment. */
+ CHECK(code_edit->is_in_comment(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3));
+
+ /* Check column after end key is not in comment. */
+ CHECK(code_edit->is_in_comment(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* check the region metadata. */
+ idx = code_edit->is_in_comment(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Next test nested comments. */
+ code_edit->add_comment_delimiter("^", "^", false);
+ CHECK(code_edit->has_comment_delimiter("^"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ code_edit->set_text(" \n # ^\n \n^ # \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in comment. */
+ CHECK(code_edit->is_in_comment(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3));
+
+ /* Check middle line. */
+ CHECK(code_edit->is_in_comment(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3));
+
+ /* Check column just before end key is in comment. */
+ CHECK(code_edit->is_in_comment(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3));
+
+ /* Check column after end key is not in comment. */
+ CHECK(code_edit->is_in_comment(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* check the region metadata. */
+ idx = code_edit->is_in_comment(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Next test no end key. */
+ code_edit->set_text(" \n # \n ");
+
+ /* check the region metadata. */
+ idx = code_edit->is_in_comment(1, 2);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(-1, -1));
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Check is in comment with no column returns true if entire line is comment excluding whitespace. */
+ code_edit->set_text(" \n # \n\n #\n ");
+ CHECK(code_edit->is_in_comment(1) != -1);
+ CHECK(code_edit->is_in_comment(2) != -1);
+ CHECK(code_edit->is_in_comment(3) != -1);
+
+ code_edit->set_text(" \n test # \n\n # test \n ");
+ CHECK(code_edit->is_in_comment(1) == -1);
+ CHECK(code_edit->is_in_comment(2) != -1);
+ CHECK(code_edit->is_in_comment(3) == -1);
+ }
+
+ SUBCASE("[CodeEdit] multiline mixed delimiters") {
+ /* Add comment delimiter. */
+ code_edit->add_comment_delimiter("#", "#", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* Add string delimiter. */
+ code_edit->add_string_delimiter("^", "^", false);
+ CHECK(code_edit->has_string_delimiter("^"));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* Nest a string inside a comment. */
+ code_edit->set_text(" \n # ^\n \n^ # \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in comment. */
+ CHECK(code_edit->is_in_comment(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3));
+
+ /* Check middle line. */
+ CHECK(code_edit->is_in_comment(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3));
+
+ /* Check column just before end key is in comment. */
+ CHECK(code_edit->is_in_comment(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3));
+
+ /* Check column after end key is not in comment. */
+ CHECK(code_edit->is_in_comment(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* check the region metadata. */
+ int idx = code_edit->is_in_comment(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Check is in comment with no column returns true as inner delimiter should not be counted. */
+ CHECK(code_edit->is_in_comment(1) != -1);
+ CHECK(code_edit->is_in_comment(2) != -1);
+ CHECK(code_edit->is_in_comment(3) != -1);
+ }
+ }
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] indent") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+ code_edit->grab_focus();
+
+ SUBCASE("[CodeEdit] indent settings") {
+ code_edit->set_indent_size(10);
+ CHECK(code_edit->get_indent_size() == 10);
+ CHECK(code_edit->get_tab_size() == 10);
+
+ code_edit->set_auto_indent_enabled(false);
+ CHECK_FALSE(code_edit->is_auto_indent_enabled());
+
+ code_edit->set_auto_indent_enabled(true);
+ CHECK(code_edit->is_auto_indent_enabled());
+
+ code_edit->set_indent_using_spaces(false);
+ CHECK_FALSE(code_edit->is_indent_using_spaces());
+
+ code_edit->set_indent_using_spaces(true);
+ CHECK(code_edit->is_indent_using_spaces());
+
+ /* Only the first char is registered. */
+ TypedArray<String> auto_indent_prefixes;
+ auto_indent_prefixes.push_back("::");
+ auto_indent_prefixes.push_back("s");
+ auto_indent_prefixes.push_back("1");
+ code_edit->set_auto_indent_prefixes(auto_indent_prefixes);
+
+ auto_indent_prefixes = code_edit->get_auto_indent_prefixes();
+ CHECK(auto_indent_prefixes.has(":"));
+ CHECK(auto_indent_prefixes.has("s"));
+ CHECK(auto_indent_prefixes.has("1"));
+ }
+
+ SUBCASE("[CodeEdit] indent tabs") {
+ code_edit->set_indent_size(4);
+ code_edit->set_auto_indent_enabled(true);
+ code_edit->set_indent_using_spaces(false);
+
+ /* Do nothing if not editable. */
+ code_edit->set_editable(false);
+
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0).is_empty());
+
+ code_edit->indent_lines();
+ CHECK(code_edit->get_line(0).is_empty());
+
+ code_edit->set_editable(true);
+
+ /* Simple indent. */
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "\t");
+
+ /* Check input action. */
+ SEND_GUI_ACTION(code_edit, "ui_text_indent");
+ CHECK(code_edit->get_line(0) == "\t\t");
+
+ /* Insert in place. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test");
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "test\t");
+
+ /* Indent lines does entire line and works without selection. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test");
+ code_edit->indent_lines();
+ CHECK(code_edit->get_line(0) == "\ttest");
+
+ /* Selection does entire line. */
+ code_edit->set_text("test");
+ code_edit->select_all();
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+
+ /* Handles multiple lines. */
+ code_edit->set_text("test\ntext");
+ code_edit->select_all();
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_line(1) == "\ttext");
+
+ /* Do not indent line if last col is zero. */
+ code_edit->set_text("test\ntext");
+ code_edit->select(0, 0, 1, 0);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Indent even if last column of first line. */
+ code_edit->set_text("test\ntext");
+ code_edit->select(0, 4, 1, 0);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Check selection is adjusted. */
+ code_edit->set_text("test");
+ code_edit->select(0, 1, 0, 2);
+ code_edit->do_indent();
+ CHECK(code_edit->get_selection_from_column() == 2);
+ CHECK(code_edit->get_selection_to_column() == 3);
+ CHECK(code_edit->get_line(0) == "\ttest");
+ code_edit->undo();
+ }
+
+ SUBCASE("[CodeEdit] indent spaces") {
+ code_edit->set_indent_size(4);
+ code_edit->set_auto_indent_enabled(true);
+ code_edit->set_indent_using_spaces(true);
+
+ /* Do nothing if not editable. */
+ code_edit->set_editable(false);
+
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0).is_empty());
+
+ code_edit->indent_lines();
+ CHECK(code_edit->get_line(0).is_empty());
+
+ code_edit->set_editable(true);
+
+ /* Simple indent. */
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " ");
+
+ /* Check input action. */
+ SEND_GUI_ACTION(code_edit, "ui_text_indent");
+ CHECK(code_edit->get_line(0) == " ");
+
+ /* Insert in place. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test");
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "test ");
+
+ /* Indent lines does entire line and works without selection. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test");
+ code_edit->indent_lines();
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* Selection does entire line. */
+ code_edit->set_text("test");
+ code_edit->select_all();
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* single indent only add required spaces. */
+ code_edit->set_text(" test");
+ code_edit->select_all();
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* Handles multiple lines. */
+ code_edit->set_text("test\ntext");
+ code_edit->select_all();
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_line(1) == " text");
+
+ /* Do not indent line if last col is zero. */
+ code_edit->set_text("test\ntext");
+ code_edit->select(0, 0, 1, 0);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Indent even if last column of first line. */
+ code_edit->set_text("test\ntext");
+ code_edit->select(0, 4, 1, 0);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Check selection is adjusted. */
+ code_edit->set_text("test");
+ code_edit->select(0, 1, 0, 2);
+ code_edit->do_indent();
+ CHECK(code_edit->get_selection_from_column() == 5);
+ CHECK(code_edit->get_selection_to_column() == 6);
+ CHECK(code_edit->get_line(0) == " test");
+ }
+
+ SUBCASE("[CodeEdit] unindent tabs") {
+ code_edit->set_indent_size(4);
+ code_edit->set_auto_indent_enabled(true);
+ code_edit->set_indent_using_spaces(false);
+
+ /* Do nothing if not editable. */
+ code_edit->set_text("\t");
+
+ code_edit->set_editable(false);
+
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "\t");
+
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == "\t");
+
+ code_edit->set_editable(true);
+
+ /* Simple unindent. */
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "");
+
+ /* Should inindent inplace. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test\t");
+
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+
+ /* Backspace does a simple unindent. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("\t");
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == "");
+
+ /* Unindent lines does entire line and works without selection. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("\ttest");
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == "test");
+
+ /* Caret on col zero unindent line. */
+ code_edit->set_text("\t\ttest");
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+
+ /* Check input action. */
+ code_edit->set_text("\t\ttest");
+ SEND_GUI_ACTION(code_edit, "ui_text_dedent");
+ CHECK(code_edit->get_line(0) == "\ttest");
+
+ /* Selection does entire line. */
+ code_edit->set_text("\t\ttest");
+ code_edit->select_all();
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+
+ /* Handles multiple lines. */
+ code_edit->set_text("\ttest\n\ttext");
+ code_edit->select_all();
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Do not unindent line if last col is zero. */
+ code_edit->set_text("\ttest\n\ttext");
+ code_edit->select(0, 0, 1, 0);
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == "\ttext");
+
+ /* Unindent even if last column of first line. */
+ code_edit->set_text("\ttest\n\ttext");
+ code_edit->select(0, 5, 1, 1);
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Check selection is adjusted. */
+ code_edit->set_text("\ttest");
+ code_edit->select(0, 1, 0, 2);
+ code_edit->do_unindent();
+ CHECK(code_edit->get_selection_from_column() == 0);
+ CHECK(code_edit->get_selection_to_column() == 1);
+ CHECK(code_edit->get_line(0) == "test");
+ }
+
+ SUBCASE("[CodeEdit] unindent spaces") {
+ code_edit->set_indent_size(4);
+ code_edit->set_auto_indent_enabled(true);
+ code_edit->set_indent_using_spaces(true);
+
+ /* Do nothing if not editable. */
+ code_edit->set_text(" ");
+
+ code_edit->set_editable(false);
+
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == " ");
+
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == " ");
+
+ code_edit->set_editable(true);
+
+ /* Simple unindent. */
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "");
+
+ /* Should inindent inplace. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test ");
+
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+
+ /* Backspace does a simple unindent. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret(" ");
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == "");
+
+ /* Backspace with letter. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret(" a");
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == " ");
+
+ /* Unindent lines does entire line and works without selection. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret(" test");
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == "test");
+
+ /* Caret on col zero unindent line. */
+ code_edit->set_text(" test");
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* Only as far as needed */
+ code_edit->set_text(" test");
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* Check input action. */
+ code_edit->set_text(" test");
+ SEND_GUI_ACTION(code_edit, "ui_text_dedent");
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* Selection does entire line. */
+ code_edit->set_text(" test");
+ code_edit->select_all();
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* Handles multiple lines. */
+ code_edit->set_text(" test\n text");
+ code_edit->select_all();
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Do not unindent line if last col is zero. */
+ code_edit->set_text(" test\n text");
+ code_edit->select(0, 0, 1, 0);
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == " text");
+
+ /* Unindent even if last column of first line. */
+ code_edit->set_text(" test\n text");
+ code_edit->select(0, 5, 1, 1);
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Check selection is adjusted. */
+ code_edit->set_text(" test");
+ code_edit->select(0, 4, 0, 5);
+ code_edit->do_unindent();
+ CHECK(code_edit->get_selection_from_column() == 0);
+ CHECK(code_edit->get_selection_to_column() == 1);
+ CHECK(code_edit->get_line(0) == "test");
+ }
+
+ SUBCASE("[CodeEdit] auto indent") {
+ SUBCASE("[CodeEdit] auto indent tabs") {
+ code_edit->set_indent_size(4);
+ code_edit->set_auto_indent_enabled(true);
+ code_edit->set_indent_using_spaces(false);
+
+ /* Simple indent on new line. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test:");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test:");
+ CHECK(code_edit->get_line(1) == "\t");
+
+ /* new blank line should still indent. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test:");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == "test:");
+ CHECK(code_edit->get_line(1) == "\t");
+
+ /* new line above should not indent. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test:");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line(0) == "");
+ CHECK(code_edit->get_line(1) == "test:");
+
+ /* Whitespace between symbol and caret is okay. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test: ");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test: ");
+ CHECK(code_edit->get_line(1) == "\t");
+
+ /* Comment between symbol and caret is okay. */
+ code_edit->add_comment_delimiter("#", "");
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test: # comment");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test: # comment");
+ CHECK(code_edit->get_line(1) == "\t");
+ code_edit->remove_comment_delimiter("#");
+
+ /* Strings between symbol and caret are not okay. */
+ code_edit->add_string_delimiter("#", "");
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test: # string");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test: # string");
+ CHECK(code_edit->get_line(1) == "");
+ code_edit->remove_string_delimiter("#");
+
+ /* Non-whitespace prevents auto-indentation. */
+ code_edit->add_comment_delimiter("#", "");
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test := 0 # comment");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test := 0 # comment");
+ CHECK(code_edit->get_line(1) == "");
+ code_edit->remove_comment_delimiter("#");
+
+ /* Even when there's no comments. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test := 0");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test := 0");
+ CHECK(code_edit->get_line(1) == "");
+
+ /* If between brace pairs an extra line is added. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test{}");
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test{");
+ CHECK(code_edit->get_line(1) == "\t");
+ CHECK(code_edit->get_line(2) == "}");
+
+ /* Except when we are going above. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test{}");
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line(0) == "");
+ CHECK(code_edit->get_line(1) == "test{}");
+
+ /* or below. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test{}");
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == "test{}");
+ CHECK(code_edit->get_line(1) == "");
+ }
+
+ SUBCASE("[CodeEdit] auto indent spaces") {
+ code_edit->set_indent_size(4);
+ code_edit->set_auto_indent_enabled(true);
+ code_edit->set_indent_using_spaces(true);
+
+ /* Simple indent on new line. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test:");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test:");
+ CHECK(code_edit->get_line(1) == " ");
+
+ /* new blank line should still indent. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test:");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == "test:");
+ CHECK(code_edit->get_line(1) == " ");
+
+ /* new line above should not indent. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test:");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line(0) == "");
+ CHECK(code_edit->get_line(1) == "test:");
+
+ /* Whitespace between symbol and caret is okay. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test: ");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test: ");
+ CHECK(code_edit->get_line(1) == " ");
+
+ /* Comment between symbol and caret is okay. */
+ code_edit->add_comment_delimiter("#", "");
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test: # comment");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test: # comment");
+ CHECK(code_edit->get_line(1) == " ");
+ code_edit->remove_comment_delimiter("#");
+
+ /* Strings between symbol and caret are not okay. */
+ code_edit->add_string_delimiter("#", "");
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test: # string");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test: # string");
+ CHECK(code_edit->get_line(1) == "");
+ code_edit->remove_string_delimiter("#");
+
+ /* Non-whitespace prevents auto-indentation. */
+ code_edit->add_comment_delimiter("#", "");
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test := 0 # comment");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test := 0 # comment");
+ CHECK(code_edit->get_line(1) == "");
+ code_edit->remove_comment_delimiter("#");
+
+ /* Even when there's no comments. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test := 0");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test := 0");
+ CHECK(code_edit->get_line(1) == "");
+
+ /* If between brace pairs an extra line is added. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test{}");
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test{");
+ CHECK(code_edit->get_line(1) == " ");
+ CHECK(code_edit->get_line(2) == "}");
+
+ /* Except when we are going above. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test{}");
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line(0) == "");
+ CHECK(code_edit->get_line(1) == "test{}");
+
+ /* or below. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test{}");
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == "test{}");
+ CHECK(code_edit->get_line(1) == "");
+ }
+ }
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] folding") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+ code_edit->grab_focus();
+
+ SUBCASE("[CodeEdit] folding settings") {
+ code_edit->set_line_folding_enabled(true);
+ CHECK(code_edit->is_line_folding_enabled());
+
+ code_edit->set_line_folding_enabled(false);
+ CHECK_FALSE(code_edit->is_line_folding_enabled());
+ }
+
+ SUBCASE("[CodeEdit] folding") {
+ code_edit->set_line_folding_enabled(true);
+
+ // No indent.
+ code_edit->set_text("line1\nline2\nline3");
+ for (int i = 0; i < 2; i++) {
+ CHECK_FALSE(code_edit->can_fold_line(i));
+ code_edit->fold_line(i);
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Indented lines.
+ code_edit->set_text("\tline1\n\tline2\n\tline3");
+ for (int i = 0; i < 2; i++) {
+ CHECK_FALSE(code_edit->can_fold_line(i));
+ code_edit->fold_line(i);
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Indent.
+ code_edit->set_text("line1\n\tline2\nline3");
+ CHECK(code_edit->can_fold_line(0));
+ for (int i = 1; i < 2; i++) {
+ CHECK_FALSE(code_edit->can_fold_line(i));
+ code_edit->fold_line(i);
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2);
+
+ // Indent with blank lines.
+ code_edit->set_text("line1\n\tline2\n\n\nline3");
+ CHECK(code_edit->can_fold_line(0));
+ for (int i = 1; i < 2; i++) {
+ CHECK_FALSE(code_edit->can_fold_line(i));
+ code_edit->fold_line(i);
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2);
+
+ // Nested indents.
+ code_edit->set_text("line1\n\tline2\n\t\tline3\nline4");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK(code_edit->can_fold_line(1));
+ for (int i = 2; i < 3; i++) {
+ CHECK_FALSE(code_edit->can_fold_line(i));
+ code_edit->fold_line(i);
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK(code_edit->is_line_folded(1));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ CHECK_FALSE(code_edit->is_line_folded(3));
+ CHECK(code_edit->get_next_visible_line_offset_from(2, 1) == 2);
+
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ CHECK_FALSE(code_edit->is_line_folded(3));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3);
+
+ // Check metadata.
+ CHECK(code_edit->get_folded_lines().size() == 1);
+ CHECK((int)code_edit->get_folded_lines()[0] == 0);
+
+ // Cannot unfold nested.
+ code_edit->unfold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // (un)Fold all / toggle.
+ code_edit->unfold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Check metadata.
+ CHECK(code_edit->get_folded_lines().size() == 0);
+
+ code_edit->fold_all_lines();
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3);
+
+ code_edit->unfold_all_lines();
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ code_edit->toggle_foldable_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3);
+
+ // Can also unfold from hidden line.
+ code_edit->unfold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Blank lines.
+ code_edit->set_text("line1\n\tline2\n\n\n\ttest\n\nline3");
+ CHECK(code_edit->can_fold_line(0));
+ for (int i = 1; i < code_edit->get_line_count(); i++) {
+ CHECK_FALSE(code_edit->can_fold_line(i));
+ code_edit->fold_line(i);
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ for (int i = 1; i < code_edit->get_line_count(); i++) {
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 5);
+
+ // End of file.
+ code_edit->set_text("line1\n\tline2");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Comment & string blocks.
+ // Single line block
+ code_edit->add_comment_delimiter("#", "", true);
+ code_edit->set_text("#line1\n#\tline2");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Has to be full line.
+ code_edit->set_text("test #line1\n#\tline2");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ code_edit->set_text("#line1\ntest #\tline2");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // String.
+ code_edit->add_string_delimiter("^", "", true);
+ code_edit->set_text("^line1\n^\tline2");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Has to be full line.
+ code_edit->set_text("test ^line1\n^\tline2");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ code_edit->set_text("^line1\ntest ^\tline2");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Multiline blocks.
+ code_edit->add_comment_delimiter("&", "&", false);
+ code_edit->set_text("&line1\n\tline2&\nline3");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2);
+
+ // Multiline comment before last line.
+ code_edit->set_text("&line1\nline2&\ntest");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(2));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2);
+
+ // Has to be full line.
+ code_edit->set_text("test &line1\n\tline2&");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ code_edit->set_text("&line1\n\tline2& test");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Strings.
+ code_edit->add_string_delimiter("$", "$", false);
+ code_edit->set_text("$line1\n\tline2$");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Has to be full line.
+ code_edit->set_text("test $line1\n\tline2$");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ code_edit->set_text("$line1\n\tline2$ test");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Non-indented comments/strings.
+ // Single line
+ code_edit->set_text("test\n\tline1\n#line1\n#line2\n\ttest");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4);
+
+ code_edit->set_text("test\n\tline1\n^line1\n^line2\n\ttest");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4);
+
+ // Indent level 0->1, comment after lines
+ code_edit->set_text("line1\n\tline2\n#test");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2);
+
+ // Indent level 0->1, comment between lines
+ code_edit->set_text("line1\n#test\n\tline2\nline3");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(2));
+ code_edit->fold_line(2);
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3);
+
+ // Indent level 1->2, comment after lines
+ code_edit->set_text("\tline1\n\t\tline2\n#test");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2);
+
+ // Indent level 1->2, comment between lines
+ code_edit->set_text("\tline1\n#test\n\t\tline2\nline3");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(2));
+ code_edit->fold_line(2);
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3);
+
+ // Multiline
+ code_edit->set_text("test\n\tline1\n&line1\nline2&\n\ttest");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4);
+
+ code_edit->set_text("test\n\tline1\n$line1\nline2$\n\ttest");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4);
+ }
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] completion") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+ code_edit->grab_focus();
+
+ SUBCASE("[CodeEdit] auto brace completion") {
+ code_edit->set_auto_brace_completion_enabled(true);
+ CHECK(code_edit->is_auto_brace_completion_enabled());
+
+ code_edit->set_highlight_matching_braces_enabled(true);
+ CHECK(code_edit->is_highlight_matching_braces_enabled());
+
+ /* Try setters, any length. */
+ Dictionary auto_brace_completion_pairs;
+ auto_brace_completion_pairs["["] = "]";
+ auto_brace_completion_pairs["'"] = "'";
+ auto_brace_completion_pairs[";"] = "'";
+ auto_brace_completion_pairs["'''"] = "'''";
+ code_edit->set_auto_brace_completion_pairs(auto_brace_completion_pairs);
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+ CHECK(code_edit->get_auto_brace_completion_pairs()["["] == "]");
+ CHECK(code_edit->get_auto_brace_completion_pairs()["'"] == "'");
+ CHECK(code_edit->get_auto_brace_completion_pairs()[";"] == "'");
+ CHECK(code_edit->get_auto_brace_completion_pairs()["'''"] == "'''");
+
+ ERR_PRINT_OFF;
+
+ /* No duplicate start keys. */
+ code_edit->add_auto_brace_completion_pair("[", "]");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ /* No empty keys. */
+ code_edit->add_auto_brace_completion_pair("[", "");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ code_edit->add_auto_brace_completion_pair("", "]");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ code_edit->add_auto_brace_completion_pair("", "");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ /* Must be a symbol. */
+ code_edit->add_auto_brace_completion_pair("a", "]");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ code_edit->add_auto_brace_completion_pair("[", "a");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ code_edit->add_auto_brace_completion_pair("a", "a");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ ERR_PRINT_ON;
+
+ /* Check metadata. */
+ CHECK(code_edit->has_auto_brace_completion_open_key("["));
+ CHECK(code_edit->has_auto_brace_completion_open_key("'"));
+ CHECK(code_edit->has_auto_brace_completion_open_key(";"));
+ CHECK(code_edit->has_auto_brace_completion_open_key("'''"));
+ CHECK_FALSE(code_edit->has_auto_brace_completion_open_key("("));
+
+ CHECK(code_edit->has_auto_brace_completion_close_key("]"));
+ CHECK(code_edit->has_auto_brace_completion_close_key("'"));
+ CHECK(code_edit->has_auto_brace_completion_close_key("'''"));
+ CHECK_FALSE(code_edit->has_auto_brace_completion_close_key(")"));
+
+ CHECK(code_edit->get_auto_brace_completion_close_key("[") == "]");
+ CHECK(code_edit->get_auto_brace_completion_close_key("'") == "'");
+ CHECK(code_edit->get_auto_brace_completion_close_key(";") == "'");
+ CHECK(code_edit->get_auto_brace_completion_close_key("'''") == "'''");
+ CHECK(code_edit->get_auto_brace_completion_close_key("(").is_empty());
+
+ /* Check typing inserts closing pair. */
+ code_edit->clear();
+ SEND_GUI_KEY_EVENT(code_edit, Key::BRACKETLEFT);
+ CHECK(code_edit->get_line(0) == "[]");
+
+ /* Should first match and insert smaller key. */
+ code_edit->clear();
+ SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE);
+ CHECK(code_edit->get_line(0) == "''");
+ CHECK(code_edit->get_caret_column() == 1);
+
+ /* Move out from centre, Should match and insert larger key. */
+ SEND_GUI_ACTION(code_edit, "ui_text_caret_right");
+ SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE);
+ CHECK(code_edit->get_line(0) == "''''''");
+ CHECK(code_edit->get_caret_column() == 3);
+
+ /* Backspace should remove all. */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK(code_edit->get_line(0).is_empty());
+
+ /* If in between and typing close key should "skip". */
+ SEND_GUI_KEY_EVENT(code_edit, Key::BRACKETLEFT);
+ CHECK(code_edit->get_line(0) == "[]");
+ CHECK(code_edit->get_caret_column() == 1);
+ SEND_GUI_KEY_EVENT(code_edit, Key::BRACKETRIGHT);
+ CHECK(code_edit->get_line(0) == "[]");
+ CHECK(code_edit->get_caret_column() == 2);
+
+ /* If current is char and inserting a string, do not autocomplete. */
+ code_edit->clear();
+ SEND_GUI_KEY_EVENT(code_edit, Key::A);
+ SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE);
+ CHECK(code_edit->get_line(0) == "A'");
+
+ /* If in comment, do not complete. */
+ code_edit->add_comment_delimiter("#", "");
+ code_edit->clear();
+ SEND_GUI_KEY_EVENT(code_edit, Key::NUMBERSIGN);
+ SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE);
+ CHECK(code_edit->get_line(0) == "#'");
+
+ /* If in string, and inserting string do not complete. */
+ code_edit->clear();
+ SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE);
+ SEND_GUI_KEY_EVENT(code_edit, Key::QUOTEDBL);
+ CHECK(code_edit->get_line(0) == "'\"'");
+
+ /* Wrap single line selection with brackets */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("abc");
+ code_edit->select_all();
+ SEND_GUI_KEY_EVENT(code_edit, Key::BRACKETLEFT);
+ CHECK(code_edit->get_line(0) == "[abc]");
+
+ /* Caret should be after the last character of the single line selection */
+ CHECK(code_edit->get_caret_column() == 4);
+
+ /* Wrap multi line selection with brackets */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("abc\nabc");
+ code_edit->select_all();
+ SEND_GUI_KEY_EVENT(code_edit, Key::BRACKETLEFT);
+ CHECK(code_edit->get_text() == "[abc\nabc]");
+
+ /* Caret should be after the last character of the multi line selection */
+ CHECK(code_edit->get_caret_line() == 1);
+ CHECK(code_edit->get_caret_column() == 3);
+
+ /* If inserted character is not a auto brace completion open key, replace selected text with the inserted character */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("abc");
+ code_edit->select_all();
+ SEND_GUI_KEY_EVENT(code_edit, Key::KEY_1);
+ CHECK(code_edit->get_text() == "1");
+
+ /* If potential multichar and single brace completion is matched, it should wrap the single. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("\'\'abc");
+ code_edit->select(0, 2, 0, 5);
+ SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE);
+ CHECK(code_edit->get_text() == "\'\'\'abc\'");
+
+ /* If only the potential multichar brace completion is matched, it does not wrap or complete. */
+ auto_brace_completion_pairs.erase("\'");
+ code_edit->set_auto_brace_completion_pairs(auto_brace_completion_pairs);
+ CHECK_FALSE(code_edit->has_auto_brace_completion_open_key("\'"));
+
+ code_edit->clear();
+ code_edit->insert_text_at_caret("\'\'abc");
+ code_edit->select(0, 2, 0, 5);
+ SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE);
+ CHECK(code_edit->get_text() == "\'\'\'");
+ }
+
+ SUBCASE("[CodeEdit] autocomplete") {
+ code_edit->set_code_completion_enabled(true);
+ CHECK(code_edit->is_code_completion_enabled());
+
+ /* Set prefixes, single char only, disallow empty. */
+ TypedArray<String> completion_prefixes;
+ completion_prefixes.push_back("");
+ completion_prefixes.push_back(".");
+ completion_prefixes.push_back(".");
+ completion_prefixes.push_back(",,");
+
+ ERR_PRINT_OFF;
+ code_edit->set_code_completion_prefixes(completion_prefixes);
+ ERR_PRINT_ON;
+ completion_prefixes = code_edit->get_code_completion_prefixes();
+ CHECK(completion_prefixes.size() == 2);
+ CHECK(completion_prefixes.has("."));
+ CHECK(completion_prefixes.has(","));
+
+ code_edit->set_text("test\ntest");
+ CHECK(code_edit->get_text_for_code_completion() == String::chr(0xFFFF) + "test\ntest");
+ }
+
+ SUBCASE("[CodeEdit] autocomplete request") {
+ SIGNAL_WATCH(code_edit, "code_completion_requested");
+ code_edit->set_code_completion_enabled(true);
+
+ Array signal_args;
+ signal_args.push_back(Array());
+
+ /* Force request. */
+ code_edit->request_code_completion();
+ SIGNAL_CHECK_FALSE("code_completion_requested");
+ code_edit->request_code_completion(true);
+ SIGNAL_CHECK("code_completion_requested", signal_args);
+
+ /* Manual request should force. */
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_query");
+ SIGNAL_CHECK("code_completion_requested", signal_args);
+
+ /* Insert prefix. */
+ TypedArray<String> completion_prefixes;
+ completion_prefixes.push_back(".");
+ code_edit->set_code_completion_prefixes(completion_prefixes);
+
+ code_edit->insert_text_at_caret(".");
+ code_edit->request_code_completion();
+ SIGNAL_CHECK("code_completion_requested", signal_args);
+
+ /* Should work with space too. */
+ code_edit->insert_text_at_caret(" ");
+ code_edit->request_code_completion();
+ SIGNAL_CHECK("code_completion_requested", signal_args);
+
+ /* Should work when complete ends with prefix. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("t");
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "test.", "test.");
+ code_edit->update_code_completion_options();
+ code_edit->confirm_code_completion();
+ CHECK(code_edit->get_line(0) == "test.");
+ SIGNAL_CHECK("code_completion_requested", signal_args);
+
+ SIGNAL_UNWATCH(code_edit, "code_completion_requested");
+ }
+
+ SUBCASE("[CodeEdit] autocomplete completion") {
+ if (TS->has_feature(TextServer::FEATURE_FONT_DYNAMIC) && TS->has_feature(TextServer::FEATURE_SIMPLE_LAYOUT)) {
+ CHECK(code_edit->get_code_completion_selected_index() == -1);
+ code_edit->set_code_completion_enabled(true);
+ CHECK(code_edit->get_code_completion_selected_index() == -1);
+
+ code_edit->update_code_completion_options();
+ code_edit->set_code_completion_selected_index(1);
+ CHECK(code_edit->get_code_completion_selected_index() == -1);
+ CHECK(code_edit->get_code_completion_option(0).size() == 0);
+ CHECK(code_edit->get_code_completion_options().size() == 0);
+
+ /* Adding does not update the list. */
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_0.", "item_0");
+
+ code_edit->set_code_completion_selected_index(1);
+ CHECK(code_edit->get_code_completion_selected_index() == -1);
+ CHECK(code_edit->get_code_completion_option(0).size() == 0);
+ CHECK(code_edit->get_code_completion_options().size() == 0);
+
+ /* After update, pending add should not be counted, */
+ /* also does not work on col 0 */
+ code_edit->insert_text_at_caret("i");
+ code_edit->update_code_completion_options();
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0", Color(1, 0, 0), Ref<Resource>(), Color(1, 0, 0));
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_1.", "item_1");
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_2.", "item_2");
+
+ ERR_PRINT_OFF;
+ code_edit->set_code_completion_selected_index(1);
+ ERR_PRINT_ON;
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+ CHECK(code_edit->get_code_completion_option(0).size() == 6);
+ CHECK(code_edit->get_code_completion_options().size() == 1);
+
+ /* Check cancel closes completion. */
+ SEND_GUI_ACTION(code_edit, "ui_cancel");
+ CHECK(code_edit->get_code_completion_selected_index() == -1);
+
+ code_edit->update_code_completion_options();
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+ code_edit->set_code_completion_selected_index(1);
+ CHECK(code_edit->get_code_completion_selected_index() == 1);
+ CHECK(code_edit->get_code_completion_option(0).size() == 6);
+ CHECK(code_edit->get_code_completion_options().size() == 3);
+
+ /* Check data. */
+ Dictionary option = code_edit->get_code_completion_option(0);
+ CHECK((int)option["kind"] == (int)CodeEdit::CodeCompletionKind::KIND_CLASS);
+ CHECK(option["display_text"] == "item_0.");
+ CHECK(option["insert_text"] == "item_0");
+ CHECK(option["font_color"] == Color(1, 0, 0));
+ CHECK(option["icon"] == Ref<Resource>());
+ CHECK(option["default_value"] == Color(1, 0, 0));
+
+ /* Set size for mouse input. */
+ code_edit->set_size(Size2(100, 100));
+
+ /* Check input. */
+ SEND_GUI_ACTION(code_edit, "ui_end");
+ CHECK(code_edit->get_code_completion_selected_index() == 2);
+
+ SEND_GUI_ACTION(code_edit, "ui_home");
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ SEND_GUI_ACTION(code_edit, "ui_page_down");
+ CHECK(code_edit->get_code_completion_selected_index() == 2);
+
+ SEND_GUI_ACTION(code_edit, "ui_page_up");
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ SEND_GUI_ACTION(code_edit, "ui_up");
+ CHECK(code_edit->get_code_completion_selected_index() == 2);
+
+ SEND_GUI_ACTION(code_edit, "ui_down");
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ SEND_GUI_KEY_EVENT(code_edit, Key::T);
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ SEND_GUI_ACTION(code_edit, "ui_left");
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ SEND_GUI_ACTION(code_edit, "ui_right");
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ Point2 caret_pos = code_edit->get_caret_draw_pos();
+ caret_pos.y -= code_edit->get_line_height();
+ SEND_GUI_MOUSE_BUTTON_EVENT(code_edit, caret_pos, MouseButton::WHEEL_DOWN, MouseButton::NONE, Key::NONE);
+ CHECK(code_edit->get_code_completion_selected_index() == 1);
+
+ SEND_GUI_MOUSE_BUTTON_EVENT(code_edit, caret_pos, MouseButton::WHEEL_UP, MouseButton::NONE, Key::NONE);
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ /* Single click selects. */
+ SEND_GUI_MOUSE_BUTTON_EVENT(code_edit, caret_pos, MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE);
+ CHECK(code_edit->get_code_completion_selected_index() == 2);
+
+ /* Double click inserts. */
+ SEND_GUI_DOUBLE_CLICK(code_edit, caret_pos, Key::NONE);
+ CHECK(code_edit->get_code_completion_selected_index() == -1);
+ CHECK(code_edit->get_line(0) == "item_2");
+
+ code_edit->set_auto_brace_completion_enabled(false);
+
+ /* Does nothing in readonly. */
+ code_edit->undo();
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+ code_edit->update_code_completion_options();
+ code_edit->set_editable(false);
+ code_edit->confirm_code_completion();
+ code_edit->set_editable(true);
+ CHECK(code_edit->get_line(0) == "i");
+
+ /* Replace */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0 test");
+
+ /* Replace string. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("\"item_1 test\"");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "\"item_0\"");
+
+ /* Normal replace if no end is given. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("\"item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "\"item_0\" test");
+
+ /* Insert at completion. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_accept");
+ CHECK(code_edit->get_line(0) == "item_01 test");
+
+ /* Insert at completion with string should have same output. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("\"item_1 test\"");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_accept");
+ CHECK(code_edit->get_line(0) == "\"item_0\"1 test\"");
+
+ /* Merge symbol at end on insert text. */
+ /* End on completion entry. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0(");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0( test");
+ CHECK(code_edit->get_caret_column() == 7);
+
+ /* End of text*/
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1( test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0( test");
+ CHECK(code_edit->get_caret_column() == 6);
+
+ /* End of both. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1( test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0(");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0( test");
+ CHECK(code_edit->get_caret_column() == 7);
+
+ /* Full set. */
+ /* End on completion entry. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 8);
+
+ /* End of text*/
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1() test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 6);
+
+ /* End of both. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1() test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 8);
+
+ /* Autobrace completion. */
+ code_edit->set_auto_brace_completion_enabled(true);
+
+ /* End on completion entry. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0(");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 7);
+
+ /* End of text*/
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1( test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0( test");
+ CHECK(code_edit->get_caret_column() == 6);
+
+ /* End of both. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1( test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0(");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0( test");
+ CHECK(code_edit->get_caret_column() == 7);
+
+ /* Full set. */
+ /* End on completion entry. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 8);
+
+ /* End of text*/
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1() test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 6);
+
+ /* End of both. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1() test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 8);
+ }
+ }
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] symbol lookup") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+ code_edit->grab_focus();
+
+ code_edit->set_symbol_lookup_on_click_enabled(true);
+ CHECK(code_edit->is_symbol_lookup_on_click_enabled());
+
+ if (TS->has_feature(TextServer::FEATURE_FONT_DYNAMIC) && TS->has_feature(TextServer::FEATURE_SIMPLE_LAYOUT)) {
+ /* Set size for mouse input. */
+ code_edit->set_size(Size2(100, 100));
+
+ code_edit->set_text("this is some text");
+
+ Point2 caret_pos = code_edit->get_caret_draw_pos();
+ caret_pos.x += 60;
+ SEND_GUI_MOUSE_BUTTON_EVENT(code_edit, caret_pos, MouseButton::NONE, MouseButton::NONE, Key::NONE);
+ CHECK(code_edit->get_text_for_symbol_lookup() == "this is s" + String::chr(0xFFFF) + "ome text");
+
+ SIGNAL_WATCH(code_edit, "symbol_validate");
+
+#ifdef MACOS_ENABLED
+ SEND_GUI_KEY_EVENT(code_edit, Key::META);
+#else
+ SEND_GUI_KEY_EVENT(code_edit, Key::CTRL);
+#endif
+
+ Array signal_args;
+ Array arg;
+ arg.push_back("some");
+ signal_args.push_back(arg);
+ SIGNAL_CHECK("symbol_validate", signal_args);
+
+ SIGNAL_UNWATCH(code_edit, "symbol_validate");
+
+ memdelete(code_edit);
+ }
+}
+
+TEST_CASE("[SceneTree][CodeEdit] line length guidelines") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+ code_edit->grab_focus();
+
+ TypedArray<int> guide_lines;
+
+ code_edit->set_line_length_guidelines(guide_lines);
+ CHECK(code_edit->get_line_length_guidelines().size() == 0);
+
+ guide_lines.push_back(80);
+ guide_lines.push_back(120);
+
+ /* Order should be preserved. */
+ code_edit->set_line_length_guidelines(guide_lines);
+ CHECK((int)code_edit->get_line_length_guidelines()[0] == 80);
+ CHECK((int)code_edit->get_line_length_guidelines()[1] == 120);
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] Backspace delete") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+ code_edit->grab_focus();
+
+ /* Backspace with selection on first line. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test backspace");
+ code_edit->select(0, 0, 0, 5);
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == "backspace");
+
+ /* Backspace with selection on first line and caret at the beginning of file. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test backspace");
+ code_edit->select(0, 0, 0, 5);
+ code_edit->set_caret_column(0);
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == "backspace");
+
+ /* Move caret up to the previous line on backspace if caret is at the first column. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("line 1\nline 2");
+ code_edit->set_caret_line(1);
+ code_edit->set_caret_column(0);
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == "line 1line 2");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 6);
+
+ /* Backspace delete all text if all text is selected. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("line 1\nline 2\nline 3");
+ code_edit->select_all();
+ code_edit->backspace();
+ CHECK(code_edit->get_text().is_empty());
+
+ /* Backspace at the beginning without selection has no effect. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("line 1\nline 2\nline 3");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(0);
+ code_edit->backspace();
+ CHECK(code_edit->get_text() == "line 1\nline 2\nline 3");
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] New Line") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+ code_edit->grab_focus();
+
+ /* Add a new line. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test new line");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(13);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test new line");
+ CHECK(code_edit->get_line(1) == "");
+
+ /* Split line with new line. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test new line");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test ");
+ CHECK(code_edit->get_line(1) == "new line");
+
+ /* Delete selection and split with new line. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test new line");
+ code_edit->select(0, 0, 0, 5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "");
+ CHECK(code_edit->get_line(1) == "new line");
+
+ /* Blank new line below with selection should not split. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test new line");
+ code_edit->select(0, 0, 0, 5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == "test new line");
+ CHECK(code_edit->get_line(1) == "");
+
+ /* Blank new line above with selection should not split. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test new line");
+ code_edit->select(0, 0, 0, 5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line(0) == "");
+ CHECK(code_edit->get_line(1) == "test new line");
+
+ memdelete(code_edit);
+}
+
+} // namespace TestCodeEdit
+
+#endif // TEST_CODE_EDIT_H
diff --git a/tests/scene/test_curve.h b/tests/scene/test_curve.h
new file mode 100644
index 0000000000..ad7625ddc5
--- /dev/null
+++ b/tests/scene/test_curve.h
@@ -0,0 +1,254 @@
+/*************************************************************************/
+/* test_curve.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_CURVE_H
+#define TEST_CURVE_H
+
+#include "scene/resources/curve.h"
+
+#include "tests/test_macros.h"
+
+namespace TestCurve {
+
+TEST_CASE("[Curve] Default curve") {
+ const Ref<Curve> curve = memnew(Curve);
+
+ CHECK_MESSAGE(
+ curve->get_point_count() == 0,
+ "Default curve should contain the expected number of points.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->sample(0)),
+ "Default curve should return the expected value at offset 0.0.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->sample(0.5)),
+ "Default curve should return the expected value at offset 0.5.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->sample(1)),
+ "Default curve should return the expected value at offset 1.0.");
+}
+
+TEST_CASE("[Curve] Custom curve with free tangents") {
+ Ref<Curve> curve = memnew(Curve);
+ // "Sawtooth" curve with an open ending towards the 1.0 offset.
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(0.25, 1));
+ curve->add_point(Vector2(0.5, 0));
+ curve->add_point(Vector2(0.75, 1));
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->get_point_left_tangent(0)),
+ "get_point_left_tangent() should return the expected value for point index 0.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->get_point_right_tangent(0)),
+ "get_point_right_tangent() should return the expected value for point index 0.");
+ CHECK_MESSAGE(
+ curve->get_point_left_mode(0) == Curve::TangentMode::TANGENT_FREE,
+ "get_point_left_mode() should return the expected value for point index 0.");
+ CHECK_MESSAGE(
+ curve->get_point_right_mode(0) == Curve::TangentMode::TANGENT_FREE,
+ "get_point_right_mode() should return the expected value for point index 0.");
+
+ CHECK_MESSAGE(
+ curve->get_point_count() == 4,
+ "Custom free curve should contain the expected number of points.");
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->sample(-0.1)),
+ "Custom free curve should return the expected value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample(0.1), (real_t)0.352),
+ "Custom free curve should return the expected value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample(0.4), (real_t)0.352),
+ "Custom free curve should return the expected value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample(0.7), (real_t)0.896),
+ "Custom free curve should return the expected value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample(1), 1),
+ "Custom free curve should return the expected value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample(2), 1),
+ "Custom free curve should return the expected value at offset 0.1.");
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->sample_baked(-0.1)),
+ "Custom free curve should return the expected baked value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample_baked(0.1), (real_t)0.352),
+ "Custom free curve should return the expected baked value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample_baked(0.4), (real_t)0.352),
+ "Custom free curve should return the expected baked value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample_baked(0.7), (real_t)0.896),
+ "Custom free curve should return the expected baked value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample_baked(1), 1),
+ "Custom free curve should return the expected baked value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample_baked(2), 1),
+ "Custom free curve should return the expected baked value at offset 0.1.");
+
+ curve->remove_point(1);
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample(0.1), 0),
+ "Custom free curve should return the expected value at offset 0.1 after removing point at index 1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample_baked(0.1), 0),
+ "Custom free curve should return the expected baked value at offset 0.1 after removing point at index 1.");
+
+ curve->clear_points();
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample(0.6), 0),
+ "Custom free curve should return the expected value at offset 0.6 after clearing all points.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample_baked(0.6), 0),
+ "Custom free curve should return the expected baked value at offset 0.6 after clearing all points.");
+}
+
+TEST_CASE("[Curve] Custom curve with linear tangents") {
+ Ref<Curve> curve = memnew(Curve);
+ // "Sawtooth" curve with an open ending towards the 1.0 offset.
+ curve->add_point(Vector2(0, 0), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR);
+ curve->add_point(Vector2(0.25, 1), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR);
+ curve->add_point(Vector2(0.5, 0), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR);
+ curve->add_point(Vector2(0.75, 1), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR);
+
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->get_point_left_tangent(3), 4),
+ "get_point_left_tangent() should return the expected value for point index 3.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->get_point_right_tangent(3)),
+ "get_point_right_tangent() should return the expected value for point index 3.");
+ CHECK_MESSAGE(
+ curve->get_point_left_mode(3) == Curve::TangentMode::TANGENT_LINEAR,
+ "get_point_left_mode() should return the expected value for point index 3.");
+ CHECK_MESSAGE(
+ curve->get_point_right_mode(3) == Curve::TangentMode::TANGENT_LINEAR,
+ "get_point_right_mode() should return the expected value for point index 3.");
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->get_point_right_tangent(300)),
+ "get_point_right_tangent() should return the expected value for invalid point index 300.");
+ CHECK_MESSAGE(
+ curve->get_point_left_mode(-12345) == Curve::TangentMode::TANGENT_FREE,
+ "get_point_left_mode() should return the expected value for invalid point index -12345.");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ curve->get_point_count() == 4,
+ "Custom linear curve should contain the expected number of points.");
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->sample(-0.1)),
+ "Custom linear curve should return the expected value at offset -0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample(0.1), (real_t)0.4),
+ "Custom linear curve should return the expected value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample(0.4), (real_t)0.4),
+ "Custom linear curve should return the expected value at offset 0.4.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample(0.7), (real_t)0.8),
+ "Custom linear curve should return the expected value at offset 0.7.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample(1), 1),
+ "Custom linear curve should return the expected value at offset 1.0.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample(2), 1),
+ "Custom linear curve should return the expected value at offset 2.0.");
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->sample_baked(-0.1)),
+ "Custom linear curve should return the expected baked value at offset -0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample_baked(0.1), (real_t)0.4),
+ "Custom linear curve should return the expected baked value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample_baked(0.4), (real_t)0.4),
+ "Custom linear curve should return the expected baked value at offset 0.4.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample_baked(0.7), (real_t)0.8),
+ "Custom linear curve should return the expected baked value at offset 0.7.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample_baked(1), 1),
+ "Custom linear curve should return the expected baked value at offset 1.0.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample_baked(2), 1),
+ "Custom linear curve should return the expected baked value at offset 2.0.");
+
+ ERR_PRINT_OFF;
+ curve->remove_point(10);
+ ERR_PRINT_ON;
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample(0.7), (real_t)0.8),
+ "Custom free curve should return the expected value at offset 0.7 after removing point at invalid index 10.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->sample_baked(0.7), (real_t)0.8),
+ "Custom free curve should return the expected baked value at offset 0.7 after removing point at invalid index 10.");
+}
+
+TEST_CASE("[Curve2D] Linear sampling should return exact value") {
+ Ref<Curve2D> curve = memnew(Curve2D);
+ real_t len = 2048.0;
+
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(len, 0));
+
+ real_t baked_length = curve->get_baked_length();
+ CHECK(len == baked_length);
+
+ for (int i = 0; i < len; i++) {
+ Vector2 pos = curve->sample_baked(i);
+ CHECK_MESSAGE(pos.x == i, "sample_baked should return exact value");
+ }
+}
+
+TEST_CASE("[Curve3D] Linear sampling should return exact value") {
+ Ref<Curve3D> curve = memnew(Curve3D);
+ real_t len = 2048.0;
+
+ curve->add_point(Vector3(0, 0, 0));
+ curve->add_point(Vector3(len, 0, 0));
+
+ real_t baked_length = curve->get_baked_length();
+ CHECK(len == baked_length);
+
+ for (int i = 0; i < len; i++) {
+ Vector3 pos = curve->sample_baked(i);
+ CHECK_MESSAGE(pos.x == i, "sample_baked should return exact value");
+ }
+}
+
+} // namespace TestCurve
+
+#endif // TEST_CURVE_H
diff --git a/tests/test_gradient.h b/tests/scene/test_gradient.h
index 88fe06b3ec..b0e6128932 100644
--- a/tests/test_gradient.h
+++ b/tests/scene/test_gradient.h
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -31,8 +31,6 @@
#ifndef TEST_GRADIENT_H
#define TEST_GRADIENT_H
-#include "core/class_db.h"
-#include "core/color.h"
#include "scene/resources/gradient.h"
#include "thirdparty/doctest/doctest.h"
@@ -146,7 +144,6 @@ TEST_CASE("[Gradient] Custom gradient (points specified out-of-order)") {
gradient->get_color_at_offset(0.1).is_equal_approx(Color(1, 0, 0)),
"Custom out-of-order gradient should return the expected interpolated value at offset 0.1 after removing point at index 0.");
}
-
} // namespace TestGradient
#endif // TEST_GRADIENT_H
diff --git a/tests/scene/test_path_3d.h b/tests/scene/test_path_3d.h
new file mode 100644
index 0000000000..8ac3d7b5b4
--- /dev/null
+++ b/tests/scene/test_path_3d.h
@@ -0,0 +1,84 @@
+/*************************************************************************/
+/* test_path_3d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_PATH_3D_H
+#define TEST_PATH_3D_H
+
+#include "scene/3d/path_3d.h"
+
+#include "tests/test_macros.h"
+
+namespace TestPath3D {
+
+TEST_CASE("[Path3D] Initialization") {
+ SUBCASE("Path should be empty right after initialization") {
+ Path3D *test_path = memnew(Path3D);
+ CHECK(test_path->get_curve() == nullptr);
+ memdelete(test_path);
+ }
+}
+
+TEST_CASE("[Path3D] Curve setter and getter") {
+ SUBCASE("Curve passed to the class should remain the same") {
+ Path3D *test_path = memnew(Path3D);
+ const Ref<Curve3D> &curve = memnew(Curve3D);
+
+ test_path->set_curve(curve);
+ CHECK(test_path->get_curve() == curve);
+ memdelete(test_path);
+ }
+ SUBCASE("Curve passed many times to the class should remain the same") {
+ Path3D *test_path = memnew(Path3D);
+ const Ref<Curve3D> &curve = memnew(Curve3D);
+
+ test_path->set_curve(curve);
+ test_path->set_curve(curve);
+ test_path->set_curve(curve);
+ CHECK(test_path->get_curve() == curve);
+ memdelete(test_path);
+ }
+ SUBCASE("Curve rewrite testing") {
+ Path3D *test_path = memnew(Path3D);
+ const Ref<Curve3D> &curve1 = memnew(Curve3D);
+ const Ref<Curve3D> &curve2 = memnew(Curve3D);
+
+ test_path->set_curve(curve1);
+ test_path->set_curve(curve2);
+ CHECK_MESSAGE(test_path->get_curve() != curve1,
+ "After rewrite, second curve should be in class");
+ CHECK_MESSAGE(test_path->get_curve() == curve2,
+ "After rewrite, second curve should be in class");
+ memdelete(test_path);
+ }
+}
+
+} // namespace TestPath3D
+
+#endif // TEST_PATH_3D_H
diff --git a/tests/scene/test_path_follow_2d.h b/tests/scene/test_path_follow_2d.h
new file mode 100644
index 0000000000..57261116a2
--- /dev/null
+++ b/tests/scene/test_path_follow_2d.h
@@ -0,0 +1,240 @@
+/*************************************************************************/
+/* test_path_follow_2d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_PATH_FOLLOW_2D_H
+#define TEST_PATH_FOLLOW_2D_H
+
+#include "scene/2d/path_2d.h"
+
+#include "tests/test_macros.h"
+
+namespace TestPathFollow2D {
+
+TEST_CASE("[PathFollow2D] Sampling with progress ratio") {
+ const Ref<Curve2D> &curve = memnew(Curve2D());
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(100, 0));
+ curve->add_point(Vector2(100, 100));
+ curve->add_point(Vector2(0, 100));
+ curve->add_point(Vector2(0, 0));
+ const Path2D *path = memnew(Path2D);
+ path->set_curve(curve);
+ const PathFollow2D *path_follow_2d = memnew(PathFollow2D);
+ path->add_child(path_follow_2d);
+
+ path_follow_2d->set_progress_ratio(0);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0)));
+
+ path_follow_2d->set_progress_ratio(0.125);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 0)));
+
+ path_follow_2d->set_progress_ratio(0.25);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 0)));
+
+ path_follow_2d->set_progress_ratio(0.375);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 50)));
+
+ path_follow_2d->set_progress_ratio(0.5);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 100)));
+
+ path_follow_2d->set_progress_ratio(0.625);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 100)));
+
+ path_follow_2d->set_progress_ratio(0.75);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 100)));
+
+ path_follow_2d->set_progress_ratio(0.875);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 50)));
+
+ path_follow_2d->set_progress_ratio(1);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0)));
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow2D] Sampling with progress") {
+ const Ref<Curve2D> &curve = memnew(Curve2D());
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(100, 0));
+ curve->add_point(Vector2(100, 100));
+ curve->add_point(Vector2(0, 100));
+ curve->add_point(Vector2(0, 0));
+ const Path2D *path = memnew(Path2D);
+ path->set_curve(curve);
+ const PathFollow2D *path_follow_2d = memnew(PathFollow2D);
+ path->add_child(path_follow_2d);
+
+ path_follow_2d->set_progress(0);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0)));
+
+ path_follow_2d->set_progress(50);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 0)));
+
+ path_follow_2d->set_progress(100);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 0)));
+
+ path_follow_2d->set_progress(150);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 50)));
+
+ path_follow_2d->set_progress(200);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 100)));
+
+ path_follow_2d->set_progress(250);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 100)));
+
+ path_follow_2d->set_progress(300);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 100)));
+
+ path_follow_2d->set_progress(350);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 50)));
+
+ path_follow_2d->set_progress(400);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0)));
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow2D] Removal of a point in curve") {
+ const Ref<Curve2D> &curve = memnew(Curve2D());
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(100, 0));
+ curve->add_point(Vector2(100, 100));
+ const Path2D *path = memnew(Path2D);
+ path->set_curve(curve);
+ const PathFollow2D *path_follow_2d = memnew(PathFollow2D);
+ path->add_child(path_follow_2d);
+
+ path_follow_2d->set_progress_ratio(0.5);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 0)));
+
+ curve->remove_point(1);
+
+ CHECK_MESSAGE(
+ path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 50)),
+ "Path follow's position should be updated after removing a point from the curve");
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow2D] Setting h_offset and v_offset") {
+ const Ref<Curve2D> &curve = memnew(Curve2D());
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(100, 0));
+ const Path2D *path = memnew(Path2D);
+ path->set_curve(curve);
+ const PathFollow2D *path_follow_2d = memnew(PathFollow2D);
+ path->add_child(path_follow_2d);
+
+ path_follow_2d->set_progress_ratio(0.5);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 0)));
+
+ path_follow_2d->set_h_offset(25);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(75, 0)));
+
+ path_follow_2d->set_v_offset(25);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(75, 25)));
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow2D] Unit offset out of range") {
+ const Ref<Curve2D> &curve = memnew(Curve2D());
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(100, 0));
+ const Path2D *path = memnew(Path2D);
+ path->set_curve(curve);
+ const PathFollow2D *path_follow_2d = memnew(PathFollow2D);
+ path->add_child(path_follow_2d);
+
+ path_follow_2d->set_loop(true);
+
+ path_follow_2d->set_progress_ratio(-0.3);
+ CHECK_MESSAGE(
+ path_follow_2d->get_progress_ratio() == 0.7,
+ "Progress Ratio should loop back from the end in the opposite direction");
+
+ path_follow_2d->set_progress_ratio(1.3);
+ CHECK_MESSAGE(
+ path_follow_2d->get_progress_ratio() == 0.3,
+ "Progress Ratio should loop back from the end in the opposite direction");
+
+ path_follow_2d->set_loop(false);
+
+ path_follow_2d->set_progress_ratio(-0.3);
+ CHECK_MESSAGE(
+ path_follow_2d->get_progress_ratio() == 0,
+ "Progress Ratio should be clamped at 0");
+
+ path_follow_2d->set_progress_ratio(1.3);
+ CHECK_MESSAGE(
+ path_follow_2d->get_progress_ratio() == 1,
+ "Progress Ratio should be clamped at 1");
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow2D] Progress out of range") {
+ const Ref<Curve2D> &curve = memnew(Curve2D());
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(100, 0));
+ const Path2D *path = memnew(Path2D);
+ path->set_curve(curve);
+ const PathFollow2D *path_follow_2d = memnew(PathFollow2D);
+ path->add_child(path_follow_2d);
+
+ path_follow_2d->set_loop(true);
+
+ path_follow_2d->set_progress(-50);
+ CHECK_MESSAGE(
+ path_follow_2d->get_progress() == 50,
+ "Progress should loop back from the end in the opposite direction");
+
+ path_follow_2d->set_progress(150);
+ CHECK_MESSAGE(
+ path_follow_2d->get_progress() == 50,
+ "Progress should loop back from the end in the opposite direction");
+
+ path_follow_2d->set_loop(false);
+
+ path_follow_2d->set_progress(-50);
+ CHECK_MESSAGE(
+ path_follow_2d->get_progress() == 0,
+ "Progress should be clamped at 0");
+
+ path_follow_2d->set_progress(150);
+ CHECK_MESSAGE(
+ path_follow_2d->get_progress() == 100,
+ "Progress should be clamped at 1");
+
+ memdelete(path);
+}
+} // namespace TestPathFollow2D
+
+#endif // TEST_PATH_FOLLOW_2D_H
diff --git a/tests/scene/test_path_follow_3d.h b/tests/scene/test_path_follow_3d.h
new file mode 100644
index 0000000000..6334fa56de
--- /dev/null
+++ b/tests/scene/test_path_follow_3d.h
@@ -0,0 +1,219 @@
+/*************************************************************************/
+/* test_path_follow_3d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_PATH_FOLLOW_3D_H
+#define TEST_PATH_FOLLOW_3D_H
+
+#include "scene/3d/path_3d.h"
+
+#include "tests/test_macros.h"
+
+namespace TestPathFollow3D {
+
+TEST_CASE("[PathFollow3D] Sampling with progress ratio") {
+ const Ref<Curve3D> &curve = memnew(Curve3D());
+ curve->add_point(Vector3(0, 0, 0));
+ curve->add_point(Vector3(100, 0, 0));
+ curve->add_point(Vector3(100, 100, 0));
+ curve->add_point(Vector3(100, 100, 100));
+ curve->add_point(Vector3(100, 0, 100));
+ const Path3D *path = memnew(Path3D);
+ path->set_curve(curve);
+ const PathFollow3D *path_follow_3d = memnew(PathFollow3D);
+ path->add_child(path_follow_3d);
+
+ path_follow_3d->set_progress_ratio(0);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(0, 0, 0));
+
+ path_follow_3d->set_progress_ratio(0.125);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(50, 0, 0));
+
+ path_follow_3d->set_progress_ratio(0.25);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 0, 0);
+
+ path_follow_3d->set_progress_ratio(0.375);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 50, 0)));
+
+ path_follow_3d->set_progress_ratio(0.5);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 0)));
+
+ path_follow_3d->set_progress_ratio(0.625);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 50)));
+
+ path_follow_3d->set_progress_ratio(0.75);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 100)));
+
+ path_follow_3d->set_progress_ratio(0.875);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 50, 100)));
+
+ path_follow_3d->set_progress_ratio(1);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 0, 100)));
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow3D] Sampling with progress") {
+ const Ref<Curve3D> &curve = memnew(Curve3D());
+ curve->add_point(Vector3(0, 0, 0));
+ curve->add_point(Vector3(100, 0, 0));
+ curve->add_point(Vector3(100, 100, 0));
+ curve->add_point(Vector3(100, 100, 100));
+ curve->add_point(Vector3(100, 0, 100));
+ const Path3D *path = memnew(Path3D);
+ path->set_curve(curve);
+ const PathFollow3D *path_follow_3d = memnew(PathFollow3D);
+ path->add_child(path_follow_3d);
+
+ path_follow_3d->set_progress(0);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(0, 0, 0));
+
+ path_follow_3d->set_progress(50);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(50, 0, 0));
+
+ path_follow_3d->set_progress(100);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 0, 0);
+
+ path_follow_3d->set_progress(150);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 50, 0)));
+
+ path_follow_3d->set_progress(200);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 0)));
+
+ path_follow_3d->set_progress(250);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 50)));
+
+ path_follow_3d->set_progress(300);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 100)));
+
+ path_follow_3d->set_progress(350);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 50, 100)));
+
+ path_follow_3d->set_progress(400);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 0, 100)));
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow3D] Removal of a point in curve") {
+ const Ref<Curve3D> &curve = memnew(Curve3D());
+ curve->add_point(Vector3(0, 0, 0));
+ curve->add_point(Vector3(100, 0, 0));
+ curve->add_point(Vector3(100, 100, 0));
+ const Path3D *path = memnew(Path3D);
+ path->set_curve(curve);
+ const PathFollow3D *path_follow_3d = memnew(PathFollow3D);
+ path->add_child(path_follow_3d);
+
+ path_follow_3d->set_progress_ratio(0.5);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector2(100, 0, 0)));
+
+ curve->remove_point(1);
+
+ CHECK_MESSAGE(
+ path_follow_3d->get_transform().get_origin().is_equal_approx(Vector2(50, 50, 0)),
+ "Path follow's position should be updated after removing a point from the curve");
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow3D] Progress ratio out of range") {
+ const Ref<Curve3D> &curve = memnew(Curve3D());
+ curve->add_point(Vector3(0, 0, 0));
+ curve->add_point(Vector3(100, 0, 0));
+ const Path3D *path = memnew(Path3D);
+ path->set_curve(curve);
+ const PathFollow3D *path_follow_3d = memnew(PathFollow3D);
+ path->add_child(path_follow_3d);
+
+ path_follow_3d->set_loop(true);
+
+ path_follow_3d->set_progress_ratio(-0.3);
+ CHECK_MESSAGE(
+ path_follow_3d->get_progress_ratio() == 0.7,
+ "Progress Ratio should loop back from the end in the opposite direction");
+
+ path_follow_3d->set_progress_ratio(1.3);
+ CHECK_MESSAGE(
+ path_follow_3d->get_progress_ratio() == 0.3,
+ "Progress Ratio should loop back from the end in the opposite direction");
+
+ path_follow_3d->set_loop(false);
+
+ path_follow_3d->set_progress_ratio(-0.3);
+ CHECK_MESSAGE(
+ path_follow_3d->get_progress_ratio() == 0,
+ "Progress Ratio should be clamped at 0");
+
+ path_follow_3d->set_progress_ratio(1.3);
+ CHECK_MESSAGE(
+ path_follow_3d->get_progress_ratio() == 1,
+ "Progress Ratio should be clamped at 1");
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow3D] Progress out of range") {
+ const Ref<Curve3D> &curve = memnew(Curve3D());
+ curve->add_point(Vector3(0, 0, 0));
+ curve->add_point(Vector3(100, 0, 0));
+ const Path3D *path = memnew(Path3D);
+ path->set_curve(curve);
+ const PathFollow3D *path_follow_3d = memnew(PathFollow3D);
+ path->add_child(path_follow_3d);
+
+ path_follow_3d->set_loop(true);
+
+ path_follow_3d->set_progress(-50);
+ CHECK_MESSAGE(
+ path_follow_3d->get_progress() == 50,
+ "Progress should loop back from the end in the opposite direction");
+
+ path_follow_3d->set_progress(150);
+ CHECK_MESSAGE(
+ path_follow_3d->get_progress() == 50,
+ "Progress should loop back from the end in the opposite direction");
+
+ path_follow_3d->set_loop(false);
+
+ path_follow_3d->set_progress(-50);
+ CHECK_MESSAGE(
+ path_follow_3d->get_progress() == 0,
+ "Progress should be clamped at 0");
+
+ path_follow_3d->set_progress(150);
+ CHECK_MESSAGE(
+ path_follow_3d->get_progress() == 100,
+ "Progress should be clamped at max value of curve");
+
+ memdelete(path);
+}
+} // namespace TestPathFollow3D
+
+#endif // TEST_PATH_FOLLOW_3D_H
diff --git a/tests/scene/test_sprite_frames.h b/tests/scene/test_sprite_frames.h
new file mode 100644
index 0000000000..61bbd16493
--- /dev/null
+++ b/tests/scene/test_sprite_frames.h
@@ -0,0 +1,247 @@
+/*************************************************************************/
+/* test_sprite_frames.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_SPRITE_FRAMES_H
+#define TEST_SPRITE_FRAMES_H
+
+#include "scene/resources/sprite_frames.h"
+
+#include "tests/test_macros.h"
+
+namespace TestSpriteFrames {
+const String test_animation_name = "GodotTest";
+
+TEST_CASE("[SpriteFrames] Constructor methods") {
+ const SpriteFrames frames;
+ CHECK_MESSAGE(
+ frames.get_animation_names().size() == 1,
+ "Should be initialized with 1 entry.");
+ CHECK_MESSAGE(
+ frames.get_animation_names().get(0) == "default",
+ "Should be initialized with default entry.");
+}
+
+TEST_CASE("[SpriteFrames] Animation addition, list getter, renaming, removal, and retrieval") {
+ SpriteFrames frames;
+ Vector<String> test_names = { "default", "2", "1", "3" };
+
+ // "default" is there already
+ frames.add_animation("2");
+ frames.add_animation("1");
+ frames.add_animation("3");
+
+ for (int i = 0; i < test_names.size(); i++) {
+ CHECK_MESSAGE(
+ frames.has_animation(test_names[i]),
+ "Add animation properly worked for each value");
+ }
+
+ CHECK_MESSAGE(
+ !frames.has_animation("999"),
+ "Return false when checking for animation that does not exist");
+
+ List<StringName> sname_list;
+ frames.get_animation_list(&sname_list);
+
+ CHECK_MESSAGE(
+ sname_list.size() == test_names.size(),
+ "StringName List getter returned list of expected size");
+
+ for (int i = 0; i < test_names.size(); i++) {
+ CHECK_MESSAGE(
+ sname_list[i] == StringName(test_names[i]),
+ "StringName List getter returned expected values");
+ }
+
+ // get_animation_names() sorts the results.
+ Vector<String> string_vector = frames.get_animation_names();
+ test_names.sort();
+
+ for (int i = 0; i < test_names.size(); i++) {
+ CHECK_MESSAGE(
+ string_vector[i] == test_names[i],
+ "String Vector getter returned expected values");
+ }
+
+ // These error handling cases should not crash.
+ ERR_PRINT_OFF;
+ frames.rename_animation("This does not exist", "0");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ !frames.has_animation("0"),
+ "Correctly handles rename error when entry does not exist");
+
+ // These error handling cases should not crash.
+ ERR_PRINT_OFF;
+ frames.rename_animation("3", "1");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ frames.has_animation("3"),
+ "Correctly handles rename error when entry exists, but new name already exists");
+
+ ERR_PRINT_OFF;
+ frames.add_animation("1");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ frames.get_animation_names().size() == 4,
+ "Correctly does not add when entry already exists");
+
+ frames.rename_animation("3", "9");
+
+ CHECK_MESSAGE(
+ frames.has_animation("9"),
+ "Animation renamed correctly");
+
+ frames.remove_animation("9");
+
+ CHECK_MESSAGE(
+ !frames.has_animation("9"),
+ "Animation removed correctly");
+
+ frames.clear_all();
+
+ CHECK_MESSAGE(
+ frames.get_animation_names().size() == 1,
+ "Clear all removed all animations and re-added the default animation entry");
+}
+
+TEST_CASE("[SpriteFrames] Animation Speed getter and setter") {
+ SpriteFrames frames;
+
+ frames.add_animation(test_animation_name);
+
+ CHECK_MESSAGE(
+ frames.get_animation_speed(test_animation_name) == 5.0,
+ "Sets new animation to default speed");
+
+ frames.set_animation_speed(test_animation_name, 123.0004);
+
+ CHECK_MESSAGE(
+ frames.get_animation_speed(test_animation_name) == 123.0004,
+ "Sets animation to positive double");
+
+ // These error handling cases should not crash.
+ ERR_PRINT_OFF;
+ frames.get_animation_speed("This does not exist");
+ frames.set_animation_speed("This does not exist", 100);
+ frames.set_animation_speed(test_animation_name, -999.999);
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ frames.get_animation_speed(test_animation_name) == 123.0004,
+ "Prevents speed of animation being set to a negative value");
+
+ frames.set_animation_speed(test_animation_name, 0.0);
+
+ CHECK_MESSAGE(
+ frames.get_animation_speed(test_animation_name) == 0.0,
+ "Sets animation to zero");
+}
+
+TEST_CASE("[SpriteFrames] Animation Loop getter and setter") {
+ SpriteFrames frames;
+
+ frames.add_animation(test_animation_name);
+
+ CHECK_MESSAGE(
+ frames.get_animation_loop(test_animation_name),
+ "Sets new animation to default loop value.");
+
+ frames.set_animation_loop(test_animation_name, true);
+
+ CHECK_MESSAGE(
+ frames.get_animation_loop(test_animation_name),
+ "Sets animation loop to true");
+
+ frames.set_animation_loop(test_animation_name, false);
+
+ CHECK_MESSAGE(
+ !frames.get_animation_loop(test_animation_name),
+ "Sets animation loop to false");
+
+ // These error handling cases should not crash.
+ ERR_PRINT_OFF;
+ frames.get_animation_loop("This does not exist");
+ frames.set_animation_loop("This does not exist", false);
+ ERR_PRINT_ON;
+}
+
+// TODO
+TEST_CASE("[SpriteFrames] Frame addition, removal, and retrieval") {
+ Ref<Texture2D> dummy_frame1;
+ dummy_frame1.instantiate();
+
+ SpriteFrames frames;
+ frames.add_animation(test_animation_name);
+ frames.add_animation("1");
+ frames.add_animation("2");
+
+ CHECK_MESSAGE(
+ frames.get_frame_count(test_animation_name) == 0,
+ "Animation has a default frame count of 0");
+
+ frames.add_frame(test_animation_name, dummy_frame1, 0);
+ frames.add_frame(test_animation_name, dummy_frame1, 1);
+ frames.add_frame(test_animation_name, dummy_frame1, 2);
+
+ CHECK_MESSAGE(
+ frames.get_frame_count(test_animation_name) == 3,
+ "Adds multiple frames");
+
+ frames.remove_frame(test_animation_name, 1);
+ frames.remove_frame(test_animation_name, 0);
+
+ CHECK_MESSAGE(
+ frames.get_frame_count(test_animation_name) == 1,
+ "Removes multiple frames");
+
+ // These error handling cases should not crash.
+ ERR_PRINT_OFF;
+ frames.add_frame("does not exist", dummy_frame1, 0);
+ frames.remove_frame(test_animation_name, -99);
+ frames.remove_frame("does not exist", 0);
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ frames.get_frame_count(test_animation_name) == 1,
+ "Handles bad values when adding or removing frames.");
+
+ frames.clear(test_animation_name);
+
+ CHECK_MESSAGE(
+ frames.get_frame_count(test_animation_name) == 0,
+ "Clears frames.");
+}
+} // namespace TestSpriteFrames
+
+#endif // TEST_SPRITE_FRAMES_H
diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h
new file mode 100644
index 0000000000..652cbed6e8
--- /dev/null
+++ b/tests/scene/test_text_edit.h
@@ -0,0 +1,4264 @@
+/*************************************************************************/
+/* test_text_edit.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_TEXT_EDIT_H
+#define TEST_TEXT_EDIT_H
+
+#include "scene/gui/text_edit.h"
+
+#include "tests/test_macros.h"
+
+namespace TestTextEdit {
+
+TEST_CASE("[SceneTree][TextEdit] text entry") {
+ TextEdit *text_edit = memnew(TextEdit);
+ SceneTree::get_singleton()->get_root()->add_child(text_edit);
+ text_edit->grab_focus();
+
+ Array empty_signal_args;
+ empty_signal_args.push_back(Array());
+
+ SUBCASE("[TextEdit] text entry") {
+ SIGNAL_WATCH(text_edit, "text_set");
+ SIGNAL_WATCH(text_edit, "text_changed");
+ SIGNAL_WATCH(text_edit, "lines_edited_from");
+ SIGNAL_WATCH(text_edit, "caret_changed");
+
+ Array args1;
+ args1.push_back(0);
+ args1.push_back(0);
+ Array lines_edited_args;
+ lines_edited_args.push_back(args1);
+ lines_edited_args.push_back(args1.duplicate());
+
+ SUBCASE("[TextEdit] clear and set text") {
+ // "text_changed" should not be emitted on clear / set.
+ text_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "");
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_line_count() == 1);
+ SIGNAL_CHECK("text_set", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+
+ text_edit->set_text("test text");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test text");
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_line_count() == 1);
+ SIGNAL_CHECK("text_set", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+
+ text_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "");
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK("text_set", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+
+ // Can undo / redo words when editable.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test text");
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "");
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Cannot undo when not-editable but should still clear.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test text");
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Clear.
+ text_edit->set_editable(false);
+
+ Array lines_edited_clear_args;
+ Array new_args = args1.duplicate();
+ new_args[0] = 1;
+ lines_edited_clear_args.push_back(new_args);
+
+ text_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "");
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK("text_set", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_clear_args);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+
+ text_edit->set_editable(true);
+
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "");
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK_FALSE("text_set");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("caret_changed");
+
+ // Can still undo set_text.
+ text_edit->set_editable(false);
+
+ text_edit->set_text("test text");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test text");
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK("text_set", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+
+ text_edit->set_editable(true);
+
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "");
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Any selections are removed.
+ text_edit->set_text("test text");
+ MessageQueue::get_singleton()->flush();
+ text_edit->select_all();
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test text");
+ CHECK(text_edit->get_caret_column() == 9);
+ CHECK(text_edit->has_selection());
+ SIGNAL_CHECK("text_set", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+
+ text_edit->set_text("test");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test");
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection());
+ SIGNAL_CHECK("text_set", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+
+ text_edit->select_all();
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ CHECK(text_edit->has_selection());
+
+ text_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "");
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection());
+ SIGNAL_CHECK("text_set", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ }
+
+ SUBCASE("[TextEdit] set and get line") {
+ // Set / Get line is 0 indexed.
+ text_edit->set_line(1, "test");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ SIGNAL_CHECK_FALSE("text_set");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("caret_changed");
+
+ text_edit->set_line(0, "test");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "test");
+ CHECK(text_edit->get_line(0) == "test");
+ CHECK(text_edit->get_line(1) == "");
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ SIGNAL_CHECK_FALSE("caret_changed");
+
+ // Setting to a longer line, caret and selections should be preserved.
+ text_edit->select_all();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->has_selection());
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+
+ text_edit->set_line(0, "test text");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_line(0) == "test text");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "test");
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Setting to a shorter line, selection and caret should be adjusted. Also works if not editable.
+ text_edit->set_editable(false);
+ text_edit->set_line(0, "te");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_line(0) == "te");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "te");
+ CHECK(text_edit->get_caret_column() == 2);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->set_editable(true);
+
+ // Undo / redo should work.
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_line(0) == "test text");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "test");
+ CHECK(text_edit->get_caret_column() == 4);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_line(0) == "te");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_caret_column() == 2);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Out of range.
+ ERR_PRINT_OFF;
+ text_edit->set_line(-1, "test");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_line(0) == "te");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+
+ text_edit->set_line(1, "test");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_line(0) == "te");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[TextEdit] swap lines") {
+ ((Array)lines_edited_args[1])[1] = 1;
+
+ text_edit->set_text("testing\nswap");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "testing\nswap");
+ SIGNAL_CHECK("text_set", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+
+ text_edit->set_caret_column(text_edit->get_line(0).length());
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+
+ ((Array)lines_edited_args[1])[1] = 0;
+ Array swap_args;
+ swap_args.push_back(1);
+ swap_args.push_back(1);
+ lines_edited_args.push_back(swap_args);
+ lines_edited_args.push_back(swap_args);
+
+ // Order does not matter. Should also work if not editable.
+ text_edit->set_editable(false);
+ text_edit->swap_lines(1, 0);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "swap\ntesting");
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->set_editable(true);
+
+ lines_edited_args.reverse();
+
+ // Single undo/redo action
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "testing\nswap");
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ lines_edited_args.reverse();
+
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "swap\ntesting");
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Out of range.
+ ERR_PRINT_OFF;
+ text_edit->swap_lines(-1, 0);
+ CHECK(text_edit->get_text() == "swap\ntesting");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+
+ text_edit->swap_lines(0, -1);
+ CHECK(text_edit->get_text() == "swap\ntesting");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+
+ text_edit->swap_lines(2, 0);
+ CHECK(text_edit->get_text() == "swap\ntesting");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+
+ text_edit->swap_lines(0, 2);
+ CHECK(text_edit->get_text() == "swap\ntesting");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[TextEdit] insert line at") {
+ ((Array)lines_edited_args[1])[1] = 1;
+
+ text_edit->set_text("testing\nswap");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "testing\nswap");
+ SIGNAL_CHECK("text_set", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+
+ text_edit->select_all();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selection_from_line() == 0);
+ CHECK(text_edit->get_selection_to_line() == 1);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+
+ // insert before should move caret and selecion, and works when not editable.
+ text_edit->set_editable(false);
+ lines_edited_args.remove_at(0);
+ text_edit->insert_line_at(0, "new");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "new\ntesting\nswap");
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_caret_column() == text_edit->get_line(2).size() - 1);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selection_from_line() == 1);
+ CHECK(text_edit->get_selection_to_line() == 2);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->set_editable(true);
+
+ // can undo/redo as single action
+ ((Array)lines_edited_args[0])[0] = 1;
+ ((Array)lines_edited_args[0])[1] = 0;
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "testing\nswap");
+ CHECK(text_edit->has_selection());
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ ((Array)lines_edited_args[0])[0] = 0;
+ ((Array)lines_edited_args[0])[1] = 1;
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "new\ntesting\nswap");
+ CHECK(text_edit->has_selection());
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Adding inside selection extends selection.
+ text_edit->select_all();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selection_from_line() == 0);
+ CHECK(text_edit->get_selection_to_line() == 2);
+ SIGNAL_CHECK_FALSE("caret_changed");
+
+ ((Array)lines_edited_args[0])[0] = 2;
+ ((Array)lines_edited_args[0])[1] = 3;
+ text_edit->insert_line_at(2, "after");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap");
+ CHECK(text_edit->get_caret_line() == 3);
+ CHECK(text_edit->get_caret_column() == text_edit->get_line(3).size() - 1);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selection_from_line() == 0);
+ CHECK(text_edit->get_selection_to_line() == 3);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ // Out of range.
+ ERR_PRINT_OFF;
+ text_edit->insert_line_at(-1, "after");
+ CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+
+ text_edit->insert_line_at(4, "after");
+ CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("text_set");
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[TextEdit] insert line at caret") {
+ lines_edited_args.pop_back();
+ ((Array)lines_edited_args[0])[1] = 1;
+
+ text_edit->insert_text_at_caret("testing\nswap");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "testing\nswap");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == text_edit->get_line(1).size() - 1);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ text_edit->set_caret_line(0, false);
+ text_edit->set_caret_column(2);
+ SIGNAL_DISCARD("caret_changed");
+
+ ((Array)lines_edited_args[0])[1] = 0;
+ text_edit->insert_text_at_caret("mid");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "temidsting\nswap");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 5);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ text_edit->select(0, 0, 0, text_edit->get_line(0).length());
+ CHECK(text_edit->has_selection());
+ lines_edited_args.push_back(args1.duplicate());
+
+ text_edit->set_editable(false);
+ text_edit->insert_text_at_caret("new line");
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "new line\nswap");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == text_edit->get_line(0).size() - 1);
+ CHECK_FALSE(text_edit->has_selection());
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ text_edit->set_editable(true);
+
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "temidsting\nswap");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 5);
+ CHECK(text_edit->has_selection());
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "new line\nswap");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 8);
+ CHECK_FALSE(text_edit->has_selection());
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_set");
+ }
+
+ SIGNAL_UNWATCH(text_edit, "text_set");
+ SIGNAL_UNWATCH(text_edit, "text_changed");
+ SIGNAL_UNWATCH(text_edit, "lines_edited_from");
+ SIGNAL_UNWATCH(text_edit, "caret_changed");
+ }
+
+ SUBCASE("[TextEdit] indent level") {
+ CHECK(text_edit->get_indent_level(0) == 0);
+ CHECK(text_edit->get_first_non_whitespace_column(0) == 0);
+
+ text_edit->set_line(0, "a");
+ CHECK(text_edit->get_indent_level(0) == 0);
+ CHECK(text_edit->get_first_non_whitespace_column(0) == 0);
+
+ text_edit->set_line(0, "\t");
+ CHECK(text_edit->get_indent_level(0) == 4);
+ CHECK(text_edit->get_first_non_whitespace_column(0) == 1);
+
+ text_edit->set_tab_size(8);
+ CHECK(text_edit->get_indent_level(0) == 8);
+
+ text_edit->set_line(0, "\t a");
+ CHECK(text_edit->get_first_non_whitespace_column(0) == 2);
+ CHECK(text_edit->get_indent_level(0) == 9);
+ }
+
+ SUBCASE("[TextEdit] selection") {
+ SIGNAL_WATCH(text_edit, "text_set");
+ SIGNAL_WATCH(text_edit, "text_changed");
+ SIGNAL_WATCH(text_edit, "lines_edited_from");
+ SIGNAL_WATCH(text_edit, "caret_changed");
+
+ Array args1;
+ args1.push_back(0);
+ args1.push_back(0);
+ Array lines_edited_args;
+ lines_edited_args.push_back(args1);
+ lines_edited_args.push_back(args1.duplicate());
+
+ SUBCASE("[TextEdit] select all") {
+ text_edit->select_all();
+ CHECK_FALSE(text_edit->has_selection());
+ ERR_PRINT_OFF;
+ CHECK(text_edit->get_selection_from_line() == -1);
+ CHECK(text_edit->get_selection_from_column() == -1);
+ CHECK(text_edit->get_selection_to_line() == -1);
+ CHECK(text_edit->get_selection_to_column() == -1);
+ CHECK(text_edit->get_selected_text() == "");
+ ERR_PRINT_ON;
+
+ text_edit->set_text("test\nselection");
+ SEND_GUI_ACTION(text_edit, "ui_text_select_all");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_selected_text() == "test\nselection");
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selection_from_line() == 0);
+ CHECK(text_edit->get_selection_from_column() == 0);
+ CHECK(text_edit->get_selection_to_line() == 1);
+ CHECK(text_edit->get_selection_to_column() == 9);
+ CHECK(text_edit->get_selection_mode() == TextEdit::SelectionMode::SELECTION_MODE_SHIFT);
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 9);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+ text_edit->set_selecting_enabled(false);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+
+ text_edit->select_all();
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ }
+
+ SUBCASE("[TextEdit] select word under caret") {
+ text_edit->set_text("\ntest test\ntest test");
+
+ text_edit->set_caret_column(0);
+ text_edit->set_caret_line(1);
+
+ text_edit->add_caret(2, 0);
+ text_edit->add_caret(2, 2);
+ CHECK(text_edit->get_caret_count() == 3);
+
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->select_word_under_caret();
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "test");
+ CHECK(text_edit->get_selection_from_line(0) == 1);
+ CHECK(text_edit->get_selection_from_column(0) == 0);
+ CHECK(text_edit->get_selection_to_line(0) == 1);
+ CHECK(text_edit->get_selection_to_column(0) == 4);
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 4);
+
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == "test");
+ CHECK(text_edit->get_selection_from_line(1) == 2);
+ CHECK(text_edit->get_selection_from_column(1) == 0);
+ CHECK(text_edit->get_selection_to_line(1) == 2);
+ CHECK(text_edit->get_selection_to_column(1) == 4);
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 4);
+
+ CHECK(text_edit->get_caret_count() == 2);
+
+ text_edit->select_word_under_caret();
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "");
+
+ SEND_GUI_ACTION(text_edit, "ui_text_select_word_under_caret");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "test");
+ CHECK(text_edit->get_selection_from_line(0) == 1);
+ CHECK(text_edit->get_selection_from_column(0) == 0);
+ CHECK(text_edit->get_selection_to_line(0) == 1);
+ CHECK(text_edit->get_selection_to_column(0) == 4);
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 4);
+
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == "test");
+ CHECK(text_edit->get_selection_from_line(1) == 2);
+ CHECK(text_edit->get_selection_from_column(1) == 0);
+ CHECK(text_edit->get_selection_to_line(1) == 2);
+ CHECK(text_edit->get_selection_to_column(1) == 4);
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 4);
+
+ CHECK(text_edit->get_selected_text() == "test\ntest");
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+
+ text_edit->set_selecting_enabled(false);
+ text_edit->select_word_under_caret();
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "");
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 4);
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ text_edit->set_selecting_enabled(true);
+
+ text_edit->set_caret_line(1, false, true, 0, 0);
+ text_edit->set_caret_column(5, false, 0);
+
+ text_edit->set_caret_line(2, false, true, 0, 1);
+ text_edit->set_caret_column(5, false, 1);
+
+ text_edit->select_word_under_caret();
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "");
+
+ text_edit->select_word_under_caret();
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "");
+ CHECK(text_edit->get_caret_line(0) == 1);
+ CHECK(text_edit->get_caret_column(0) == 5);
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 5);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ }
+
+ SUBCASE("[TextEdit] add selection for next occurrence") {
+ text_edit->set_text("\ntest other_test\nrandom test\nword test word");
+ text_edit->set_caret_column(0);
+ text_edit->set_caret_line(1);
+
+ text_edit->select_word_under_caret();
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "test");
+
+ text_edit->add_selection_for_next_occurrence();
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_selected_text(1) == "test");
+ CHECK(text_edit->get_selection_from_line(1) == 1);
+ CHECK(text_edit->get_selection_from_column(1) == 13);
+ CHECK(text_edit->get_selection_to_line(1) == 1);
+ CHECK(text_edit->get_selection_to_column(1) == 17);
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 17);
+
+ text_edit->add_selection_for_next_occurrence();
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->get_selected_text(2) == "test");
+ CHECK(text_edit->get_selection_from_line(2) == 2);
+ CHECK(text_edit->get_selection_from_column(2) == 9);
+ CHECK(text_edit->get_selection_to_line(2) == 2);
+ CHECK(text_edit->get_selection_to_column(2) == 13);
+ CHECK(text_edit->get_caret_line(2) == 2);
+ CHECK(text_edit->get_caret_column(2) == 13);
+
+ text_edit->add_selection_for_next_occurrence();
+ CHECK(text_edit->get_caret_count() == 4);
+ CHECK(text_edit->get_selected_text(3) == "test");
+ CHECK(text_edit->get_selection_from_line(3) == 3);
+ CHECK(text_edit->get_selection_from_column(3) == 5);
+ CHECK(text_edit->get_selection_to_line(3) == 3);
+ CHECK(text_edit->get_selection_to_column(3) == 9);
+ CHECK(text_edit->get_caret_line(3) == 3);
+ CHECK(text_edit->get_caret_column(3) == 9);
+ }
+
+ SUBCASE("[TextEdit] deselect on focus loss") {
+ text_edit->set_text("test");
+
+ text_edit->set_deselect_on_focus_loss_enabled(true);
+ CHECK(text_edit->is_deselect_on_focus_loss_enabled());
+
+ text_edit->grab_focus();
+ text_edit->select_all();
+ CHECK(text_edit->has_focus());
+ CHECK(text_edit->has_selection());
+
+ text_edit->release_focus();
+ CHECK_FALSE(text_edit->has_focus());
+ CHECK_FALSE(text_edit->has_selection());
+
+ text_edit->set_deselect_on_focus_loss_enabled(false);
+ CHECK_FALSE(text_edit->is_deselect_on_focus_loss_enabled());
+
+ text_edit->grab_focus();
+ text_edit->select_all();
+ CHECK(text_edit->has_focus());
+ CHECK(text_edit->has_selection());
+
+ text_edit->release_focus();
+ CHECK_FALSE(text_edit->has_focus());
+ CHECK(text_edit->has_selection());
+
+ text_edit->set_deselect_on_focus_loss_enabled(true);
+ CHECK_FALSE(text_edit->has_selection());
+ }
+
+ SUBCASE("[TextEdit] key select") {
+ text_edit->set_text("test");
+
+ text_edit->grab_focus();
+ SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::SHIFT)
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "t");
+
+#ifdef MACOS_ENABLED
+ SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::SHIFT | KeyModifierMask::ALT)
+#else
+ SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::SHIFT | KeyModifierMask::CMD_OR_CTRL)
+#endif
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "test");
+
+ SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::SHIFT)
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "tes");
+
+#ifdef MACOS_ENABLED
+ SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::SHIFT | KeyModifierMask::ALT)
+#else
+ SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::SHIFT | KeyModifierMask::CMD_OR_CTRL)
+#endif
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "");
+
+ SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::SHIFT)
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "t");
+
+ SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT)
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "");
+
+ SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::SHIFT)
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "t");
+
+ SEND_GUI_KEY_EVENT(text_edit, Key::LEFT)
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "");
+
+ text_edit->set_selecting_enabled(false);
+ SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::SHIFT)
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "");
+ text_edit->set_selecting_enabled(true);
+ }
+
+ SUBCASE("[TextEdit] mouse drag select") {
+ /* Set size for mouse input. */
+ text_edit->set_size(Size2(200, 200));
+
+ text_edit->set_text("this is some text\nfor selection");
+ text_edit->grab_focus();
+ MessageQueue::get_singleton()->flush();
+
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 1), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE);
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 7), MouseButton::MASK_LEFT, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "for s");
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER);
+ CHECK(text_edit->get_selection_from_line() == 1);
+ CHECK(text_edit->get_selection_from_column() == 0);
+ CHECK(text_edit->get_selection_to_line() == 1);
+ CHECK(text_edit->get_selection_to_column() == 5);
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 5);
+
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 9), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE);
+ CHECK_FALSE(text_edit->has_selection());
+
+ text_edit->set_selecting_enabled(false);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 1), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE);
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 7), MouseButton::MASK_LEFT, Key::NONE);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 5);
+ text_edit->set_selecting_enabled(true);
+ }
+
+ SUBCASE("[TextEdit] mouse word select") {
+ /* Set size for mouse input. */
+ text_edit->set_size(Size2(200, 200));
+
+ text_edit->set_text("this is some text\nfor selection");
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ SEND_GUI_DOUBLE_CLICK(text_edit, text_edit->get_pos_at_line_column(0, 2), Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "for");
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD);
+ CHECK(text_edit->get_selection_from_line() == 1);
+ CHECK(text_edit->get_selection_from_column() == 0);
+ CHECK(text_edit->get_selection_to_line() == 1);
+ CHECK(text_edit->get_selection_to_column() == 3);
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 3);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 7), MouseButton::MASK_LEFT, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "for selection");
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD);
+ CHECK(text_edit->get_selection_from_line() == 1);
+ CHECK(text_edit->get_selection_from_column() == 0);
+ CHECK(text_edit->get_selection_to_line() == 1);
+ CHECK(text_edit->get_selection_to_column() == 13);
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 13);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+
+ Point2i line_0 = text_edit->get_pos_at_line_column(0, 0);
+ line_0.y /= 2;
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, line_0, MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE);
+ CHECK_FALSE(text_edit->has_selection());
+
+ text_edit->set_selecting_enabled(false);
+ SEND_GUI_DOUBLE_CLICK(text_edit, text_edit->get_pos_at_line_column(0, 2), Key::NONE);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 3);
+ text_edit->set_selecting_enabled(true);
+ }
+
+ SUBCASE("[TextEdit] mouse line select") {
+ /* Set size for mouse input. */
+ text_edit->set_size(Size2(200, 200));
+
+ text_edit->set_text("this is some text\nfor selection");
+ MessageQueue::get_singleton()->flush();
+
+ SEND_GUI_DOUBLE_CLICK(text_edit, text_edit->get_pos_at_line_column(0, 2), Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 2), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "for selection");
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE);
+ CHECK(text_edit->get_selection_from_line() == 1);
+ CHECK(text_edit->get_selection_from_column() == 0);
+ CHECK(text_edit->get_selection_to_line() == 1);
+ CHECK(text_edit->get_selection_to_column() == 13);
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+
+ Point2i line_0 = text_edit->get_pos_at_line_column(0, 0);
+ line_0.y /= 2;
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, line_0, MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE);
+ CHECK_FALSE(text_edit->has_selection());
+
+ text_edit->set_selecting_enabled(false);
+ SEND_GUI_DOUBLE_CLICK(text_edit, text_edit->get_pos_at_line_column(0, 2), Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 2), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ text_edit->set_selecting_enabled(true);
+ }
+
+ SUBCASE("[TextEdit] mouse shift click select") {
+ /* Set size for mouse input. */
+ text_edit->set_size(Size2(200, 200));
+
+ text_edit->set_text("this is some text\nfor selection");
+ MessageQueue::get_singleton()->flush();
+
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 0), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 7), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE | KeyModifierMask::SHIFT);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "for s");
+ CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER);
+ CHECK(text_edit->get_selection_from_line() == 1);
+ CHECK(text_edit->get_selection_from_column() == 0);
+ CHECK(text_edit->get_selection_to_line() == 1);
+ CHECK(text_edit->get_selection_to_column() == 5);
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 5);
+
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 9), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE);
+ CHECK_FALSE(text_edit->has_selection());
+
+ text_edit->set_selecting_enabled(false);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 0), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 7), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE | KeyModifierMask::SHIFT);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 5);
+ text_edit->set_selecting_enabled(true);
+ }
+
+ SUBCASE("[TextEdit] select and deselect") {
+ text_edit->set_text("this is some text\nfor selection");
+ MessageQueue::get_singleton()->flush();
+
+ text_edit->select(-1, -1, 500, 500);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "this is some text\nfor selection");
+
+ text_edit->deselect();
+ CHECK_FALSE(text_edit->has_selection());
+
+ text_edit->select(500, 500, -1, -1);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == "this is some text\nfor selection");
+
+ text_edit->deselect();
+ CHECK_FALSE(text_edit->has_selection());
+
+ text_edit->select(0, 4, 0, 8);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == " is ");
+
+ text_edit->deselect();
+ CHECK_FALSE(text_edit->has_selection());
+
+ text_edit->select(0, 8, 0, 4);
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_selected_text() == " is ");
+
+ text_edit->set_selecting_enabled(false);
+ CHECK_FALSE(text_edit->has_selection());
+ text_edit->select(0, 8, 0, 4);
+ CHECK_FALSE(text_edit->has_selection());
+ text_edit->set_selecting_enabled(true);
+
+ text_edit->select(0, 8, 0, 4);
+ CHECK(text_edit->has_selection());
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_right");
+ CHECK_FALSE(text_edit->has_selection());
+
+ text_edit->delete_selection();
+ CHECK(text_edit->get_text() == "this is some text\nfor selection");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 8);
+
+ text_edit->select(0, 8, 0, 4);
+ CHECK(text_edit->has_selection());
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace");
+ CHECK(text_edit->get_text() == "thissome text\nfor selection");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
+
+ text_edit->undo();
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "this is some text\nfor selection");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 8);
+
+ text_edit->redo();
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "thissome text\nfor selection");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
+
+ text_edit->undo();
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "this is some text\nfor selection");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 8);
+
+ text_edit->select(0, 8, 0, 4);
+ CHECK(text_edit->has_selection());
+
+ text_edit->delete_selection();
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "thissome text\nfor selection");
+
+ text_edit->undo();
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "this is some text\nfor selection");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 8);
+
+ text_edit->redo();
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "thissome text\nfor selection");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
+
+ text_edit->undo();
+ CHECK(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "this is some text\nfor selection");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 8);
+
+ text_edit->set_editable(false);
+ text_edit->delete_selection();
+ text_edit->set_editable(false);
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "thissome text\nfor selection");
+
+ text_edit->undo();
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "thissome text\nfor selection");
+ }
+
+ // Add readonly test?
+ SUBCASE("[TextEdit] text drag") {
+ TextEdit *target_text_edit = memnew(TextEdit);
+ SceneTree::get_singleton()->get_root()->add_child(target_text_edit);
+ text_edit->get_viewport()->set_embedding_subwindows(true); // Bypass display server for drop handling.
+
+ target_text_edit->set_size(Size2(200, 200));
+ target_text_edit->set_position(Point2(400, 0));
+
+ text_edit->set_size(Size2(200, 200));
+
+ CHECK_FALSE(text_edit->is_mouse_over_selection());
+ text_edit->set_text("drag me");
+ text_edit->select_all();
+ text_edit->grab_click_focus();
+ MessageQueue::get_singleton()->flush();
+
+ Point2i line_0 = text_edit->get_pos_at_line_column(0, 0);
+ line_0.y /= 2;
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, line_0, MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE);
+ CHECK(text_edit->is_mouse_over_selection());
+ SEND_GUI_MOUSE_MOTION_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 7), MouseButton::MASK_LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag me");
+
+ line_0 = target_text_edit->get_pos_at_line_column(0, 0);
+ line_0.y /= 2;
+ line_0.x += 401; // As empty add one.
+ SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit, line_0, MouseButton::MASK_LEFT, Key::NONE);
+ CHECK(text_edit->get_viewport()->gui_is_dragging());
+
+ SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit, line_0, MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE);
+
+ CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging());
+ CHECK(text_edit->get_text() == "");
+ CHECK(target_text_edit->get_text() == "drag me");
+
+ memdelete(target_text_edit);
+ }
+
+ SIGNAL_UNWATCH(text_edit, "text_set");
+ SIGNAL_UNWATCH(text_edit, "text_changed");
+ SIGNAL_UNWATCH(text_edit, "lines_edited_from");
+ SIGNAL_UNWATCH(text_edit, "caret_changed");
+ }
+
+ SUBCASE("[TextEdit] overridable actions") {
+ SIGNAL_WATCH(text_edit, "text_set");
+ SIGNAL_WATCH(text_edit, "text_changed");
+ SIGNAL_WATCH(text_edit, "lines_edited_from");
+ SIGNAL_WATCH(text_edit, "caret_changed");
+
+ Array args1;
+ args1.push_back(0);
+ args1.push_back(0);
+ Array lines_edited_args;
+ lines_edited_args.push_back(args1);
+
+ SUBCASE("[TextEdit] backspace") {
+ text_edit->set_text("this is\nsome\n");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->backspace();
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ text_edit->set_caret_line(2);
+ text_edit->set_caret_column(0);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ ((Array)lines_edited_args[0])[0] = 2;
+ ((Array)lines_edited_args[0])[1] = 1;
+ text_edit->backspace();
+ MessageQueue::get_singleton()->flush();
+
+ CHECK(text_edit->get_text() == "this is\nsome");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 4);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ ((Array)lines_edited_args[0])[0] = 1;
+ text_edit->backspace();
+ MessageQueue::get_singleton()->flush();
+
+ CHECK(text_edit->get_text() == "this is\nsom");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 3);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->end_complex_operation();
+ text_edit->select(1, 0, 1, 3);
+ text_edit->backspace();
+ MessageQueue::get_singleton()->flush();
+
+ CHECK(text_edit->get_text() == "this is\n");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->set_editable(false);
+ text_edit->backspace();
+ text_edit->set_editable(true);
+ MessageQueue::get_singleton()->flush();
+
+ CHECK(text_edit->get_text() == "this is\n");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "this is\nsom");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 3);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ }
+
+ SUBCASE("[TextEdit] cut") {
+ text_edit->set_text("this is\nsome\n");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(6);
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ ERR_PRINT_OFF;
+ text_edit->cut();
+ MessageQueue::get_singleton()->flush();
+ ERR_PRINT_ON; // Can't check display server content.
+
+ ((Array)lines_edited_args[0])[0] = 1;
+ CHECK(text_edit->get_text() == "some\n");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ ((Array)lines_edited_args[0])[0] = 0;
+ ((Array)lines_edited_args[0])[1] = 1;
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "this is\nsome\n");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 6);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ ((Array)lines_edited_args[0])[0] = 1;
+ ((Array)lines_edited_args[0])[1] = 0;
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "some\n");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 4);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->set_text("this is\nsome\n");
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ ((Array)lines_edited_args[0])[0] = 0;
+ text_edit->select(0, 5, 0, 7);
+ ERR_PRINT_OFF;
+ SEND_GUI_ACTION(text_edit, "ui_cut");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ MessageQueue::get_singleton()->flush();
+ ERR_PRINT_ON; // Can't check display server content.
+ CHECK(text_edit->get_text() == "this \nsome\n");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 5);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->set_editable(false);
+ text_edit->cut();
+ MessageQueue::get_singleton()->flush();
+ text_edit->set_editable(true);
+ CHECK(text_edit->get_text() == "this \nsome\n");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 5);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ }
+
+ SUBCASE("[TextEdit] copy") {
+ // TODO: Cannot test need display server support.
+ }
+
+ SUBCASE("[TextEdit] paste") {
+ // TODO: Cannot test need display server support.
+ }
+
+ SUBCASE("[TextEdit] paste primary") {
+ // TODO: Cannot test need display server support.
+ }
+
+ SIGNAL_UNWATCH(text_edit, "text_set");
+ SIGNAL_UNWATCH(text_edit, "text_changed");
+ SIGNAL_UNWATCH(text_edit, "lines_edited_from");
+ SIGNAL_UNWATCH(text_edit, "caret_changed");
+ }
+
+ // Add undo / redo tests?
+ SUBCASE("[TextEdit] input") {
+ SIGNAL_WATCH(text_edit, "text_set");
+ SIGNAL_WATCH(text_edit, "text_changed");
+ SIGNAL_WATCH(text_edit, "lines_edited_from");
+ SIGNAL_WATCH(text_edit, "caret_changed");
+
+ Array args1;
+ args1.push_back(0);
+ args1.push_back(0);
+ Array lines_edited_args;
+ lines_edited_args.push_back(args1);
+
+ SUBCASE("[TextEdit] ui_text_newline_above") {
+ text_edit->set_text("this is some test text.\nthis is some test text.");
+ text_edit->select(0, 0, 0, 4);
+ text_edit->set_caret_column(4);
+
+ text_edit->add_caret(1, 4);
+ text_edit->select(1, 0, 1, 4, 1);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // For the second caret.
+ Array args2;
+ args2.push_back(0);
+ args2.push_back(1);
+ lines_edited_args.push_front(args2);
+
+ ((Array)lines_edited_args[1])[1] = 1;
+ SEND_GUI_ACTION(text_edit, "ui_text_newline_above");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(4);
+
+ text_edit->set_caret_line(3, false, true, 0, 1);
+ text_edit->set_caret_column(4, false, 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->set_editable(false);
+ SEND_GUI_ACTION(text_edit, "ui_text_newline_above");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text.");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 4);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 4);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->set_editable(true);
+
+ ((Array)lines_edited_args[0])[0] = 2;
+ ((Array)lines_edited_args[0])[1] = 3;
+
+ SEND_GUI_ACTION(text_edit, "ui_text_newline_above");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "\n\nthis is some test text.\n\n\nthis is some test text.");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 4);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ }
+
+ SUBCASE("[TextEdit] ui_text_newline_blank") {
+ text_edit->set_text("this is some test text.\nthis is some test text.");
+ text_edit->select(0, 0, 0, 4);
+ text_edit->set_caret_column(4);
+
+ text_edit->add_caret(1, 4);
+ text_edit->select(1, 0, 1, 4, 1);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // For the second caret.
+ Array args2;
+ args2.push_back(1);
+ args2.push_back(2);
+ lines_edited_args.push_front(args2);
+
+ ((Array)lines_edited_args[1])[1] = 1;
+ SEND_GUI_ACTION(text_edit, "ui_text_newline_blank");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->set_editable(false);
+ SEND_GUI_ACTION(text_edit, "ui_text_newline_blank");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->set_editable(true);
+ }
+
+ SUBCASE("[TextEdit] ui_text_newline") {
+ text_edit->set_text("this is some test text.\nthis is some test text.");
+ text_edit->select(0, 0, 0, 4);
+ text_edit->set_caret_column(4);
+
+ text_edit->add_caret(1, 4);
+ text_edit->select(1, 0, 1, 4, 1);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // For the second caret.
+ Array args2;
+ args2.push_back(1);
+ args2.push_back(1);
+ lines_edited_args.push_front(args2);
+ lines_edited_args.push_front(args2.duplicate());
+ ((Array)lines_edited_args[1])[1] = 2;
+
+ lines_edited_args.push_back(lines_edited_args[2].duplicate());
+ ((Array)lines_edited_args[3])[1] = 1;
+
+ SEND_GUI_ACTION(text_edit, "ui_text_newline");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text.");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->set_editable(false);
+ SEND_GUI_ACTION(text_edit, "ui_text_newline");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text.");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->set_editable(true);
+ }
+
+ SUBCASE("[TextEdit] ui_text_backspace_all_to_left") {
+ text_edit->set_text("\nthis is some test text.\n\nthis is some test text.");
+ text_edit->select(1, 0, 1, 4);
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(4);
+
+ text_edit->add_caret(3, 4);
+ text_edit->select(3, 0, 3, 4, 1);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ Ref<InputEvent> tmpevent = InputEventKey::create_reference(Key::BACKSPACE | KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL);
+ InputMap::get_singleton()->action_add_event("ui_text_backspace_all_to_left", tmpevent);
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // For the second caret.
+ Array args2;
+ args2.push_back(3);
+ args2.push_back(3);
+ lines_edited_args.push_front(args2);
+
+ // With selection should be a normal backspace.
+ ((Array)lines_edited_args[1])[0] = 1;
+ ((Array)lines_edited_args[1])[1] = 1;
+
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace_all_to_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text.");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ ((Array)lines_edited_args[0])[1] = 2;
+ ((Array)lines_edited_args[1])[1] = 0;
+
+ // Start of line should also be a normal backspace.
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace_all_to_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some test text.\n is some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->set_caret_column(text_edit->get_line(0).length());
+ text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1);
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->set_editable(false);
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace_all_to_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some test text.\n is some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length());
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length());
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->set_editable(true);
+
+ ((Array)lines_edited_args[0])[0] = 1;
+ ((Array)lines_edited_args[0])[1] = 1;
+ ((Array)lines_edited_args[1])[0] = 0;
+
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace_all_to_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "\n");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ InputMap::get_singleton()->action_erase_event("ui_text_backspace_all_to_left", tmpevent);
+ }
+
+ SUBCASE("[TextEdit] ui_text_backspace_word") {
+ text_edit->set_text("\nthis is some test text.\n\nthis is some test text.");
+ text_edit->select(1, 0, 1, 4);
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(4);
+
+ text_edit->add_caret(3, 4);
+ text_edit->select(3, 0, 3, 4, 1);
+ CHECK(text_edit->get_caret_count() == 2);
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // For the second caret.
+ Array args2;
+ args2.push_back(3);
+ args2.push_back(3);
+ lines_edited_args.push_front(args2);
+
+ // With selection should be a normal backspace.
+ ((Array)lines_edited_args[1])[0] = 1;
+ ((Array)lines_edited_args[1])[1] = 1;
+
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text.");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ text_edit->end_complex_operation();
+
+ ((Array)lines_edited_args[0])[1] = 2;
+ ((Array)lines_edited_args[1])[1] = 0;
+
+ // Start of line should also be a normal backspace.
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some test text.\n is some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->set_editable(false);
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some test text.\n is some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->set_editable(true);
+
+ text_edit->set_caret_column(text_edit->get_line(0).length());
+ text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1);
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ ((Array)lines_edited_args[0])[0] = 1;
+ ((Array)lines_edited_args[0])[1] = 1;
+ ((Array)lines_edited_args[1])[0] = 0;
+
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some test \n is some test ");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 14);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 14);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ }
+
+ SUBCASE("[TextEdit] ui_text_backspace") {
+ text_edit->set_text("\nthis is some test text.\n\nthis is some test text.");
+ text_edit->select(1, 0, 1, 4);
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(4);
+
+ text_edit->add_caret(3, 4);
+ text_edit->select(3, 0, 3, 4, 1);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // For the second caret.
+ Array args2;
+ args2.push_back(3);
+ args2.push_back(3);
+ lines_edited_args.push_front(args2);
+
+ // With selection should be a normal backspace.
+ ((Array)lines_edited_args[1])[0] = 1;
+ ((Array)lines_edited_args[1])[1] = 1;
+
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text.");
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ ((Array)lines_edited_args[0])[1] = 2;
+ ((Array)lines_edited_args[1])[1] = 0;
+
+ // Start of line should also be a normal backspace.
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some test text.\n is some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->set_caret_column(text_edit->get_line(0).length());
+ text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1);
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->set_editable(false);
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some test text.\n is some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length());
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length());
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->set_editable(true);
+
+ ((Array)lines_edited_args[0])[0] = 1;
+ ((Array)lines_edited_args[0])[1] = 1;
+ ((Array)lines_edited_args[1])[0] = 0;
+
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some test text\n is some test text");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 18);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 18);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Select the entire text, from right to left
+ text_edit->select(0, 18, 0, 0);
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+
+ text_edit->select(1, 18, 1, 0, 1);
+ text_edit->set_caret_line(1, false, true, 0, 1);
+ text_edit->set_caret_column(0, false, 1);
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ SEND_GUI_ACTION(text_edit, "ui_text_backspace");
+ CHECK(text_edit->get_text() == "\n");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ }
+
+ SUBCASE("[TextEdit] ui_text_delete_all_to_right") {
+ Ref<InputEvent> tmpevent = InputEventKey::create_reference(Key::BACKSPACE | KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL);
+ InputMap::get_singleton()->action_add_event("ui_text_delete_all_to_right", tmpevent);
+
+ text_edit->set_text("this is some test text.\nthis is some test text.\n");
+ text_edit->select(0, 0, 0, 4);
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+
+ text_edit->add_caret(1, 4);
+ text_edit->select(1, 0, 1, 4, 1);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // For the second caret.
+ Array args2;
+ args2.push_back(1);
+ args2.push_back(1);
+ lines_edited_args.push_front(args2);
+
+ // With selection should be a normal delete.
+ SEND_GUI_ACTION(text_edit, "ui_text_delete_all_to_right");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // End of line should not do anything.
+ text_edit->set_caret_column(text_edit->get_line(0).length());
+ text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1);
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ SEND_GUI_ACTION(text_edit, "ui_text_delete_all_to_right");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length());
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length());
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ text_edit->set_caret_column(0);
+ text_edit->set_caret_column(0, false, 1);
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->set_editable(false);
+ SEND_GUI_ACTION(text_edit, "ui_text_delete_all_to_right");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->set_editable(true);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_delete_all_to_right");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "\n\n");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ InputMap::get_singleton()->action_erase_event("ui_text_delete_all_to_right", tmpevent);
+ }
+
+ SUBCASE("[TextEdit] ui_text_delete_word") {
+ text_edit->set_caret_mid_grapheme_enabled(true);
+ CHECK(text_edit->is_caret_mid_grapheme_enabled());
+
+ text_edit->set_text("this ffi some test text.\n\nthis ffi some test text.\n");
+ text_edit->select(0, 0, 0, 4);
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+
+ text_edit->add_caret(2, 4);
+ text_edit->select(2, 0, 2, 4, 1);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // For the second caret.
+ Array args2;
+ args2.push_back(2);
+ args2.push_back(2);
+ lines_edited_args.push_front(args2);
+
+ // With selection should be a normal delete.
+ SEND_GUI_ACTION(text_edit, "ui_text_delete_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " ffi some test text.\n\n ffi some test text.\n");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // With selection should be a normal delete.
+ ((Array)lines_edited_args[0])[0] = 3;
+ ((Array)lines_edited_args[1])[0] = 1;
+ text_edit->set_caret_column(text_edit->get_line(0).length());
+ text_edit->set_caret_column(text_edit->get_line(2).length(), false, 1);
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ SEND_GUI_ACTION(text_edit, "ui_text_delete_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length());
+ CHECK_FALSE(text_edit->has_selection());
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length());
+ CHECK_FALSE(text_edit->has_selection(0));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ ((Array)lines_edited_args[1])[0] = 0;
+ ((Array)lines_edited_args[0])[0] = 1;
+ ((Array)lines_edited_args[0])[1] = 1;
+ text_edit->set_caret_column(0);
+ text_edit->set_caret_column(0, false, 1);
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->set_editable(false);
+ SEND_GUI_ACTION(text_edit, "ui_text_delete_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->set_editable(true);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_delete_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " some test text.\n some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ }
+
+ SUBCASE("[TextEdit] ui_text_delete") {
+ text_edit->set_caret_mid_grapheme_enabled(true);
+ CHECK(text_edit->is_caret_mid_grapheme_enabled());
+
+ text_edit->set_text("this ffi some test text.\nthis ffi some test text.");
+ text_edit->select(0, 0, 0, 4);
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+
+ text_edit->add_caret(1, 4);
+ text_edit->select(1, 0, 1, 4, 1);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // For the second caret.
+ Array args2;
+ args2.push_back(1);
+ args2.push_back(1);
+ lines_edited_args.push_front(args2);
+
+ // With selection should be a normal delete.
+ SEND_GUI_ACTION(text_edit, "ui_text_delete");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // With selection should be a normal delete.
+ lines_edited_args.remove_at(0);
+ ((Array)lines_edited_args[0])[0] = 1;
+ text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1);
+ text_edit->set_caret_column(text_edit->get_line(0).length());
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ SEND_GUI_ACTION(text_edit, "ui_text_delete");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " ffi some test text. ffi some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 20);
+ CHECK_FALSE(text_edit->has_selection(0));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ // Caret should be removed due to column preservation.
+ CHECK(text_edit->get_caret_count() == 1);
+
+ // Lets add it back.
+ text_edit->set_caret_column(0);
+ text_edit->add_caret(0, 20);
+
+ ((Array)lines_edited_args[0])[0] = 0;
+ lines_edited_args.push_back(args2);
+ ((Array)lines_edited_args[1])[0] = 0;
+ ((Array)lines_edited_args[1])[1] = 0;
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ text_edit->set_editable(false);
+ SEND_GUI_ACTION(text_edit, "ui_text_delete");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " ffi some test text. ffi some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 20);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->set_editable(true);
+
+ text_edit->start_action(TextEdit::EditAction::ACTION_NONE);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_delete");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "ffi some test text.ffi some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 19);
+ CHECK_FALSE(text_edit->has_selection(0));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->start_action(TextEdit::EditAction::ACTION_NONE);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_delete");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "fi some test text.fi some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 18);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->set_caret_mid_grapheme_enabled(false);
+ CHECK_FALSE(text_edit->is_caret_mid_grapheme_enabled());
+
+ text_edit->start_action(TextEdit::EditAction::ACTION_NONE);
+
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_text() == "ffi some test text.ffi some test text.");
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ SEND_GUI_ACTION(text_edit, "ui_text_delete");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == " some test text. some test text.");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 16);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ }
+
+ SUBCASE("[TextEdit] ui_text_caret_word_left") {
+ text_edit->set_text("\nthis is some test text.\nthis is some test text.");
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(7);
+
+ text_edit->add_caret(2, 7);
+ CHECK(text_edit->get_caret_count() == 2);
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // Shift should select.
+#ifdef MACOS_ENABLED
+ SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::ALT | KeyModifierMask::SHIFT);
+#else
+ SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT);
+#endif
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 5);
+ CHECK(text_edit->get_selected_text(0) == "is");
+ CHECK(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 5);
+ CHECK(text_edit->get_selected_text(1) == "is");
+ CHECK(text_edit->has_selection(1));
+
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Should still move caret with selection.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_word_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Normal word left.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_word_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 23);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ }
+
+ SUBCASE("[TextEdit] ui_text_caret_left") {
+ text_edit->set_text("\nthis is some test text.\nthis is some test text.");
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(7);
+ text_edit->select(1, 2, 1, 7);
+
+ text_edit->add_caret(2, 7);
+ text_edit->select(2, 2, 2, 7, 1);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // Normal left shoud deselect and place at selection start.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 2);
+ CHECK_FALSE(text_edit->has_selection(1));
+
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // With shift should select.
+ SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::SHIFT);
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 1);
+ CHECK(text_edit->get_selected_text(0) == "h");
+ CHECK(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 1);
+ CHECK(text_edit->get_selected_text(1) == "h");
+ CHECK(text_edit->has_selection(1));
+
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // All ready at select left, should only deselect.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 1);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 1);
+ CHECK_FALSE(text_edit->has_selection(1));
+
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Normal left.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection());
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Left at col 0 should go up a line.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 23);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ }
+
+ SUBCASE("[TextEdit] ui_text_caret_word_right") {
+ text_edit->set_text("this is some test text\n\nthis is some test text\n");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(13);
+
+ text_edit->add_caret(2, 13);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // Shift should select.
+#ifdef MACOS_ENABLED
+ SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::ALT | KeyModifierMask::SHIFT);
+#else
+ SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT);
+#endif
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 17);
+ CHECK(text_edit->get_selected_text(0) == "test");
+ CHECK(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 17);
+ CHECK(text_edit->get_selected_text(1) == "test");
+ CHECK(text_edit->has_selection(1));
+
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Should still move caret with selection.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_word_right");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 22);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 22);
+ CHECK_FALSE(text_edit->has_selection(1));
+
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Normal word right.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_word_right");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ }
+
+ SUBCASE("[TextEdit] ui_text_caret_right") {
+ text_edit->set_text("this is some test text\n\nthis is some test text\n");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(16);
+ text_edit->select(0, 16, 0, 20);
+
+ text_edit->add_caret(2, 16);
+ text_edit->select(2, 16, 2, 20, 1);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // Normal right shoud deselect and place at selection start.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_right");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 20);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 20);
+ CHECK_FALSE(text_edit->has_selection(1));
+
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // With shift should select.
+ SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::SHIFT);
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 21);
+ CHECK(text_edit->get_selected_text(0) == "x");
+ CHECK(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 21);
+ CHECK(text_edit->get_selected_text(1) == "x");
+ CHECK(text_edit->has_selection(1));
+
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // All ready at select right, should only deselect.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_right");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 21);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 21);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Normal right.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_right");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 22);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 22);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Right at end col should go down a line.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_right");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 3);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ }
+
+ SUBCASE("[TextEdit] ui_text_caret_up") {
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
+
+ text_edit->set_size(Size2(110, 100));
+ text_edit->set_text("this is some\nother test\nlines\ngo here\nthis is some\nother test\nlines\ngo here");
+ text_edit->set_caret_line(3);
+ text_edit->set_caret_column(7);
+
+ text_edit->add_caret(7, 7);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ CHECK(text_edit->is_line_wrapped(0));
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // Select + up should select everything to the left on that line.
+ SEND_GUI_KEY_EVENT(text_edit, Key::UP | KeyModifierMask::SHIFT);
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_caret_column() == 5);
+ CHECK(text_edit->get_selected_text(0) == "\ngo here");
+ CHECK(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 6);
+ CHECK(text_edit->get_caret_column(1) == 5);
+ CHECK(text_edit->get_selected_text(1) == "\ngo here");
+ CHECK(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Should deselect and move up.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_up");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 8);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 5);
+ CHECK(text_edit->get_caret_column(1) == 8);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Normal up over wrapped line.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_up");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 12);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 4);
+ CHECK(text_edit->get_caret_column(1) == 12);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->set_caret_column(12, false);
+
+ // Normal up over wrapped line to line 0.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_up");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 7);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 4);
+ CHECK(text_edit->get_caret_column(1) == 7);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ }
+
+ SUBCASE("[TextEdit] ui_text_caret_down") {
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
+
+ text_edit->set_size(Size2(110, 100));
+ text_edit->set_text("go here\nlines\nother test\nthis is some\ngo here\nlines\nother test\nthis is some");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(7);
+
+ text_edit->add_caret(4, 7);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ CHECK(text_edit->is_line_wrapped(3));
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // Select + down should select everything to the right on that line.
+ SEND_GUI_KEY_EVENT(text_edit, Key::DOWN | KeyModifierMask::SHIFT);
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_caret_column() == 5);
+ CHECK(text_edit->get_selected_text(0) == "\nlines");
+ CHECK(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 5);
+ CHECK(text_edit->get_caret_column(1) == 5);
+ CHECK(text_edit->get_selected_text(1) == "\nlines");
+ CHECK(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Should deselect and move down.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_down");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_caret_column() == 8);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 6);
+ CHECK(text_edit->get_caret_column(1) == 8);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Normal down over wrapped line.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_down");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 3);
+ CHECK(text_edit->get_caret_column() == 7);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 7);
+ CHECK(text_edit->get_caret_column(1) == 7);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->set_caret_column(7, false);
+
+ // Normal down over wrapped line to last wrapped line.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_down");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 3);
+ CHECK(text_edit->get_caret_column() == 12);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 7);
+ CHECK(text_edit->get_caret_column(1) == 12);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ }
+
+ SUBCASE("[TextEdit] ui_text_caret_document_start") {
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
+
+ text_edit->set_size(Size2(110, 100));
+ text_edit->set_text("this is some\nother test\nlines\ngo here");
+ text_edit->set_caret_line(4);
+ text_edit->set_caret_column(7);
+
+ text_edit->add_caret(3, 2);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ MessageQueue::get_singleton()->flush();
+
+ CHECK(text_edit->is_line_wrapped(0));
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+#ifdef MACOS_ENABLED
+ SEND_GUI_KEY_EVENT(text_edit, Key::UP | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT);
+#else
+ SEND_GUI_KEY_EVENT(text_edit, Key::HOME | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT);
+#endif
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "this is some\nother test\nlines\ngo here");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK(text_edit->get_selected_text() == "this is some\nother test\nlines\ngo here");
+ CHECK(text_edit->has_selection());
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ CHECK(text_edit->get_caret_count() == 1);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_document_start");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "this is some\nother test\nlines\ngo here");
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection());
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ }
+
+ SUBCASE("[TextEdit] ui_text_caret_document_end") {
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
+
+ text_edit->set_size(Size2(110, 100));
+ text_edit->set_text("go here\nlines\nother test\nthis is some");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+
+ text_edit->add_caret(1, 0);
+ CHECK(text_edit->get_caret_count() == 2);
+ MessageQueue::get_singleton()->flush();
+
+ CHECK(text_edit->is_line_wrapped(3));
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+#ifdef MACOS_ENABLED
+ SEND_GUI_KEY_EVENT(text_edit, Key::DOWN | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT);
+#else
+ SEND_GUI_KEY_EVENT(text_edit, Key::END | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT);
+#endif
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "go here\nlines\nother test\nthis is some");
+ CHECK(text_edit->get_caret_line() == 3);
+ CHECK(text_edit->get_caret_column() == 12);
+ CHECK(text_edit->get_selected_text() == "go here\nlines\nother test\nthis is some");
+ CHECK(text_edit->has_selection());
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ CHECK(text_edit->get_caret_count() == 1);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_document_end");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "go here\nlines\nother test\nthis is some");
+ CHECK(text_edit->get_caret_line() == 3);
+ CHECK(text_edit->get_caret_column() == 12);
+ CHECK_FALSE(text_edit->has_selection());
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ }
+
+ SUBCASE("[TextEdit] ui_text_caret_line_start") {
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
+
+ text_edit->set_size(Size2(110, 100));
+ text_edit->set_text(" this is some\n this is some");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(text_edit->get_line(0).length());
+
+ text_edit->add_caret(1, text_edit->get_line(1).length());
+ CHECK(text_edit->get_caret_count() == 2);
+ MessageQueue::get_singleton()->flush();
+
+ CHECK(text_edit->is_line_wrapped(0));
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+#ifdef MACOS_ENABLED
+ SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT);
+#else
+ SEND_GUI_KEY_EVENT(text_edit, Key::HOME | KeyModifierMask::SHIFT);
+#endif
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 10);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == "some");
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 10);
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == "some");
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_line_start");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 2);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_line_start");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 0);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 0);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_line_start");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 2);
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 2);
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ }
+
+ SUBCASE("[TextEdit] ui_text_caret_line_end") {
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
+
+ text_edit->set_size(Size2(110, 100));
+ text_edit->set_text(" this is some\n this is some");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+
+ text_edit->add_caret(1, 0);
+ CHECK(text_edit->get_caret_count() == 2);
+ MessageQueue::get_singleton()->flush();
+
+ CHECK(text_edit->is_line_wrapped(0));
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+#ifdef MACOS_ENABLED
+ SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT);
+#else
+ SEND_GUI_KEY_EVENT(text_edit, Key::END | KeyModifierMask::SHIFT);
+#endif
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == 9);
+ CHECK(text_edit->has_selection(0));
+ CHECK(text_edit->get_selected_text(0) == " this is");
+
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == 9);
+ CHECK(text_edit->has_selection(1));
+ CHECK(text_edit->get_selected_text(1) == " this is");
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_line_end");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 0);
+ CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length());
+ CHECK_FALSE(text_edit->has_selection(0));
+
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line(1) == 1);
+ CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length());
+ CHECK_FALSE(text_edit->has_selection(1));
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ }
+
+ SUBCASE("[TextEdit] unicode") {
+ text_edit->set_text("\n");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+
+ text_edit->add_caret(1, 0);
+ CHECK(text_edit->get_caret_count() == 2);
+ text_edit->insert_text_at_caret("a");
+ MessageQueue::get_singleton()->flush();
+
+ SIGNAL_DISCARD("text_set");
+ SIGNAL_DISCARD("text_changed");
+ SIGNAL_DISCARD("lines_edited_from");
+ SIGNAL_DISCARD("caret_changed");
+
+ // For the second caret.
+ Array args2;
+ args2.push_back(1);
+ args2.push_back(1);
+ lines_edited_args.push_front(args2);
+
+ SEND_GUI_KEY_EVENT(text_edit, Key::A);
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "aA\naA");
+ CHECK(text_edit->get_caret_column() == 2);
+ CHECK(text_edit->get_caret_column(1) == 2);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->set_editable(false);
+ SEND_GUI_KEY_EVENT(text_edit, Key::A);
+ CHECK_FALSE(text_edit->get_viewport()->is_input_handled()); // Should this be handled?
+ CHECK(text_edit->get_text() == "aA\naA");
+ CHECK(text_edit->get_caret_column() == 2);
+ CHECK(text_edit->get_caret_column(1) == 2);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ text_edit->set_editable(true);
+
+ lines_edited_args.push_back(lines_edited_args[1].duplicate());
+ lines_edited_args.push_front(args2.duplicate());
+
+ text_edit->select(0, 0, 0, 1);
+ text_edit->select(1, 0, 1, 1, 1);
+ SEND_GUI_KEY_EVENT(text_edit, Key::B);
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "BA\nBA");
+ CHECK(text_edit->get_caret_column() == 1);
+ CHECK(text_edit->get_caret_column(1) == 1);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_toggle_insert_mode");
+ CHECK(text_edit->is_overtype_mode_enabled());
+
+ SEND_GUI_KEY_EVENT(text_edit, Key::B);
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "BB\nBB");
+ CHECK(text_edit->get_caret_column() == 2);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+
+ text_edit->select(0, 0, 0, 1);
+ text_edit->select(1, 0, 1, 1, 1);
+ SEND_GUI_KEY_EVENT(text_edit, Key::A);
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "AB\nAB");
+ CHECK(text_edit->get_caret_column() == 1);
+ CHECK(text_edit->get_caret_column(1) == 1);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ text_edit->set_overtype_mode_enabled(false);
+ CHECK_FALSE(text_edit->is_overtype_mode_enabled());
+
+ lines_edited_args.remove_at(0);
+ lines_edited_args.remove_at(1);
+
+ SEND_GUI_KEY_EVENT(text_edit, Key::TAB);
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_text() == "A\tB\nA\tB");
+ CHECK(text_edit->get_caret_column() == 2);
+ CHECK(text_edit->get_caret_column(1) == 2);
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ SIGNAL_CHECK("text_changed", empty_signal_args);
+ SIGNAL_CHECK("lines_edited_from", lines_edited_args);
+ }
+
+ SIGNAL_UNWATCH(text_edit, "text_set");
+ SIGNAL_UNWATCH(text_edit, "text_changed");
+ SIGNAL_UNWATCH(text_edit, "lines_edited_from");
+ SIGNAL_UNWATCH(text_edit, "caret_changed");
+ }
+
+ memdelete(text_edit);
+}
+
+TEST_CASE("[SceneTree][TextEdit] context menu") {
+ TextEdit *text_edit = memnew(TextEdit);
+ SceneTree::get_singleton()->get_root()->add_child(text_edit);
+
+ text_edit->get_viewport()->set_embedding_subwindows(true); // Bypass display server for drop handling.
+
+ text_edit->set_size(Size2(800, 200));
+ text_edit->set_line(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec varius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque.");
+ MessageQueue::get_singleton()->flush();
+
+ text_edit->set_context_menu_enabled(false);
+ CHECK_FALSE(text_edit->is_context_menu_enabled());
+
+ CHECK_FALSE(text_edit->is_menu_visible());
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(600, 10), MouseButton::RIGHT, MouseButton::MASK_RIGHT, Key::NONE);
+ CHECK_FALSE(text_edit->is_menu_visible());
+
+ text_edit->set_context_menu_enabled(true);
+ CHECK(text_edit->is_context_menu_enabled());
+
+ CHECK_FALSE(text_edit->is_menu_visible());
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(700, 10), MouseButton::RIGHT, MouseButton::MASK_RIGHT, Key::NONE);
+ CHECK(text_edit->is_menu_visible());
+
+ memdelete(text_edit);
+}
+
+TEST_CASE("[SceneTree][TextEdit] versioning") {
+ TextEdit *text_edit = memnew(TextEdit);
+ SceneTree::get_singleton()->get_root()->add_child(text_edit);
+
+ // Action undo / redo states are tested in the action test e.g selection_delete.
+ CHECK_FALSE(text_edit->has_undo());
+ CHECK_FALSE(text_edit->has_redo());
+ CHECK(text_edit->get_version() == 0);
+ CHECK(text_edit->get_saved_version() == 0);
+
+ text_edit->begin_complex_operation();
+ text_edit->begin_complex_operation();
+ text_edit->begin_complex_operation();
+
+ text_edit->insert_text_at_caret("test");
+ CHECK(text_edit->get_version() == 1);
+ CHECK(text_edit->get_saved_version() == 0);
+ CHECK(text_edit->has_undo());
+ CHECK_FALSE(text_edit->has_redo());
+
+ text_edit->end_complex_operation();
+
+ // Can undo and redo mid op.
+ text_edit->insert_text_at_caret(" nested");
+ CHECK(text_edit->get_version() == 2);
+ CHECK(text_edit->get_saved_version() == 0);
+ CHECK(text_edit->has_undo());
+ CHECK_FALSE(text_edit->has_redo());
+ text_edit->undo();
+
+ CHECK(text_edit->has_redo());
+ text_edit->redo();
+
+ text_edit->end_complex_operation();
+
+ text_edit->insert_text_at_caret(" ops");
+ CHECK(text_edit->get_version() == 3);
+ CHECK(text_edit->get_saved_version() == 0);
+ CHECK(text_edit->has_undo());
+ CHECK_FALSE(text_edit->has_redo());
+
+ text_edit->end_complex_operation();
+
+ text_edit->tag_saved_version();
+ CHECK(text_edit->get_saved_version() == 3);
+
+ text_edit->undo();
+ CHECK(text_edit->get_line(0) == "");
+ CHECK(text_edit->get_version() == 0);
+ CHECK(text_edit->get_saved_version() == 3);
+ CHECK_FALSE(text_edit->has_undo());
+ CHECK(text_edit->has_redo());
+
+ text_edit->redo();
+ CHECK(text_edit->get_line(0) == "test ops nested");
+ CHECK(text_edit->get_version() == 3);
+ CHECK(text_edit->get_saved_version() == 3);
+ CHECK(text_edit->has_undo());
+ CHECK_FALSE(text_edit->has_redo());
+
+ text_edit->clear_undo_history();
+ CHECK_FALSE(text_edit->has_undo());
+ CHECK_FALSE(text_edit->has_redo());
+ CHECK(text_edit->get_version() == 3); // Should this be cleared?
+ CHECK(text_edit->get_saved_version() == 0);
+
+ memdelete(text_edit);
+}
+
+TEST_CASE("[SceneTree][TextEdit] search") {
+ TextEdit *text_edit = memnew(TextEdit);
+ SceneTree::get_singleton()->get_root()->add_child(text_edit);
+
+ text_edit->set_text("hay needle, hay\nHAY NEEDLE, HAY");
+ int length = text_edit->get_line(1).length();
+
+ CHECK(text_edit->search("test", 0, 0, 0) == Point2i(-1, -1));
+ CHECK(text_edit->search("test", TextEdit::SEARCH_MATCH_CASE, 0, 0) == Point2i(-1, -1));
+ CHECK(text_edit->search("test", TextEdit::SEARCH_WHOLE_WORDS, 0, 0) == Point2i(-1, -1));
+ CHECK(text_edit->search("test", TextEdit::SEARCH_BACKWARDS, 0, 0) == Point2i(-1, -1));
+
+ CHECK(text_edit->search("test", 0, 1, length) == Point2i(-1, -1));
+ CHECK(text_edit->search("test", TextEdit::SEARCH_MATCH_CASE, 1, length) == Point2i(-1, -1));
+ CHECK(text_edit->search("test", TextEdit::SEARCH_WHOLE_WORDS, 1, length) == Point2i(-1, -1));
+ CHECK(text_edit->search("test", TextEdit::SEARCH_BACKWARDS, 1, length) == Point2i(-1, -1));
+
+ CHECK(text_edit->search("needle", 0, 0, 0) == Point2i(4, 0));
+ CHECK(text_edit->search("needle", 0, 1, length) == Point2i(4, 0));
+ CHECK(text_edit->search("needle", 0, 0, 5) == Point2i(4, 1));
+ CHECK(text_edit->search("needle", TextEdit::SEARCH_BACKWARDS, 0, 0) == Point2i(4, 1));
+ CHECK(text_edit->search("needle", TextEdit::SEARCH_BACKWARDS, 1, 5) == Point2i(4, 1));
+ CHECK(text_edit->search("needle", TextEdit::SEARCH_BACKWARDS, 1, 3) == Point2i(4, 0));
+
+ CHECK(text_edit->search("needle", TextEdit::SEARCH_MATCH_CASE, 0, 0) == Point2i(4, 0));
+ CHECK(text_edit->search("needle", TextEdit::SEARCH_MATCH_CASE | TextEdit::SEARCH_BACKWARDS, 0, 0) == Point2i(4, 0));
+
+ CHECK(text_edit->search("needle", TextEdit::SEARCH_WHOLE_WORDS | TextEdit::SEARCH_MATCH_CASE, 0, 0) == Point2i(4, 0));
+ CHECK(text_edit->search("needle", TextEdit::SEARCH_WHOLE_WORDS | TextEdit::SEARCH_MATCH_CASE | TextEdit::SEARCH_BACKWARDS, 0, 0) == Point2i(4, 0));
+
+ CHECK(text_edit->search("need", TextEdit::SEARCH_MATCH_CASE, 0, 0) == Point2i(4, 0));
+ CHECK(text_edit->search("need", TextEdit::SEARCH_MATCH_CASE | TextEdit::SEARCH_BACKWARDS, 0, 0) == Point2i(4, 0));
+
+ CHECK(text_edit->search("need", TextEdit::SEARCH_WHOLE_WORDS | TextEdit::SEARCH_MATCH_CASE, 0, 0) == Point2i(-1, -1));
+ CHECK(text_edit->search("need", TextEdit::SEARCH_WHOLE_WORDS | TextEdit::SEARCH_MATCH_CASE | TextEdit::SEARCH_BACKWARDS, 0, 0) == Point2i(-1, -1));
+
+ ERR_PRINT_OFF;
+ CHECK(text_edit->search("", 0, 0, 0) == Point2i(-1, -1));
+ CHECK(text_edit->search("needle", 0, -1, 0) == Point2i(-1, -1));
+ CHECK(text_edit->search("needle", 0, 0, -1) == Point2i(-1, -1));
+ CHECK(text_edit->search("needle", 0, 100, 0) == Point2i(-1, -1));
+ CHECK(text_edit->search("needle", 0, 0, 100) == Point2i(-1, -1));
+ ERR_PRINT_ON;
+
+ memdelete(text_edit);
+}
+
+TEST_CASE("[SceneTree][TextEdit] mouse") {
+ TextEdit *text_edit = memnew(TextEdit);
+ SceneTree::get_singleton()->get_root()->add_child(text_edit);
+
+ text_edit->set_size(Size2(800, 200));
+ text_edit->set_line(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec varius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque.");
+ MessageQueue::get_singleton()->flush();
+
+ CHECK(text_edit->get_word_at_pos(text_edit->get_pos_at_line_column(0, 1)) == "Lorem");
+ CHECK(text_edit->get_word_at_pos(text_edit->get_pos_at_line_column(0, 9)) == "ipsum");
+
+ ERR_PRINT_OFF;
+ CHECK(text_edit->get_pos_at_line_column(0, -1) == Point2i(-1, -1));
+ CHECK(text_edit->get_pos_at_line_column(-1, 0) == Point2i(-1, -1));
+ CHECK(text_edit->get_pos_at_line_column(-1, -1) == Point2i(-1, -1));
+
+ CHECK(text_edit->get_pos_at_line_column(0, 500) == Point2i(-1, -1));
+ CHECK(text_edit->get_pos_at_line_column(2, 0) == Point2i(-1, -1));
+ CHECK(text_edit->get_pos_at_line_column(2, 500) == Point2i(-1, -1));
+
+ // Out of view.
+ CHECK(text_edit->get_pos_at_line_column(0, text_edit->get_line(0).length() - 1) == Point2i(-1, -1));
+ ERR_PRINT_ON;
+
+ // Add method to get drawn column count?
+ Point2i start_pos = text_edit->get_pos_at_line_column(0, 0);
+ Point2i end_pos = text_edit->get_pos_at_line_column(0, 105);
+
+ CHECK(text_edit->get_line_column_at_pos(Point2i(start_pos.x, start_pos.y)) == Point2i(0, 0));
+ CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x, end_pos.y)) == Point2i(104, 0));
+
+ // Should this return Point2i(-1, -1) if its also < 0 not just > vis_lines.
+ CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x - 100, end_pos.y), false) == Point2i(90, 0));
+ CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x, end_pos.y + 100), false) == Point2i(-1, -1));
+ CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x - 100, end_pos.y + 100), false) == Point2i(-1, -1));
+ CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x, end_pos.y - 100), false) == Point2i(104, 0));
+ CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x - 100, end_pos.y - 100), false) == Point2i(90, 0));
+
+ CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x - 100, end_pos.y)) == Point2i(90, 0));
+ CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x, end_pos.y + 100)) == Point2i(141, 0));
+ CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x - 100, end_pos.y + 100)) == Point2i(141, 0));
+ CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x, end_pos.y - 100)) == Point2i(104, 0));
+ CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x - 100, end_pos.y - 100)) == Point2i(90, 0));
+
+ memdelete(text_edit);
+}
+
+TEST_CASE("[SceneTree][TextEdit] caret") {
+ TextEdit *text_edit = memnew(TextEdit);
+ SceneTree::get_singleton()->get_root()->add_child(text_edit);
+
+ text_edit->set_size(Size2(800, 200));
+ text_edit->grab_focus();
+ text_edit->set_line(0, "ffi");
+
+ text_edit->set_caret_mid_grapheme_enabled(true);
+ CHECK(text_edit->is_caret_mid_grapheme_enabled());
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_right");
+ CHECK(text_edit->get_caret_column() == 1);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_right");
+ CHECK(text_edit->get_caret_column() == 2);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_right");
+ CHECK(text_edit->get_caret_column() == 3);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_left");
+ CHECK(text_edit->get_caret_column() == 2);
+
+ text_edit->set_caret_mid_grapheme_enabled(false);
+ CHECK_FALSE(text_edit->is_caret_mid_grapheme_enabled());
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_left");
+ CHECK(text_edit->get_caret_column() == 0);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_right");
+ CHECK(text_edit->get_caret_column() == 3);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_left");
+ CHECK(text_edit->get_caret_column() == 0);
+
+ text_edit->set_line(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec varius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque.");
+ for (int i = 0; i < 3; i++) {
+ text_edit->insert_line_at(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec varius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque.");
+ }
+ MessageQueue::get_singleton()->flush();
+
+ text_edit->set_caret_blink_enabled(false);
+ CHECK_FALSE(text_edit->is_caret_blink_enabled());
+
+ text_edit->set_caret_blink_enabled(true);
+ CHECK(text_edit->is_caret_blink_enabled());
+
+ text_edit->set_caret_blink_interval(10);
+ CHECK(text_edit->get_caret_blink_interval() == 10);
+
+ ERR_PRINT_OFF;
+ text_edit->set_caret_blink_interval(-1);
+ CHECK(text_edit->get_caret_blink_interval() == 10);
+
+ text_edit->set_caret_blink_interval(0);
+ CHECK(text_edit->get_caret_blink_interval() == 10);
+ ERR_PRINT_ON;
+
+ text_edit->set_caret_type(TextEdit::CaretType::CARET_TYPE_LINE);
+ CHECK(text_edit->get_caret_type() == TextEdit::CaretType::CARET_TYPE_LINE);
+
+ text_edit->set_caret_type(TextEdit::CaretType::CARET_TYPE_BLOCK);
+ CHECK(text_edit->get_caret_type() == TextEdit::CaretType::CARET_TYPE_BLOCK);
+
+ text_edit->set_caret_type(TextEdit::CaretType::CARET_TYPE_LINE);
+ CHECK(text_edit->get_caret_type() == TextEdit::CaretType::CARET_TYPE_LINE);
+
+ int caret_col = text_edit->get_caret_column();
+ text_edit->set_move_caret_on_right_click_enabled(false);
+ CHECK_FALSE(text_edit->is_move_caret_on_right_click_enabled());
+
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(100, 1), MouseButton::RIGHT, MouseButton::MASK_RIGHT, Key::NONE);
+ CHECK(text_edit->get_caret_column() == caret_col);
+
+ text_edit->set_move_caret_on_right_click_enabled(true);
+ CHECK(text_edit->is_move_caret_on_right_click_enabled());
+
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(100, 1), MouseButton::RIGHT, MouseButton::MASK_RIGHT, Key::NONE);
+ CHECK(text_edit->get_caret_column() != caret_col);
+
+ text_edit->set_move_caret_on_right_click_enabled(false);
+ CHECK_FALSE(text_edit->is_move_caret_on_right_click_enabled());
+
+ text_edit->set_caret_column(0);
+ CHECK(text_edit->get_word_under_caret() == "Lorem");
+
+ text_edit->set_caret_column(4);
+ CHECK(text_edit->get_word_under_caret() == "Lorem");
+
+ // Should this work?
+ text_edit->set_caret_column(5);
+ CHECK(text_edit->get_word_under_caret() == "");
+
+ text_edit->set_caret_column(6);
+ CHECK(text_edit->get_word_under_caret() == "");
+
+ text_edit->set_caret_line(1);
+ CHECK(text_edit->get_caret_line() == 1);
+
+ text_edit->set_caret_line(-1);
+ CHECK(text_edit->get_caret_line() == 0);
+ text_edit->set_caret_line(100);
+ CHECK(text_edit->get_caret_line() == 3);
+
+ text_edit->set_caret_column(-1);
+ CHECK(text_edit->get_caret_column() == 0);
+ text_edit->set_caret_column(10000000);
+ CHECK(text_edit->get_caret_column() == 141);
+
+ memdelete(text_edit);
+}
+
+TEST_CASE("[SceneTree][TextEdit] multicaret") {
+ TextEdit *text_edit = memnew(TextEdit);
+ SceneTree::get_singleton()->get_root()->add_child(text_edit);
+ text_edit->set_multiple_carets_enabled(true);
+
+ Array empty_signal_args;
+ empty_signal_args.push_back(Array());
+
+ SIGNAL_WATCH(text_edit, "caret_changed");
+
+ text_edit->set_text("this is\nsome test\ntext");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("caret_changed");
+
+ SUBCASE("[TextEdit] add remove caret") {
+ // Overlapping
+ CHECK(text_edit->add_caret(0, 0) == -1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_CHECK_FALSE("caret_changed");
+
+ // Selection
+ text_edit->select(0, 0, 2, 4);
+ CHECK(text_edit->add_caret(0, 0) == -1);
+ CHECK(text_edit->add_caret(2, 4) == -1);
+ CHECK(text_edit->add_caret(1, 2) == -1);
+
+ // Out of bounds
+ CHECK(text_edit->add_caret(-1, 0) == -1);
+ CHECK(text_edit->add_caret(5, 0) == -1);
+ CHECK(text_edit->add_caret(0, 100) == -1);
+
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_CHECK_FALSE("caret_changed");
+
+ CHECK(text_edit->get_caret_count() == 1);
+
+ text_edit->deselect();
+ SIGNAL_CHECK_FALSE("caret_changed");
+
+ CHECK(text_edit->add_caret(0, 1) == 1);
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_CHECK("caret_changed", empty_signal_args);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
+
+ CHECK(text_edit->get_caret_line(1) == 0);
+ CHECK(text_edit->get_caret_column(1) == 1);
+
+ ERR_PRINT_OFF;
+ text_edit->remove_caret(-1);
+ text_edit->remove_caret(5);
+ ERR_PRINT_ON;
+ CHECK(text_edit->get_caret_count() == 2);
+ SIGNAL_CHECK_FALSE("caret_changed");
+
+ text_edit->remove_caret(0);
+ SIGNAL_CHECK_FALSE("caret_changed");
+ CHECK(text_edit->get_caret_count() == 1);
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 1);
+
+ ERR_PRINT_OFF;
+ text_edit->remove_caret(0);
+ CHECK(text_edit->get_caret_count() == 1);
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[TextEdit] caret index edit order") {
+ Vector<int> caret_index_get_order;
+ caret_index_get_order.push_back(1);
+ caret_index_get_order.push_back(0);
+
+ CHECK(text_edit->add_caret(1, 0));
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_caret_index_edit_order() == caret_index_get_order);
+
+ text_edit->remove_secondary_carets();
+ text_edit->set_caret_line(1);
+ CHECK(text_edit->add_caret(0, 0));
+ CHECK(text_edit->get_caret_count() == 2);
+
+ caret_index_get_order.write[0] = 0;
+ caret_index_get_order.write[1] = 1;
+ CHECK(text_edit->get_caret_index_edit_order() == caret_index_get_order);
+ }
+
+ SUBCASE("[TextEdit] add caret at carets") {
+ text_edit->remove_secondary_carets();
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(9);
+
+ text_edit->add_caret_at_carets(true);
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 4);
+
+ text_edit->add_caret_at_carets(true);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ text_edit->add_caret_at_carets(false);
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 7);
+
+ text_edit->remove_secondary_carets();
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(4);
+ text_edit->select(0, 0, 0, 4);
+ text_edit->add_caret_at_carets(true);
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_selection_from_line(1) == 1);
+ CHECK(text_edit->get_selection_to_line(1) == 1);
+ CHECK(text_edit->get_selection_from_column(1) == 0);
+ CHECK(text_edit->get_selection_to_column(1) == 3);
+
+ text_edit->add_caret_at_carets(true);
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->get_selection_from_line(2) == 2);
+ CHECK(text_edit->get_selection_to_line(2) == 2);
+ CHECK(text_edit->get_selection_from_column(2) == 0);
+ CHECK(text_edit->get_selection_to_column(2) == 4);
+ }
+
+ memdelete(text_edit);
+}
+
+TEST_CASE("[SceneTree][TextEdit] line wrapping") {
+ TextEdit *text_edit = memnew(TextEdit);
+ SceneTree::get_singleton()->get_root()->add_child(text_edit);
+ text_edit->grab_focus();
+
+ // Set size for boundary.
+ text_edit->set_size(Size2(800, 200));
+ text_edit->set_line(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec varius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque.");
+ CHECK_FALSE(text_edit->is_line_wrapped(0));
+ CHECK(text_edit->get_line_wrap_count(0) == 0);
+ CHECK(text_edit->get_line_wrap_index_at_column(0, 130) == 0);
+ CHECK(text_edit->get_line_wrapped_text(0).size() == 1);
+
+ SIGNAL_WATCH(text_edit, "text_set");
+ SIGNAL_WATCH(text_edit, "text_changed");
+ SIGNAL_WATCH(text_edit, "lines_edited_from");
+ SIGNAL_WATCH(text_edit, "caret_changed");
+
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
+ SIGNAL_CHECK_FALSE("text_set");
+ SIGNAL_CHECK_FALSE("text_changed");
+ SIGNAL_CHECK_FALSE("lines_edited_from");
+ SIGNAL_CHECK_FALSE("caret_changed");
+
+ CHECK(text_edit->is_line_wrapped(0));
+ CHECK(text_edit->get_line_wrap_count(0) == 1);
+ CHECK(text_edit->get_line_wrap_index_at_column(0, 130) == 1);
+ CHECK(text_edit->get_line_wrapped_text(0).size() == 2);
+
+ SIGNAL_UNWATCH(text_edit, "text_set");
+ SIGNAL_UNWATCH(text_edit, "text_changed");
+ SIGNAL_UNWATCH(text_edit, "lines_edited_from");
+ SIGNAL_UNWATCH(text_edit, "caret_changed");
+
+ ERR_PRINT_OFF;
+ CHECK_FALSE(text_edit->is_line_wrapped(-1));
+ CHECK_FALSE(text_edit->is_line_wrapped(1));
+ CHECK(text_edit->get_line_wrap_count(-1) == 0);
+ CHECK(text_edit->get_line_wrap_count(1) == 0);
+ CHECK(text_edit->get_line_wrap_index_at_column(-1, 0) == 0);
+ CHECK(text_edit->get_line_wrap_index_at_column(0, -1) == 0);
+ CHECK(text_edit->get_line_wrap_index_at_column(1, 0) == 0);
+ CHECK(text_edit->get_line_wrap_index_at_column(0, 10000) == 0);
+ CHECK(text_edit->get_line_wrapped_text(-1).size() == 0);
+ CHECK(text_edit->get_line_wrapped_text(1).size() == 0);
+ ERR_PRINT_ON;
+
+ memdelete(text_edit);
+}
+
+TEST_CASE("[SceneTree][TextEdit] viewport") {
+ TextEdit *text_edit = memnew(TextEdit);
+ SceneTree::get_singleton()->get_root()->add_child(text_edit);
+
+ // No subcases here for performance.
+ text_edit->set_size(Size2(800, 600));
+ for (int i = 0; i < 50; i++) {
+ text_edit->insert_line_at(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec varius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque.");
+ }
+ MessageQueue::get_singleton()->flush();
+
+ const int visible_lines = text_edit->get_visible_line_count();
+ const int total_visible_lines = text_edit->get_total_visible_line_count();
+ CHECK(total_visible_lines == 51);
+
+ // First visible line.
+ CHECK(text_edit->get_first_visible_line() == 0);
+ CHECK(text_edit->get_v_scroll() == 0);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ text_edit->set_line_as_first_visible(visible_lines);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == visible_lines);
+ CHECK(text_edit->get_v_scroll() == visible_lines);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ ERR_PRINT_OFF;
+ text_edit->set_line_as_first_visible(-1);
+ text_edit->set_line_as_first_visible(500);
+ text_edit->set_line_as_first_visible(0, -1);
+ text_edit->set_line_as_first_visible(0, 500);
+ CHECK(text_edit->get_first_visible_line() == visible_lines);
+ ERR_PRINT_ON;
+
+ // Wrap.
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_total_visible_line_count() > total_visible_lines);
+
+ text_edit->set_line_as_first_visible(5, 1);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 5);
+ CHECK(text_edit->get_v_scroll() == 11);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines - 6);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 1);
+
+ // Reset.
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_NONE);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_total_visible_line_count() == total_visible_lines);
+ text_edit->set_line_as_first_visible(0);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 0);
+ CHECK(text_edit->get_v_scroll() == 0);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ // Last visible line.
+ text_edit->set_line_as_last_visible(visible_lines * 2);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == visible_lines);
+ CHECK(text_edit->get_v_scroll() == visible_lines);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ ERR_PRINT_OFF;
+ text_edit->set_line_as_last_visible(-1);
+ text_edit->set_line_as_last_visible(500);
+ text_edit->set_line_as_last_visible(0, -1);
+ text_edit->set_line_as_last_visible(0, 500);
+ CHECK(text_edit->get_first_visible_line() == visible_lines);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1);
+ ERR_PRINT_ON;
+
+ // Wrap.
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_total_visible_line_count() > total_visible_lines);
+
+ text_edit->set_line_as_last_visible(visible_lines + 5, 1);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 16);
+ CHECK(text_edit->get_v_scroll() == 32.0);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines + 5);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ // Reset.
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_NONE);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_total_visible_line_count() == total_visible_lines);
+ text_edit->set_line_as_first_visible(0);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 0);
+ CHECK(text_edit->get_v_scroll() == 0);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ // Center.
+ text_edit->set_line_as_center_visible(visible_lines + (visible_lines / 2));
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == visible_lines);
+ CHECK(text_edit->get_v_scroll() == visible_lines);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ ERR_PRINT_OFF;
+ text_edit->set_line_as_last_visible(-1);
+ text_edit->set_line_as_last_visible(500);
+ text_edit->set_line_as_last_visible(0, -1);
+ text_edit->set_line_as_last_visible(0, 500);
+ CHECK(text_edit->get_first_visible_line() == visible_lines);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1);
+ ERR_PRINT_ON;
+
+ // Wrap.
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_total_visible_line_count() > total_visible_lines);
+
+ text_edit->set_line_as_center_visible(visible_lines + (visible_lines / 2) + 5, 1);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == visible_lines + (visible_lines / 2));
+ CHECK(text_edit->get_v_scroll() == (visible_lines * 3));
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 1);
+
+ // Scroll past eof.
+ int line_count = text_edit->get_line_count();
+ text_edit->set_scroll_past_end_of_file_enabled(true);
+ MessageQueue::get_singleton()->flush();
+ text_edit->set_line_as_center_visible(line_count - 1);
+ MessageQueue::get_singleton()->flush();
+
+ CHECK(text_edit->get_first_visible_line() == (visible_lines * 2) + 3);
+ CHECK(text_edit->get_v_scroll() == (visible_lines * 4) + 6);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) + 8);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ text_edit->set_scroll_past_end_of_file_enabled(false);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == (visible_lines * 2) + 3);
+ CHECK(text_edit->get_v_scroll() == (visible_lines * 4) - 4);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) + 8);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ // Reset.
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_NONE);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_total_visible_line_count() == total_visible_lines);
+ text_edit->set_line_as_first_visible(0);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 0);
+ CHECK(text_edit->get_v_scroll() == 0);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ // Auto adjust - todo: horizontal scroll.
+ // Below.
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(text_edit->is_caret_visible());
+ text_edit->set_caret_line(visible_lines + 5, false);
+ CHECK_FALSE(text_edit->is_caret_visible());
+ text_edit->adjust_viewport_to_caret();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->is_caret_visible());
+ CHECK(text_edit->get_first_visible_line() == 5);
+ CHECK(text_edit->get_v_scroll() == 5);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines - 1) + 5);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ text_edit->center_viewport_to_caret();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == visible_lines - 5);
+ CHECK(text_edit->get_v_scroll() == visible_lines - 5);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 6);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ // Caret visible, do nothing.
+ text_edit->adjust_viewport_to_caret();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == visible_lines - 5);
+ CHECK(text_edit->get_v_scroll() == visible_lines - 5);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 6);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ // Above.
+ text_edit->set_caret_line(1, false);
+ MessageQueue::get_singleton()->flush();
+ text_edit->adjust_viewport_to_caret();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->is_caret_visible());
+ CHECK(text_edit->get_first_visible_line() == 1);
+ CHECK(text_edit->get_v_scroll() == 1);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ text_edit->set_line_as_first_visible(0);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 0);
+ CHECK(text_edit->get_v_scroll() == 0);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ text_edit->adjust_viewport_to_caret();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 0);
+ CHECK(text_edit->get_v_scroll() == 0);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ // Wrap
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_total_visible_line_count() > total_visible_lines);
+
+ text_edit->set_caret_line(visible_lines + 5, false, true, 1);
+ MessageQueue::get_singleton()->flush();
+ text_edit->adjust_viewport_to_caret();
+ MessageQueue::get_singleton()->flush();
+
+ CHECK(text_edit->get_first_visible_line() == (visible_lines / 2) + 4);
+ CHECK(text_edit->get_v_scroll() == (visible_lines + (visible_lines / 2)) - 1);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines) + 3);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 1);
+ CHECK(text_edit->get_caret_wrap_index() == 1);
+
+ text_edit->center_viewport_to_caret();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == visible_lines);
+ CHECK(text_edit->get_v_scroll() == (visible_lines * 2) + 1);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 11);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 1);
+
+ // Caret visible, do nothing.
+ text_edit->adjust_viewport_to_caret();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == visible_lines);
+ CHECK(text_edit->get_v_scroll() == (visible_lines * 2) + 1);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 11);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 1);
+
+ // Above.
+ text_edit->set_caret_line(1, false, true, 1);
+ MessageQueue::get_singleton()->flush();
+ text_edit->adjust_viewport_to_caret();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->is_caret_visible());
+ CHECK(text_edit->get_first_visible_line() == 1);
+ CHECK(text_edit->get_v_scroll() == 3);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines / 2) + 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 1);
+ CHECK(text_edit->get_caret_wrap_index() == 1);
+
+ text_edit->set_line_as_first_visible(0);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->is_caret_visible());
+ CHECK(text_edit->get_first_visible_line() == 0);
+ CHECK(text_edit->get_v_scroll() == 0);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines - 11);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ text_edit->adjust_viewport_to_caret();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 0);
+ CHECK(text_edit->get_v_scroll() == 0);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines - 11);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ // Reset.
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_NONE);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_total_visible_line_count() == total_visible_lines);
+ text_edit->set_line_as_first_visible(0);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 0);
+ CHECK(text_edit->get_v_scroll() == 0);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ // Smooth scroll.
+ text_edit->set_v_scroll_speed(10);
+ CHECK(text_edit->get_v_scroll_speed() == 10);
+ ERR_PRINT_OFF;
+ text_edit->set_v_scroll_speed(-1);
+ CHECK(text_edit->get_v_scroll_speed() == 10);
+
+ text_edit->set_v_scroll_speed(0);
+ CHECK(text_edit->get_v_scroll_speed() == 10);
+
+ text_edit->set_v_scroll_speed(1);
+ CHECK(text_edit->get_v_scroll_speed() == 1);
+ ERR_PRINT_ON;
+
+ // Scroll.
+ int v_scroll = text_edit->get_v_scroll();
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(10, 10), MouseButton::WHEEL_DOWN, MouseButton::WHEEL_DOWN, Key::NONE);
+ CHECK(text_edit->get_v_scroll() > v_scroll);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(10, 10), MouseButton::WHEEL_UP, MouseButton::WHEEL_UP, Key::NONE);
+ CHECK(text_edit->get_v_scroll() == v_scroll);
+
+ // smooth scroll speed.
+ text_edit->set_smooth_scroll_enabled(true);
+
+ v_scroll = text_edit->get_v_scroll();
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(10, 10), MouseButton::WHEEL_DOWN, MouseButton::WHEEL_DOWN, Key::NONE);
+ text_edit->notification(TextEdit::NOTIFICATION_INTERNAL_PHYSICS_PROCESS);
+ CHECK(text_edit->get_v_scroll() >= v_scroll);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(10, 10), MouseButton::WHEEL_UP, MouseButton::WHEEL_UP, Key::NONE);
+ text_edit->notification(TextEdit::NOTIFICATION_INTERNAL_PHYSICS_PROCESS);
+ CHECK(text_edit->get_v_scroll() == v_scroll);
+
+ v_scroll = text_edit->get_v_scroll();
+ text_edit->set_v_scroll_speed(10000);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(10, 10), MouseButton::WHEEL_DOWN, MouseButton::WHEEL_DOWN, Key::NONE);
+ text_edit->notification(TextEdit::NOTIFICATION_INTERNAL_PHYSICS_PROCESS);
+ CHECK(text_edit->get_v_scroll() >= v_scroll);
+ SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(10, 10), MouseButton::WHEEL_UP, MouseButton::WHEEL_UP, Key::NONE);
+ text_edit->notification(TextEdit::NOTIFICATION_INTERNAL_PHYSICS_PROCESS);
+ CHECK(text_edit->get_v_scroll() == v_scroll);
+
+ ERR_PRINT_OFF;
+ CHECK(text_edit->get_scroll_pos_for_line(-1) == 0);
+ CHECK(text_edit->get_scroll_pos_for_line(1000) == 0);
+ CHECK(text_edit->get_scroll_pos_for_line(1, -1) == 0);
+ CHECK(text_edit->get_scroll_pos_for_line(1, 100) == 0);
+ ERR_PRINT_ON;
+
+ text_edit->set_h_scroll(-100);
+ CHECK(text_edit->get_h_scroll() == 0);
+
+ text_edit->set_h_scroll(10000000);
+ CHECK(text_edit->get_h_scroll() == 313);
+
+ text_edit->set_h_scroll(-100);
+ CHECK(text_edit->get_h_scroll() == 0);
+
+ text_edit->set_smooth_scroll_enabled(false);
+
+ CHECK(text_edit->get_first_visible_line() == 0);
+ CHECK(text_edit->get_v_scroll() == 0);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+
+ text_edit->grab_focus();
+ SEND_GUI_ACTION(text_edit, "ui_text_scroll_down");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_first_visible_line() == 1);
+ CHECK(text_edit->get_v_scroll() == 1);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_scroll_up");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_first_visible_line() == 0);
+ CHECK(text_edit->get_v_scroll() == 0);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ // Page down, similar to VSCode, to end of page then scroll.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_page_down");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 21);
+ CHECK(text_edit->get_first_visible_line() == 0);
+ CHECK(text_edit->get_v_scroll() == 0);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_page_down");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 41);
+ CHECK(text_edit->get_first_visible_line() == 20);
+ CHECK(text_edit->get_v_scroll() == 20);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines - 1) * 2);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_page_up");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 21);
+ CHECK(text_edit->get_first_visible_line() == 20);
+ CHECK(text_edit->get_v_scroll() == 20);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines - 1) * 2);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_page_up");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 1);
+ CHECK(text_edit->get_first_visible_line() == 1);
+ CHECK(text_edit->get_v_scroll() == 1);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_NONE);
+ MessageQueue::get_singleton()->flush();
+
+ text_edit->grab_focus();
+ SEND_GUI_ACTION(text_edit, "ui_text_scroll_down");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_first_visible_line() == 2);
+ CHECK(text_edit->get_v_scroll() == 2);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines + 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_scroll_up");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_first_visible_line() == 1);
+ CHECK(text_edit->get_v_scroll() == 1);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ // Page down, similar to VSCode, to end of page then scroll.
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_page_down");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 22);
+ CHECK(text_edit->get_first_visible_line() == 1);
+ CHECK(text_edit->get_v_scroll() == 1);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_page_down");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 42);
+ CHECK(text_edit->get_first_visible_line() == 21);
+ CHECK(text_edit->get_v_scroll() == 21);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_page_up");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 22);
+ CHECK(text_edit->get_first_visible_line() == 21);
+ CHECK(text_edit->get_v_scroll() == 21);
+ CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ SEND_GUI_ACTION(text_edit, "ui_text_caret_page_up");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line() == 2);
+ CHECK(text_edit->get_first_visible_line() == 2);
+ CHECK(text_edit->get_v_scroll() == 2);
+ CHECK(text_edit->get_last_full_visible_line() == visible_lines + 1);
+ CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0);
+ CHECK(text_edit->get_caret_wrap_index() == 0);
+
+ // Typing and undo / redo should adjust viewport
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+ text_edit->set_line_as_first_visible(5);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 5);
+
+ SEND_GUI_KEY_EVENT(text_edit, Key::A);
+ CHECK(text_edit->get_first_visible_line() == 0);
+
+ text_edit->set_line_as_first_visible(5);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 5);
+
+ text_edit->undo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 0);
+
+ text_edit->set_line_as_first_visible(5);
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 5);
+
+ text_edit->redo();
+ MessageQueue::get_singleton()->flush();
+ CHECK(text_edit->get_first_visible_line() == 0);
+
+ memdelete(text_edit);
+}
+
+TEST_CASE("[SceneTree][TextEdit] setter getters") {
+ TextEdit *text_edit = memnew(TextEdit);
+ SceneTree::get_singleton()->get_root()->add_child(text_edit);
+
+ SUBCASE("[TextEdit] set and get placeholder") {
+ text_edit->set_placeholder("test\nplaceholder");
+ CHECK(text_edit->get_placeholder() == "test\nplaceholder");
+
+ CHECK(text_edit->get_text() == "");
+ CHECK(text_edit->get_line_count() == 1);
+ CHECK(text_edit->get_last_full_visible_line() == 0);
+ }
+
+ SUBCASE("[TextEdit] highlight current line") {
+ text_edit->set_highlight_current_line(true);
+ CHECK(text_edit->is_highlight_current_line_enabled());
+ text_edit->set_highlight_current_line(false);
+ CHECK_FALSE(text_edit->is_highlight_current_line_enabled());
+ }
+
+ SUBCASE("[TextEdit] highlight all occurrences") {
+ text_edit->set_highlight_all_occurrences(true);
+ CHECK(text_edit->is_highlight_all_occurrences_enabled());
+ text_edit->set_highlight_all_occurrences(false);
+ CHECK_FALSE(text_edit->is_highlight_all_occurrences_enabled());
+ }
+
+ SUBCASE("[TextEdit] draw control chars") {
+ text_edit->set_draw_control_chars(true);
+ CHECK(text_edit->get_draw_control_chars());
+ text_edit->set_draw_control_chars(false);
+ CHECK_FALSE(text_edit->get_draw_control_chars());
+ }
+
+ SUBCASE("[TextEdit] draw tabs") {
+ text_edit->set_draw_tabs(true);
+ CHECK(text_edit->is_drawing_tabs());
+ text_edit->set_draw_tabs(false);
+ CHECK_FALSE(text_edit->is_drawing_tabs());
+ }
+
+ SUBCASE("[TextEdit] draw spaces") {
+ text_edit->set_draw_spaces(true);
+ CHECK(text_edit->is_drawing_spaces());
+ text_edit->set_draw_spaces(false);
+ CHECK_FALSE(text_edit->is_drawing_spaces());
+ }
+
+ SUBCASE("[TextEdit] draw minimao") {
+ text_edit->set_draw_minimap(true);
+ CHECK(text_edit->is_drawing_minimap());
+ text_edit->set_draw_minimap(false);
+ CHECK_FALSE(text_edit->is_drawing_minimap());
+ }
+
+ SUBCASE("[TextEdit] minimap width") {
+ text_edit->set_minimap_width(-1);
+ CHECK(text_edit->get_minimap_width() == -1);
+ text_edit->set_minimap_width(1000);
+ CHECK(text_edit->get_minimap_width() == 1000);
+ }
+
+ SUBCASE("[TextEdit] line color background") {
+ ERR_PRINT_OFF;
+ text_edit->set_line_background_color(-1, Color("#ff0000"));
+ text_edit->set_line_background_color(0, Color("#00ff00"));
+ text_edit->set_line_background_color(1, Color("#0000ff"));
+
+ CHECK(text_edit->get_line_background_color(-1) == Color());
+ CHECK(text_edit->get_line_background_color(0) == Color("#00ff00"));
+ CHECK(text_edit->get_line_background_color(1) == Color());
+ ERR_PRINT_ON;
+
+ text_edit->set_line_background_color(0, Color("#ffff00"));
+ CHECK(text_edit->get_line_background_color(0) == Color("#ffff00"));
+ }
+
+ memdelete(text_edit);
+}
+
+TEST_CASE("[SceneTree][TextEdit] gutters") {
+ TextEdit *text_edit = memnew(TextEdit);
+ SceneTree::get_singleton()->get_root()->add_child(text_edit);
+
+ Array empty_signal_args;
+ empty_signal_args.push_back(Array());
+
+ SIGNAL_WATCH(text_edit, "gutter_clicked");
+ SIGNAL_WATCH(text_edit, "gutter_added");
+ SIGNAL_WATCH(text_edit, "gutter_removed");
+
+ SUBCASE("[TextEdit] gutter add and remove") {
+ text_edit->add_gutter();
+ CHECK(text_edit->get_gutter_count() == 1);
+ CHECK(text_edit->get_gutter_width(0) == 24);
+ CHECK(text_edit->get_total_gutter_width() == 24 + 2);
+ SIGNAL_CHECK("gutter_added", empty_signal_args);
+
+ text_edit->set_gutter_name(0, "test_gutter");
+ CHECK(text_edit->get_gutter_name(0) == "test_gutter");
+
+ text_edit->set_gutter_width(0, 10);
+ CHECK(text_edit->get_gutter_width(0) == 10);
+ CHECK(text_edit->get_total_gutter_width() == 10 + 2);
+
+ text_edit->add_gutter(-100);
+ text_edit->set_gutter_width(1, 10);
+ CHECK(text_edit->get_gutter_width(1) == 10);
+ CHECK(text_edit->get_total_gutter_width() == 20 + 2);
+ CHECK(text_edit->get_gutter_count() == 2);
+ CHECK(text_edit->get_gutter_name(0) == "test_gutter");
+ SIGNAL_CHECK("gutter_added", empty_signal_args);
+
+ text_edit->set_gutter_draw(1, false);
+ CHECK(text_edit->get_total_gutter_width() == 10 + 2);
+
+ text_edit->add_gutter(100);
+ CHECK(text_edit->get_gutter_count() == 3);
+ CHECK(text_edit->get_gutter_width(2) == 24);
+ CHECK(text_edit->get_total_gutter_width() == 34 + 2);
+ CHECK(text_edit->get_gutter_name(0) == "test_gutter");
+ SIGNAL_CHECK("gutter_added", empty_signal_args);
+
+ text_edit->add_gutter(0);
+ CHECK(text_edit->get_gutter_count() == 4);
+ CHECK(text_edit->get_gutter_width(0) == 24);
+ CHECK(text_edit->get_total_gutter_width() == 58 + 2);
+ CHECK(text_edit->get_gutter_name(1) == "test_gutter");
+ SIGNAL_CHECK("gutter_added", empty_signal_args);
+
+ text_edit->remove_gutter(2);
+ CHECK(text_edit->get_gutter_name(1) == "test_gutter");
+ CHECK(text_edit->get_gutter_count() == 3);
+ CHECK(text_edit->get_total_gutter_width() == 58 + 2);
+ SIGNAL_CHECK("gutter_removed", empty_signal_args);
+
+ text_edit->remove_gutter(0);
+ CHECK(text_edit->get_gutter_name(0) == "test_gutter");
+ CHECK(text_edit->get_gutter_count() == 2);
+ CHECK(text_edit->get_total_gutter_width() == 34 + 2);
+ SIGNAL_CHECK("gutter_removed", empty_signal_args);
+
+ ERR_PRINT_OFF;
+ text_edit->remove_gutter(-1);
+ SIGNAL_CHECK_FALSE("gutter_removed");
+
+ text_edit->remove_gutter(100);
+ SIGNAL_CHECK_FALSE("gutter_removed");
+
+ CHECK(text_edit->get_gutter_name(-1) == "");
+ CHECK(text_edit->get_gutter_name(100) == "");
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[TextEdit] gutter data") {
+ text_edit->add_gutter();
+ CHECK(text_edit->get_gutter_count() == 1);
+ SIGNAL_CHECK("gutter_added", empty_signal_args);
+
+ text_edit->set_gutter_name(0, "test_gutter");
+ CHECK(text_edit->get_gutter_name(0) == "test_gutter");
+
+ text_edit->set_gutter_width(0, 10);
+ CHECK(text_edit->get_gutter_width(0) == 10);
+
+ text_edit->set_gutter_clickable(0, true);
+ CHECK(text_edit->is_gutter_clickable(0));
+
+ text_edit->set_gutter_overwritable(0, true);
+ CHECK(text_edit->is_gutter_overwritable(0));
+
+ text_edit->set_gutter_type(0, TextEdit::GutterType::GUTTER_TYPE_CUSTOM);
+ CHECK(text_edit->get_gutter_type(0) == TextEdit::GutterType::GUTTER_TYPE_CUSTOM);
+
+ text_edit->set_text("test\ntext");
+
+ ERR_PRINT_OFF;
+ text_edit->set_line_gutter_metadata(1, 0, "test");
+ text_edit->set_line_gutter_metadata(0, -1, "test");
+ text_edit->set_line_gutter_metadata(0, 2, "test");
+ text_edit->set_line_gutter_metadata(2, 0, "test");
+ text_edit->set_line_gutter_metadata(-1, 0, "test");
+
+ CHECK(text_edit->get_line_gutter_metadata(1, 0) == "test");
+ CHECK(text_edit->get_line_gutter_metadata(0, -1) == "");
+ CHECK(text_edit->get_line_gutter_metadata(0, 2) == "");
+ CHECK(text_edit->get_line_gutter_metadata(2, 0) == "");
+ CHECK(text_edit->get_line_gutter_metadata(-1, 0) == "");
+
+ text_edit->set_line_gutter_text(1, 0, "test");
+ text_edit->set_line_gutter_text(0, -1, "test");
+ text_edit->set_line_gutter_text(0, 2, "test");
+ text_edit->set_line_gutter_text(2, 0, "test");
+ text_edit->set_line_gutter_text(-1, 0, "test");
+
+ CHECK(text_edit->get_line_gutter_text(1, 0) == "test");
+ CHECK(text_edit->get_line_gutter_text(0, -1) == "");
+ CHECK(text_edit->get_line_gutter_text(0, 2) == "");
+ CHECK(text_edit->get_line_gutter_text(2, 0) == "");
+ CHECK(text_edit->get_line_gutter_text(-1, 0) == "");
+
+ text_edit->set_line_gutter_item_color(1, 0, Color(1, 0, 0));
+ text_edit->set_line_gutter_item_color(0, -1, Color(1, 0, 0));
+ text_edit->set_line_gutter_item_color(0, 2, Color(1, 0, 0));
+ text_edit->set_line_gutter_item_color(2, 0, Color(1, 0, 0));
+ text_edit->set_line_gutter_item_color(-1, 0, Color(1, 0, 0));
+
+ CHECK(text_edit->get_line_gutter_item_color(1, 0) == Color(1, 0, 0));
+ CHECK(text_edit->get_line_gutter_item_color(0, -1) == Color());
+ CHECK(text_edit->get_line_gutter_item_color(0, 2) == Color());
+ CHECK(text_edit->get_line_gutter_item_color(2, 0) == Color());
+ CHECK(text_edit->get_line_gutter_item_color(-1, 0) == Color());
+
+ text_edit->set_line_gutter_clickable(1, 0, true);
+ text_edit->set_line_gutter_clickable(0, -1, true);
+ text_edit->set_line_gutter_clickable(0, 2, true);
+ text_edit->set_line_gutter_clickable(2, 0, true);
+ text_edit->set_line_gutter_clickable(-1, 0, true);
+
+ CHECK(text_edit->is_line_gutter_clickable(1, 0) == true);
+ CHECK(text_edit->is_line_gutter_clickable(0, -1) == false);
+ CHECK(text_edit->is_line_gutter_clickable(0, 2) == false);
+ CHECK(text_edit->is_line_gutter_clickable(2, 0) == false);
+ CHECK(text_edit->is_line_gutter_clickable(-1, 0) == false);
+ ERR_PRINT_ON;
+
+ // Merging tested via CodeEdit gutters.
+ }
+
+ SIGNAL_UNWATCH(text_edit, "gutter_clicked");
+ SIGNAL_UNWATCH(text_edit, "gutter_added");
+ SIGNAL_UNWATCH(text_edit, "gutter_removed");
+ memdelete(text_edit);
+}
+
+} // namespace TestTextEdit
+
+#endif // TEST_TEXT_EDIT_H
diff --git a/tests/scene/test_theme.h b/tests/scene/test_theme.h
new file mode 100644
index 0000000000..f5b21eec32
--- /dev/null
+++ b/tests/scene/test_theme.h
@@ -0,0 +1,271 @@
+/*************************************************************************/
+/* test_theme.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_THEME_H
+#define TEST_THEME_H
+
+#include "scene/resources/theme.h"
+#include "tests/test_tools.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestTheme {
+
+class Fixture {
+public:
+ struct DataEntry {
+ Theme::DataType type;
+ Variant value;
+ } const valid_data[Theme::DATA_TYPE_MAX] = {
+ { Theme::DATA_TYPE_COLOR, Color() },
+ { Theme::DATA_TYPE_CONSTANT, 42 },
+ { Theme::DATA_TYPE_FONT, Ref<FontFile>(memnew(FontFile)) },
+ { Theme::DATA_TYPE_FONT_SIZE, 42 },
+ { Theme::DATA_TYPE_ICON, Ref<Texture>(memnew(ImageTexture)) },
+ { Theme::DATA_TYPE_STYLEBOX, Ref<StyleBox>(memnew(StyleBoxFlat)) },
+ };
+
+ const StringName valid_item_name = "valid_item_name";
+ const StringName valid_type_name = "ValidTypeName";
+};
+
+TEST_CASE_FIXTURE(Fixture, "[Theme] Good theme type names") {
+ StringName names[] = {
+ "", // Empty name.
+ "CapitalizedName",
+ "snake_cased_name",
+ "42",
+ "_Underscore_",
+ };
+
+ SUBCASE("add_type") {
+ for (const StringName &name : names) {
+ Ref<Theme> theme = memnew(Theme);
+
+ ErrorDetector ed;
+ theme->add_type(name);
+ CHECK_FALSE(ed.has_error);
+ }
+ }
+
+ SUBCASE("set_theme_item") {
+ for (const StringName &name : names) {
+ for (const DataEntry &entry : valid_data) {
+ Ref<Theme> theme = memnew(Theme);
+
+ ErrorDetector ed;
+ theme->set_theme_item(entry.type, valid_item_name, name, entry.value);
+ CHECK_FALSE(ed.has_error);
+ }
+ }
+ }
+
+ SUBCASE("add_theme_item_type") {
+ for (const StringName &name : names) {
+ for (const DataEntry &entry : valid_data) {
+ Ref<Theme> theme = memnew(Theme);
+
+ ErrorDetector ed;
+ theme->add_theme_item_type(entry.type, name);
+ CHECK_FALSE(ed.has_error);
+ }
+ }
+ }
+
+ SUBCASE("set_type_variation") {
+ for (const StringName &name : names) {
+ if (name == StringName()) { // Skip empty here, not allowed.
+ continue;
+ }
+ Ref<Theme> theme = memnew(Theme);
+
+ ErrorDetector ed;
+ theme->set_type_variation(valid_type_name, name);
+ CHECK_FALSE(ed.has_error);
+ }
+ for (const StringName &name : names) {
+ if (name == StringName()) { // Skip empty here, not allowed.
+ continue;
+ }
+ Ref<Theme> theme = memnew(Theme);
+
+ ErrorDetector ed;
+ theme->set_type_variation(name, valid_type_name);
+ CHECK_FALSE(ed.has_error);
+ }
+ }
+}
+
+TEST_CASE_FIXTURE(Fixture, "[Theme] Bad theme type names") {
+ StringName names[] = {
+ "With/Slash",
+ "With Space",
+ "With@various$symbols!",
+ String::utf8("contains_汉字"),
+ };
+
+ ERR_PRINT_OFF; // All these rightfully print errors.
+
+ SUBCASE("add_type") {
+ for (const StringName &name : names) {
+ Ref<Theme> theme = memnew(Theme);
+
+ ErrorDetector ed;
+ theme->add_type(name);
+ CHECK(ed.has_error);
+ }
+ }
+
+ SUBCASE("set_theme_item") {
+ for (const StringName &name : names) {
+ for (const DataEntry &entry : valid_data) {
+ Ref<Theme> theme = memnew(Theme);
+
+ ErrorDetector ed;
+ theme->set_theme_item(entry.type, valid_item_name, name, entry.value);
+ CHECK(ed.has_error);
+ }
+ }
+ }
+
+ SUBCASE("add_theme_item_type") {
+ for (const StringName &name : names) {
+ for (const DataEntry &entry : valid_data) {
+ Ref<Theme> theme = memnew(Theme);
+
+ ErrorDetector ed;
+ theme->add_theme_item_type(entry.type, name);
+ CHECK(ed.has_error);
+ }
+ }
+ }
+
+ SUBCASE("set_type_variation") {
+ for (const StringName &name : names) {
+ Ref<Theme> theme = memnew(Theme);
+
+ ErrorDetector ed;
+ theme->set_type_variation(valid_type_name, name);
+ CHECK(ed.has_error);
+ }
+ for (const StringName &name : names) {
+ Ref<Theme> theme = memnew(Theme);
+
+ ErrorDetector ed;
+ theme->set_type_variation(name, valid_type_name);
+ CHECK(ed.has_error);
+ }
+ }
+
+ ERR_PRINT_ON;
+}
+
+TEST_CASE_FIXTURE(Fixture, "[Theme] Good theme item names") {
+ StringName names[] = {
+ "CapitalizedName",
+ "snake_cased_name",
+ "42",
+ "_Underscore_",
+ };
+
+ SUBCASE("set_theme_item") {
+ for (const StringName &name : names) {
+ for (const DataEntry &entry : valid_data) {
+ Ref<Theme> theme = memnew(Theme);
+
+ ErrorDetector ed;
+ theme->set_theme_item(entry.type, name, valid_type_name, entry.value);
+ CHECK_FALSE(ed.has_error);
+ CHECK(theme->has_theme_item(entry.type, name, valid_type_name));
+ }
+ }
+ }
+
+ SUBCASE("rename_theme_item") {
+ for (const StringName &name : names) {
+ for (const DataEntry &entry : valid_data) {
+ Ref<Theme> theme = memnew(Theme);
+ theme->set_theme_item(entry.type, valid_item_name, valid_type_name, entry.value);
+
+ ErrorDetector ed;
+ theme->rename_theme_item(entry.type, valid_item_name, name, valid_type_name);
+ CHECK_FALSE(ed.has_error);
+ CHECK_FALSE(theme->has_theme_item(entry.type, valid_item_name, valid_type_name));
+ CHECK(theme->has_theme_item(entry.type, name, valid_type_name));
+ }
+ }
+ }
+}
+
+TEST_CASE_FIXTURE(Fixture, "[Theme] Bad theme item names") {
+ StringName names[] = {
+ "", // Empty name.
+ "With/Slash",
+ "With Space",
+ "With@various$symbols!",
+ String::utf8("contains_汉字"),
+ };
+
+ ERR_PRINT_OFF; // All these rightfully print errors.
+
+ SUBCASE("set_theme_item") {
+ for (const StringName &name : names) {
+ for (const DataEntry &entry : valid_data) {
+ Ref<Theme> theme = memnew(Theme);
+
+ ErrorDetector ed;
+ theme->set_theme_item(entry.type, name, valid_type_name, entry.value);
+ CHECK(ed.has_error);
+ CHECK_FALSE(theme->has_theme_item(entry.type, name, valid_type_name));
+ }
+ }
+ }
+
+ SUBCASE("rename_theme_item") {
+ for (const StringName &name : names) {
+ for (const DataEntry &entry : valid_data) {
+ Ref<Theme> theme = memnew(Theme);
+ theme->set_theme_item(entry.type, valid_item_name, valid_type_name, entry.value);
+
+ ErrorDetector ed;
+ theme->rename_theme_item(entry.type, valid_item_name, name, valid_type_name);
+ CHECK(ed.has_error);
+ CHECK(theme->has_theme_item(entry.type, valid_item_name, valid_type_name));
+ CHECK_FALSE(theme->has_theme_item(entry.type, name, valid_type_name));
+ }
+ }
+ }
+
+ ERR_PRINT_ON;
+}
+
+} // namespace TestTheme
+
+#endif // TEST_THEME_H
diff --git a/tests/servers/test_text_server.h b/tests/servers/test_text_server.h
new file mode 100644
index 0000000000..9ebd0f34b4
--- /dev/null
+++ b/tests/servers/test_text_server.h
@@ -0,0 +1,629 @@
+/*************************************************************************/
+/* test_text_server.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef TEST_TEXT_SERVER_H
+#define TEST_TEXT_SERVER_H
+
+#ifdef TOOLS_ENABLED
+
+#include "editor/builtin_fonts.gen.h"
+#include "servers/text_server.h"
+#include "tests/test_macros.h"
+
+namespace TestTextServer {
+
+TEST_SUITE("[TextServer]") {
+ TEST_CASE("[TextServer] Init, font loading and shaping") {
+ SUBCASE("[TextServer] Loading fonts") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ CHECK_FALSE_MESSAGE(ts.is_null(), "Invalid TS interface.");
+
+ if (!ts->has_feature(TextServer::FEATURE_FONT_DYNAMIC)) {
+ continue;
+ }
+
+ RID font = ts->create_font();
+ ts->font_set_data_ptr(font, _font_NotoSans_Regular, _font_NotoSans_Regular_size);
+ CHECK_FALSE_MESSAGE(font == RID(), "Loading font failed.");
+ ts->free_rid(font);
+ }
+ }
+
+ SUBCASE("[TextServer] Text layout: Font fallback") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ CHECK_FALSE_MESSAGE(ts.is_null(), "Invalid TS interface.");
+
+ if (!ts->has_feature(TextServer::FEATURE_FONT_DYNAMIC) || !ts->has_feature(TextServer::FEATURE_SIMPLE_LAYOUT)) {
+ continue;
+ }
+
+ RID font1 = ts->create_font();
+ ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size);
+ RID font2 = ts->create_font();
+ ts->font_set_data_ptr(font2, _font_NotoSansThaiUI_Regular, _font_NotoSansThaiUI_Regular_size);
+
+ Array font;
+ font.push_back(font1);
+ font.push_back(font2);
+
+ String test = U"คนอ้วน khon uan ראה";
+ // 6^ 17^
+
+ RID ctx = ts->create_shaped_text();
+ CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed.");
+ bool ok = ts->shaped_text_add_string(ctx, test, font, 16);
+ CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed.");
+
+ const Glyph *glyphs = ts->shaped_text_get_glyphs(ctx);
+ int gl_size = ts->shaped_text_get_glyph_count(ctx);
+ CHECK_FALSE_MESSAGE(gl_size == 0, "Shaping failed");
+ for (int j = 0; j < gl_size; j++) {
+ if (glyphs[j].start < 6) {
+ CHECK_FALSE_MESSAGE(glyphs[j].font_rid != font[1], "Incorrect font selected.");
+ }
+ if ((glyphs[j].start > 6) && (glyphs[j].start < 16)) {
+ CHECK_FALSE_MESSAGE(glyphs[j].font_rid != font[0], "Incorrect font selected.");
+ }
+ if (glyphs[j].start > 16) {
+ CHECK_FALSE_MESSAGE(glyphs[j].font_rid != RID(), "Incorrect font selected.");
+ CHECK_FALSE_MESSAGE(glyphs[j].index != test[glyphs[j].start], "Incorrect glyph index.");
+ }
+ CHECK_FALSE_MESSAGE((glyphs[j].start < 0 || glyphs[j].end > test.length()), "Incorrect glyph range.");
+ CHECK_FALSE_MESSAGE(glyphs[j].font_size != 16, "Incorrect glyph font size.");
+ }
+
+ ts->free_rid(ctx);
+
+ for (int j = 0; j < font.size(); j++) {
+ ts->free_rid(font[j]);
+ }
+ font.clear();
+ }
+ }
+
+ SUBCASE("[TextServer] Text layout: BiDi") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ CHECK_FALSE_MESSAGE(ts.is_null(), "Invalid TS interface.");
+
+ if (!ts->has_feature(TextServer::FEATURE_FONT_DYNAMIC) || !ts->has_feature(TextServer::FEATURE_BIDI_LAYOUT)) {
+ continue;
+ }
+
+ RID font1 = ts->create_font();
+ ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size);
+ RID font2 = ts->create_font();
+ ts->font_set_data_ptr(font2, _font_NotoNaskhArabicUI_Regular, _font_NotoNaskhArabicUI_Regular_size);
+
+ Array font;
+ font.push_back(font1);
+ font.push_back(font2);
+
+ String test = U"Arabic (اَلْعَرَبِيَّةُ, al-ʿarabiyyah)";
+ // 7^ 26^
+
+ RID ctx = ts->create_shaped_text();
+ CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed.");
+ bool ok = ts->shaped_text_add_string(ctx, test, font, 16);
+ CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed.");
+
+ const Glyph *glyphs = ts->shaped_text_get_glyphs(ctx);
+ int gl_size = ts->shaped_text_get_glyph_count(ctx);
+ CHECK_FALSE_MESSAGE(gl_size == 0, "Shaping failed");
+ for (int j = 0; j < gl_size; j++) {
+ if (glyphs[j].count > 0) {
+ if (glyphs[j].start < 7) {
+ CHECK_FALSE_MESSAGE(((glyphs[j].flags & TextServer::GRAPHEME_IS_RTL) == TextServer::GRAPHEME_IS_RTL), "Incorrect direction.");
+ }
+ if ((glyphs[j].start > 8) && (glyphs[j].start < 23)) {
+ CHECK_FALSE_MESSAGE(((glyphs[j].flags & TextServer::GRAPHEME_IS_RTL) != TextServer::GRAPHEME_IS_RTL), "Incorrect direction.");
+ }
+ if (glyphs[j].start > 26) {
+ CHECK_FALSE_MESSAGE(((glyphs[j].flags & TextServer::GRAPHEME_IS_RTL) == TextServer::GRAPHEME_IS_RTL), "Incorrect direction.");
+ }
+ }
+ }
+
+ ts->free_rid(ctx);
+
+ for (int j = 0; j < font.size(); j++) {
+ ts->free_rid(font[j]);
+ }
+ font.clear();
+ }
+ }
+
+ SUBCASE("[TextServer] Text layout: Line break and align points") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ CHECK_FALSE_MESSAGE(ts.is_null(), "Invalid TS interface.");
+
+ if (!ts->has_feature(TextServer::FEATURE_FONT_DYNAMIC) || !ts->has_feature(TextServer::FEATURE_SIMPLE_LAYOUT)) {
+ continue;
+ }
+
+ RID font1 = ts->create_font();
+ ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size);
+ RID font2 = ts->create_font();
+ ts->font_set_data_ptr(font2, _font_NotoSansThaiUI_Regular, _font_NotoSansThaiUI_Regular_size);
+ RID font3 = ts->create_font();
+ ts->font_set_data_ptr(font3, _font_NotoNaskhArabicUI_Regular, _font_NotoNaskhArabicUI_Regular_size);
+
+ Array font;
+ font.push_back(font1);
+ font.push_back(font2);
+ font.push_back(font3);
+
+ {
+ String test = U"Test test long text long text\n";
+ RID ctx = ts->create_shaped_text();
+ CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed.");
+ bool ok = ts->shaped_text_add_string(ctx, test, font, 16);
+ CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed.");
+ ts->shaped_text_update_breaks(ctx);
+ ts->shaped_text_update_justification_ops(ctx);
+
+ const Glyph *glyphs = ts->shaped_text_get_glyphs(ctx);
+ int gl_size = ts->shaped_text_get_glyph_count(ctx);
+
+ CHECK_FALSE_MESSAGE(gl_size != 30, "Invalid glyph count.");
+ for (int j = 0; j < gl_size; j++) {
+ bool hard = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_HARD) == TextServer::GRAPHEME_IS_BREAK_HARD;
+ bool soft = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_SOFT) == TextServer::GRAPHEME_IS_BREAK_SOFT;
+ bool space = (glyphs[j].flags & TextServer::GRAPHEME_IS_SPACE) == TextServer::GRAPHEME_IS_SPACE;
+ bool virt = (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) == TextServer::GRAPHEME_IS_VIRTUAL;
+ bool elo = (glyphs[j].flags & TextServer::GRAPHEME_IS_ELONGATION) == TextServer::GRAPHEME_IS_ELONGATION;
+ if (j == 4 || j == 9 || j == 14 || j == 19 || j == 24) {
+ CHECK_FALSE_MESSAGE((!soft || !space || hard || virt || elo), "Invalid glyph flags.");
+ } else if (j == 29) {
+ CHECK_FALSE_MESSAGE((soft || !space || !hard || virt || elo), "Invalid glyph flags.");
+ } else {
+ CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags.");
+ }
+ }
+ ts->free_rid(ctx);
+ }
+
+ {
+ String test = U"الحمـد";
+ RID ctx = ts->create_shaped_text();
+ CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed.");
+ bool ok = ts->shaped_text_add_string(ctx, test, font, 16);
+ CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed.");
+ ts->shaped_text_update_breaks(ctx);
+
+ const Glyph *glyphs = ts->shaped_text_get_glyphs(ctx);
+ int gl_size = ts->shaped_text_get_glyph_count(ctx);
+ CHECK_FALSE_MESSAGE(gl_size != 6, "Invalid glyph count.");
+ for (int j = 0; j < gl_size; j++) {
+ bool hard = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_HARD) == TextServer::GRAPHEME_IS_BREAK_HARD;
+ bool soft = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_SOFT) == TextServer::GRAPHEME_IS_BREAK_SOFT;
+ bool space = (glyphs[j].flags & TextServer::GRAPHEME_IS_SPACE) == TextServer::GRAPHEME_IS_SPACE;
+ bool virt = (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) == TextServer::GRAPHEME_IS_VIRTUAL;
+ bool elo = (glyphs[j].flags & TextServer::GRAPHEME_IS_ELONGATION) == TextServer::GRAPHEME_IS_ELONGATION;
+ CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags.");
+ }
+ if (ts->has_feature(TextServer::FEATURE_KASHIDA_JUSTIFICATION)) {
+ ts->shaped_text_update_justification_ops(ctx);
+
+ glyphs = ts->shaped_text_get_glyphs(ctx);
+ gl_size = ts->shaped_text_get_glyph_count(ctx);
+
+ CHECK_FALSE_MESSAGE(gl_size != 6, "Invalid glyph count.");
+ for (int j = 0; j < gl_size; j++) {
+ bool hard = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_HARD) == TextServer::GRAPHEME_IS_BREAK_HARD;
+ bool soft = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_SOFT) == TextServer::GRAPHEME_IS_BREAK_SOFT;
+ bool space = (glyphs[j].flags & TextServer::GRAPHEME_IS_SPACE) == TextServer::GRAPHEME_IS_SPACE;
+ bool virt = (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) == TextServer::GRAPHEME_IS_VIRTUAL;
+ bool elo = (glyphs[j].flags & TextServer::GRAPHEME_IS_ELONGATION) == TextServer::GRAPHEME_IS_ELONGATION;
+ if (j == 1) {
+ CHECK_FALSE_MESSAGE((soft || space || hard || virt || !elo), "Invalid glyph flags.");
+ } else {
+ CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags.");
+ }
+ }
+ }
+ ts->free_rid(ctx);
+ }
+
+ {
+ String test = U"الحمد";
+ RID ctx = ts->create_shaped_text();
+ CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed.");
+ bool ok = ts->shaped_text_add_string(ctx, test, font, 16);
+ CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed.");
+ ts->shaped_text_update_breaks(ctx);
+
+ const Glyph *glyphs = ts->shaped_text_get_glyphs(ctx);
+ int gl_size = ts->shaped_text_get_glyph_count(ctx);
+ CHECK_FALSE_MESSAGE(gl_size != 5, "Invalid glyph count.");
+ for (int j = 0; j < gl_size; j++) {
+ bool hard = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_HARD) == TextServer::GRAPHEME_IS_BREAK_HARD;
+ bool soft = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_SOFT) == TextServer::GRAPHEME_IS_BREAK_SOFT;
+ bool space = (glyphs[j].flags & TextServer::GRAPHEME_IS_SPACE) == TextServer::GRAPHEME_IS_SPACE;
+ bool virt = (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) == TextServer::GRAPHEME_IS_VIRTUAL;
+ bool elo = (glyphs[j].flags & TextServer::GRAPHEME_IS_ELONGATION) == TextServer::GRAPHEME_IS_ELONGATION;
+ CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags.");
+ }
+
+ if (ts->has_feature(TextServer::FEATURE_KASHIDA_JUSTIFICATION)) {
+ ts->shaped_text_update_justification_ops(ctx);
+
+ glyphs = ts->shaped_text_get_glyphs(ctx);
+ gl_size = ts->shaped_text_get_glyph_count(ctx);
+
+ CHECK_FALSE_MESSAGE(gl_size != 6, "Invalid glyph count.");
+ for (int j = 0; j < gl_size; j++) {
+ bool hard = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_HARD) == TextServer::GRAPHEME_IS_BREAK_HARD;
+ bool soft = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_SOFT) == TextServer::GRAPHEME_IS_BREAK_SOFT;
+ bool space = (glyphs[j].flags & TextServer::GRAPHEME_IS_SPACE) == TextServer::GRAPHEME_IS_SPACE;
+ bool virt = (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) == TextServer::GRAPHEME_IS_VIRTUAL;
+ bool elo = (glyphs[j].flags & TextServer::GRAPHEME_IS_ELONGATION) == TextServer::GRAPHEME_IS_ELONGATION;
+ if (j == 1) {
+ CHECK_FALSE_MESSAGE((soft || space || hard || !virt || !elo), "Invalid glyph flags.");
+ } else {
+ CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags.");
+ }
+ }
+ }
+ ts->free_rid(ctx);
+ }
+
+ {
+ String test = U"الحمـد الرياضي العربي";
+ RID ctx = ts->create_shaped_text();
+ CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed.");
+ bool ok = ts->shaped_text_add_string(ctx, test, font, 16);
+ CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed.");
+ ts->shaped_text_update_breaks(ctx);
+
+ const Glyph *glyphs = ts->shaped_text_get_glyphs(ctx);
+ int gl_size = ts->shaped_text_get_glyph_count(ctx);
+
+ CHECK_FALSE_MESSAGE(gl_size != 21, "Invalid glyph count.");
+ for (int j = 0; j < gl_size; j++) {
+ bool hard = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_HARD) == TextServer::GRAPHEME_IS_BREAK_HARD;
+ bool soft = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_SOFT) == TextServer::GRAPHEME_IS_BREAK_SOFT;
+ bool space = (glyphs[j].flags & TextServer::GRAPHEME_IS_SPACE) == TextServer::GRAPHEME_IS_SPACE;
+ bool virt = (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) == TextServer::GRAPHEME_IS_VIRTUAL;
+ bool elo = (glyphs[j].flags & TextServer::GRAPHEME_IS_ELONGATION) == TextServer::GRAPHEME_IS_ELONGATION;
+ if (j == 6 || j == 14) {
+ CHECK_FALSE_MESSAGE((!soft || !space || hard || virt || elo), "Invalid glyph flags.");
+ } else {
+ CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags.");
+ }
+ }
+
+ if (ts->has_feature(TextServer::FEATURE_KASHIDA_JUSTIFICATION)) {
+ ts->shaped_text_update_justification_ops(ctx);
+
+ glyphs = ts->shaped_text_get_glyphs(ctx);
+ gl_size = ts->shaped_text_get_glyph_count(ctx);
+
+ CHECK_FALSE_MESSAGE(gl_size != 23, "Invalid glyph count.");
+ for (int j = 0; j < gl_size; j++) {
+ bool hard = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_HARD) == TextServer::GRAPHEME_IS_BREAK_HARD;
+ bool soft = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_SOFT) == TextServer::GRAPHEME_IS_BREAK_SOFT;
+ bool space = (glyphs[j].flags & TextServer::GRAPHEME_IS_SPACE) == TextServer::GRAPHEME_IS_SPACE;
+ bool virt = (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) == TextServer::GRAPHEME_IS_VIRTUAL;
+ bool elo = (glyphs[j].flags & TextServer::GRAPHEME_IS_ELONGATION) == TextServer::GRAPHEME_IS_ELONGATION;
+ if (j == 7 || j == 16) {
+ CHECK_FALSE_MESSAGE((!soft || !space || hard || virt || elo), "Invalid glyph flags.");
+ } else if (j == 3 || j == 9) {
+ CHECK_FALSE_MESSAGE((soft || space || hard || !virt || !elo), "Invalid glyph flags.");
+ } else if (j == 18) {
+ CHECK_FALSE_MESSAGE((soft || space || hard || virt || !elo), "Invalid glyph flags.");
+ } else {
+ CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags.");
+ }
+ }
+ }
+
+ ts->free_rid(ctx);
+ }
+
+ {
+ String test = U"เป็น ภาษา ราชการ และ ภาษา";
+ RID ctx = ts->create_shaped_text();
+ CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed.");
+ bool ok = ts->shaped_text_add_string(ctx, test, font, 16);
+ CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed.");
+ ts->shaped_text_update_breaks(ctx);
+ ts->shaped_text_update_justification_ops(ctx);
+
+ const Glyph *glyphs = ts->shaped_text_get_glyphs(ctx);
+ int gl_size = ts->shaped_text_get_glyph_count(ctx);
+
+ CHECK_FALSE_MESSAGE(gl_size != 25, "Invalid glyph count.");
+ for (int j = 0; j < gl_size; j++) {
+ bool hard = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_HARD) == TextServer::GRAPHEME_IS_BREAK_HARD;
+ bool soft = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_SOFT) == TextServer::GRAPHEME_IS_BREAK_SOFT;
+ bool space = (glyphs[j].flags & TextServer::GRAPHEME_IS_SPACE) == TextServer::GRAPHEME_IS_SPACE;
+ bool virt = (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) == TextServer::GRAPHEME_IS_VIRTUAL;
+ bool elo = (glyphs[j].flags & TextServer::GRAPHEME_IS_ELONGATION) == TextServer::GRAPHEME_IS_ELONGATION;
+ if (j == 4 || j == 9 || j == 16 || j == 20) {
+ CHECK_FALSE_MESSAGE((!soft || !space || hard || virt || elo), "Invalid glyph flags.");
+ } else {
+ CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags.");
+ }
+ }
+ ts->free_rid(ctx);
+ }
+
+ if (ts->has_feature(TextServer::FEATURE_BREAK_ITERATORS)) {
+ String test = U"เป็นภาษาราชการและภาษา";
+ RID ctx = ts->create_shaped_text();
+ CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed.");
+ bool ok = ts->shaped_text_add_string(ctx, test, font, 16);
+ CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed.");
+ ts->shaped_text_update_breaks(ctx);
+ ts->shaped_text_update_justification_ops(ctx);
+
+ const Glyph *glyphs = ts->shaped_text_get_glyphs(ctx);
+ int gl_size = ts->shaped_text_get_glyph_count(ctx);
+
+ CHECK_FALSE_MESSAGE(gl_size != 25, "Invalid glyph count.");
+ for (int j = 0; j < gl_size; j++) {
+ bool hard = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_HARD) == TextServer::GRAPHEME_IS_BREAK_HARD;
+ bool soft = (glyphs[j].flags & TextServer::GRAPHEME_IS_BREAK_SOFT) == TextServer::GRAPHEME_IS_BREAK_SOFT;
+ bool space = (glyphs[j].flags & TextServer::GRAPHEME_IS_SPACE) == TextServer::GRAPHEME_IS_SPACE;
+ bool virt = (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) == TextServer::GRAPHEME_IS_VIRTUAL;
+ bool elo = (glyphs[j].flags & TextServer::GRAPHEME_IS_ELONGATION) == TextServer::GRAPHEME_IS_ELONGATION;
+ if (j == 4 || j == 9 || j == 16 || j == 20) {
+ CHECK_FALSE_MESSAGE((!soft || !space || hard || !virt || elo), "Invalid glyph flags.");
+ } else {
+ CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags.");
+ }
+ }
+ ts->free_rid(ctx);
+ }
+
+ for (int j = 0; j < font.size(); j++) {
+ ts->free_rid(font[j]);
+ }
+ font.clear();
+ }
+ }
+
+ SUBCASE("[TextServer] Text layout: Line breaking") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ CHECK_FALSE_MESSAGE(ts.is_null(), "Invalid TS interface.");
+
+ if (!ts->has_feature(TextServer::FEATURE_FONT_DYNAMIC) || !ts->has_feature(TextServer::FEATURE_SIMPLE_LAYOUT)) {
+ continue;
+ }
+
+ String test_1 = U"test test test";
+ // 5^ 10^
+
+ RID font1 = ts->create_font();
+ ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size);
+ RID font2 = ts->create_font();
+ ts->font_set_data_ptr(font2, _font_NotoSansThaiUI_Regular, _font_NotoSansThaiUI_Regular_size);
+
+ Array font;
+ font.push_back(font1);
+ font.push_back(font2);
+
+ RID ctx = ts->create_shaped_text();
+ CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed.");
+ bool ok = ts->shaped_text_add_string(ctx, test_1, font, 16);
+ CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed.");
+
+ PackedInt32Array brks = ts->shaped_text_get_line_breaks(ctx, 1);
+ CHECK_FALSE_MESSAGE(brks.size() != 6, "Invalid line breaks number.");
+ if (brks.size() == 6) {
+ CHECK_FALSE_MESSAGE(brks[0] != 0, "Invalid line break position.");
+ CHECK_FALSE_MESSAGE(brks[1] != 5, "Invalid line break position.");
+
+ CHECK_FALSE_MESSAGE(brks[2] != 5, "Invalid line break position.");
+ CHECK_FALSE_MESSAGE(brks[3] != 10, "Invalid line break position.");
+
+ CHECK_FALSE_MESSAGE(brks[4] != 10, "Invalid line break position.");
+ CHECK_FALSE_MESSAGE(brks[5] != 14, "Invalid line break position.");
+ }
+
+ ts->free_rid(ctx);
+
+ for (int j = 0; j < font.size(); j++) {
+ ts->free_rid(font[j]);
+ }
+ font.clear();
+ }
+ }
+
+ SUBCASE("[TextServer] Text layout: Justification") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ CHECK_FALSE_MESSAGE(ts.is_null(), "Invalid TS interface.");
+
+ if (!ts->has_feature(TextServer::FEATURE_FONT_DYNAMIC) || !ts->has_feature(TextServer::FEATURE_SIMPLE_LAYOUT)) {
+ continue;
+ }
+
+ RID font1 = ts->create_font();
+ ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size);
+ RID font2 = ts->create_font();
+ ts->font_set_data_ptr(font2, _font_NotoNaskhArabicUI_Regular, _font_NotoNaskhArabicUI_Regular_size);
+
+ Array font;
+ font.push_back(font1);
+ font.push_back(font2);
+
+ String test_1 = U"الحمد";
+ String test_2 = U"الحمد test";
+ String test_3 = U"test test";
+ // 7^ 26^
+
+ RID ctx;
+ bool ok;
+ float width_old, width;
+ if (ts->has_feature(TextServer::FEATURE_KASHIDA_JUSTIFICATION)) {
+ ctx = ts->create_shaped_text();
+ CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed.");
+ ok = ts->shaped_text_add_string(ctx, test_1, font, 16);
+ CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed.");
+
+ width_old = ts->shaped_text_get_width(ctx);
+ width = ts->shaped_text_fit_to_width(ctx, 100, TextServer::JUSTIFICATION_WORD_BOUND);
+ CHECK_FALSE_MESSAGE((width != width_old), "Invalid fill width.");
+ width = ts->shaped_text_fit_to_width(ctx, 100, TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA);
+ CHECK_FALSE_MESSAGE((width <= width_old || width > 100), "Invalid fill width.");
+
+ ts->free_rid(ctx);
+
+ ctx = ts->create_shaped_text();
+ CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed.");
+ ok = ts->shaped_text_add_string(ctx, test_2, font, 16);
+ CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed.");
+
+ width_old = ts->shaped_text_get_width(ctx);
+ width = ts->shaped_text_fit_to_width(ctx, 100, TextServer::JUSTIFICATION_WORD_BOUND);
+ CHECK_FALSE_MESSAGE((width <= width_old || width > 100), "Invalid fill width.");
+ width = ts->shaped_text_fit_to_width(ctx, 100, TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA);
+ CHECK_FALSE_MESSAGE((width <= width_old || width > 100), "Invalid fill width.");
+
+ ts->free_rid(ctx);
+ }
+
+ ctx = ts->create_shaped_text();
+ CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed.");
+ ok = ts->shaped_text_add_string(ctx, test_3, font, 16);
+ CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed.");
+
+ width_old = ts->shaped_text_get_width(ctx);
+ width = ts->shaped_text_fit_to_width(ctx, 100, TextServer::JUSTIFICATION_WORD_BOUND);
+ CHECK_FALSE_MESSAGE((width <= width_old || width > 100), "Invalid fill width.");
+
+ ts->free_rid(ctx);
+
+ for (int j = 0; j < font.size(); j++) {
+ ts->free_rid(font[j]);
+ }
+ font.clear();
+ }
+ }
+
+ SUBCASE("[TextServer] Unicode identifiers") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ CHECK_FALSE_MESSAGE(ts.is_null(), "Invalid TS interface.");
+
+ static const char32_t *data[19] = { U"-30", U"100", U"10.1", U"10,1", U"1e2", U"1e-2", U"1e2e3", U"0xAB", U"AB", U"Test1", U"1Test", U"Test*1", U"test_testeT", U"test_tes teT", U"عَلَيْكُمْ", U"عَلَيْكُمْTest", U"ӒӖӚӜ", U"_test", U"ÂÃÄÅĀĂĄÇĆĈĊ" };
+ static bool isid[19] = { false, false, false, false, false, false, false, false, true, true, false, false, true, false, true, true, true, true, true };
+ for (int j = 0; j < 19; j++) {
+ String s = String(data[j]);
+ CHECK(ts->is_valid_identifier(s) == isid[j]);
+ }
+
+ if (ts->has_feature(TextServer::FEATURE_UNICODE_IDENTIFIERS)) {
+ // Test UAX 3.2 ZW(N)J usage.
+ CHECK(ts->is_valid_identifier(U"\u0646\u0627\u0645\u0647\u200C\u0627\u06CC"));
+ CHECK(ts->is_valid_identifier(U"\u0D26\u0D43\u0D15\u0D4D\u200C\u0D38\u0D3E\u0D15\u0D4D\u0D37\u0D3F"));
+ CHECK(ts->is_valid_identifier(U"\u0DC1\u0DCA\u200D\u0DBB\u0DD3"));
+ }
+ }
+ }
+
+ SUBCASE("[TextServer] Strip Diacritics") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ CHECK_FALSE_MESSAGE(ts.is_null(), "Invalid TS interface.");
+
+ if (ts->has_feature(TextServer::FEATURE_SHAPING)) {
+ CHECK(ts->strip_diacritics(U"ٱلسَّلَامُ عَلَيْكُمْ") == U"ٱلسلام عليكم");
+ }
+
+ CHECK(ts->strip_diacritics(U"pêches épinards tomates fraises") == U"peches epinards tomates fraises");
+ CHECK(ts->strip_diacritics(U"ΆΈΉΊΌΎΏΪΫϓϔ") == U"ΑΕΗΙΟΥΩΙΥΥΥ");
+ CHECK(ts->strip_diacritics(U"άέήίΐϊΰϋόύώ") == U"αεηιιιυυουω");
+ CHECK(ts->strip_diacritics(U"ЀЁЃ ЇЌЍӢӤЙ ЎӮӰӲ ӐӒӖӚӜӞ ӦӪ Ӭ Ӵ Ӹ") == U"ЕЕГ ІКИИИИ УУУУ ААЕӘЖЗ ОӨ Э Ч Ы");
+ CHECK(ts->strip_diacritics(U"ѐёѓ їќѝӣӥй ўӯӱӳ ӑӓӗӛӝӟ ӧӫ ӭ ӵ ӹ") == U"еег ікииии уууу ааеәжз оө э ч ы");
+ CHECK(ts->strip_diacritics(U"ÀÁÂÃÄÅĀĂĄÇĆĈĊČĎÈÉÊËĒĔĖĘĚĜĞĠĢĤÌÍÎÏĨĪĬĮİĴĶĹĻĽÑŃŅŇŊÒÓÔÕÖØŌŎŐƠŔŖŘŚŜŞŠŢŤÙÚÛÜŨŪŬŮŰŲƯŴÝŶŹŻŽ") == U"AAAAAAAAACCCCCDEEEEEEEEEGGGGHIIIIIIIIIJKLLLNNNNŊOOOOOØOOOORRRSSSSTTUUUUUUUUUUUWYYZZZ");
+ CHECK(ts->strip_diacritics(U"àáâãäåāăąçćĉċčďèéêëēĕėęěĝğġģĥìíîïĩīĭįĵķĺļľñńņňŋòóôõöøōŏőơŕŗřśŝşšţťùúûüũūŭůűųưŵýÿŷźżž") == U"aaaaaaaaacccccdeeeeeeeeegggghiiiiiiiijklllnnnnŋoooooøoooorrrssssttuuuuuuuuuuuwyyyzzz");
+ CHECK(ts->strip_diacritics(U"ǍǏȈǑǪǬȌȎȪȬȮȰǓǕǗǙǛȔȖǞǠǺȀȂȦǢǼǦǴǨǸȆȐȒȘȚȞȨ Ḁ ḂḄḆ Ḉ ḊḌḎḐḒ ḔḖḘḚḜ Ḟ Ḡ ḢḤḦḨḪ ḬḮ ḰḲḴ ḶḸḺḼ ḾṀṂ ṄṆṈṊ ṌṎṐṒ ṔṖ ṘṚṜṞ ṠṢṤṦṨ ṪṬṮṰ ṲṴṶṸṺ") == U"AIIOOOOOOOOOUUUUUUUAAAAAAÆÆGGKNERRSTHE A BBB C DDDDD EEEEE F G HHHHH II KKK LLLL MMM NNNN OOOO PP RRRR SSSSS TTTT UUUUU");
+ CHECK(ts->strip_diacritics(U"ǎǐȉȋǒǫǭȍȏȫȭȯȱǔǖǘǚǜȕȗǟǡǻȁȃȧǣǽǧǵǩǹȇȑȓșțȟȩ ḁ ḃḅḇ ḉ ḋḍḏḑḓ ḟ ḡ ḭḯ ḱḳḵ ḷḹḻḽ ḿṁṃ ṅṇṉṋ ṍṏṑṓ ṗṕ ṙṛṝṟ ṡṣṥṧṩ ṫṭṯṱ ṳṵṷṹṻ") == U"aiiiooooooooouuuuuuuaaaaaaææggknerrsthe a bbb c ddddd f g ii kkk llll mmm nnnn oooo pp rrrr sssss tttt uuuuu");
+ CHECK(ts->strip_diacritics(U"ṼṾ ẀẂẄẆẈ ẊẌ Ẏ ẐẒẔ") == U"VV WWWWW XX Y ZZZ");
+ CHECK(ts->strip_diacritics(U"ṽṿ ẁẃẅẇẉ ẋẍ ẏ ẑẓẕ ẖ ẗẘẙẛ") == U"vv wwwww xx y zzz h twys");
+ }
+ }
+
+ SUBCASE("[TextServer] Word break") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+
+ if (!ts->has_feature(TextServer::FEATURE_SIMPLE_LAYOUT)) {
+ continue;
+ }
+
+ CHECK_FALSE_MESSAGE(ts.is_null(), "Invalid TS interface.");
+ {
+ String text1 = U"linguistically similar and effectively form";
+ // 14^ 22^ 26^ 38^
+ PackedInt32Array breaks = ts->string_get_word_breaks(text1, "en");
+ CHECK(breaks.size() == 4);
+ if (breaks.size() == 4) {
+ CHECK(breaks[0] == 14);
+ CHECK(breaks[1] == 22);
+ CHECK(breaks[2] == 26);
+ CHECK(breaks[3] == 38);
+ }
+ }
+
+ if (ts->has_feature(TextServer::FEATURE_BREAK_ITERATORS)) {
+ String text2 = U"เป็นภาษาราชการและภาษาประจำชาติของประเทศไทย";
+ // เป็น ภาษา ราชการ และ ภาษา ประจำ ชาติ ของ ประเทศไทย
+ // 3^ 7^ 13^ 16^ 20^ 25^ 29^ 32^
+
+ PackedInt32Array breaks = ts->string_get_word_breaks(text2, "th");
+ CHECK(breaks.size() == 8);
+ if (breaks.size() == 8) {
+ CHECK(breaks[0] == 3);
+ CHECK(breaks[1] == 7);
+ CHECK(breaks[2] == 13);
+ CHECK(breaks[3] == 16);
+ CHECK(breaks[4] == 20);
+ CHECK(breaks[5] == 25);
+ CHECK(breaks[6] == 29);
+ CHECK(breaks[7] == 32);
+ }
+ }
+ }
+ }
+ }
+}
+}; // namespace TestTextServer
+
+#endif // TOOLS_ENABLED
+
+#endif // TEST_TEXT_SERVER_H
diff --git a/tests/test_gui.cpp b/tests/test_gui.cpp
deleted file mode 100644
index d46a13d2c0..0000000000
--- a/tests/test_gui.cpp
+++ /dev/null
@@ -1,271 +0,0 @@
-/*************************************************************************/
-/* test_gui.cpp */
-/*************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
-/* */
-/* 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. */
-/*************************************************************************/
-
-#ifndef _3D_DISABLED
-
-#include "test_gui.h"
-
-#include "core/io/image_loader.h"
-#include "core/os/os.h"
-#include "core/print_string.h"
-#include "scene/2d/sprite_2d.h"
-#include "scene/gui/button.h"
-#include "scene/gui/control.h"
-#include "scene/gui/label.h"
-#include "scene/gui/line_edit.h"
-#include "scene/gui/menu_button.h"
-#include "scene/gui/option_button.h"
-#include "scene/gui/panel.h"
-#include "scene/gui/popup_menu.h"
-#include "scene/gui/progress_bar.h"
-#include "scene/gui/rich_text_label.h"
-#include "scene/gui/scroll_bar.h"
-#include "scene/gui/spin_box.h"
-#include "scene/gui/tab_container.h"
-#include "scene/gui/texture_rect.h"
-#include "scene/gui/tree.h"
-#include "scene/main/scene_tree.h"
-
-#include "scene/3d/camera_3d.h"
-#include "scene/main/window.h"
-
-namespace TestGUI {
-
-class TestMainLoop : public SceneTree {
-public:
- virtual void request_quit() {
- quit();
- }
- virtual void init() {
- SceneTree::init();
-
- Panel *frame = memnew(Panel);
- frame->set_anchor(MARGIN_RIGHT, Control::ANCHOR_END);
- frame->set_anchor(MARGIN_BOTTOM, Control::ANCHOR_END);
- frame->set_end(Point2(0, 0));
-
- Ref<Theme> t = memnew(Theme);
- frame->set_theme(t);
-
- get_root()->add_child(frame);
-
- Label *label = memnew(Label);
-
- label->set_position(Point2(80, 90));
- label->set_size(Point2(170, 80));
- label->set_align(Label::ALIGN_FILL);
- label->set_text("There was once upon a time a beautiful unicorn that loved to play with little girls...");
-
- frame->add_child(label);
-
- Button *button = memnew(Button);
-
- button->set_position(Point2(20, 20));
- button->set_size(Point2(1, 1));
- button->set_text("This is a biggie button");
-
- frame->add_child(button);
-
- Tree *tree = memnew(Tree);
- tree->set_columns(2);
-
- tree->set_position(Point2(230, 210));
- tree->set_size(Point2(150, 250));
-
- TreeItem *item = tree->create_item();
- item->set_editable(0, true);
- item->set_text(0, "root");
- item = tree->create_item(tree->get_root());
- item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
- item->set_editable(0, true);
- item->set_text(0, "check");
- item->set_cell_mode(1, TreeItem::CELL_MODE_CHECK);
- item->set_editable(1, true);
- item->set_text(1, "check2");
- item = tree->create_item(tree->get_root());
- item->set_cell_mode(0, TreeItem::CELL_MODE_RANGE);
- item->set_editable(0, true);
- item->set_range_config(0, 0, 20, 0.1);
- item->set_range(0, 2);
- item->add_button(0, Theme::get_default()->get_icon("folder", "FileDialog"));
- item->set_cell_mode(1, TreeItem::CELL_MODE_RANGE);
- item->set_editable(1, true);
- item->set_range_config(1, 0, 20, 0.1);
- item->set_range(1, 3);
-
- item = tree->create_item(tree->get_root());
- item->set_cell_mode(0, TreeItem::CELL_MODE_RANGE);
- item->set_editable(0, true);
- item->set_text(0, "Have,Many,Several,Options!");
- item->set_range(0, 2);
-
- item = tree->create_item(item);
- item->set_editable(0, true);
- item->set_text(0, "Gershwin!");
-
- frame->add_child(tree);
-
- LineEdit *line_edit = memnew(LineEdit);
-
- line_edit->set_position(Point2(30, 190));
- line_edit->set_size(Point2(180, 1));
-
- frame->add_child(line_edit);
-
- HScrollBar *hscroll = memnew(HScrollBar);
-
- hscroll->set_position(Point2(30, 290));
- hscroll->set_size(Point2(180, 1));
- hscroll->set_max(10);
- hscroll->set_page(4);
-
- frame->add_child(hscroll);
-
- SpinBox *spin = memnew(SpinBox);
-
- spin->set_position(Point2(30, 260));
- spin->set_size(Point2(120, 1));
-
- frame->add_child(spin);
- hscroll->share(spin);
-
- ProgressBar *progress = memnew(ProgressBar);
-
- progress->set_position(Point2(30, 330));
- progress->set_size(Point2(120, 1));
-
- frame->add_child(progress);
- hscroll->share(progress);
-
- MenuButton *menu_button = memnew(MenuButton);
-
- menu_button->set_text("I'm a menu!");
- menu_button->set_position(Point2(30, 380));
- menu_button->set_size(Point2(1, 1));
-
- frame->add_child(menu_button);
-
- PopupMenu *popup = menu_button->get_popup();
-
- popup->add_item("Hello, testing");
- popup->add_item("My Dearest");
- popup->add_separator();
- popup->add_item("Popup");
- popup->add_check_item("Check Popup");
- popup->set_item_checked(4, true);
- popup->add_separator();
- popup->add_radio_check_item("Option A");
- popup->set_item_checked(6, true);
- popup->add_radio_check_item("Option B");
-
- OptionButton *options = memnew(OptionButton);
-
- options->add_item("Hello, testing");
- options->add_item("My Dearest");
-
- options->set_position(Point2(230, 180));
- options->set_size(Point2(1, 1));
-
- frame->add_child(options);
-
- RichTextLabel *richtext = memnew(RichTextLabel);
-
- richtext->set_position(Point2(600, 210));
- richtext->set_size(Point2(180, 250));
- richtext->set_anchor_and_margin(MARGIN_RIGHT, Control::ANCHOR_END, -20);
-
- frame->add_child(richtext);
-
- richtext->add_text("Hello, My Friends!\n\nWelcome to the amazing world of ");
-
- richtext->add_newline();
- richtext->add_newline();
-
- richtext->push_color(Color(1, 0.5, 0.5));
- richtext->add_text("leprechauns");
- richtext->pop();
-
- richtext->add_text(" and ");
- richtext->push_color(Color(0, 1.0, 0.5));
- richtext->add_text("faeries.\n");
- richtext->pop();
- richtext->add_text("In this new episode, we will attempt to ");
- richtext->push_font(richtext->get_theme_font("mono_font", "Fonts"));
- richtext->push_color(Color(0.7, 0.5, 1.0));
- richtext->add_text("deliver something nice");
- richtext->pop();
- richtext->pop();
- richtext->add_text(" to all the viewers! Unfortunately, I need to ");
- richtext->push_underline();
- richtext->add_text("keep writing a lot of text");
- richtext->pop();
- richtext->add_text(" so the label control overflows and the scrollbar appears.\n");
- richtext->push_meta("http://www.scrollingcapabilities.xz");
- richtext->add_text("This allows to test for the scrolling capabilities ");
- richtext->pop();
- richtext->add_text("of the rich text label for huge text (not like this text will really be huge but, you know).\nAs long as it is so long that it will work nicely for a test/demo, then it's welcomed in my book...\nChanging subject, the day is cloudy today and I'm wondering if I'll get che chance to travel somewhere nice. Sometimes, watching the clouds from satellite images may give a nice insight about how pressure zones in our planet work, although it also makes it pretty obvious to see why most weather forecasts get it wrong so often.\nClouds are so difficult to predict!\nBut it's pretty cool how our civilization has adapted to having water falling from the sky each time it rains...");
-
- TabContainer *tabc = memnew(TabContainer);
-
- Control *ctl = memnew(Control);
- ctl->set_name("tab 1");
- tabc->add_child(ctl);
-
- ctl = memnew(Control);
- ctl->set_name("tab 2");
- tabc->add_child(ctl);
- label = memnew(Label);
- label->set_text("Some Label");
- label->set_position(Point2(20, 20));
- ctl->add_child(label);
-
- ctl = memnew(Control);
- ctl->set_name("tab 3");
- button = memnew(Button);
- button->set_text("Some Button");
- button->set_position(Point2(30, 50));
- ctl->add_child(button);
-
- tabc->add_child(ctl);
-
- frame->add_child(tabc);
-
- tabc->set_position(Point2(400, 210));
- tabc->set_size(Point2(180, 250));
- }
-};
-
-MainLoop *test() {
- return memnew(TestMainLoop);
-}
-
-} // namespace TestGUI
-
-#endif
diff --git a/tests/test_macros.cpp b/tests/test_macros.cpp
index 2317223b23..8c510cb4a5 100644
--- a/tests/test_macros.cpp
+++ b/tests/test_macros.cpp
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -31,11 +31,11 @@
#define DOCTEST_CONFIG_IMPLEMENT
#include "test_macros.h"
-Map<String, TestFunc> *test_commands = nullptr;
+HashMap<String, TestFunc> *test_commands = nullptr;
int register_test_command(String p_command, TestFunc p_function) {
if (!test_commands) {
- test_commands = new Map<String, TestFunc>;
+ test_commands = new HashMap<String, TestFunc>;
}
test_commands->insert(p_command, p_function);
return 0;
diff --git a/tests/test_macros.h b/tests/test_macros.h
index 3486c68bb7..3b734b9699 100644
--- a/tests/test_macros.h
+++ b/tests/test_macros.h
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -31,8 +31,10 @@
#ifndef TEST_MACROS_H
#define TEST_MACROS_H
-#include "core/map.h"
-#include "core/variant.h"
+#include "core/core_globals.h"
+#include "core/input/input_map.h"
+#include "core/object/message_queue.h"
+#include "core/variant/variant.h"
// See documentation for doctest at:
// https://github.com/onqtam/doctest/blob/master/doc/markdown/readme.md#reference
@@ -41,34 +43,43 @@
// The test is skipped with this, run pending tests with `--test --no-skip`.
#define TEST_CASE_PENDING(name) TEST_CASE(name *doctest::skip())
+// The test case is marked as failed, but does not fail the entire test run.
+#define TEST_CASE_MAY_FAIL(name) TEST_CASE(name *doctest::may_fail())
+
+// Provide aliases to conform with Godot naming conventions (see error macros).
+#define TEST_COND(cond, ...) DOCTEST_CHECK_FALSE_MESSAGE(cond, __VA_ARGS__)
+#define TEST_FAIL(cond, ...) DOCTEST_FAIL(cond, __VA_ARGS__)
+#define TEST_FAIL_COND(cond, ...) DOCTEST_REQUIRE_FALSE_MESSAGE(cond, __VA_ARGS__)
+#define TEST_FAIL_COND_WARN(cond, ...) DOCTEST_WARN_FALSE_MESSAGE(cond, __VA_ARGS__)
+
// Temporarily disable error prints to test failure paths.
// This allows to avoid polluting the test summary with error messages.
-// The `_print_error_enabled` boolean is defined in `core/print_string.cpp` and
+// The `print_error_enabled` boolean is defined in `core/core_globals.cpp` and
// works at global scope. It's used by various loggers in `should_log()` method,
// which are used by error macros which call into `OS::print_error`, effectively
// disabling any error messages to be printed from the engine side (not tests).
-#define ERR_PRINT_OFF _print_error_enabled = false;
-#define ERR_PRINT_ON _print_error_enabled = true;
+#define ERR_PRINT_OFF CoreGlobals::print_error_enabled = false;
+#define ERR_PRINT_ON CoreGlobals::print_error_enabled = true;
// Stringify all `Variant` compatible types for doctest output by default.
// https://github.com/onqtam/doctest/blob/master/doc/markdown/stringification.md
-#define DOCTEST_STRINGIFY_VARIANT(m_type) \
- template <> \
- struct doctest::StringMaker<m_type> { \
- static doctest::String convert(const m_type &p_val) { \
- const Variant val = p_val; \
- return val.get_construct_string().utf8().get_data(); \
- } \
+#define DOCTEST_STRINGIFY_VARIANT(m_type) \
+ template <> \
+ struct doctest::StringMaker<m_type> { \
+ static doctest::String convert(const m_type &p_val) { \
+ const Variant val = p_val; \
+ return val.operator ::String().utf8().get_data(); \
+ } \
};
-#define DOCTEST_STRINGIFY_VARIANT_POINTER(m_type) \
- template <> \
- struct doctest::StringMaker<m_type> { \
- static doctest::String convert(const m_type *p_val) { \
- const Variant val = p_val; \
- return val.get_construct_string().utf8().get_data(); \
- } \
+#define DOCTEST_STRINGIFY_VARIANT_POINTER(m_type) \
+ template <> \
+ struct doctest::StringMaker<m_type> { \
+ static doctest::String convert(const m_type *p_val) { \
+ const Variant val = p_val; \
+ return val.operator ::String().utf8().get_data(); \
+ } \
};
DOCTEST_STRINGIFY_VARIANT(Variant);
@@ -82,10 +93,10 @@ DOCTEST_STRINGIFY_VARIANT(Vector3);
DOCTEST_STRINGIFY_VARIANT(Vector3i);
DOCTEST_STRINGIFY_VARIANT(Transform2D);
DOCTEST_STRINGIFY_VARIANT(Plane);
-DOCTEST_STRINGIFY_VARIANT(Quat);
+DOCTEST_STRINGIFY_VARIANT(Quaternion);
DOCTEST_STRINGIFY_VARIANT(AABB);
DOCTEST_STRINGIFY_VARIANT(Basis);
-DOCTEST_STRINGIFY_VARIANT(Transform);
+DOCTEST_STRINGIFY_VARIANT(Transform3D);
DOCTEST_STRINGIFY_VARIANT(::Color); // Disambiguate from `doctest::Color`.
DOCTEST_STRINGIFY_VARIANT(StringName);
@@ -112,12 +123,260 @@ DOCTEST_STRINGIFY_VARIANT(PackedColorArray);
// Example usage: `godot --test gdscript-parser`.
typedef void (*TestFunc)();
-extern Map<String, TestFunc> *test_commands;
+extern HashMap<String, TestFunc> *test_commands;
int register_test_command(String p_command, TestFunc p_function);
-#define REGISTER_TEST_COMMAND(m_command, m_function) \
- DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_VAR_)) = \
- register_test_command(m_command, m_function); \
- DOCTEST_GLOBAL_NO_WARNINGS_END()
+#define REGISTER_TEST_COMMAND(m_command, m_function) \
+ DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), \
+ register_test_command(m_command, m_function))
+
+// Utility macros to send an event actions to a given object
+// Requires Message Queue and InputMap to be setup.
+// SEND_GUI_ACTION - takes an object and a input map key. e.g SEND_GUI_ACTION(code_edit, "ui_text_newline").
+// SEND_GUI_KEY_EVENT - takes an object and a keycode set. e.g SEND_GUI_KEY_EVENT(code_edit, Key::A | KeyModifierMask::META).
+// SEND_GUI_MOUSE_BUTTON_EVENT - takes an object, position, mouse button, mouse mask and modifiers e.g SEND_GUI_MOUSE_BUTTON_EVENT(code_edit, Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE, Key::None);
+// SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT - takes an object, position, mouse button, mouse mask and modifiers e.g SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(code_edit, Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE, Key::None);
+// SEND_GUI_MOUSE_MOTION_EVENT - takes an object, position, mouse mask and modifiers e.g SEND_GUI_MOUSE_MOTION_EVENT(code_edit, Vector2(50, 50), MouseButton::MASK_LEFT, KeyModifierMask::META);
+// SEND_GUI_DOUBLE_CLICK - takes an object, position and modifiers. e.g SEND_GUI_DOUBLE_CLICK(code_edit, Vector2(50, 50), KeyModifierMask::META);
+
+#define SEND_GUI_ACTION(m_object, m_action) \
+ { \
+ const List<Ref<InputEvent>> *events = InputMap::get_singleton()->action_get_events(m_action); \
+ const List<Ref<InputEvent>>::Element *first_event = events->front(); \
+ Ref<InputEventKey> event = first_event->get(); \
+ event->set_pressed(true); \
+ m_object->get_viewport()->push_input(event); \
+ MessageQueue::get_singleton()->flush(); \
+ }
+
+#define SEND_GUI_KEY_EVENT(m_object, m_input) \
+ { \
+ Ref<InputEventKey> event = InputEventKey::create_reference(m_input); \
+ event->set_pressed(true); \
+ m_object->get_viewport()->push_input(event); \
+ MessageQueue::get_singleton()->flush(); \
+ }
+
+#define _UPDATE_EVENT_MODIFERS(m_event, m_modifers) \
+ m_event->set_shift_pressed(((m_modifers)&KeyModifierMask::SHIFT) != Key::NONE); \
+ m_event->set_alt_pressed(((m_modifers)&KeyModifierMask::ALT) != Key::NONE); \
+ m_event->set_ctrl_pressed(((m_modifers)&KeyModifierMask::CTRL) != Key::NONE); \
+ m_event->set_meta_pressed(((m_modifers)&KeyModifierMask::META) != Key::NONE);
+
+#define _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask, m_modifers) \
+ Ref<InputEventMouseButton> event; \
+ event.instantiate(); \
+ event->set_position(m_local_pos); \
+ event->set_button_index(m_input); \
+ event->set_button_mask(m_mask); \
+ event->set_factor(1); \
+ _UPDATE_EVENT_MODIFERS(event, m_modifers); \
+ event->set_pressed(true);
+
+#define SEND_GUI_MOUSE_BUTTON_EVENT(m_object, m_local_pos, m_input, m_mask, m_modifers) \
+ { \
+ _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask, m_modifers); \
+ m_object->get_viewport()->push_input(event); \
+ MessageQueue::get_singleton()->flush(); \
+ }
+
+#define SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(m_object, m_local_pos, m_input, m_mask, m_modifers) \
+ { \
+ _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask, m_modifers); \
+ event->set_pressed(false); \
+ m_object->get_viewport()->push_input(event); \
+ MessageQueue::get_singleton()->flush(); \
+ }
+
+#define SEND_GUI_DOUBLE_CLICK(m_object, m_local_pos, m_modifers) \
+ { \
+ _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, MouseButton::LEFT, MouseButton::LEFT, m_modifers); \
+ event->set_double_click(true); \
+ m_object->get_viewport()->push_input(event); \
+ MessageQueue::get_singleton()->flush(); \
+ }
+
+// We toggle _print_error_enabled to prevent display server not supported warnings.
+#define SEND_GUI_MOUSE_MOTION_EVENT(m_object, m_local_pos, m_mask, m_modifers) \
+ { \
+ bool errors_enabled = CoreGlobals::print_error_enabled; \
+ CoreGlobals::print_error_enabled = false; \
+ Ref<InputEventMouseMotion> event; \
+ event.instantiate(); \
+ event->set_position(m_local_pos); \
+ event->set_button_mask(m_mask); \
+ event->set_relative(Vector2(10, 10)); \
+ _UPDATE_EVENT_MODIFERS(event, m_modifers); \
+ m_object->get_viewport()->push_input(event); \
+ MessageQueue::get_singleton()->flush(); \
+ CoreGlobals::print_error_enabled = errors_enabled; \
+ }
+
+// Utility class / macros for testing signals
+//
+// Use SIGNAL_WATCH(*object, "signal_name") to start watching
+// Makes sure to call SIGNAL_UNWATCH(*object, "signal_name") to stop watching in cleanup, this is not done automatically.
+//
+// The SignalWatcher will capture all signals and their args sent between checks.
+//
+// Use SIGNAL_CHECK("signal_name"), Vector<Vector<Variant>>), to check the arguments of all fired signals.
+// The outer vector is each fired signal, the inner vector the list of arguments for that signal. Order does matter.
+//
+// Use SIGNAL_CHECK_FALSE("signal_name") to check if a signal was not fired.
+//
+// Use SIGNAL_DISCARD("signal_name") to discard records all of the given signal, use only in placed you don't need to check.
+//
+// All signals are automatically discarded between test/sub test cases.
+
+class SignalWatcher : public Object {
+private:
+ inline static SignalWatcher *singleton;
+
+ /* Equal to: RBMap<String, Vector<Vector<Variant>>> */
+ HashMap<String, Array> _signals;
+ void _add_signal_entry(const Array &p_args, const String &p_name) {
+ if (!_signals.has(p_name)) {
+ _signals[p_name] = Array();
+ }
+ _signals[p_name].push_back(p_args);
+ }
+
+ void _signal_callback_zero(const String &p_name) {
+ Array args;
+ _add_signal_entry(args, p_name);
+ }
+
+ void _signal_callback_one(Variant p_arg1, const String &p_name) {
+ Array args;
+ args.push_back(p_arg1);
+ _add_signal_entry(args, p_name);
+ }
+
+ void _signal_callback_two(Variant p_arg1, Variant p_arg2, const String &p_name) {
+ Array args;
+ args.push_back(p_arg1);
+ args.push_back(p_arg2);
+ _add_signal_entry(args, p_name);
+ }
+
+ void _signal_callback_three(Variant p_arg1, Variant p_arg2, Variant p_arg3, const String &p_name) {
+ Array args;
+ args.push_back(p_arg1);
+ args.push_back(p_arg2);
+ args.push_back(p_arg3);
+ _add_signal_entry(args, p_name);
+ }
+
+public:
+ static SignalWatcher *get_singleton() { return singleton; }
+
+ void watch_signal(Object *p_object, const String &p_signal) {
+ MethodInfo method_info;
+ ClassDB::get_signal(p_object->get_class(), p_signal, &method_info);
+ switch (method_info.arguments.size()) {
+ case 0: {
+ p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_zero).bind(p_signal));
+ } break;
+ case 1: {
+ p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_one).bind(p_signal));
+ } break;
+ case 2: {
+ p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_two).bind(p_signal));
+ } break;
+ case 3: {
+ p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_three).bind(p_signal));
+ } break;
+ default: {
+ MESSAGE("Signal ", p_signal, " arg count not supported.");
+ } break;
+ }
+ }
+
+ void unwatch_signal(Object *p_object, const String &p_signal) {
+ MethodInfo method_info;
+ ClassDB::get_signal(p_object->get_class(), p_signal, &method_info);
+ switch (method_info.arguments.size()) {
+ case 0: {
+ p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_zero));
+ } break;
+ case 1: {
+ p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_one));
+ } break;
+ case 2: {
+ p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_two));
+ } break;
+ case 3: {
+ p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_three));
+ } break;
+ default: {
+ MESSAGE("Signal ", p_signal, " arg count not supported.");
+ } break;
+ }
+ }
+
+ bool check(const String &p_name, const Array &p_args) {
+ if (!_signals.has(p_name)) {
+ MESSAGE("Signal ", p_name, " not emitted");
+ return false;
+ }
+
+ if (p_args.size() != _signals[p_name].size()) {
+ MESSAGE("Signal has " << _signals[p_name] << " expected " << p_args);
+ discard_signal(p_name);
+ return false;
+ }
+
+ bool match = true;
+ for (int i = 0; i < p_args.size(); i++) {
+ if (((Array)p_args[i]).size() != ((Array)_signals[p_name][i]).size()) {
+ MESSAGE("Signal has " << _signals[p_name][i] << " expected " << p_args[i]);
+ match = false;
+ continue;
+ }
+
+ for (int j = 0; j < ((Array)p_args[i]).size(); j++) {
+ if (((Array)p_args[i])[j] != ((Array)_signals[p_name][i])[j]) {
+ MESSAGE("Signal has " << _signals[p_name][i] << " expected " << p_args[i]);
+ match = false;
+ break;
+ }
+ }
+ }
+
+ discard_signal(p_name);
+ return match;
+ }
+
+ bool check_false(const String &p_name) {
+ bool has = _signals.has(p_name);
+ discard_signal(p_name);
+ return !has;
+ }
+
+ void discard_signal(const String &p_name) {
+ if (_signals.has(p_name)) {
+ _signals.erase(p_name);
+ }
+ }
+
+ void _clear_signals() {
+ _signals.clear();
+ }
+
+ SignalWatcher() {
+ singleton = this;
+ }
+
+ ~SignalWatcher() {
+ singleton = nullptr;
+ }
+};
+
+#define SIGNAL_WATCH(m_object, m_signal) SignalWatcher::get_singleton()->watch_signal(m_object, m_signal);
+#define SIGNAL_UNWATCH(m_object, m_signal) SignalWatcher::get_singleton()->unwatch_signal(m_object, m_signal);
+
+#define SIGNAL_CHECK(m_signal, m_args) CHECK(SignalWatcher::get_singleton()->check(m_signal, m_args));
+#define SIGNAL_CHECK_FALSE(m_signal) CHECK(SignalWatcher::get_singleton()->check_false(m_signal));
+#define SIGNAL_DISCARD(m_signal) SignalWatcher::get_singleton()->discard_signal(m_signal);
#endif // TEST_MACROS_H
diff --git a/tests/test_main.cpp b/tests/test_main.cpp
index f0dec22738..80099d1dd4 100644
--- a/tests/test_main.cpp
+++ b/tests/test_main.cpp
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -30,31 +30,84 @@
#include "test_main.h"
-#include "core/list.h"
-
-#include "test_astar.h"
-#include "test_basis.h"
-#include "test_class_db.h"
-#include "test_color.h"
-#include "test_expression.h"
-#include "test_gradient.h"
-#include "test_gui.h"
-#include "test_list.h"
-#include "test_math.h"
-#include "test_oa_hash_map.h"
-#include "test_ordered_hash_map.h"
-#include "test_physics_2d.h"
-#include "test_physics_3d.h"
-#include "test_render.h"
-#include "test_shader_lang.h"
-#include "test_string.h"
-#include "test_validate_testing.h"
-#include "test_variant.h"
+#include "tests/core/input/test_input_event_key.h"
+#include "tests/core/input/test_shortcut.h"
+#include "tests/core/io/test_config_file.h"
+#include "tests/core/io/test_file_access.h"
+#include "tests/core/io/test_image.h"
+#include "tests/core/io/test_json.h"
+#include "tests/core/io/test_marshalls.h"
+#include "tests/core/io/test_pck_packer.h"
+#include "tests/core/io/test_resource.h"
+#include "tests/core/io/test_xml_parser.h"
+#include "tests/core/math/test_aabb.h"
+#include "tests/core/math/test_astar.h"
+#include "tests/core/math/test_basis.h"
+#include "tests/core/math/test_color.h"
+#include "tests/core/math/test_expression.h"
+#include "tests/core/math/test_geometry_2d.h"
+#include "tests/core/math/test_geometry_3d.h"
+#include "tests/core/math/test_plane.h"
+#include "tests/core/math/test_quaternion.h"
+#include "tests/core/math/test_random_number_generator.h"
+#include "tests/core/math/test_rect2.h"
+#include "tests/core/math/test_rect2i.h"
+#include "tests/core/math/test_transform_2d.h"
+#include "tests/core/math/test_transform_3d.h"
+#include "tests/core/math/test_vector2.h"
+#include "tests/core/math/test_vector2i.h"
+#include "tests/core/math/test_vector3.h"
+#include "tests/core/math/test_vector3i.h"
+#include "tests/core/math/test_vector4.h"
+#include "tests/core/math/test_vector4i.h"
+#include "tests/core/object/test_class_db.h"
+#include "tests/core/object/test_method_bind.h"
+#include "tests/core/object/test_object.h"
+#include "tests/core/os/test_os.h"
+#include "tests/core/string/test_node_path.h"
+#include "tests/core/string/test_string.h"
+#include "tests/core/string/test_translation.h"
+#include "tests/core/templates/test_command_queue.h"
+#include "tests/core/templates/test_hash_map.h"
+#include "tests/core/templates/test_hash_set.h"
+#include "tests/core/templates/test_list.h"
+#include "tests/core/templates/test_local_vector.h"
+#include "tests/core/templates/test_lru.h"
+#include "tests/core/templates/test_paged_array.h"
+#include "tests/core/templates/test_rid.h"
+#include "tests/core/templates/test_vector.h"
+#include "tests/core/test_crypto.h"
+#include "tests/core/test_hashing_context.h"
+#include "tests/core/test_time.h"
+#include "tests/core/threads/test_worker_thread_pool.h"
+#include "tests/core/variant/test_array.h"
+#include "tests/core/variant/test_dictionary.h"
+#include "tests/core/variant/test_variant.h"
+#include "tests/scene/test_animation.h"
+#include "tests/scene/test_arraymesh.h"
+#include "tests/scene/test_audio_stream_wav.h"
+#include "tests/scene/test_bit_map.h"
+#include "tests/scene/test_code_edit.h"
+#include "tests/scene/test_curve.h"
+#include "tests/scene/test_gradient.h"
+#include "tests/scene/test_path_3d.h"
+#include "tests/scene/test_sprite_frames.h"
+#include "tests/scene/test_text_edit.h"
+#include "tests/scene/test_theme.h"
+#include "tests/servers/test_text_server.h"
+#include "tests/test_validate_testing.h"
#include "modules/modules_tests.gen.h"
#include "tests/test_macros.h"
+#include "scene/theme/theme_db.h"
+#include "servers/navigation_server_2d.h"
+#include "servers/navigation_server_3d.h"
+#include "servers/physics_server_2d.h"
+#include "servers/physics_server_3d.h"
+#include "servers/rendering/rendering_server_default.h"
+
int test_main(int argc, char *argv[]) {
bool run_tests = true;
@@ -64,13 +117,13 @@ int test_main(int argc, char *argv[]) {
for (int i = 0; i < argc; i++) {
args.push_back(String::utf8(argv[i]));
}
- OS::get_singleton()->set_cmdline("", args);
+ OS::get_singleton()->set_cmdline("", args, List<String>());
// Run custom test tools.
if (test_commands) {
- for (Map<String, TestFunc>::Element *E = test_commands->front(); E; E = E->next()) {
- if (args.find(E->key())) {
- const TestFunc &test_func = E->get();
+ for (const KeyValue<String, TestFunc> &E : (*test_commands)) {
+ if (args.find(E.key)) {
+ const TestFunc &test_func = E.value;
test_func();
run_tests = false;
break;
@@ -92,28 +145,192 @@ int test_main(int argc, char *argv[]) {
test_args.push_back(arg);
}
}
- // Convert Godot command line arguments back to standard arguments.
- char **doctest_args = new char *[test_args.size()];
- for (int x = 0; x < test_args.size(); x++) {
- // Operation to convert Godot string to non wchar string.
- CharString cs = test_args[x].utf8();
- const char *str = cs.get_data();
- // Allocate the string copy.
- doctest_args[x] = new char[strlen(str) + 1];
- // Copy this into memory.
- memcpy(doctest_args[x], str, strlen(str) + 1);
- }
- test_context.applyCommandLine(test_args.size(), doctest_args);
+ if (test_args.size() > 0) {
+ // Convert Godot command line arguments back to standard arguments.
+ char **doctest_args = new char *[test_args.size()];
+ for (int x = 0; x < test_args.size(); x++) {
+ // Operation to convert Godot string to non wchar string.
+ CharString cs = test_args[x].utf8();
+ const char *str = cs.get_data();
+ // Allocate the string copy.
+ doctest_args[x] = new char[strlen(str) + 1];
+ // Copy this into memory.
+ memcpy(doctest_args[x], str, strlen(str) + 1);
+ }
- test_context.setOption("order-by", "name");
- test_context.setOption("abort-after", 5);
- test_context.setOption("no-breaks", true);
+ test_context.applyCommandLine(test_args.size(), doctest_args);
- for (int x = 0; x < test_args.size(); x++) {
- delete[] doctest_args[x];
+ for (int x = 0; x < test_args.size(); x++) {
+ delete[] doctest_args[x];
+ }
+ delete[] doctest_args;
}
- delete[] doctest_args;
return test_context.run();
}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+struct GodotTestCaseListener : public doctest::IReporter {
+ GodotTestCaseListener(const doctest::ContextOptions &p_in) {}
+
+ SignalWatcher *signal_watcher = nullptr;
+
+ PhysicsServer3D *physics_server_3d = nullptr;
+ PhysicsServer2D *physics_server_2d = nullptr;
+ NavigationServer3D *navigation_server_3d = nullptr;
+ NavigationServer2D *navigation_server_2d = nullptr;
+ ThemeDB *theme_db = nullptr;
+
+ void test_case_start(const doctest::TestCaseData &p_in) override {
+ SignalWatcher::get_singleton()->_clear_signals();
+
+ String name = String(p_in.m_name);
+
+ if (name.find("[SceneTree]") != -1) {
+ GLOBAL_DEF("memory/limits/multithreaded_server/rid_pool_prealloc", 60);
+ memnew(MessageQueue);
+
+ GLOBAL_DEF("internationalization/rendering/force_right_to_left_layout_direction", false);
+
+ memnew(Input);
+
+ Error err = OK;
+ OS::get_singleton()->set_has_server_feature_callback(nullptr);
+ for (int i = 0; i < DisplayServer::get_create_function_count(); i++) {
+ if (String("headless") == DisplayServer::get_create_function_name(i)) {
+ DisplayServer::create(i, "", DisplayServer::WindowMode::WINDOW_MODE_MINIMIZED, DisplayServer::VSyncMode::VSYNC_ENABLED, 0, nullptr, Vector2i(0, 0), err);
+ break;
+ }
+ }
+ memnew(RenderingServerDefault());
+ RenderingServerDefault::get_singleton()->init();
+ RenderingServerDefault::get_singleton()->set_render_loop_enabled(false);
+
+ physics_server_3d = PhysicsServer3DManager::get_singleton()->new_default_server();
+ physics_server_3d->init();
+
+ physics_server_2d = PhysicsServer2DManager::get_singleton()->new_default_server();
+ physics_server_2d->init();
+
+ navigation_server_3d = NavigationServer3DManager::new_default_server();
+ navigation_server_2d = memnew(NavigationServer2D);
+
+ memnew(InputMap);
+ InputMap::get_singleton()->load_default();
+
+ theme_db = memnew(ThemeDB);
+ theme_db->initialize_theme_noproject();
+
+ memnew(SceneTree);
+ SceneTree::get_singleton()->initialize();
+ return;
+ }
+
+ if (name.find("Audio") != -1) {
+ // The last driver index should always be the dummy driver.
+ int dummy_idx = AudioDriverManager::get_driver_count() - 1;
+ AudioDriverManager::initialize(dummy_idx);
+ AudioServer *audio_server = memnew(AudioServer);
+ audio_server->init();
+ return;
+ }
+ }
+
+ void test_case_end(const doctest::CurrentTestCaseStats &) override {
+ if (SceneTree::get_singleton()) {
+ SceneTree::get_singleton()->finalize();
+ }
+
+ if (MessageQueue::get_singleton()) {
+ MessageQueue::get_singleton()->flush();
+ }
+
+ if (SceneTree::get_singleton()) {
+ memdelete(SceneTree::get_singleton());
+ }
+
+ if (theme_db) {
+ memdelete(theme_db);
+ theme_db = nullptr;
+ }
+
+ if (navigation_server_3d) {
+ memdelete(navigation_server_3d);
+ navigation_server_3d = nullptr;
+ }
+
+ if (navigation_server_2d) {
+ memdelete(navigation_server_2d);
+ navigation_server_2d = nullptr;
+ }
+
+ if (physics_server_3d) {
+ physics_server_3d->finish();
+ memdelete(physics_server_3d);
+ physics_server_3d = nullptr;
+ }
+
+ if (physics_server_2d) {
+ physics_server_2d->finish();
+ memdelete(physics_server_2d);
+ physics_server_2d = nullptr;
+ }
+
+ if (Input::get_singleton()) {
+ memdelete(Input::get_singleton());
+ }
+
+ if (RenderingServer::get_singleton()) {
+ RenderingServer::get_singleton()->sync();
+ RenderingServer::get_singleton()->global_shader_parameters_clear();
+ RenderingServer::get_singleton()->finish();
+ memdelete(RenderingServer::get_singleton());
+ }
+
+ if (DisplayServer::get_singleton()) {
+ memdelete(DisplayServer::get_singleton());
+ }
+
+ if (InputMap::get_singleton()) {
+ memdelete(InputMap::get_singleton());
+ }
+
+ if (MessageQueue::get_singleton()) {
+ MessageQueue::get_singleton()->flush();
+ memdelete(MessageQueue::get_singleton());
+ }
+
+ if (AudioServer::get_singleton()) {
+ AudioServer::get_singleton()->finish();
+ memdelete(AudioServer::get_singleton());
+ }
+ }
+
+ void test_run_start() override {
+ signal_watcher = memnew(SignalWatcher);
+ }
+
+ void test_run_end(const doctest::TestRunStats &) override {
+ memdelete(signal_watcher);
+ }
+
+ void test_case_reenter(const doctest::TestCaseData &) override {
+ SignalWatcher::get_singleton()->_clear_signals();
+ }
+
+ void subcase_start(const doctest::SubcaseSignature &) override {
+ SignalWatcher::get_singleton()->_clear_signals();
+ }
+
+ void report_query(const doctest::QueryData &) override {}
+ void test_case_exception(const doctest::TestCaseException &) override {}
+ void subcase_end() override {}
+
+ void log_assert(const doctest::AssertData &in) override {}
+ void log_message(const doctest::MessageData &) override {}
+ void test_case_skipped(const doctest::TestCaseData &) override {}
+};
+
+REGISTER_LISTENER("GodotTestCaseListener", 1, GodotTestCaseListener);
diff --git a/tests/test_main.h b/tests/test_main.h
index 983bfde402..8e6a7361fc 100644
--- a/tests/test_main.h
+++ b/tests/test_main.h
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
diff --git a/tests/test_math.cpp b/tests/test_math.cpp
deleted file mode 100644
index 862535b57e..0000000000
--- a/tests/test_math.cpp
+++ /dev/null
@@ -1,703 +0,0 @@
-/*************************************************************************/
-/* test_math.cpp */
-/*************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
-/* */
-/* 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. */
-/*************************************************************************/
-
-#include "test_math.h"
-
-#include "core/math/basis.h"
-#include "core/math/camera_matrix.h"
-#include "core/math/delaunay_3d.h"
-#include "core/math/geometry_2d.h"
-#include "core/math/math_funcs.h"
-#include "core/math/transform.h"
-#include "core/method_ptrcall.h"
-#include "core/os/file_access.h"
-#include "core/os/keyboard.h"
-#include "core/os/os.h"
-#include "core/print_string.h"
-#include "core/ustring.h"
-#include "core/variant.h"
-#include "core/vmap.h"
-#include "scene/main/node.h"
-#include "scene/resources/texture.h"
-#include "servers/rendering/shader_language.h"
-
-namespace TestMath {
-
-class GetClassAndNamespace {
- String code;
- int idx;
- int line;
- String error_str;
- bool error;
- Variant value;
-
- String class_name;
-
- enum Token {
- TK_BRACKET_OPEN,
- TK_BRACKET_CLOSE,
- TK_CURLY_BRACKET_OPEN,
- TK_CURLY_BRACKET_CLOSE,
- TK_PERIOD,
- TK_COLON,
- TK_COMMA,
- TK_SYMBOL,
- TK_IDENTIFIER,
- TK_STRING,
- TK_NUMBER,
- TK_EOF,
- TK_ERROR
- };
-
- Token get_token() {
- while (true) {
- switch (code[idx]) {
- case '\n': {
- line++;
- idx++;
- break;
- };
- case 0: {
- return TK_EOF;
-
- } break;
- case '{': {
- idx++;
- return TK_CURLY_BRACKET_OPEN;
- };
- case '}': {
- idx++;
- return TK_CURLY_BRACKET_CLOSE;
- };
- case '[': {
- idx++;
- return TK_BRACKET_OPEN;
- };
- case ']': {
- idx++;
- return TK_BRACKET_CLOSE;
- };
- case ':': {
- idx++;
- return TK_COLON;
- };
- case ',': {
- idx++;
- return TK_COMMA;
- };
- case '.': {
- idx++;
- return TK_PERIOD;
- };
- case '#': {
- //compiler directive
- while (code[idx] != '\n' && code[idx] != 0) {
- idx++;
- }
- continue;
- } break;
- case '/': {
- switch (code[idx + 1]) {
- case '*': { // block comment
-
- idx += 2;
- while (true) {
- if (code[idx] == 0) {
- error_str = "Unterminated comment";
- error = true;
- return TK_ERROR;
- } else if (code[idx] == '*' && code[idx + 1] == '/') {
- idx += 2;
- break;
- } else if (code[idx] == '\n') {
- line++;
- }
-
- idx++;
- }
-
- } break;
- case '/': { // line comment skip
-
- while (code[idx] != '\n' && code[idx] != 0) {
- idx++;
- }
-
- } break;
- default: {
- value = "/";
- idx++;
- return TK_SYMBOL;
- }
- }
-
- continue; // a comment
- } break;
- case '\'':
- case '"': {
- char32_t begin_str = code[idx];
- idx++;
- String tk_string = String();
- while (true) {
- if (code[idx] == 0) {
- error_str = "Unterminated String";
- error = true;
- return TK_ERROR;
- } else if (code[idx] == begin_str) {
- idx++;
- break;
- } else if (code[idx] == '\\') {
- //escaped characters...
- idx++;
- char32_t next = code[idx];
- if (next == 0) {
- error_str = "Unterminated String";
- error = true;
- return TK_ERROR;
- }
- char32_t res = 0;
-
- switch (next) {
- case 'b':
- res = 8;
- break;
- case 't':
- res = 9;
- break;
- case 'n':
- res = 10;
- break;
- case 'f':
- res = 12;
- break;
- case 'r':
- res = 13;
- break;
- case '\"':
- res = '\"';
- break;
- case '\\':
- res = '\\';
- break;
- default: {
- res = next;
- } break;
- }
-
- tk_string += res;
-
- } else {
- if (code[idx] == '\n') {
- line++;
- }
- tk_string += code[idx];
- }
- idx++;
- }
-
- value = tk_string;
-
- return TK_STRING;
-
- } break;
- default: {
- if (code[idx] <= 32) {
- idx++;
- break;
- }
-
- if ((code[idx] >= 33 && code[idx] <= 47) || (code[idx] >= 58 && code[idx] <= 64) || (code[idx] >= 91 && code[idx] <= 96) || (code[idx] >= 123 && code[idx] <= 127)) {
- value = String::chr(code[idx]);
- idx++;
- return TK_SYMBOL;
- }
-
- if (code[idx] == '-' || (code[idx] >= '0' && code[idx] <= '9')) {
- //a number
- const char32_t *rptr;
- double number = String::to_float(&code[idx], &rptr);
- idx += (rptr - &code[idx]);
- value = number;
- return TK_NUMBER;
-
- } else if ((code[idx] >= 'A' && code[idx] <= 'Z') || (code[idx] >= 'a' && code[idx] <= 'z') || code[idx] > 127) {
- String id;
-
- while ((code[idx] >= 'A' && code[idx] <= 'Z') || (code[idx] >= 'a' && code[idx] <= 'z') || code[idx] > 127) {
- id += code[idx];
- idx++;
- }
-
- value = id;
- return TK_IDENTIFIER;
- } else {
- error_str = "Unexpected character.";
- error = true;
- return TK_ERROR;
- }
- }
- }
- }
- }
-
-public:
- Error parse(const String &p_code, const String &p_known_class_name = String()) {
- code = p_code;
- idx = 0;
- line = 0;
- error_str = String();
- error = false;
- value = Variant();
- class_name = String();
-
- bool use_next_class = false;
- Token tk = get_token();
-
- Map<int, String> namespace_stack;
- int curly_stack = 0;
-
- while (!error || tk != TK_EOF) {
- if (tk == TK_BRACKET_OPEN) {
- tk = get_token();
- if (tk == TK_IDENTIFIER && String(value) == "ScriptClass") {
- if (get_token() == TK_BRACKET_CLOSE) {
- use_next_class = true;
- }
- }
- } else if (tk == TK_IDENTIFIER && String(value) == "class") {
- tk = get_token();
- if (tk == TK_IDENTIFIER) {
- String name = value;
- if (use_next_class || p_known_class_name == name) {
- for (Map<int, String>::Element *E = namespace_stack.front(); E; E = E->next()) {
- class_name += E->get() + ".";
- }
- class_name += String(value);
- break;
- }
- }
-
- } else if (tk == TK_IDENTIFIER && String(value) == "namespace") {
- String name;
- int at_level = curly_stack;
- while (true) {
- tk = get_token();
- if (tk == TK_IDENTIFIER) {
- name += String(value);
- }
-
- tk = get_token();
- if (tk == TK_PERIOD) {
- name += ".";
- } else if (tk == TK_CURLY_BRACKET_OPEN) {
- curly_stack++;
- break;
- } else {
- break; //whathever else
- }
- }
-
- if (name != String()) {
- namespace_stack[at_level] = name;
- }
-
- } else if (tk == TK_CURLY_BRACKET_OPEN) {
- curly_stack++;
- } else if (tk == TK_CURLY_BRACKET_CLOSE) {
- curly_stack--;
- if (namespace_stack.has(curly_stack)) {
- namespace_stack.erase(curly_stack);
- }
- }
-
- tk = get_token();
- }
-
- if (error) {
- return ERR_PARSE_ERROR;
- }
-
- return OK;
- }
-
- String get_error() {
- return error_str;
- }
-
- String get_class() {
- return class_name;
- }
-};
-
-void test_vec(Plane p_vec) {
- CameraMatrix cm;
- cm.set_perspective(45, 1, 0, 100);
- Plane v0 = cm.xform4(p_vec);
-
- print_line("out: " + v0);
- v0.normal.z = (v0.d / 100.0 * 2.0 - 1.0) * v0.d;
- print_line("out_F: " + v0);
-}
-
-uint32_t ihash(uint32_t a) {
- a = (a + 0x7ed55d16) + (a << 12);
- a = (a ^ 0xc761c23c) ^ (a >> 19);
- a = (a + 0x165667b1) + (a << 5);
- a = (a + 0xd3a2646c) ^ (a << 9);
- a = (a + 0xfd7046c5) + (a << 3);
- a = (a ^ 0xb55a4f09) ^ (a >> 16);
- return a;
-}
-
-uint32_t ihash2(uint32_t a) {
- a = (a ^ 61) ^ (a >> 16);
- a = a + (a << 3);
- a = a ^ (a >> 4);
- a = a * 0x27d4eb2d;
- a = a ^ (a >> 15);
- return a;
-}
-
-uint32_t ihash3(uint32_t a) {
- a = (a + 0x479ab41d) + (a << 8);
- a = (a ^ 0xe4aa10ce) ^ (a >> 5);
- a = (a + 0x9942f0a6) - (a << 14);
- a = (a ^ 0x5aedd67d) ^ (a >> 3);
- a = (a + 0x17bea992) + (a << 7);
- return a;
-}
-
-MainLoop *test() {
- {
- Vector<Vector3> points;
- points.push_back(Vector3(0, 0, 0));
- points.push_back(Vector3(0, 0, 1));
- points.push_back(Vector3(0, 1, 0));
- points.push_back(Vector3(0, 1, 1));
- points.push_back(Vector3(1, 1, 0));
- points.push_back(Vector3(1, 0, 0));
- points.push_back(Vector3(1, 0, 1));
- points.push_back(Vector3(1, 1, 1));
-
- for (int i = 0; i < 800; i++) {
- points.push_back(Vector3(Math::randf() * 2.0 - 1.0, Math::randf() * 2.0 - 1.0, Math::randf() * 2.0 - 1.0) * Vector3(25, 30, 33));
- }
-
- Vector<Delaunay3D::OutputSimplex> os = Delaunay3D::tetrahedralize(points);
- print_line("simplices in the end: " + itos(os.size()));
- for (int i = 0; i < os.size(); i++) {
- print_line("Simplex " + itos(i) + ": ");
- print_line(points[os[i].points[0]]);
- print_line(points[os[i].points[1]]);
- print_line(points[os[i].points[2]]);
- print_line(points[os[i].points[3]]);
- }
-
- {
- FileAccessRef f = FileAccess::open("res://bsp.obj", FileAccess::WRITE);
- for (int i = 0; i < os.size(); i++) {
- f->store_line("o Simplex" + itos(i));
- for (int j = 0; j < 4; j++) {
- f->store_line(vformat("v %f %f %f", points[os[i].points[j]].x, points[os[i].points[j]].y, points[os[i].points[j]].z));
- }
- static const int face_order[4][3] = {
- { 1, 2, 3 },
- { 1, 3, 4 },
- { 1, 2, 4 },
- { 2, 3, 4 }
- };
-
- for (int j = 0; j < 4; j++) {
- f->store_line(vformat("f %d %d %d", 4 * i + face_order[j][0], 4 * i + face_order[j][1], 4 * i + face_order[j][2]));
- }
- }
- f->close();
- }
-
- return nullptr;
- }
-
- {
- float r = 1;
- float g = 0.5;
- float b = 0.1;
-
- const float pow2to9 = 512.0f;
- const float B = 15.0f;
- const float N = 9.0f;
-
- float sharedexp = 65408.000f;
-
- float cRed = MAX(0.0f, MIN(sharedexp, r));
- float cGreen = MAX(0.0f, MIN(sharedexp, g));
- float cBlue = MAX(0.0f, MIN(sharedexp, b));
-
- float cMax = MAX(cRed, MAX(cGreen, cBlue));
-
- float expp = MAX(-B - 1.0f, floor(Math::log(cMax) / Math_LN2)) + 1.0f + B;
-
- float sMax = (float)floor((cMax / Math::pow(2.0f, expp - B - N)) + 0.5f);
-
- float exps = expp + 1.0f;
-
- if (0.0 <= sMax && sMax < pow2to9) {
- exps = expp;
- }
-
- float sRed = Math::floor((cRed / pow(2.0f, exps - B - N)) + 0.5f);
- float sGreen = Math::floor((cGreen / pow(2.0f, exps - B - N)) + 0.5f);
- float sBlue = Math::floor((cBlue / pow(2.0f, exps - B - N)) + 0.5f);
-
- print_line("R: " + rtos(sRed) + " G: " + rtos(sGreen) + " B: " + rtos(sBlue) + " EXP: " + rtos(exps));
-
- uint32_t rgbe = (Math::fast_ftoi(sRed) & 0x1FF) | ((Math::fast_ftoi(sGreen) & 0x1FF) << 9) | ((Math::fast_ftoi(sBlue) & 0x1FF) << 18) | ((Math::fast_ftoi(exps) & 0x1F) << 27);
-
- float rb = rgbe & 0x1ff;
- float gb = (rgbe >> 9) & 0x1ff;
- float bb = (rgbe >> 18) & 0x1ff;
- float eb = (rgbe >> 27);
- float mb = Math::pow(2.0, eb - 15.0 - 9.0);
- float rd = rb * mb;
- float gd = gb * mb;
- float bd = bb * mb;
-
- print_line("RGBE: " + Color(rd, gd, bd));
- }
-
- Vector<int> ints;
- ints.resize(20);
-
- {
- int *w;
- w = ints.ptrw();
- for (int i = 0; i < ints.size(); i++) {
- w[i] = i;
- }
- }
-
- Vector<int> posho = ints;
-
- {
- const int *r = posho.ptr();
- for (int i = 0; i < posho.size(); i++) {
- print_line(itos(i) + " : " + itos(r[i]));
- }
- }
-
- List<String> cmdlargs = OS::get_singleton()->get_cmdline_args();
-
- if (cmdlargs.empty()) {
- //try editor!
- return nullptr;
- }
-
- String test = cmdlargs.back()->get();
- if (test == "math") {
- // Not a file name but the test name, abort.
- // FIXME: This test is ugly as heck, needs fixing :)
- return nullptr;
- }
-
- FileAccess *fa = FileAccess::open(test, FileAccess::READ);
- ERR_FAIL_COND_V_MSG(!fa, nullptr, "Could not open file: " + test);
-
- Vector<uint8_t> buf;
- int flen = fa->get_len();
- buf.resize(fa->get_len() + 1);
- fa->get_buffer(buf.ptrw(), flen);
- buf.write[flen] = 0;
-
- String code;
- code.parse_utf8((const char *)&buf[0]);
-
- GetClassAndNamespace getclass;
- if (getclass.parse(code)) {
- print_line("Parse error: " + getclass.get_error());
- } else {
- print_line("Found class: " + getclass.get_class());
- }
-
- {
- Vector<int> hashes;
- List<StringName> tl;
- ClassDB::get_class_list(&tl);
-
- for (List<StringName>::Element *E = tl.front(); E; E = E->next()) {
- Vector<uint8_t> m5b = E->get().operator String().md5_buffer();
- hashes.push_back(hashes.size());
- }
-
- for (int i = nearest_shift(hashes.size()); i < 20; i++) {
- bool success = true;
- for (int s = 0; s < 10000; s++) {
- Set<uint32_t> existing;
- success = true;
-
- for (int j = 0; j < hashes.size(); j++) {
- uint32_t eh = ihash2(ihash3(hashes[j] + ihash(s) + s)) & ((1 << i) - 1);
- if (existing.has(eh)) {
- success = false;
- break;
- }
- existing.insert(eh);
- }
-
- if (success) {
- print_line("success at " + itos(i) + "/" + itos(nearest_shift(hashes.size())) + " shift " + itos(s));
- break;
- }
- }
- if (success) {
- break;
- }
- }
-
- print_line("DONE");
- }
-
- {
- print_line("NUM: " + itos(-128));
- }
-
- {
- Vector3 v(1, 2, 3);
- v.normalize();
- float a = 0.3;
-
- Basis m(v, a);
-
- Vector3 v2(7, 3, 1);
- v2.normalize();
- float a2 = 0.8;
-
- Basis m2(v2, a2);
-
- Quat q = m;
- Quat q2 = m2;
-
- Basis m3 = m.inverse() * m2;
- Quat q3 = (q.inverse() * q2); //.normalized();
-
- print_line(Quat(m3));
- print_line(q3);
-
- print_line("before v: " + v + " a: " + rtos(a));
- q.get_axis_angle(v, a);
- print_line("after v: " + v + " a: " + rtos(a));
- }
-
- String ret;
-
- List<String> args;
- args.push_back("-l");
- Error err = OS::get_singleton()->execute("/bin/ls", args, true, nullptr, &ret);
- print_line("error: " + itos(err));
- print_line(ret);
-
- Basis m3;
- m3.rotate(Vector3(1, 0, 0), 0.2);
- m3.rotate(Vector3(0, 1, 0), 1.77);
- m3.rotate(Vector3(0, 0, 1), 212);
- Basis m32;
- m32.set_euler(m3.get_euler());
- print_line("ELEULEEEEEEEEEEEEEEEEEER: " + m3.get_euler() + " vs " + m32.get_euler());
-
- {
- Dictionary d;
- d["momo"] = 1;
- Dictionary b = d;
- b["44"] = 4;
- }
-
- print_line("inters: " + rtos(Geometry2D::segment_intersects_circle(Vector2(-5, 0), Vector2(-2, 0), Vector2(), 1.0)));
-
- print_line("cross: " + Vector3(1, 2, 3).cross(Vector3(4, 5, 7)));
- print_line("dot: " + rtos(Vector3(1, 2, 3).dot(Vector3(4, 5, 7))));
- print_line("abs: " + Vector3(-1, 2, -3).abs());
- print_line("distance_to: " + rtos(Vector3(1, 2, 3).distance_to(Vector3(4, 5, 7))));
- print_line("distance_squared_to: " + rtos(Vector3(1, 2, 3).distance_squared_to(Vector3(4, 5, 7))));
- print_line("plus: " + (Vector3(1, 2, 3) + Vector3(Vector3(4, 5, 7))));
- print_line("minus: " + (Vector3(1, 2, 3) - Vector3(Vector3(4, 5, 7))));
- print_line("mul: " + (Vector3(1, 2, 3) * Vector3(Vector3(4, 5, 7))));
- print_line("div: " + (Vector3(1, 2, 3) / Vector3(Vector3(4, 5, 7))));
- print_line("mul scalar: " + (Vector3(1, 2, 3) * 2.0));
- print_line("premul scalar: " + (2.0 * Vector3(1, 2, 3)));
- print_line("div scalar: " + (Vector3(1, 2, 3) / 3.0));
- print_line("length: " + rtos(Vector3(1, 2, 3).length()));
- print_line("length squared: " + rtos(Vector3(1, 2, 3).length_squared()));
- print_line("normalized: " + Vector3(1, 2, 3).normalized());
- print_line("inverse: " + Vector3(1, 2, 3).inverse());
-
- {
- Vector3 v(4, 5, 7);
- v.normalize();
- print_line("normalize: " + v);
- }
-
- {
- Vector3 v(4, 5, 7);
- v += Vector3(1, 2, 3);
- print_line("+=: " + v);
- }
-
- {
- Vector3 v(4, 5, 7);
- v -= Vector3(1, 2, 3);
- print_line("-=: " + v);
- }
-
- {
- Vector3 v(4, 5, 7);
- v *= Vector3(1, 2, 3);
- print_line("*=: " + v);
- }
-
- {
- Vector3 v(4, 5, 7);
- v /= Vector3(1, 2, 3);
- print_line("/=: " + v);
- }
-
- {
- Vector3 v(4, 5, 7);
- v *= 2.0;
- print_line("scalar *=: " + v);
- }
-
- {
- Vector3 v(4, 5, 7);
- v /= 2.0;
- print_line("scalar /=: " + v);
- }
-
- return nullptr;
-}
-
-} // namespace TestMath
diff --git a/tests/test_oa_hash_map.cpp b/tests/test_oa_hash_map.cpp
deleted file mode 100644
index 9182f66b61..0000000000
--- a/tests/test_oa_hash_map.cpp
+++ /dev/null
@@ -1,299 +0,0 @@
-/*************************************************************************/
-/* test_oa_hash_map.cpp */
-/*************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
-/* */
-/* 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. */
-/*************************************************************************/
-
-#include "test_oa_hash_map.h"
-
-#include "core/oa_hash_map.h"
-#include "core/os/os.h"
-
-namespace TestOAHashMap {
-
-struct CountedItem {
- static int count;
-
- int id = -1;
- bool destroyed = false;
-
- CountedItem() {
- count++;
- }
-
- CountedItem(int p_id) :
- id(p_id) {
- count++;
- }
-
- CountedItem(const CountedItem &p_other) :
- id(p_other.id) {
- count++;
- }
-
- CountedItem &operator=(const CountedItem &p_other) = default;
-
- ~CountedItem() {
- CRASH_COND(destroyed);
- count--;
- destroyed = true;
- }
-};
-
-int CountedItem::count;
-
-MainLoop *test() {
- OS::get_singleton()->print("\n\n\nHello from test\n");
-
- // test element tracking.
- {
- OAHashMap<int, int> map;
-
- map.set(42, 1337);
- map.set(1337, 21);
- map.set(42, 11880);
-
- int value = 0;
- map.lookup(42, value);
-
- OS::get_singleton()->print("capacity %d\n", map.get_capacity());
- OS::get_singleton()->print("elements %d\n", map.get_num_elements());
-
- OS::get_singleton()->print("map[42] = %d\n", value);
- }
-
- // rehashing and deletion
- {
- OAHashMap<int, int> map;
-
- for (int i = 0; i < 500; i++) {
- map.set(i, i * 2);
- }
-
- for (int i = 0; i < 500; i += 2) {
- map.remove(i);
- }
-
- uint32_t num_elems = 0;
- for (int i = 0; i < 500; i++) {
- int tmp;
- if (map.lookup(i, tmp) && tmp == i * 2) {
- num_elems++;
- }
- }
-
- OS::get_singleton()->print("elements %d == %d.\n", map.get_num_elements(), num_elems);
- }
-
- // iteration
- {
- OAHashMap<String, int> map;
-
- map.set("Hello", 1);
- map.set("World", 2);
- map.set("Godot rocks", 42);
-
- for (OAHashMap<String, int>::Iterator it = map.iter(); it.valid; it = map.next_iter(it)) {
- OS::get_singleton()->print("map[\"%s\"] = %d\n", it.key->utf8().get_data(), *it.value);
- }
- }
-
- // stress test / test for issue #22928
- {
- OAHashMap<int, int> map;
- int dummy = 0;
- const int N = 1000;
- uint32_t *keys = new uint32_t[N];
-
- Math::seed(0);
-
- // insert a couple of random keys (with a dummy value, which is ignored)
- for (int i = 0; i < N; i++) {
- keys[i] = Math::rand();
- map.set(keys[i], dummy);
-
- if (!map.lookup(keys[i], dummy)) {
- OS::get_singleton()->print("could not find 0x%X despite it was just inserted!\n", unsigned(keys[i]));
- }
- }
-
- // check whether the keys are still present
- for (int i = 0; i < N; i++) {
- if (!map.lookup(keys[i], dummy)) {
- OS::get_singleton()->print("could not find 0x%X despite it has been inserted previously! (not checking the other keys, breaking...)\n", unsigned(keys[i]));
- break;
- }
- }
-
- delete[] keys;
- }
-
- // regression test / test for issue related to #31402
- {
- OS::get_singleton()->print("test for issue #31402 started...\n");
-
- const int num_test_values = 12;
- int test_values[num_test_values] = { 0, 24, 48, 72, 96, 120, 144, 168, 192, 216, 240, 264 };
-
- int dummy = 0;
- OAHashMap<int, int> map;
- map.clear();
-
- for (int i = 0; i < num_test_values; ++i) {
- map.set(test_values[i], dummy);
- }
-
- OS::get_singleton()->print("test for issue #31402 passed.\n");
- }
-
- // test collision resolution, should not crash or run indefinitely
- {
- OAHashMap<int, int> map(4);
- map.set(1, 1);
- map.set(5, 1);
- map.set(9, 1);
- map.set(13, 1);
- map.remove(5);
- map.remove(9);
- map.remove(13);
- map.set(5, 1);
- }
-
- // test memory management of items, should not crash or leak items
- {
- // Exercise different patterns of removal
- for (int i = 0; i < 4; ++i) {
- {
- OAHashMap<String, CountedItem> map;
- int id = 0;
- for (int j = 0; j < 100; ++j) {
- map.insert(itos(j), CountedItem(id));
- }
- if (i <= 1) {
- for (int j = 0; j < 100; ++j) {
- map.remove(itos(j));
- }
- }
- if (i % 2 == 0) {
- map.clear();
- }
- }
-
- if (CountedItem::count != 0) {
- OS::get_singleton()->print("%d != 0 (not performing the other test sub-cases, breaking...)\n", CountedItem::count);
- break;
- }
- }
- }
-
- // Test map with 0 capacity.
- {
- OAHashMap<int, String> original_map(0);
- original_map.set(1, "1");
- OS::get_singleton()->print("OAHashMap 0 capacity initialization passed.\n");
- }
-
- // Test copy constructor.
- {
- OAHashMap<int, String> original_map;
- original_map.set(1, "1");
- original_map.set(2, "2");
- original_map.set(3, "3");
- original_map.set(4, "4");
- original_map.set(5, "5");
-
- OAHashMap<int, String> map_copy(original_map);
-
- bool pass = true;
- for (
- OAHashMap<int, String>::Iterator it = original_map.iter();
- it.valid;
- it = original_map.next_iter(it)) {
- if (map_copy.lookup_ptr(*it.key) == nullptr) {
- pass = false;
- }
- if (*it.value != *map_copy.lookup_ptr(*it.key)) {
- pass = false;
- }
- }
- if (pass) {
- OS::get_singleton()->print("OAHashMap copy constructor test passed.\n");
- } else {
- OS::get_singleton()->print("OAHashMap copy constructor test FAILED.\n");
- }
-
- map_copy.set(1, "Random String");
- if (*map_copy.lookup_ptr(1) == *original_map.lookup_ptr(1)) {
- OS::get_singleton()->print("OAHashMap copy constructor, atomic copy test FAILED.\n");
- } else {
- OS::get_singleton()->print("OAHashMap copy constructor, atomic copy test passed.\n");
- }
- }
-
- // Test assign operator.
- {
- OAHashMap<int, String> original_map;
- original_map.set(1, "1");
- original_map.set(2, "2");
- original_map.set(3, "3");
- original_map.set(4, "4");
- original_map.set(5, "5");
-
- OAHashMap<int, String> map_copy(100000);
- map_copy.set(1, "Just a string.");
- map_copy = original_map;
-
- bool pass = true;
- for (
- OAHashMap<int, String>::Iterator it = map_copy.iter();
- it.valid;
- it = map_copy.next_iter(it)) {
- if (original_map.lookup_ptr(*it.key) == nullptr) {
- pass = false;
- }
- if (*it.value != *original_map.lookup_ptr(*it.key)) {
- pass = false;
- }
- }
- if (pass) {
- OS::get_singleton()->print("OAHashMap assign operation test passed.\n");
- } else {
- OS::get_singleton()->print("OAHashMap assign operation test FAILED.\n");
- }
-
- map_copy.set(1, "Random String");
- if (*map_copy.lookup_ptr(1) == *original_map.lookup_ptr(1)) {
- OS::get_singleton()->print("OAHashMap assign operation atomic copy test FAILED.\n");
- } else {
- OS::get_singleton()->print("OAHashMap assign operation atomic copy test passed.\n");
- }
- }
-
- return nullptr;
-}
-
-} // namespace TestOAHashMap
diff --git a/tests/test_physics_2d.cpp b/tests/test_physics_2d.cpp
deleted file mode 100644
index c82ae920bc..0000000000
--- a/tests/test_physics_2d.cpp
+++ /dev/null
@@ -1,407 +0,0 @@
-/*************************************************************************/
-/* test_physics_2d.cpp */
-/*************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
-/* */
-/* 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. */
-/*************************************************************************/
-
-#include "test_physics_2d.h"
-
-#include "core/map.h"
-#include "core/os/main_loop.h"
-#include "core/os/os.h"
-#include "core/print_string.h"
-#include "scene/resources/texture.h"
-#include "servers/display_server.h"
-#include "servers/physics_server_2d.h"
-#include "servers/rendering_server.h"
-
-static const unsigned char convex_png[] = {
- 0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa, 0x0, 0x0, 0x0, 0xd, 0x49, 0x48, 0x44, 0x52, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x40, 0x8, 0x6, 0x0, 0x0, 0x0, 0xaa, 0x69, 0x71, 0xde, 0x0, 0x0, 0x0, 0x1, 0x73, 0x52, 0x47, 0x42, 0x0, 0xae, 0xce, 0x1c, 0xe9, 0x0, 0x0, 0x0, 0x6, 0x62, 0x4b, 0x47, 0x44, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf9, 0x43, 0xbb, 0x7f, 0x0, 0x0, 0x0, 0x9, 0x70, 0x48, 0x59, 0x73, 0x0, 0x0, 0xb, 0x13, 0x0, 0x0, 0xb, 0x13, 0x1, 0x0, 0x9a, 0x9c, 0x18, 0x0, 0x0, 0x0, 0x7, 0x74, 0x49, 0x4d, 0x45, 0x7, 0xdb, 0x6, 0xa, 0x3, 0x13, 0x31, 0x66, 0xa7, 0xac, 0x79, 0x0, 0x0, 0x4, 0xef, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0xed, 0x9b, 0xdd, 0x4e, 0x2a, 0x57, 0x14, 0xc7, 0xf7, 0x1e, 0xc0, 0x19, 0x38, 0x32, 0x80, 0xa, 0x6a, 0xda, 0x18, 0xa3, 0xc6, 0x47, 0x50, 0x7b, 0xa1, 0xd9, 0x36, 0x27, 0x7e, 0x44, 0xed, 0x45, 0x4d, 0x93, 0x3e, 0x40, 0x1f, 0x64, 0x90, 0xf4, 0x1, 0xbc, 0xf0, 0xc2, 0x9c, 0x57, 0x30, 0x4d, 0xbc, 0xa8, 0x6d, 0xc, 0x69, 0x26, 0xb5, 0x68, 0x8b, 0x35, 0x7e, 0x20, 0xb4, 0xf5, 0x14, 0xbf, 0x51, 0x3c, 0x52, 0xe, 0xc, 0xe, 0xc8, 0xf0, 0xb1, 0x7a, 0x51, 0x3d, 0xb1, 0x9e, 0x19, 0x1c, 0x54, 0x70, 0x1c, 0xdc, 0x9, 0x17, 0x64, 0x8, 0xc9, 0xff, 0xb7, 0xd6, 0x7f, 0xcd, 0x3f, 0x2b, 0xd9, 0x8, 0xbd, 0x9c, 0xda, 0x3e, 0xf8, 0x31, 0xff, 0xc, 0x0, 0x8, 0x42, 0x88, 0x9c, 0x9f, 0x9f, 0xbf, 0xa, 0x87, 0xc3, 0xad, 0x7d, 0x7d, 0x7d, 0x7f, 0x23, 0x84, 0x78, 0x8c, 0x31, 0xaf, 0x55, 0x0, 0xc6, 0xc7, 0x14, 0x1e, 0x8f, 0xc7, 0xbf, 0x38, 0x3c, 0x3c, 0x6c, 0x9b, 0x9f, 0x9f, 0x6f, 0xb8, 0x82, 0x9b, 0xee, 0xe8, 0xe8, 0xf8, 0x12, 0x0, 0xbe, 0xd3, 0x2a, 0x8, 0xfc, 0x50, 0xd1, 0xf9, 0x7c, 0x9e, 0x8a, 0x46, 0xa3, 0x5f, 0x9d, 0x9e, 0x9e, 0x7e, 0xb2, 0xb0, 0xb0, 0x60, 0xe5, 0x79, 0x1e, 0xf1, 0xfc, 0x7f, 0x3a, 0x9, 0x21, 0x88, 0x10, 0x82, 0x26, 0x26, 0x26, 0xde, 0x77, 0x75, 0x75, 0x85, 0x59, 0x96, 0xfd, 0x5e, 0x6b, 0x20, 0xf0, 0x7d, 0x85, 0x4b, 0x92, 0xf4, 0xfa, 0xe0, 0xe0, 0xe0, 0xd3, 0xb9, 0xb9, 0xb9, 0x46, 0x49, 0x92, 0xea, 0x6f, 0xa, 0xbf, 0x7d, 0x8, 0x21, 0x68, 0x70, 0x70, 0xb0, 0x38, 0x39, 0x39, 0x79, 0xd6, 0xd9, 0xd9, 0xb9, 0xcf, 0x30, 0xcc, 0xa2, 0xd6, 0xad, 0x21, 0x2b, 0x1c, 0x0, 0x38, 0x41, 0x10, 0xfc, 0xdb, 0xdb, 0xdb, 0x27, 0x1e, 0x8f, 0x27, 0x4b, 0x8, 0x1, 0x84, 0x90, 0xea, 0xf, 0x21, 0x4, 0x3c, 0x1e, 0x4f, 0x76, 0x67, 0x67, 0x67, 0x3f, 0x9f, 0xcf, 0xff, 0x7c, 0x5, 0xf3, 0xd9, 0x0, 0xe0, 0x2, 0x81, 0xc0, 0xa9, 0xdb, 0xed, 0x2e, 0x94, 0x2b, 0x5c, 0xe, 0xc4, 0xca, 0xca, 0x8a, 0x18, 0x8d, 0x46, 0x3, 0x0, 0xc0, 0x69, 0x1e, 0x4, 0x0, 0x90, 0x48, 0x24, 0x12, 0xe4, 0x38, 0xee, 0x41, 0xc2, 0x6f, 0x43, 0xe0, 0x38, 0xe, 0xfc, 0x7e, 0xbf, 0x10, 0x8b, 0xc5, 0xd6, 0x35, 0xd, 0x22, 0x9b, 0xcd, 0x7a, 0x96, 0x97, 0x97, 0x33, 0xf, 0xad, 0x7c, 0x29, 0x10, 0x9b, 0x9b, 0x9b, 0xef, 0x2e, 0x2e, 0x2e, 0x7e, 0xd5, 0x1c, 0x8, 0x0, 0x20, 0xe1, 0x70, 0x38, 0xfc, 0x98, 0xd5, 0x57, 0x2, 0xe1, 0x76, 0xbb, 0xf3, 0xa1, 0x50, 0xe8, 0x38, 0x9b, 0xcd, 0xfe, 0xa2, 0x9, 0x8, 0x0, 0x40, 0x2e, 0x2f, 0x2f, 0x7d, 0x4b, 0x4b, 0x4b, 0xb9, 0x4a, 0x54, 0x5f, 0x9, 0xc4, 0xd2, 0xd2, 0x92, 0xb4, 0xb7, 0xb7, 0xf7, 0x36, 0x97, 0xcb, 0x4d, 0x3d, 0x29, 0x8, 0x0, 0xe0, 0x42, 0xa1, 0xd0, 0x71, 0xb5, 0xc4, 0xdf, 0xb6, 0xc5, 0x93, 0xe, 0x4a, 0x0, 0x20, 0xa9, 0x54, 0xea, 0x37, 0xb7, 0xdb, 0x5d, 0xa8, 0xa6, 0x78, 0x39, 0x10, 0x6b, 0x6b, 0x6b, 0xf1, 0x64, 0x32, 0xb9, 0x5a, 0x55, 0x10, 0x0, 0xc0, 0x6d, 0x6c, 0x6c, 0x9c, 0x57, 0xbb, 0xfa, 0x25, 0x40, 0x14, 0x3, 0x81, 0x40, 0x34, 0x93, 0xc9, 0x2c, 0x57, 0x1c, 0x4, 0x0, 0x90, 0x58, 0x2c, 0xb6, 0x5e, 0xe9, 0xc1, 0x77, 0x1f, 0x10, 0x53, 0x53, 0x53, 0x52, 0xc5, 0x83, 0x14, 0x0, 0x70, 0x7e, 0xbf, 0x5f, 0xd0, 0x42, 0xf5, 0x95, 0x40, 0xf8, 0x7c, 0xbe, 0xcb, 0xa3, 0xa3, 0xa3, 0x3f, 0x1e, 0xbd, 0x1b, 0x0, 0x80, 0x1c, 0x1f, 0x1f, 0x87, 0xb4, 0x56, 0xfd, 0xaa, 0x5, 0x29, 0x51, 0x14, 0xbf, 0xf5, 0xf9, 0x7c, 0x97, 0x5a, 0xad, 0xbe, 0x12, 0x88, 0xf5, 0xf5, 0xf5, 0xd8, 0x83, 0x83, 0x54, 0xb5, 0x42, 0x8f, 0x66, 0x83, 0x94, 0xd6, 0xbd, 0x5f, 0xce, 0x7c, 0x38, 0x3c, 0x3c, 0xfc, 0xb3, 0x50, 0x28, 0xb8, 0xcb, 0x2, 0x1, 0x0, 0xdc, 0xf4, 0xf4, 0xf4, 0xfe, 0x73, 0x15, 0x2f, 0x17, 0xa4, 0x22, 0x91, 0x48, 0x50, 0xb5, 0x2d, 0x0, 0x80, 0x9b, 0x99, 0x99, 0x79, 0xfb, 0xdc, 0x1, 0xc8, 0x5, 0xa9, 0x44, 0x22, 0xf1, 0xfb, 0x9d, 0x10, 0x0, 0x80, 0x9b, 0x9d, 0x9d, 0xd, 0xea, 0x5, 0xc0, 0xad, 0xfd, 0x43, 0x1a, 0x0, 0xb8, 0xdb, 0x9a, 0xa9, 0x8f, 0xb6, 0xa4, 0x46, 0xa3, 0xa4, 0xb7, 0xd5, 0x37, 0xcf, 0xf3, 0x68, 0x75, 0x75, 0xf5, 0x4c, 0xee, 0x99, 0x1c, 0x80, 0x9c, 0x1e, 0xf7, 0xff, 0x16, 0x8b, 0x45, 0x50, 0x5, 0xa0, 0xb7, 0xb7, 0xb7, 0x85, 0x10, 0xa2, 0x2b, 0xf1, 0x84, 0x10, 0xd4, 0xdf, 0xdf, 0x6f, 0x57, 0x3, 0x80, 0x37, 0x18, 0xc, 0x5, 0x3d, 0x2, 0xa0, 0x69, 0x3a, 0x8b, 0x10, 0xe2, 0x4b, 0x2, 0xc0, 0x18, 0xf3, 0xc1, 0x60, 0x70, 0x47, 0x8f, 0x16, 0x38, 0x3a, 0x3a, 0x5a, 0x93, 0x5b, 0xc3, 0x7f, 0x64, 0x81, 0xba, 0xba, 0x3a, 0x49, 0x8f, 0x0, 0x1a, 0x1a, 0x1a, 0xd4, 0xcd, 0x0, 0x93, 0xc9, 0xa4, 0xcb, 0x21, 0xe8, 0x74, 0x3a, 0xd5, 0x1, 0xa0, 0x69, 0x5a, 0x77, 0x1d, 0x80, 0x31, 0x2e, 0x38, 0x9d, 0x4e, 0xb1, 0x66, 0x1, 0x30, 0xc, 0x23, 0x28, 0x3d, 0x93, 0x9b, 0x1, 0xb9, 0x9a, 0x6, 0x60, 0x36, 0x9b, 0x75, 0xd7, 0x1, 0x4a, 0x21, 0xa8, 0x26, 0x0, 0x94, 0xa, 0x41, 0xb2, 0x0, 0x18, 0x86, 0xc9, 0xe9, 0xd, 0x80, 0x52, 0x8, 0x92, 0x5, 0x60, 0xb1, 0x58, 0x74, 0x67, 0x1, 0xa5, 0x10, 0xa4, 0x4, 0x40, 0x77, 0x43, 0xd0, 0xe1, 0x70, 0xa8, 0x9f, 0x1, 0x14, 0x45, 0x1, 0x45, 0x51, 0x79, 0x3d, 0x1, 0x68, 0x6e, 0x6e, 0x4e, 0xaa, 0x6, 0x80, 0x10, 0x42, 0x6, 0x83, 0x41, 0x37, 0x36, 0x28, 0x15, 0x82, 0x6a, 0x2, 0x0, 0x4d, 0xd3, 0xa9, 0x52, 0xcf, 0x95, 0x0, 0xe8, 0x66, 0xe, 0x98, 0xcd, 0x66, 0xa1, 0x6c, 0x0, 0x7a, 0x5a, 0x8b, 0x59, 0x2c, 0x96, 0x64, 0xcd, 0x2, 0xb8, 0x2b, 0x4, 0xe9, 0xde, 0x2, 0x77, 0x85, 0xa0, 0x9a, 0xb0, 0x40, 0xa9, 0x10, 0xa4, 0x8, 0xc0, 0x64, 0x32, 0xe9, 0x6, 0x40, 0xa9, 0x10, 0x54, 0xaa, 0x3, 0x74, 0xf3, 0x16, 0x70, 0xb9, 0x5c, 0xe5, 0x3, 0xe8, 0xe9, 0xe9, 0x69, 0xd5, 0xc3, 0x66, 0x18, 0x63, 0x5c, 0x68, 0x6a, 0x6a, 0x12, 0xcb, 0x5, 0xa0, 0x9b, 0xd5, 0x38, 0x4d, 0xd3, 0x29, 0x8a, 0xa2, 0xa0, 0x2c, 0x0, 0x18, 0x63, 0x3e, 0x14, 0xa, 0xfd, 0x55, 0xb, 0x21, 0x48, 0xd1, 0x2, 0x7a, 0x59, 0x8d, 0xdf, 0x1b, 0x80, 0x1e, 0x56, 0xe3, 0x84, 0x10, 0x34, 0x30, 0x30, 0x60, 0xbb, 0xeb, 0x77, 0x46, 0x5, 0xef, 0x48, 0xcf, 0x4d, 0xec, 0x8d, 0x99, 0x5, 0xf5, 0xf5, 0xf5, 0xef, 0x46, 0x47, 0x47, 0xb, 0x2e, 0x97, 0xeb, 0xbc, 0x54, 0x8, 0x52, 0x4, 0xc0, 0x30, 0x8c, 0xf4, 0x5c, 0x4, 0x9b, 0x4c, 0xa6, 0xf4, 0xf8, 0xf8, 0xb8, 0xc8, 0xb2, 0x6c, 0x32, 0x9d, 0x4e, 0xff, 0xd4, 0xdd, 0xdd, 0x7d, 0x66, 0x34, 0x1a, 0x8b, 0xd7, 0x3, 0xfd, 0xae, 0x5b, 0x29, 0xb2, 0x57, 0x66, 0xb6, 0xb6, 0xb6, 0xde, 0xc4, 0xe3, 0xf1, 0x6f, 0xae, 0xaf, 0xc1, 0x28, 0x5d, 0x85, 0x79, 0x2, 0xc1, 0x60, 0xb5, 0x5a, 0xa3, 0xa3, 0xa3, 0xa3, 0x45, 0xab, 0xd5, 0x9a, 0x2a, 0x16, 0x8b, 0x8b, 0x6d, 0x6d, 0x6d, 0xef, 0xd5, 0x8a, 0x55, 0xd, 0x20, 0x91, 0x48, 0xbc, 0x3e, 0x38, 0x38, 0xf8, 0xda, 0x6e, 0xb7, 0xf7, 0x5f, 0x5c, 0x5c, 0xd4, 0x7b, 0xbd, 0xde, 0xbc, 0x20, 0x8, 0xcd, 0x85, 0x42, 0x81, 0xfe, 0xf0, 0xae, 0xac, 0x10, 0x98, 0x9b, 0xd5, 0xc5, 0x18, 0x17, 0x59, 0x96, 0x3d, 0x1d, 0x19, 0x19, 0x1, 0x96, 0x65, 0x5, 0x8a, 0xa2, 0x7e, 0x6c, 0x69, 0x69, 0x49, 0x3d, 0x44, 0xb0, 0x2a, 0x0, 0x1f, 0xcc, 0x74, 0x75, 0x41, 0xea, 0xfa, 0x7b, 0x32, 0x99, 0x64, 0x76, 0x77, 0x77, 0x5d, 0xe, 0x87, 0xa3, 0x5f, 0x14, 0xc5, 0x57, 0x57, 0x60, 0x5a, 0x8b, 0xc5, 0xa2, 0xf1, 0xbe, 0x50, 0x6e, 0xa, 0x66, 0x18, 0x26, 0x31, 0x36, 0x36, 0x96, 0x65, 0x59, 0x36, 0x29, 0x49, 0x92, 0xb7, 0xbd, 0xbd, 0xfd, 0x9f, 0x72, 0xda, 0xf9, 0xd1, 0x1, 0xa8, 0x1, 0x93, 0xcf, 0xe7, 0xa9, 0x93, 0x93, 0x13, 0x1b, 0x4d, 0xd3, 0x9f, 0xb, 0x82, 0x60, 0xf5, 0x7a, 0xbd, 0xd9, 0x54, 0x2a, 0xe5, 0xcc, 0x64, 0x32, 0xe, 0xb9, 0x6e, 0xb9, 0x16, 0x8c, 0x31, 0x2e, 0xda, 0x6c, 0xb6, 0xc8, 0xd0, 0xd0, 0x10, 0x65, 0xb3, 0xd9, 0x92, 0x95, 0xa8, 0x6e, 0xc5, 0x0, 0xa8, 0xe9, 0x96, 0x68, 0x34, 0x6a, 0xdd, 0xdf, 0xdf, 0x6f, 0x76, 0xb9, 0x5c, 0x9f, 0x89, 0xa2, 0x58, 0xbf, 0xb8, 0xb8, 0x8, 0x26, 0x93, 0x29, 0x3b, 0x3c, 0x3c, 0x8c, 0xed, 0x76, 0x7b, 0xd2, 0x68, 0x34, 0xfe, 0xd0, 0xd8, 0xd8, 0x98, 0xae, 0xb6, 0xe0, 0x8a, 0x1, 0x50, 0xb, 0xe6, 0xa9, 0x5, 0xbf, 0x9c, 0x97, 0xf3, 0xff, 0xf3, 0x2f, 0x6a, 0x82, 0x7f, 0xf6, 0x4e, 0xca, 0x1b, 0xf5, 0x0, 0x0, 0x0, 0x0, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
-};
-
-class TestPhysics2DMainLoop : public MainLoop {
- GDCLASS(TestPhysics2DMainLoop, MainLoop);
-
- RID circle_img;
- RID circle_shape;
- RID space;
- RID canvas;
- RID ray;
- RID ray_query;
- Transform2D view_xform;
-
- Vector2 ray_from, ray_to;
-
- struct BodyShapeData {
- RID image;
- RID shape;
- };
-
- BodyShapeData body_shape_data[8];
-
- void _create_body_shape_data() {
- RenderingServer *vs = RenderingServer::get_singleton();
- PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
-
- // SEGMENT
-
- {
- Vector<uint8_t> pixels;
- pixels.resize(32 * 2 * 2);
- for (int i = 0; i < 2; i++) {
- for (int j = 0; j < 32; j++) {
- pixels.set(i * 32 * 2 + j * 2 + 0, (j == 0) ? 255 : 0);
- pixels.set(i * 32 * 2 + j * 2 + 1, 255);
- }
- }
-
- Ref<Image> image = memnew(Image(32, 2, 0, Image::FORMAT_LA8, pixels));
-
- body_shape_data[PhysicsServer2D::SHAPE_SEGMENT].image = vs->texture_2d_create(image);
-
- RID segment_shape = ps->segment_shape_create();
- Rect2 sg(Point2(-16, 0), Point2(16, 0));
- ps->shape_set_data(segment_shape, sg);
-
- body_shape_data[PhysicsServer2D::SHAPE_SEGMENT].shape = segment_shape;
- }
- // CIRCLE
-
- {
- Vector<uint8_t> pixels;
- pixels.resize(32 * 32 * 2);
- for (int i = 0; i < 32; i++) {
- for (int j = 0; j < 32; j++) {
- bool black = Vector2(i - 16, j - 16).length_squared() < 16 * 16;
-
- pixels.set(i * 32 * 2 + j * 2 + 0, (i == 16 || j == 16) ? 255 : 0);
- pixels.set(i * 32 * 2 + j * 2 + 1, black ? 255 : 0);
- }
- }
-
- Ref<Image> image = memnew(Image(32, 32, 0, Image::FORMAT_LA8, pixels));
-
- body_shape_data[PhysicsServer2D::SHAPE_CIRCLE].image = vs->texture_2d_create(image);
-
- RID circle_shape = ps->circle_shape_create();
- ps->shape_set_data(circle_shape, 16);
-
- body_shape_data[PhysicsServer2D::SHAPE_CIRCLE].shape = circle_shape;
- }
-
- // BOX
-
- {
- Vector<uint8_t> pixels;
- pixels.resize(32 * 32 * 2);
- for (int i = 0; i < 32; i++) {
- for (int j = 0; j < 32; j++) {
- bool black = i > 0 && i < 31 && j > 0 && j < 31;
-
- pixels.set(i * 32 * 2 + j * 2 + 0, black ? 0 : 255);
- pixels.set(i * 32 * 2 + j * 2 + 1, 255);
- }
- }
-
- Ref<Image> image = memnew(Image(32, 32, 0, Image::FORMAT_LA8, pixels));
-
- body_shape_data[PhysicsServer2D::SHAPE_RECTANGLE].image = vs->texture_2d_create(image);
-
- RID rectangle_shape = ps->rectangle_shape_create();
- ps->shape_set_data(rectangle_shape, Vector2(16, 16));
-
- body_shape_data[PhysicsServer2D::SHAPE_RECTANGLE].shape = rectangle_shape;
- }
-
- // CAPSULE
-
- {
- Vector<uint8_t> pixels;
- pixels.resize(32 * 64 * 2);
- for (int i = 0; i < 64; i++) {
- for (int j = 0; j < 32; j++) {
- int si = i > 48 ? i - 32 : (i < 16 ? i : 16);
- bool black = Vector2(si - 16, j - 16).length_squared() < 16 * 16;
-
- pixels.set(i * 32 * 2 + j * 2 + 0, (i == 16 || j == 16 || i == 48) ? 255 : 0);
- pixels.set(i * 32 * 2 + j * 2 + 1, black ? 255 : 0);
- }
- }
-
- Ref<Image> image = memnew(Image(32, 64, 0, Image::FORMAT_LA8, pixels));
-
- body_shape_data[PhysicsServer2D::SHAPE_CAPSULE].image = vs->texture_2d_create(image);
-
- RID capsule_shape = ps->capsule_shape_create();
- ps->shape_set_data(capsule_shape, Vector2(16, 32));
-
- body_shape_data[PhysicsServer2D::SHAPE_CAPSULE].shape = capsule_shape;
- }
-
- // CONVEX
-
- {
- Ref<Image> image = memnew(Image(convex_png));
-
- body_shape_data[PhysicsServer2D::SHAPE_CONVEX_POLYGON].image = vs->texture_2d_create(image);
-
- RID convex_polygon_shape = ps->convex_polygon_shape_create();
-
- Vector<Vector2> arr;
- Point2 sb(32, 32);
- arr.push_back(Point2(20, 3) - sb);
- arr.push_back(Point2(58, 23) - sb);
- arr.push_back(Point2(55, 54) - sb);
- arr.push_back(Point2(27, 60) - sb);
- arr.push_back(Point2(5, 56) - sb);
- arr.push_back(Point2(4, 20) - sb);
- arr.push_back(Point2(11, 7) - sb);
- ps->shape_set_data(convex_polygon_shape, arr);
-
- body_shape_data[PhysicsServer2D::SHAPE_CONVEX_POLYGON].shape = convex_polygon_shape;
- }
- }
-
- void _do_ray_query() {
- /*
- PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
- ps->query_intersection_segment(ray_query,ray_from,ray_to);
- */
- }
-
-protected:
- void input_event(const Ref<InputEvent> &p_event) {
- Ref<InputEventMouseButton> mb = p_event;
-
- if (mb.is_valid()) {
- if (mb->is_pressed()) {
- Point2 p(mb->get_position().x, mb->get_position().y);
-
- if (mb->get_button_index() == 1) {
- ray_to = p;
- _do_ray_query();
- } else if (mb->get_button_index() == 2) {
- ray_from = p;
- _do_ray_query();
- }
- }
- }
-
- Ref<InputEventMouseMotion> mm = p_event;
-
- if (mm.is_valid()) {
- Point2 p = mm->get_position();
-
- if (mm->get_button_mask() & BUTTON_MASK_LEFT) {
- ray_to = p;
- _do_ray_query();
- } else if (mm->get_button_mask() & BUTTON_MASK_RIGHT) {
- ray_from = p;
- _do_ray_query();
- }
- }
- }
-
- RID _add_body(PhysicsServer2D::ShapeType p_shape, const Transform2D &p_xform) {
- RenderingServer *vs = RenderingServer::get_singleton();
- PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
-
- RID body = ps->body_create();
- ps->body_add_shape(body, body_shape_data[p_shape].shape);
- ps->body_set_space(body, space);
- ps->body_set_continuous_collision_detection_mode(body, PhysicsServer2D::CCD_MODE_CAST_SHAPE);
- ps->body_set_state(body, PhysicsServer2D::BODY_STATE_TRANSFORM, p_xform);
-
- //print_line("add body with xform: "+p_xform);
- RID sprite = vs->canvas_item_create();
- vs->canvas_item_set_parent(sprite, canvas);
- vs->canvas_item_set_transform(sprite, p_xform);
- Size2 imgsize(5, 5); //vs->texture_get_width(body_shape_data[p_shape].image), vs->texture_get_height(body_shape_data[p_shape].image));
- vs->canvas_item_add_texture_rect(sprite, Rect2(-imgsize / 2.0, imgsize), body_shape_data[p_shape].image);
-
- ps->body_set_force_integration_callback(body, this, "_body_moved", sprite);
- //RID q = ps->query_create(this,"_body_moved",sprite);
- //ps->query_body_state(q,body);
-
- return body;
- }
-
- void _add_plane(const Vector2 &p_normal, real_t p_d) {
- PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
-
- Array arr;
- arr.push_back(p_normal);
- arr.push_back(p_d);
-
- RID plane = ps->line_shape_create();
- ps->shape_set_data(plane, arr);
-
- RID plane_body = ps->body_create();
- ps->body_set_mode(plane_body, PhysicsServer2D::BODY_MODE_STATIC);
- ps->body_set_space(plane_body, space);
- ps->body_add_shape(plane_body, plane);
- }
-
- void _add_concave(const Vector<Vector2> &p_points, const Transform2D &p_xform = Transform2D()) {
- PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
- RenderingServer *vs = RenderingServer::get_singleton();
-
- RID concave = ps->concave_polygon_shape_create();
- ps->shape_set_data(concave, p_points);
- RID body = ps->body_create();
- ps->body_set_mode(body, PhysicsServer2D::BODY_MODE_STATIC);
- ps->body_set_space(body, space);
- ps->body_add_shape(body, concave);
- ps->body_set_state(body, PhysicsServer2D::BODY_STATE_TRANSFORM, p_xform);
-
- RID sprite = vs->canvas_item_create();
- vs->canvas_item_set_parent(sprite, canvas);
- vs->canvas_item_set_transform(sprite, p_xform);
- for (int i = 0; i < p_points.size(); i += 2) {
- vs->canvas_item_add_line(sprite, p_points[i], p_points[i + 1], Color(0, 0, 0), 2);
- }
- }
-
- void _body_moved(Object *p_state, RID p_sprite) {
- PhysicsDirectBodyState2D *state = (PhysicsDirectBodyState2D *)p_state;
- RenderingServer::get_singleton()->canvas_item_set_transform(p_sprite, state->get_transform());
- }
-
- void _ray_query_callback(const RID &p_rid, ObjectID p_id, int p_shape, const Vector2 &p_point, const Vector2 &p_normal) {
- Vector2 ray_end;
-
- if (p_rid.is_valid()) {
- ray_end = p_point;
- } else {
- ray_end = ray_to;
- }
-
- RenderingServer *vs = RenderingServer::get_singleton();
-
- vs->canvas_item_clear(ray);
- vs->canvas_item_add_line(ray, ray_from, ray_end, p_rid.is_valid() ? Color(0, 1, 0.4) : Color(1, 0.4, 0), 2);
- if (p_rid.is_valid()) {
- vs->canvas_item_add_line(ray, ray_end, ray_end + p_normal * 20, p_rid.is_valid() ? Color(0, 1, 0.4) : Color(1, 0.4, 0), 2);
- }
- }
-
- static void _bind_methods() {
- ClassDB::bind_method(D_METHOD("_body_moved"), &TestPhysics2DMainLoop::_body_moved);
- ClassDB::bind_method(D_METHOD("_ray_query_callback"), &TestPhysics2DMainLoop::_ray_query_callback);
- }
-
-public:
- virtual void init() override {
- RenderingServer *vs = RenderingServer::get_singleton();
- PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
-
- space = ps->space_create();
- ps->space_set_active(space, true);
- ps->set_active(true);
- ps->area_set_param(space, PhysicsServer2D::AREA_PARAM_GRAVITY_VECTOR, Vector2(0, 1));
- ps->area_set_param(space, PhysicsServer2D::AREA_PARAM_GRAVITY, 98);
-
- {
- RID vp = vs->viewport_create();
- canvas = vs->canvas_create();
-
- Size2i screen_size = DisplayServer::get_singleton()->window_get_size();
- vs->viewport_attach_canvas(vp, canvas);
- vs->viewport_set_size(vp, screen_size.x, screen_size.y);
- vs->viewport_attach_to_screen(vp, Rect2(Vector2(), screen_size));
- vs->viewport_set_active(vp, true);
-
- Transform2D smaller;
- //smaller.scale(Vector2(0.6,0.6));
- //smaller.elements[2]=Vector2(100,0);
-
- //view_xform = smaller;
- vs->viewport_set_canvas_transform(vp, canvas, view_xform);
- }
-
- ray = vs->canvas_item_create();
- vs->canvas_item_set_parent(ray, canvas);
- //ray_query = ps->query_create(this,"_ray_query_callback",Variant());
- //ps->query_intersection(ray_query,space);
-
- _create_body_shape_data();
-
- for (int i = 0; i < 32; i++) {
- PhysicsServer2D::ShapeType types[4] = {
- PhysicsServer2D::SHAPE_CIRCLE,
- PhysicsServer2D::SHAPE_CAPSULE,
- PhysicsServer2D::SHAPE_RECTANGLE,
- PhysicsServer2D::SHAPE_CONVEX_POLYGON,
-
- };
-
- PhysicsServer2D::ShapeType type = types[i % 4];
- //type=PhysicsServer2D::SHAPE_SEGMENT;
- _add_body(type, Transform2D(i * 0.8, Point2(152 + i * 40, 100 - 40 * i)));
- /*
- if (i==0)
- ps->body_set_mode(b,PhysicsServer2D::BODY_MODE_STATIC);
- */
- }
-
- //RID b= _add_body(PhysicsServer2D::SHAPE_CIRCLE,Transform2D(0,Point2(101,140)));
- //ps->body_set_mode(b,PhysicsServer2D::BODY_MODE_STATIC);
-
- Point2 prev;
-
- Vector<Point2> parr;
- for (int i = 0; i < 30; i++) {
- Point2 p(i * 60, Math::randf() * 70 + 340);
- if (i > 0) {
- parr.push_back(prev);
- parr.push_back(p);
- }
- prev = p;
- }
-
- _add_concave(parr);
- //_add_plane(Vector2(0.0,-1).normalized(),-300);
- //_add_plane(Vector2(1,0).normalized(),50);
- //_add_plane(Vector2(-1,0).normalized(),-600);
- }
-
- virtual bool idle(float p_time) override {
- return false;
- }
- virtual void finish() override {
- }
-
- TestPhysics2DMainLoop() {}
-};
-
-namespace TestPhysics2D {
-
-MainLoop *test() {
- return memnew(TestPhysics2DMainLoop);
-}
-
-} // namespace TestPhysics2D
diff --git a/tests/test_physics_3d.cpp b/tests/test_physics_3d.cpp
deleted file mode 100644
index 72de2041e4..0000000000
--- a/tests/test_physics_3d.cpp
+++ /dev/null
@@ -1,413 +0,0 @@
-/*************************************************************************/
-/* test_physics_3d.cpp */
-/*************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
-/* */
-/* 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. */
-/*************************************************************************/
-
-#include "test_physics_3d.h"
-
-#include "core/map.h"
-#include "core/math/math_funcs.h"
-#include "core/math/quick_hull.h"
-#include "core/os/main_loop.h"
-#include "core/os/os.h"
-#include "core/print_string.h"
-#include "servers/display_server.h"
-#include "servers/physics_server_3d.h"
-#include "servers/rendering_server.h"
-
-class TestPhysics3DMainLoop : public MainLoop {
- GDCLASS(TestPhysics3DMainLoop, MainLoop);
-
- enum {
- LINK_COUNT = 20,
- };
-
- RID test_cube;
-
- RID plane;
- RID sphere;
- RID light;
- RID camera;
- RID mover;
- RID scenario;
- RID space;
-
- RID character;
-
- float ofs_x, ofs_y;
-
- Point2 joy_direction;
-
- List<RID> bodies;
- Map<PhysicsServer3D::ShapeType, RID> type_shape_map;
- Map<PhysicsServer3D::ShapeType, RID> type_mesh_map;
-
- void body_changed_transform(Object *p_state, RID p_visual_instance) {
- PhysicsDirectBodyState3D *state = (PhysicsDirectBodyState3D *)p_state;
- RenderingServer *vs = RenderingServer::get_singleton();
- Transform t = state->get_transform();
- vs->instance_set_transform(p_visual_instance, t);
- }
-
- bool quit;
-
-protected:
- static void _bind_methods() {
- ClassDB::bind_method("body_changed_transform", &TestPhysics3DMainLoop::body_changed_transform);
- }
-
- RID create_body(PhysicsServer3D::ShapeType p_shape, PhysicsServer3D::BodyMode p_body, const Transform p_location, bool p_active_default = true, const Transform &p_shape_xform = Transform()) {
- RenderingServer *vs = RenderingServer::get_singleton();
- PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
-
- RID mesh_instance = vs->instance_create2(type_mesh_map[p_shape], scenario);
- RID body = ps->body_create(p_body, !p_active_default);
- ps->body_set_space(body, space);
- ps->body_set_param(body, PhysicsServer3D::BODY_PARAM_BOUNCE, 0.0);
- //todo set space
- ps->body_add_shape(body, type_shape_map[p_shape]);
- ps->body_set_force_integration_callback(body, this, "body_changed_transform", mesh_instance);
-
- ps->body_set_state(body, PhysicsServer3D::BODY_STATE_TRANSFORM, p_location);
- bodies.push_back(body);
-
- if (p_body == PhysicsServer3D::BODY_MODE_STATIC) {
- vs->instance_set_transform(mesh_instance, p_location);
- }
- return body;
- }
-
- RID create_static_plane(const Plane &p_plane) {
- PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
-
- RID world_margin_shape = ps->shape_create(PhysicsServer3D::SHAPE_PLANE);
- ps->shape_set_data(world_margin_shape, p_plane);
-
- RID b = ps->body_create(PhysicsServer3D::BODY_MODE_STATIC);
- ps->body_set_space(b, space);
- //todo set space
- ps->body_add_shape(b, world_margin_shape);
- return b;
- }
-
- void configure_body(RID p_body, float p_mass, float p_friction, float p_bounce) {
- PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
- ps->body_set_param(p_body, PhysicsServer3D::BODY_PARAM_MASS, p_mass);
- ps->body_set_param(p_body, PhysicsServer3D::BODY_PARAM_FRICTION, p_friction);
- ps->body_set_param(p_body, PhysicsServer3D::BODY_PARAM_BOUNCE, p_bounce);
- }
-
- void init_shapes() {
- RenderingServer *vs = RenderingServer::get_singleton();
- PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
-
- /* SPHERE SHAPE */
- RID sphere_mesh = vs->make_sphere_mesh(10, 20, 0.5);
- type_mesh_map[PhysicsServer3D::SHAPE_SPHERE] = sphere_mesh;
-
- RID sphere_shape = ps->shape_create(PhysicsServer3D::SHAPE_SPHERE);
- ps->shape_set_data(sphere_shape, 0.5);
- type_shape_map[PhysicsServer3D::SHAPE_SPHERE] = sphere_shape;
-
- /* BOX SHAPE */
-
- Vector<Plane> box_planes = Geometry3D::build_box_planes(Vector3(0.5, 0.5, 0.5));
- RID box_mesh = vs->mesh_create();
- Geometry3D::MeshData box_data = Geometry3D::build_convex_mesh(box_planes);
- vs->mesh_add_surface_from_mesh_data(box_mesh, box_data);
- type_mesh_map[PhysicsServer3D::SHAPE_BOX] = box_mesh;
-
- RID box_shape = ps->shape_create(PhysicsServer3D::SHAPE_BOX);
- ps->shape_set_data(box_shape, Vector3(0.5, 0.5, 0.5));
- type_shape_map[PhysicsServer3D::SHAPE_BOX] = box_shape;
-
- /* CAPSULE SHAPE */
-
- Vector<Plane> capsule_planes = Geometry3D::build_capsule_planes(0.5, 0.7, 12, Vector3::AXIS_Z);
-
- RID capsule_mesh = vs->mesh_create();
- Geometry3D::MeshData capsule_data = Geometry3D::build_convex_mesh(capsule_planes);
- vs->mesh_add_surface_from_mesh_data(capsule_mesh, capsule_data);
-
- type_mesh_map[PhysicsServer3D::SHAPE_CAPSULE] = capsule_mesh;
-
- RID capsule_shape = ps->shape_create(PhysicsServer3D::SHAPE_CAPSULE);
- Dictionary capsule_params;
- capsule_params["radius"] = 0.5;
- capsule_params["height"] = 1.4;
- ps->shape_set_data(capsule_shape, capsule_params);
- type_shape_map[PhysicsServer3D::SHAPE_CAPSULE] = capsule_shape;
-
- /* CONVEX SHAPE */
-
- Vector<Plane> convex_planes = Geometry3D::build_cylinder_planes(0.5, 0.7, 5, Vector3::AXIS_Z);
-
- RID convex_mesh = vs->mesh_create();
- Geometry3D::MeshData convex_data = Geometry3D::build_convex_mesh(convex_planes);
- QuickHull::build(convex_data.vertices, convex_data);
- vs->mesh_add_surface_from_mesh_data(convex_mesh, convex_data);
-
- type_mesh_map[PhysicsServer3D::SHAPE_CONVEX_POLYGON] = convex_mesh;
-
- RID convex_shape = ps->shape_create(PhysicsServer3D::SHAPE_CONVEX_POLYGON);
- ps->shape_set_data(convex_shape, convex_data.vertices);
- type_shape_map[PhysicsServer3D::SHAPE_CONVEX_POLYGON] = convex_shape;
- }
-
- void make_trimesh(Vector<Vector3> p_faces, const Transform &p_xform = Transform()) {
- RenderingServer *vs = RenderingServer::get_singleton();
- PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
- RID trimesh_shape = ps->shape_create(PhysicsServer3D::SHAPE_CONCAVE_POLYGON);
- ps->shape_set_data(trimesh_shape, p_faces);
- p_faces = ps->shape_get_data(trimesh_shape); // optimized one
- Vector<Vector3> normals; // for drawing
- for (int i = 0; i < p_faces.size() / 3; i++) {
- Plane p(p_faces[i * 3 + 0], p_faces[i * 3 + 1], p_faces[i * 3 + 2]);
- normals.push_back(p.normal);
- normals.push_back(p.normal);
- normals.push_back(p.normal);
- }
-
- RID trimesh_mesh = vs->mesh_create();
- Array d;
- d.resize(RS::ARRAY_MAX);
- d[RS::ARRAY_VERTEX] = p_faces;
- d[RS::ARRAY_NORMAL] = normals;
- vs->mesh_add_surface_from_arrays(trimesh_mesh, RS::PRIMITIVE_TRIANGLES, d);
-
- RID triins = vs->instance_create2(trimesh_mesh, scenario);
-
- RID tribody = ps->body_create(PhysicsServer3D::BODY_MODE_STATIC);
- ps->body_set_space(tribody, space);
- //todo set space
- ps->body_add_shape(tribody, trimesh_shape);
- Transform tritrans = p_xform;
- ps->body_set_state(tribody, PhysicsServer3D::BODY_STATE_TRANSFORM, tritrans);
- vs->instance_set_transform(triins, tritrans);
- }
-
- void make_grid(int p_width, int p_height, float p_cellsize, float p_cellheight, const Transform &p_xform = Transform()) {
- Vector<Vector<float>> grid;
-
- grid.resize(p_width);
-
- for (int i = 0; i < p_width; i++) {
- grid.write[i].resize(p_height);
-
- for (int j = 0; j < p_height; j++) {
- grid.write[i].write[j] = 1.0 + Math::random(-p_cellheight, p_cellheight);
- }
- }
-
- Vector<Vector3> faces;
-
- for (int i = 1; i < p_width; i++) {
- for (int j = 1; j < p_height; j++) {
-#define MAKE_VERTEX(m_x, m_z) \
- faces.push_back(Vector3((m_x - p_width / 2) * p_cellsize, grid[m_x][m_z], (m_z - p_height / 2) * p_cellsize))
-
- MAKE_VERTEX(i, j - 1);
- MAKE_VERTEX(i, j);
- MAKE_VERTEX(i - 1, j);
-
- MAKE_VERTEX(i - 1, j - 1);
- MAKE_VERTEX(i, j - 1);
- MAKE_VERTEX(i - 1, j);
- }
- }
-
- make_trimesh(faces, p_xform);
- }
-
-public:
- virtual void input_event(const Ref<InputEvent> &p_event) {
- Ref<InputEventMouseMotion> mm = p_event;
- if (mm.is_valid() && mm->get_button_mask() & 4) {
- ofs_y -= mm->get_relative().y / 200.0;
- ofs_x += mm->get_relative().x / 200.0;
- }
-
- if (mm.is_valid() && mm->get_button_mask() & 1) {
- float y = -mm->get_relative().y / 20.0;
- float x = mm->get_relative().x / 20.0;
-
- if (mover.is_valid()) {
- PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
- Transform t = ps->body_get_state(mover, PhysicsServer3D::BODY_STATE_TRANSFORM);
- t.origin += Vector3(x, y, 0);
-
- ps->body_set_state(mover, PhysicsServer3D::BODY_STATE_TRANSFORM, t);
- }
- }
- }
-
- virtual void request_quit() {
- quit = true;
- }
- virtual void init() override {
- ofs_x = ofs_y = 0;
- init_shapes();
-
- PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
- space = ps->space_create();
- ps->space_set_active(space, true);
-
- RenderingServer *vs = RenderingServer::get_singleton();
-
- /* LIGHT */
- RID lightaux = vs->directional_light_create();
- scenario = vs->scenario_create();
- vs->light_set_shadow(lightaux, true);
- light = vs->instance_create2(lightaux, scenario);
- Transform t;
- t.rotate(Vector3(1.0, 0, 0), 0.6);
- vs->instance_set_transform(light, t);
-
- /* CAMERA */
-
- camera = vs->camera_create();
-
- RID viewport = vs->viewport_create();
- Size2i screen_size = DisplayServer::get_singleton()->window_get_size();
- vs->viewport_set_size(viewport, screen_size.x, screen_size.y);
- vs->viewport_attach_to_screen(viewport, Rect2(Vector2(), screen_size));
- vs->viewport_set_active(viewport, true);
- vs->viewport_attach_camera(viewport, camera);
- vs->viewport_set_scenario(viewport, scenario);
-
- vs->camera_set_perspective(camera, 60, 0.1, 40.0);
- vs->camera_set_transform(camera, Transform(Basis(), Vector3(0, 9, 12)));
-
- Transform gxf;
- gxf.basis.scale(Vector3(1.4, 0.4, 1.4));
- gxf.origin = Vector3(-2, 1, -2);
- make_grid(5, 5, 2.5, 1, gxf);
- test_fall();
- quit = false;
- }
- virtual bool iteration(float p_time) override {
- if (mover.is_valid()) {
- static float joy_speed = 10;
- PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
- Transform t = ps->body_get_state(mover, PhysicsServer3D::BODY_STATE_TRANSFORM);
- t.origin += Vector3(joy_speed * joy_direction.x * p_time, -joy_speed * joy_direction.y * p_time, 0);
- ps->body_set_state(mover, PhysicsServer3D::BODY_STATE_TRANSFORM, t);
- };
-
- Transform cameratr;
- cameratr.rotate(Vector3(0, 1, 0), ofs_x);
- cameratr.rotate(Vector3(1, 0, 0), -ofs_y);
- cameratr.translate(Vector3(0, 2, 8));
- RenderingServer *vs = RenderingServer::get_singleton();
- vs->camera_set_transform(camera, cameratr);
-
- return quit;
- }
- virtual void finish() override {
- }
-
- void test_joint() {
- }
-
- void test_hinge() {
- }
-
- void test_character() {
- RenderingServer *vs = RenderingServer::get_singleton();
- PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
-
- Vector<Plane> capsule_planes = Geometry3D::build_capsule_planes(0.5, 1, 12, 5, Vector3::AXIS_Y);
-
- RID capsule_mesh = vs->mesh_create();
- Geometry3D::MeshData capsule_data = Geometry3D::build_convex_mesh(capsule_planes);
- vs->mesh_add_surface_from_mesh_data(capsule_mesh, capsule_data);
- type_mesh_map[PhysicsServer3D::SHAPE_CAPSULE] = capsule_mesh;
-
- RID capsule_shape = ps->shape_create(PhysicsServer3D::SHAPE_CAPSULE);
- Dictionary capsule_params;
- capsule_params["radius"] = 0.5;
- capsule_params["height"] = 1;
- Transform shape_xform;
- shape_xform.rotate(Vector3(1, 0, 0), Math_PI / 2.0);
- //shape_xform.origin=Vector3(1,1,1);
- ps->shape_set_data(capsule_shape, capsule_params);
-
- RID mesh_instance = vs->instance_create2(capsule_mesh, scenario);
- character = ps->body_create(PhysicsServer3D::BODY_MODE_CHARACTER);
- ps->body_set_space(character, space);
- //todo add space
- ps->body_add_shape(character, capsule_shape);
-
- ps->body_set_force_integration_callback(character, this, "body_changed_transform", mesh_instance);
-
- ps->body_set_state(character, PhysicsServer3D::BODY_STATE_TRANSFORM, Transform(Basis(), Vector3(-2, 5, -2)));
- bodies.push_back(character);
- }
-
- void test_fall() {
- for (int i = 0; i < 35; i++) {
- static const PhysicsServer3D::ShapeType shape_idx[] = {
- PhysicsServer3D::SHAPE_CAPSULE,
- PhysicsServer3D::SHAPE_BOX,
- PhysicsServer3D::SHAPE_SPHERE,
- PhysicsServer3D::SHAPE_CONVEX_POLYGON
- };
-
- PhysicsServer3D::ShapeType type = shape_idx[i % 4];
-
- Transform t;
-
- t.origin = Vector3(0.0 * i, 3.5 + 1.1 * i, 0.7 + 0.0 * i);
- t.basis.rotate(Vector3(0.2, -1, 0), Math_PI / 2 * 0.6);
-
- create_body(type, PhysicsServer3D::BODY_MODE_RIGID, t);
- }
-
- create_static_plane(Plane(Vector3(0, 1, 0), -1));
- }
-
- void test_activate() {
- create_body(PhysicsServer3D::SHAPE_BOX, PhysicsServer3D::BODY_MODE_RIGID, Transform(Basis(), Vector3(0, 2, 0)), true);
- create_static_plane(Plane(Vector3(0, 1, 0), -1));
- }
-
- virtual bool idle(float p_time) override {
- return false;
- }
-
- TestPhysics3DMainLoop() {
- }
-};
-
-namespace TestPhysics3D {
-
-MainLoop *test() {
- return memnew(TestPhysics3DMainLoop);
-}
-
-} // namespace TestPhysics3D
diff --git a/tests/test_physics_3d.h b/tests/test_physics_3d.h
deleted file mode 100644
index d03f2c6573..0000000000
--- a/tests/test_physics_3d.h
+++ /dev/null
@@ -1,41 +0,0 @@
-/*************************************************************************/
-/* test_physics_3d.h */
-/*************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
-/* */
-/* 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. */
-/*************************************************************************/
-
-#ifndef TEST_PHYSICS_H
-#define TEST_PHYSICS_H
-
-#include "core/os/main_loop.h"
-
-namespace TestPhysics3D {
-
-MainLoop *test();
-}
-
-#endif
diff --git a/tests/test_render.cpp b/tests/test_render.cpp
deleted file mode 100644
index d936dd72e7..0000000000
--- a/tests/test_render.cpp
+++ /dev/null
@@ -1,240 +0,0 @@
-/*************************************************************************/
-/* test_render.cpp */
-/*************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
-/* */
-/* 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. */
-/*************************************************************************/
-
-#include "test_render.h"
-
-#include "core/math/math_funcs.h"
-#include "core/math/quick_hull.h"
-#include "core/os/keyboard.h"
-#include "core/os/main_loop.h"
-#include "core/os/os.h"
-#include "core/print_string.h"
-#include "servers/display_server.h"
-#include "servers/rendering_server.h"
-
-#define OBJECT_COUNT 50
-
-namespace TestRender {
-
-class TestMainLoop : public MainLoop {
- RID test_cube;
- RID instance;
- RID camera;
- RID viewport;
- RID light;
- RID scenario;
-
- struct InstanceInfo {
- RID instance;
- Transform base;
- Vector3 rot_axis;
- };
-
- List<InstanceInfo> instances;
-
- float ofs;
- bool quit;
-
-protected:
-public:
- virtual void input_event(const Ref<InputEvent> &p_event) {
- if (p_event->is_pressed()) {
- quit = true;
- }
- }
-
- virtual void init() {
- print_line("INITIALIZING TEST RENDER");
- RenderingServer *vs = RenderingServer::get_singleton();
- test_cube = vs->get_test_cube();
- scenario = vs->scenario_create();
-
- Vector<Vector3> vts;
-
- /*
- Vector<Plane> sp = Geometry3D::build_sphere_planes(2,5,5);
- Geometry3D::MeshData md2 = Geometry3D::build_convex_mesh(sp);
- vts=md2.vertices;
-*/
- /*
-
- static const int s = 20;
- for(int i=0;i<s;i++) {
- Basis rot(Vector3(0,1,0),i*Math_PI/s);
-
- for(int j=0;j<s;j++) {
- Vector3 v;
- v.x=Math::sin(j*Math_PI*2/s);
- v.y=Math::cos(j*Math_PI*2/s);
-
- vts.push_back( rot.xform(v*2 ) );
- }
- }*/
- /*for(int i=0;i<100;i++) {
-
- vts.push_back( Vector3(Math::randf()*2-1.0,Math::randf()*2-1.0,Math::randf()*2-1.0).normalized()*2);
- }*/
- /*
- vts.push_back(Vector3(0,0,1));
- vts.push_back(Vector3(0,0,-1));
- vts.push_back(Vector3(0,1,0));
- vts.push_back(Vector3(0,-1,0));
- vts.push_back(Vector3(1,0,0));
- vts.push_back(Vector3(-1,0,0));*/
-
- vts.push_back(Vector3(1, 1, 1));
- vts.push_back(Vector3(1, -1, 1));
- vts.push_back(Vector3(-1, 1, 1));
- vts.push_back(Vector3(-1, -1, 1));
- vts.push_back(Vector3(1, 1, -1));
- vts.push_back(Vector3(1, -1, -1));
- vts.push_back(Vector3(-1, 1, -1));
- vts.push_back(Vector3(-1, -1, -1));
-
- Geometry3D::MeshData md;
- Error err = QuickHull::build(vts, md);
- print_line("ERR: " + itos(err));
- test_cube = vs->mesh_create();
- vs->mesh_add_surface_from_mesh_data(test_cube, md);
- //vs->scenario_set_debug(scenario,RS::SCENARIO_DEBUG_WIREFRAME);
-
- /*
- RID sm = vs->shader_create();
- //vs->shader_set_fragment_code(sm,"OUT_ALPHA=mod(TIME,1);");
- //vs->shader_set_vertex_code(sm,"OUT_VERTEX=IN_VERTEX*mod(TIME,1);");
- vs->shader_set_fragment_code(sm,"OUT_DIFFUSE=vec3(1,0,1);OUT_GLOW=abs(sin(TIME));");
- RID tcmat = vs->mesh_surface_get_material(test_cube,0);
- vs->material_set_shader(tcmat,sm);
- */
-
- List<String> cmdline = OS::get_singleton()->get_cmdline_args();
- int object_count = OBJECT_COUNT;
- if (cmdline.size() > 0 && cmdline[cmdline.size() - 1].to_int()) {
- object_count = cmdline[cmdline.size() - 1].to_int();
- };
-
- for (int i = 0; i < object_count; i++) {
- InstanceInfo ii;
-
- ii.instance = vs->instance_create2(test_cube, scenario);
-
- ii.base.translate(Math::random(-20, 20), Math::random(-20, 20), Math::random(-20, 18));
- ii.base.rotate(Vector3(0, 1, 0), Math::randf() * Math_PI);
- ii.base.rotate(Vector3(1, 0, 0), Math::randf() * Math_PI);
- vs->instance_set_transform(ii.instance, ii.base);
-
- ii.rot_axis = Vector3(Math::random(-1, 1), Math::random(-1, 1), Math::random(-1, 1)).normalized();
-
- instances.push_back(ii);
- }
-
- camera = vs->camera_create();
-
- // vs->camera_set_perspective( camera, 60.0,0.1, 100.0 );
-
- viewport = vs->viewport_create();
- Size2i screen_size = DisplayServer::get_singleton()->window_get_size();
- vs->viewport_set_size(viewport, screen_size.x, screen_size.y);
- vs->viewport_attach_to_screen(viewport, Rect2(Vector2(), screen_size));
- vs->viewport_set_active(viewport, true);
- vs->viewport_attach_camera(viewport, camera);
- vs->viewport_set_scenario(viewport, scenario);
- vs->camera_set_transform(camera, Transform(Basis(), Vector3(0, 3, 30)));
- vs->camera_set_perspective(camera, 60, 0.1, 1000);
-
- /*
- RID lightaux = vs->light_create( RenderingServer::LIGHT_OMNI );
- vs->light_set_var( lightaux, RenderingServer::LIGHT_VAR_RADIUS, 80 );
- vs->light_set_var( lightaux, RenderingServer::LIGHT_VAR_ATTENUATION, 1 );
- vs->light_set_var( lightaux, RenderingServer::LIGHT_VAR_ENERGY, 1.5 );
- light = vs->instance_create( lightaux );
- */
- RID lightaux;
-
- lightaux = vs->directional_light_create();
- //vs->light_set_color( lightaux, RenderingServer::LIGHT_COLOR_AMBIENT, Color(0.0,0.0,0.0) );
- vs->light_set_color(lightaux, Color(1.0, 1.0, 1.0));
- //vs->light_set_shadow( lightaux, true );
- light = vs->instance_create2(lightaux, scenario);
- Transform lla;
- //lla.set_look_at(Vector3(),Vector3(1,-1,1),Vector3(0,1,0));
- lla.set_look_at(Vector3(), Vector3(-0.000000, -0.836026, -0.548690), Vector3(0, 1, 0));
-
- vs->instance_set_transform(light, lla);
-
- lightaux = vs->omni_light_create();
- //vs->light_set_color( lightaux, RenderingServer::LIGHT_COLOR_AMBIENT, Color(0.0,0.0,1.0) );
- vs->light_set_color(lightaux, Color(1.0, 1.0, 0.0));
- vs->light_set_param(lightaux, RenderingServer::LIGHT_PARAM_RANGE, 4);
- vs->light_set_param(lightaux, RenderingServer::LIGHT_PARAM_ENERGY, 8);
- //vs->light_set_shadow( lightaux, true );
- //light = vs->instance_create( lightaux );
-
- ofs = 0;
- quit = false;
- }
- virtual bool iteration(float p_time) {
- RenderingServer *vs = RenderingServer::get_singleton();
- //Transform t;
- //t.rotate(Vector3(0, 1, 0), ofs);
- //t.translate(Vector3(0,0,20 ));
- //vs->camera_set_transform(camera, t);
-
- ofs += p_time * 0.05;
-
- //return quit;
-
- for (List<InstanceInfo>::Element *E = instances.front(); E; E = E->next()) {
- Transform pre(Basis(E->get().rot_axis, ofs), Vector3());
- vs->instance_set_transform(E->get().instance, pre * E->get().base);
- /*
- if( !E->next() ) {
-
- vs->free( E->get().instance );
- instances.erase(E );
- }*/
- }
-
- return quit;
- }
-
- virtual bool idle(float p_time) {
- return quit;
- }
-
- virtual void finish() {
- }
-};
-
-MainLoop *test() {
- return memnew(TestMainLoop);
-}
-
-} // namespace TestRender
diff --git a/tests/test_render.h b/tests/test_render.h
deleted file mode 100644
index 4a6340c443..0000000000
--- a/tests/test_render.h
+++ /dev/null
@@ -1,41 +0,0 @@
-/*************************************************************************/
-/* test_render.h */
-/*************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
-/* */
-/* 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. */
-/*************************************************************************/
-
-#ifndef TEST_RENDER_H
-#define TEST_RENDER_H
-
-#include "core/os/main_loop.h"
-
-namespace TestRender {
-
-MainLoop *test();
-}
-
-#endif
diff --git a/tests/test_shader_lang.cpp b/tests/test_shader_lang.cpp
deleted file mode 100644
index d363ee22b5..0000000000
--- a/tests/test_shader_lang.cpp
+++ /dev/null
@@ -1,361 +0,0 @@
-/*************************************************************************/
-/* test_shader_lang.cpp */
-/*************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
-/* */
-/* 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. */
-/*************************************************************************/
-
-#include "test_shader_lang.h"
-
-#include "core/os/file_access.h"
-#include "core/os/main_loop.h"
-#include "core/os/os.h"
-
-#include "core/print_string.h"
-#include "scene/gui/control.h"
-#include "scene/gui/text_edit.h"
-#include "servers/rendering/shader_language.h"
-
-typedef ShaderLanguage SL;
-
-namespace TestShaderLang {
-
-static String _mktab(int p_level) {
- String tb;
- for (int i = 0; i < p_level; i++) {
- tb += "\t";
- }
-
- return tb;
-}
-
-static String _typestr(SL::DataType p_type) {
- return ShaderLanguage::get_datatype_name(p_type);
-}
-
-static String _prestr(SL::DataPrecision p_pres) {
- switch (p_pres) {
- case SL::PRECISION_LOWP:
- return "lowp ";
- case SL::PRECISION_MEDIUMP:
- return "mediump ";
- case SL::PRECISION_HIGHP:
- return "highp ";
- case SL::PRECISION_DEFAULT:
- return "";
- }
- return "";
-}
-
-static String _opstr(SL::Operator p_op) {
- return ShaderLanguage::get_operator_text(p_op);
-}
-
-static String get_constant_text(SL::DataType p_type, const Vector<SL::ConstantNode::Value> &p_values) {
- switch (p_type) {
- case SL::TYPE_BOOL:
- return p_values[0].boolean ? "true" : "false";
- case SL::TYPE_BVEC2:
- return String() + "bvec2(" + (p_values[0].boolean ? "true" : "false") + (p_values[1].boolean ? "true" : "false") + ")";
- case SL::TYPE_BVEC3:
- return String() + "bvec3(" + (p_values[0].boolean ? "true" : "false") + "," + (p_values[1].boolean ? "true" : "false") + "," + (p_values[2].boolean ? "true" : "false") + ")";
- case SL::TYPE_BVEC4:
- return String() + "bvec4(" + (p_values[0].boolean ? "true" : "false") + "," + (p_values[1].boolean ? "true" : "false") + "," + (p_values[2].boolean ? "true" : "false") + "," + (p_values[3].boolean ? "true" : "false") + ")";
- case SL::TYPE_INT:
- return rtos(p_values[0].sint);
- case SL::TYPE_IVEC2:
- return String() + "ivec2(" + rtos(p_values[0].sint) + "," + rtos(p_values[1].sint) + ")";
- case SL::TYPE_IVEC3:
- return String() + "ivec3(" + rtos(p_values[0].sint) + "," + rtos(p_values[1].sint) + "," + rtos(p_values[2].sint) + ")";
- case SL::TYPE_IVEC4:
- return String() + "ivec4(" + rtos(p_values[0].sint) + "," + rtos(p_values[1].sint) + "," + rtos(p_values[2].sint) + "," + rtos(p_values[3].sint) + ")";
- case SL::TYPE_UINT:
- return rtos(p_values[0].real);
- case SL::TYPE_UVEC2:
- return String() + "uvec2(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + ")";
- case SL::TYPE_UVEC3:
- return String() + "uvec3(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + "," + rtos(p_values[2].real) + ")";
- case SL::TYPE_UVEC4:
- return String() + "uvec4(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + "," + rtos(p_values[2].real) + "," + rtos(p_values[3].real) + ")";
- case SL::TYPE_FLOAT:
- return rtos(p_values[0].real);
- case SL::TYPE_VEC2:
- return String() + "vec2(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + ")";
- case SL::TYPE_VEC3:
- return String() + "vec3(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + "," + rtos(p_values[2].real) + ")";
- case SL::TYPE_VEC4:
- return String() + "vec4(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + "," + rtos(p_values[2].real) + "," + rtos(p_values[3].real) + ")";
- default:
- ERR_FAIL_V(String());
- }
-}
-
-static String dump_node_code(SL::Node *p_node, int p_level) {
- String code;
-
- switch (p_node->type) {
- case SL::Node::TYPE_SHADER: {
- SL::ShaderNode *pnode = (SL::ShaderNode *)p_node;
-
- for (Map<StringName, SL::ShaderNode::Uniform>::Element *E = pnode->uniforms.front(); E; E = E->next()) {
- String ucode = "uniform ";
- ucode += _prestr(E->get().precision);
- ucode += _typestr(E->get().type);
- ucode += " " + String(E->key());
-
- if (E->get().default_value.size()) {
- ucode += " = " + get_constant_text(E->get().type, E->get().default_value);
- }
-
- static const char *hint_name[SL::ShaderNode::Uniform::HINT_MAX] = {
- "",
- "color",
- "range",
- "albedo",
- "normal",
- "black",
- "white"
- };
-
- if (E->get().hint) {
- ucode += " : " + String(hint_name[E->get().hint]);
- }
-
- code += ucode + "\n";
- }
-
- for (Map<StringName, SL::ShaderNode::Varying>::Element *E = pnode->varyings.front(); E; E = E->next()) {
- String vcode = "varying ";
- vcode += _prestr(E->get().precision);
- vcode += _typestr(E->get().type);
- vcode += " " + String(E->key());
-
- code += vcode + "\n";
- }
- for (int i = 0; i < pnode->functions.size(); i++) {
- SL::FunctionNode *fnode = pnode->functions[i].function;
-
- String header;
- header = _typestr(fnode->return_type) + " " + fnode->name + "(";
- for (int j = 0; j < fnode->arguments.size(); j++) {
- if (j > 0) {
- header += ", ";
- }
- header += _prestr(fnode->arguments[j].precision) + _typestr(fnode->arguments[j].type) + " " + fnode->arguments[j].name;
- }
-
- header += ")\n";
- code += header;
- code += dump_node_code(fnode->body, p_level + 1);
- }
-
- //code+=dump_node_code(pnode->body,p_level);
- } break;
- case SL::Node::TYPE_STRUCT: {
- } break;
- case SL::Node::TYPE_FUNCTION: {
- } break;
- case SL::Node::TYPE_BLOCK: {
- SL::BlockNode *bnode = (SL::BlockNode *)p_node;
-
- //variables
- code += _mktab(p_level - 1) + "{\n";
- for (Map<StringName, SL::BlockNode::Variable>::Element *E = bnode->variables.front(); E; E = E->next()) {
- code += _mktab(p_level) + _prestr(E->get().precision) + _typestr(E->get().type) + " " + E->key() + ";\n";
- }
-
- for (int i = 0; i < bnode->statements.size(); i++) {
- String scode = dump_node_code(bnode->statements[i], p_level);
-
- if (bnode->statements[i]->type == SL::Node::TYPE_CONTROL_FLOW) {
- code += scode; //use directly
- } else {
- code += _mktab(p_level) + scode + ";\n";
- }
- }
- code += _mktab(p_level - 1) + "}\n";
-
- } break;
- case SL::Node::TYPE_VARIABLE: {
- SL::VariableNode *vnode = (SL::VariableNode *)p_node;
- code = vnode->name;
-
- } break;
- case SL::Node::TYPE_VARIABLE_DECLARATION: {
- // FIXME: Implement
- } break;
- case SL::Node::TYPE_ARRAY: {
- SL::ArrayNode *vnode = (SL::ArrayNode *)p_node;
- code = vnode->name;
- } break;
- case SL::Node::TYPE_ARRAY_DECLARATION: {
- // FIXME: Implement
- } break;
- case SL::Node::TYPE_ARRAY_CONSTRUCT: {
- // FIXME: Implement
- } break;
- case SL::Node::TYPE_CONSTANT: {
- SL::ConstantNode *cnode = (SL::ConstantNode *)p_node;
- return get_constant_text(cnode->datatype, cnode->values);
-
- } break;
- case SL::Node::TYPE_OPERATOR: {
- SL::OperatorNode *onode = (SL::OperatorNode *)p_node;
-
- switch (onode->op) {
- case SL::OP_ASSIGN:
- case SL::OP_ASSIGN_ADD:
- case SL::OP_ASSIGN_SUB:
- case SL::OP_ASSIGN_MUL:
- case SL::OP_ASSIGN_DIV:
- case SL::OP_ASSIGN_SHIFT_LEFT:
- case SL::OP_ASSIGN_SHIFT_RIGHT:
- case SL::OP_ASSIGN_MOD:
- case SL::OP_ASSIGN_BIT_AND:
- case SL::OP_ASSIGN_BIT_OR:
- case SL::OP_ASSIGN_BIT_XOR:
- code = dump_node_code(onode->arguments[0], p_level) + _opstr(onode->op) + dump_node_code(onode->arguments[1], p_level);
- break;
- case SL::OP_BIT_INVERT:
- case SL::OP_NEGATE:
- case SL::OP_NOT:
- case SL::OP_DECREMENT:
- case SL::OP_INCREMENT:
- code = _opstr(onode->op) + dump_node_code(onode->arguments[0], p_level);
- break;
- case SL::OP_POST_DECREMENT:
- case SL::OP_POST_INCREMENT:
- code = dump_node_code(onode->arguments[0], p_level) + _opstr(onode->op);
- break;
- case SL::OP_CALL:
- case SL::OP_CONSTRUCT:
- code = dump_node_code(onode->arguments[0], p_level) + "(";
- for (int i = 1; i < onode->arguments.size(); i++) {
- if (i > 1) {
- code += ", ";
- }
- code += dump_node_code(onode->arguments[i], p_level);
- }
- code += ")";
- break;
- default: {
- code = "(" + dump_node_code(onode->arguments[0], p_level) + _opstr(onode->op) + dump_node_code(onode->arguments[1], p_level) + ")";
- break;
- }
- }
-
- } break;
- case SL::Node::TYPE_CONTROL_FLOW: {
- SL::ControlFlowNode *cfnode = (SL::ControlFlowNode *)p_node;
- if (cfnode->flow_op == SL::FLOW_OP_IF) {
- code += _mktab(p_level) + "if (" + dump_node_code(cfnode->expressions[0], p_level) + ")\n";
- code += dump_node_code(cfnode->blocks[0], p_level + 1);
- if (cfnode->blocks.size() == 2) {
- code += _mktab(p_level) + "else\n";
- code += dump_node_code(cfnode->blocks[1], p_level + 1);
- }
-
- } else if (cfnode->flow_op == SL::FLOW_OP_RETURN) {
- if (cfnode->blocks.size()) {
- code = "return " + dump_node_code(cfnode->blocks[0], p_level);
- } else {
- code = "return";
- }
- }
-
- } break;
- case SL::Node::TYPE_MEMBER: {
- SL::MemberNode *mnode = (SL::MemberNode *)p_node;
- code = dump_node_code(mnode->owner, p_level) + "." + mnode->name;
-
- } break;
- }
-
- return code;
-}
-
-static Error recreate_code(void *p_str, SL::ShaderNode *p_program) {
- String *str = (String *)p_str;
-
- *str = dump_node_code(p_program, 0);
-
- return OK;
-}
-
-MainLoop *test() {
- List<String> cmdlargs = OS::get_singleton()->get_cmdline_args();
-
- if (cmdlargs.empty()) {
- //try editor!
- print_line("usage: godot -test shader_lang <shader>");
- return nullptr;
- }
-
- String test = cmdlargs.back()->get();
-
- FileAccess *fa = FileAccess::open(test, FileAccess::READ);
-
- if (!fa) {
- ERR_FAIL_V(nullptr);
- }
-
- String code;
-
- while (true) {
- char32_t c = fa->get_8();
- if (fa->eof_reached()) {
- break;
- }
- code += c;
- }
-
- SL sl;
- print_line("tokens:\n\n" + sl.token_debug(code));
-
- Map<StringName, SL::FunctionInfo> dt;
- dt["fragment"].built_ins["ALBEDO"] = SL::TYPE_VEC3;
- dt["fragment"].can_discard = true;
-
- Vector<StringName> rm;
- rm.push_back("popo");
- Set<String> types;
- types.insert("spatial");
-
- Error err = sl.compile(code, dt, rm, types, nullptr);
-
- if (err) {
- print_line("Error at line: " + rtos(sl.get_error_line()) + ": " + sl.get_error_text());
- return nullptr;
- } else {
- String code2;
- recreate_code(&code2, sl.get_shader());
- print_line("code:\n\n" + code2);
- }
-
- return nullptr;
-}
-
-} // namespace TestShaderLang
diff --git a/tests/test_shader_lang.h b/tests/test_shader_lang.h
deleted file mode 100644
index 2811c5f46e..0000000000
--- a/tests/test_shader_lang.h
+++ /dev/null
@@ -1,41 +0,0 @@
-/*************************************************************************/
-/* test_shader_lang.h */
-/*************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
-/* */
-/* 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. */
-/*************************************************************************/
-
-#ifndef TEST_SHADER_LANG_H
-#define TEST_SHADER_LANG_H
-
-#include "core/os/main_loop.h"
-
-namespace TestShaderLang {
-
-MainLoop *test();
-}
-
-#endif // TEST_SHADER_LANG_H
diff --git a/tests/test_oa_hash_map.h b/tests/test_tools.h
index eb2b3d1e99..8ee7a4718f 100644
--- a/tests/test_oa_hash_map.h
+++ b/tests/test_tools.h
@@ -1,12 +1,12 @@
/*************************************************************************/
-/* test_oa_hash_map.h */
+/* test_tools.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -28,14 +28,32 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-#ifndef TEST_OA_HASH_MAP_H
-#define TEST_OA_HASH_MAP_H
+#ifndef TEST_TOOLS_H
+#define TEST_TOOLS_H
-#include "core/os/main_loop.h"
+struct ErrorDetector {
+ ErrorDetector() {
+ eh.errfunc = _detect_error;
+ eh.userdata = this;
-namespace TestOAHashMap {
+ add_error_handler(&eh);
+ }
-MainLoop *test();
-}
+ ~ErrorDetector() {
+ remove_error_handler(&eh);
+ }
-#endif // TEST_OA_HASH_MAP_H
+ void clear() {
+ has_error = false;
+ }
+
+ static void _detect_error(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type) {
+ ErrorDetector *self = (ErrorDetector *)p_self;
+ self->has_error = true;
+ }
+
+ ErrorHandlerList eh;
+ bool has_error = false;
+};
+
+#endif // TEST_TOOLS_H
diff --git a/tests/test_math.h b/tests/test_utils.cpp
index 77bce8dd66..ce12a30381 100644
--- a/tests/test_math.h
+++ b/tests/test_utils.cpp
@@ -1,12 +1,12 @@
/*************************************************************************/
-/* test_math.h */
+/* test_utils.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -28,14 +28,15 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-#ifndef TEST_MATH_H
-#define TEST_MATH_H
+#include "tests/test_utils.h"
-#include "core/os/main_loop.h"
+#include "core/os/os.h"
-namespace TestMath {
-
-MainLoop *test();
+String TestUtils::get_data_path(const String &p_file) {
+ String data_path = "../tests/data";
+ return get_executable_dir().path_join(data_path.path_join(p_file));
}
-#endif
+String TestUtils::get_executable_dir() {
+ return OS::get_singleton()->get_executable_path().get_base_dir();
+}
diff --git a/tests/test_gui.h b/tests/test_utils.h
index 5a23179eee..499ddb84b2 100644
--- a/tests/test_gui.h
+++ b/tests/test_utils.h
@@ -1,12 +1,12 @@
/*************************************************************************/
-/* test_gui.h */
+/* test_utils.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -28,14 +28,15 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-#ifndef TEST_GUI_H
-#define TEST_GUI_H
+#ifndef TEST_UTILS_H
+#define TEST_UTILS_H
-#include "core/os/main_loop.h"
+class String;
-namespace TestGUI {
+namespace TestUtils {
-MainLoop *test();
-}
+String get_data_path(const String &p_file);
+String get_executable_dir();
+} // namespace TestUtils
-#endif
+#endif // TEST_UTILS_H
diff --git a/tests/test_validate_testing.h b/tests/test_validate_testing.h
index b4ea6eb576..1471a952cd 100644
--- a/tests/test_validate_testing.h
+++ b/tests/test_validate_testing.h
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
@@ -31,9 +31,11 @@
#ifndef TEST_VALIDATE_TESTING_H
#define TEST_VALIDATE_TESTING_H
+#include "core/core_globals.h"
#include "core/os/os.h"
#include "tests/test_macros.h"
+#include "tests/test_tools.h"
TEST_SUITE("Validate tests") {
TEST_CASE("Always pass") {
@@ -48,10 +50,10 @@ TEST_SUITE("Validate tests") {
}
TEST_CASE("Muting Godot error messages") {
ERR_PRINT_OFF;
- CHECK_MESSAGE(!_print_error_enabled, "Error printing should be disabled.");
+ CHECK_MESSAGE(!CoreGlobals::print_error_enabled, "Error printing should be disabled.");
ERR_PRINT("Still waiting for Godot!"); // This should never get printed!
ERR_PRINT_ON;
- CHECK_MESSAGE(_print_error_enabled, "Error printing should be re-enabled.");
+ CHECK_MESSAGE(CoreGlobals::print_error_enabled, "Error printing should be re-enabled.");
}
TEST_CASE("Stringify Variant types") {
Variant var;
@@ -84,7 +86,7 @@ TEST_SUITE("Validate tests") {
Plane plane(Vector3(1, 1, 1), 1.0);
INFO(plane);
- Quat quat(Vector3(0.5, 1.0, 2.0));
+ Quaternion quat(Vector3(0.5, 1.0, 2.0));
INFO(quat);
AABB aabb(Vector3(), Vector3(100, 100, 100));
@@ -93,7 +95,7 @@ TEST_SUITE("Validate tests") {
Basis basis(quat);
INFO(basis);
- Transform trans(basis);
+ Transform3D trans(basis);
INFO(trans);
Color color(1, 0.5, 0.2, 0.3);
@@ -179,10 +181,19 @@ TEST_SUITE("Validate tests") {
color_arr.push_back(Color(2, 2, 2));
INFO(color_arr);
- INFO("doctest insertion operator << "
- << var << " " << vec2 << " " << rect2 << " " << color);
+ // doctest string concatenation.
+ CHECK_MESSAGE(true, var, " ", vec2, " ", rect2, " ", color);
+ }
+ TEST_CASE("Detect error messages") {
+ ErrorDetector ed;
+
+ REQUIRE_FALSE(ed.has_error);
+
+ ERR_PRINT_OFF;
+ ERR_PRINT("Still waiting for Godot!");
+ ERR_PRINT_ON;
- CHECK(true); // So all above prints.
+ REQUIRE(ed.has_error);
}
}
diff --git a/tests/test_variant.h b/tests/test_variant.h
deleted file mode 100644
index a384a3e91f..0000000000
--- a/tests/test_variant.h
+++ /dev/null
@@ -1,111 +0,0 @@
-/*************************************************************************/
-/* test_variant.h */
-/*************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
-/* */
-/* 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. */
-/*************************************************************************/
-
-#ifndef TEST_VARIANT_H
-#define TEST_VARIANT_H
-
-#include "core/variant.h"
-#include "core/variant_parser.h"
-
-#include "tests/test_macros.h"
-
-namespace TestVariant {
-
-TEST_CASE("[Variant] Writer and parser integer") {
- int64_t a32 = 2147483648; // 2^31, so out of bounds for 32-bit signed int [-2^31,-2^31-1].
- String a32_str;
- VariantWriter::write_to_string(a32, a32_str);
-
- CHECK_MESSAGE(a32_str != "-2147483648", "Should not wrap around");
-
- int64_t b64 = 9223372036854775807; // 2^63-1, upper bound for signed 64-bit int.
- String b64_str;
- VariantWriter::write_to_string(b64, b64_str);
-
- CHECK_MESSAGE(b64_str == "9223372036854775807", "Should not wrap around.");
-
- VariantParser::StreamString ss;
- String errs;
- int line;
- Variant b64_parsed;
- int64_t b64_int_parsed;
-
- ss.s = b64_str;
- VariantParser::parse(&ss, b64_parsed, errs, line);
- b64_int_parsed = b64_parsed;
-
- CHECK_MESSAGE(b64_int_parsed == 9223372036854775807, "Should parse back.");
-
- ss.s = "9223372036854775808"; // Overflowed by one.
- VariantParser::parse(&ss, b64_parsed, errs, line);
- b64_int_parsed = b64_parsed;
-
- CHECK_MESSAGE(b64_int_parsed == 9223372036854775807, "The result should be clamped to max value.");
-
- ss.s = "1e100"; // Googol! Scientific notation.
- VariantParser::parse(&ss, b64_parsed, errs, line);
- b64_int_parsed = b64_parsed;
-
- CHECK_MESSAGE(b64_int_parsed == 9223372036854775807, "The result should be clamped to max value.");
-}
-
-TEST_CASE("[Variant] Writer and parser float") {
- // Assuming real_t is double.
- real_t a64 = 340282346638528859811704183484516925440.0; // std::numeric_limits<real_t>::max()
- String a64_str;
- VariantWriter::write_to_string(a64, a64_str);
-
- CHECK_MESSAGE(a64_str == "3.40282e+38", "Writes in scientific notation.");
- CHECK_MESSAGE(a64_str != "inf", "Should not overflow.");
- CHECK_MESSAGE(a64_str != "nan", "The result should be defined.");
-
- VariantParser::StreamString ss;
- String errs;
- int line;
- Variant b64_parsed;
- real_t b64_float_parsed;
-
- ss.s = a64_str;
- VariantParser::parse(&ss, b64_parsed, errs, line);
- b64_float_parsed = b64_parsed;
-
- CHECK_MESSAGE(b64_float_parsed == 340282001837565597733306976381245063168.0, "Should parse back.");
- // Loses precision, but that's alright.
-
- ss.s = "1.0e+100"; // Float version of Googol!
- VariantParser::parse(&ss, b64_parsed, errs, line);
- b64_float_parsed = b64_parsed;
-
- CHECK_MESSAGE(b64_float_parsed == 340282001837565597733306976381245063168.0, "Should not overflow.");
-}
-
-} // namespace TestVariant
-
-#endif // TEST_VARIANT_H