diff options
Diffstat (limited to 'tests')
86 files changed, 8433 insertions, 956 deletions
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..b852f3ccb9 --- /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 behavior. + + // 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 index 355aca479e..666719febb 100644 --- a/tests/core/io/test_config_file.h +++ b/tests/core/io/test_config_file.h @@ -127,7 +127,7 @@ TEST_CASE("[ConfigFile] Saving file") { config_file.set_value("quoted", "a=b", 7); #ifdef WINDOWS_ENABLED - const String config_path = OS::get_singleton()->get_environment("TEMP").plus_file("config.ini"); + const String config_path = OS::get_singleton()->get_environment("TEMP").path_join("config.ini"); #else const String config_path = "/tmp/config.ini"; #endif diff --git a/tests/core/io/test_file_access.h b/tests/core/io/test_file_access.h index f0e1cceacf..aab62955cb 100644 --- a/tests/core/io/test_file_access.h +++ b/tests/core/io/test_file_access.h @@ -78,6 +78,29 @@ TEST_CASE("[FileAccess] CSV read") { 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 index 1c778c3228..1559c59b5c 100644 --- a/tests/core/io/test_image.h +++ b/tests/core/io/test_image.h @@ -78,8 +78,8 @@ TEST_CASE("[Image] Instantiation") { 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().plus_file("image.png"); - const String save_path_exr = OS::get_singleton()->get_cache_path().plus_file("image.exr"); + 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; @@ -124,7 +124,7 @@ TEST_CASE("[Image] Saving and loading") { image_jpg->load_jpg_from_buffer(data_jpg) == OK, "The JPG image should load successfully."); - // Load WEBP + // 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; @@ -132,7 +132,7 @@ TEST_CASE("[Image] Saving and loading") { 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."); + "The WebP image should load successfully."); // Load PNG Ref<Image> image_png = memnew(Image()); @@ -161,8 +161,8 @@ TEST_CASE("[Image] Basic getters") { CHECK(image->get_height() == 4); CHECK(image->get_size() == Vector2(8, 4)); CHECK(image->get_format() == Image::FORMAT_LA8); - CHECK(image->get_used_rect() == Rect2(0, 0, 0, 0)); - Ref<Image> image_get_rect = image->get_rect(Rect2(0, 0, 2, 1)); + CHECK(image->get_used_rect() == Rect2i(0, 0, 0, 0)); + Ref<Image> image_get_rect = image->get_region(Rect2i(0, 0, 2, 1)); CHECK(image_get_rect->get_size() == Vector2(2, 1)); } @@ -213,8 +213,8 @@ TEST_CASE("[Image] Modifying pixels of an image") { 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() == Rect2(0, 0, 1, 1), - "Image's get_used_rect should return the expected value, larger than Rect2(0, 0, 0, 0) if it's visible."); + 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)); @@ -233,19 +233,19 @@ TEST_CASE("[Image] Modifying pixels of an image") { { const int img_width = 3; const int img_height = 3; - Vector<Rect2> rects; - rects.push_back(Rect2()); - rects.push_back(Rect2(-5, -5, 3, 3)); - rects.push_back(Rect2(img_width, 0, 12, 12)); - rects.push_back(Rect2(0, img_height, 12, 12)); - rects.push_back(Rect2(img_width + 1, img_height + 2, 12, 12)); - rects.push_back(Rect2(1, 1, 1, 1)); - rects.push_back(Rect2(0, 1, 2, 3)); - rects.push_back(Rect2(-5, 0, img_width + 10, 2)); - rects.push_back(Rect2(0, -5, 2, img_height + 10)); - rects.push_back(Rect2(-1, -1, img_width + 1, img_height + 1)); - - for (const Rect2 &rect : rects) { + 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)), @@ -267,7 +267,7 @@ TEST_CASE("[Image] Modifying pixels of an image") { } // Blend two images together - image->blend_rect(image2, Rect2(Vector2(0, 0), image2->get_size()), Vector2(0, 0)); + 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."); @@ -279,7 +279,7 @@ TEST_CASE("[Image] Modifying pixels of an image") { image3->set_pixel(0, 0, Color(0, 1, 0, 1)); //blit_rect() two images together - image->blit_rect(image3, Rect2(Vector2(0, 0), image3->get_size()), Vector2(0, 0)); + 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."); @@ -300,4 +300,5 @@ TEST_CASE("[Image] Modifying pixels of an image") { "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 index 478cf1766e..af450da3b8 100644 --- a/tests/core/io/test_json.h +++ b/tests/core/io/test_json.h @@ -83,7 +83,7 @@ TEST_CASE("[JSON] Parsing single data types") { 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), + double(json.get_data()) == doctest::Approx(0.123456), "Parsing a floating-point number as JSON should return the expected value."); json.parse("\"hello\""); diff --git a/tests/core/io/test_marshalls.h b/tests/core/io/test_marshalls.h index 546a2e9358..7490df2b2c 100644 --- a/tests/core/io/test_marshalls.h +++ b/tests/core/io/test_marshalls.h @@ -254,11 +254,13 @@ TEST_CASE("[Marshalls] Invalid data Variant decoding") { 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") { diff --git a/tests/core/io/test_pck_packer.h b/tests/core/io/test_pck_packer.h index d21fbdaf50..8d0e5c402a 100644 --- a/tests/core/io/test_pck_packer.h +++ b/tests/core/io/test_pck_packer.h @@ -42,7 +42,7 @@ namespace TestPCKPacker { TEST_CASE("[PCKPacker] Pack an empty PCK file") { PCKPacker pck_packer; - const String output_pck_path = OS::get_singleton()->get_cache_path().plus_file("output_empty.pck"); + 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."); @@ -66,7 +66,7 @@ TEST_CASE("[PCKPacker] Pack an empty PCK file") { TEST_CASE("[PCKPacker] Pack empty with zero alignment invalid") { PCKPacker pck_packer; - const String output_pck_path = OS::get_singleton()->get_cache_path().plus_file("output_empty.pck"); + 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; @@ -74,7 +74,7 @@ TEST_CASE("[PCKPacker] Pack empty with zero alignment invalid") { TEST_CASE("[PCKPacker] Pack empty with invalid key") { PCKPacker pck_packer; - const String output_pck_path = OS::get_singleton()->get_cache_path().plus_file("output_empty.pck"); + 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; @@ -82,7 +82,7 @@ TEST_CASE("[PCKPacker] Pack empty with invalid key") { 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().plus_file("output_with_files.pck"); + 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."); @@ -90,16 +90,16 @@ TEST_CASE("[PCKPacker] Pack a PCK file with some files and directories") { const String base_dir = OS::get_singleton()->get_executable_path().get_base_dir(); CHECK_MESSAGE( - pck_packer.add_file("version.py", base_dir.plus_file("../version.py"), "version.py") == OK, + 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.plus_file("../icon.png")) == OK, + 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.plus_file("../icon.svg")) == OK, + 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.plus_file("../logo.png")) == OK, + 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, diff --git a/tests/core/io/test_resource.h b/tests/core/io/test_resource.h index 84d651b63f..2457e06ade 100644 --- a/tests/core/io/test_resource.h +++ b/tests/core/io/test_resource.h @@ -28,8 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef TEST_RESOURCE -#define TEST_RESOURCE +#ifndef TEST_RESOURCE_H +#define TEST_RESOURCE_H #include "core/io/resource.h" #include "core/io/resource_loader.h" @@ -74,10 +74,10 @@ TEST_CASE("[Resource] Saving and loading") { 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().plus_file("resource.res"); - const String save_path_text = OS::get_singleton()->get_cache_path().plus_file("resource.tres"); - ResourceSaver::save(save_path_binary, resource); - ResourceSaver::save(save_path_text, 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( @@ -111,4 +111,4 @@ TEST_CASE("[Resource] Saving and loading") { } } // namespace TestResource -#endif // TEST_RESOURCE +#endif // TEST_RESOURCE_H diff --git a/tests/core/io/test_xml_parser.h b/tests/core/io/test_xml_parser.h index 87592b56ce..35e86d8203 100644 --- a/tests/core/io/test_xml_parser.h +++ b/tests/core/io/test_xml_parser.h @@ -66,6 +66,170 @@ TEST_CASE("[XMLParser] End-to-end") { 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 index 526972a82f..23969556be 100644 --- a/tests/core/math/test_aabb.h +++ b/tests/core/math/test_aabb.h @@ -91,50 +91,55 @@ TEST_CASE("[AABB] Basic setters") { 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), + aabb.get_volume() == doctest::Approx(120), "get_volume() should return the expected value with positive size."); CHECK_MESSAGE( - !aabb.has_no_volume(), + 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), + aabb.get_volume() == doctest::Approx(-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), + aabb.get_volume() == doctest::Approx(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), + aabb.get_volume() == doctest::Approx(-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_no_volume(), + !aabb.has_volume(), "Non-empty flat AABB should not have a volume."); CHECK_MESSAGE( - AABB().has_no_volume(), + !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_no_surface(), + 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_no_surface(), + 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_no_surface(), + aabb.has_surface(), + "Non-empty linear AABB should have a surface."); + + CHECK_MESSAGE( + !AABB().has_surface(), "Empty AABB should not have an surface."); } @@ -299,34 +304,28 @@ TEST_CASE("[AABB] Get longest/shortest axis") { "get_shortest_axis_size() should return the expected value."); } -#ifndef _MSC_VER -#warning Support tests need to be re-done -#endif - -/* Support function was actually broken. As it was fixed, the tests now fail. Tests need to be re-done. - 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(-1.5, 7, 3.5)), + 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(-1.5, 2, 3.5)), + 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(-1.5, 2, 3.5)), + 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(2.5, 7, 3.5)), + 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(2.5, 7, 3.5)), + 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(2.5, 7, 3.5)), + 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( @@ -390,6 +389,27 @@ TEST_CASE("[AABB] Expanding") { 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/core/math/test_astar.h b/tests/core/math/test_astar.h index 1306d3c20e..9f5e98ef94 100644 --- a/tests/core/math/test_astar.h +++ b/tests/core/math/test_astar.h @@ -58,7 +58,7 @@ public: } // Disable heuristic completely. - real_t _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; } @@ -68,7 +68,7 @@ public: 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); @@ -77,7 +77,7 @@ TEST_CASE("[AStar3D] ABC 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); @@ -318,7 +318,7 @@ TEST_CASE("[Stress][AStar3D] 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) { diff --git a/tests/core/math/test_basis.h b/tests/core/math/test_basis.h index ec58d95eed..dce9d5cec3 100644 --- a/tests/core/math/test_basis.h +++ b/tests/core/math/test_basis.h @@ -38,16 +38,7 @@ 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; } @@ -55,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(p_rotation, Basis::EULER_ORDER_XYZ); - break; - - case EulerXZY: - ret.set_euler(p_rotation, Basis::EULER_ORDER_XZY); - break; - - case EulerYZX: - ret.set_euler(p_rotation, Basis::EULER_ORDER_YZX); - break; - - case EulerYXZ: - ret.set_euler(p_rotation, Basis::EULER_ORDER_YXZ); - break; - - case EulerZXY: - ret.set_euler(p_rotation, Basis::EULER_ORDER_ZXY); - break; - - case EulerZYX: - ret.set_euler(p_rotation, Basis::EULER_ORDER_ZYX); - 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(Basis::EULER_ORDER_XYZ); - - case EulerXZY: - return p_rotation.get_euler(Basis::EULER_ORDER_XZY); - - case EulerYZX: - return p_rotation.get_euler(Basis::EULER_ORDER_YZX); - - case EulerYXZ: - return p_rotation.get_euler(Basis::EULER_ORDER_YXZ); - - case EulerZXY: - return p_rotation.get_euler(Basis::EULER_ORDER_ZXY); - - case EulerZYX: - return p_rotation.get_euler(Basis::EULER_ORDER_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(EulerOrder ro) { switch (ro) { - case EulerXYZ: + case EulerOrder::XYZ: return "XYZ"; - case EulerXZY: + case EulerOrder::XZY: return "XZY"; - case EulerYZX: + case EulerOrder::YZX: return "YZX"; - case EulerYXZ: + case EulerOrder::YXZ: return "YXZ"; - case EulerZXY: + case EulerOrder::ZXY: return "ZXY"; - case EulerZYX: + case EulerOrder::ZYX: return "ZYX"; default: return "[Not supported]"; } } -void test_rotation(Vector3 deg_original_euler, RotOrder rot_order) { +void test_rotation(Vector3 deg_original_euler, EulerOrder rot_order) { // This test: // 1. Converts the rotation vector from deg to rad. // 2. Converts euler to basis. @@ -155,12 +84,12 @@ 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; @@ -169,9 +98,8 @@ void test_rotation(Vector3 deg_original_euler, RotOrder rot_order) { 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(Basis::EULER_ORDER_XYZ); - Basis rotation_from_xyz_computed_euler; - rotation_from_xyz_computed_euler.set_euler(euler_xyz_from_rotation, Basis::EULER_ORDER_XYZ); + const Vector3 euler_xyz_from_rotation = to_rotation.get_euler(EulerOrder::XYZ); + Basis rotation_from_xyz_computed_euler = Basis::from_euler(euler_xyz_from_rotation, EulerOrder::XYZ); res = to_rotation.inverse() * rotation_from_xyz_computed_euler; @@ -185,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<EulerOrder> euler_order_to_test; + euler_order_to_test.push_back(EulerOrder::XYZ); + euler_order_to_test.push_back(EulerOrder::XZY); + euler_order_to_test.push_back(EulerOrder::YZX); + euler_order_to_test.push_back(EulerOrder::YXZ); + euler_order_to_test.push_back(EulerOrder::ZXY); + euler_order_to_test.push_back(EulerOrder::ZYX); Vector<Vector3> vectors_to_test; @@ -249,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<EulerOrder> euler_order_to_test; + euler_order_to_test.push_back(EulerOrder::XYZ); + euler_order_to_test.push_back(EulerOrder::XZY); + euler_order_to_test.push_back(EulerOrder::YZX); + euler_order_to_test.push_back(EulerOrder::YXZ); + euler_order_to_test.push_back(EulerOrder::ZXY); + euler_order_to_test.push_back(EulerOrder::ZYX); Vector<Vector3> vectors_to_test; // Add 1000 random vectors with weirds numbers. @@ -275,12 +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(angle == doctest::Approx(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(angle == doctest::Approx(Math::deg_to_rad((real_t)30.0))); + CHECK(axis == Vector3(0, 0, 1)); + + z_negative.get_axis_angle(axis, angle); + CHECK(angle == doctest::Approx(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(angle == doctest::Approx(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(angle == doctest::Approx(0.001).epsilon(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/core/math/test_color.h b/tests/core/math/test_color.h index 51c3bc8bdc..c6550778e8 100644 --- a/tests/core/math/test_color.h +++ b/tests/core/math/test_color.h @@ -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.0f / 360.0f), + dark_blue.get_h() == doctest::Approx(240.0f / 360.0f), "The returned HSV hue should match the expected value."); CHECK_MESSAGE( - Math::is_equal_approx(dark_blue.get_s(), 1.0f), + dark_blue.get_s() == doctest::Approx(1.0f), "The returned HSV saturation should match the expected value."); CHECK_MESSAGE( - Math::is_equal_approx(dark_blue.get_v(), 0.5f), + dark_blue.get_v() == doctest::Approx(0.5f), "The returned HSV value should match the expected value."); } diff --git a/tests/core/math/test_expression.h b/tests/core/math/test_expression.h index 6e3be541b0..9734fd9f36 100644 --- a/tests/core/math/test_expression.h +++ b/tests/core/math/test_expression.h @@ -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(double(expression.execute()), -123.456), + double(expression.execute()) == doctest::Approx(-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(double(expression.execute()), 5), + double(expression.execute()) == doctest::Approx(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(double(expression.execute()), 0.3), + double(expression.execute()) == doctest::Approx(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(double(expression.execute()), 0.3), + double(expression.execute()) == doctest::Approx(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(double(expression.execute()), 0.3), + double(expression.execute()) == doctest::Approx(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(double(expression.execute()), 20.18905), + double(expression.execute()) == doctest::Approx(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(double(expression.execute()), 200'000), + double(expression.execute()) == doctest::Approx(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(double(expression.execute()), 2e5), + double(expression.execute()) == doctest::Approx(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(double(expression.execute()), 2), + double(expression.execute()) == doctest::Approx(2), "The expression should return the expected result."); } @@ -176,7 +176,7 @@ TEST_CASE("[Expression] Built-in functions") { expression.parse("snapped(sin(0.5), 0.01)") == OK, "The expression should parse successfully."); CHECK_MESSAGE( - Math::is_equal_approx(double(expression.execute()), 0.48), + double(expression.execute()) == doctest::Approx(0.48), "`snapped(sin(0.5), 0.01)` should return the expected result."); CHECK_MESSAGE( diff --git a/tests/core/math/test_geometry_2d.h b/tests/core/math/test_geometry_2d.h index db4e6e2177..27c9e7f58b 100644 --- a/tests/core/math/test_geometry_2d.h +++ b/tests/core/math/test_geometry_2d.h @@ -64,7 +64,7 @@ TEST_CASE("[Geometry2D] Point in triangle") { // 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). + // the behavior 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))); } @@ -171,43 +171,43 @@ TEST_CASE("[Geometry2D] Segment intersection with circle") { 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), + Geometry2D::segment_intersects_circle(Vector2(0, 0), Vector2(4, 0), Vector2(0, 0), 1.0) == doctest::Approx(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), + Geometry2D::segment_intersects_circle(Vector2(4, 0), Vector2(0, 0), Vector2(0, 0), 1.0) == doctest::Approx(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), + Geometry2D::segment_intersects_circle(Vector2(-2, 0), Vector2(2, 0), Vector2(0, 0), 1.0) == doctest::Approx(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), + Geometry2D::segment_intersects_circle(Vector2(2, 0), Vector2(-2, 0), Vector2(0, 0), 1.0) == doctest::Approx(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), + Geometry2D::segment_intersects_circle(Vector2(0, 0), Vector2(1, 0), Vector2(0, 0), 1.0) == doctest::Approx(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), + Geometry2D::segment_intersects_circle(Vector2(1, 0), Vector2(0, 0), Vector2(0, 0), 1.0) == doctest::Approx(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), + Geometry2D::segment_intersects_circle(Vector2(1, 0), Vector2(2, 0), Vector2(0, 0), 1.0) == doctest::Approx(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), + Geometry2D::segment_intersects_circle(Vector2(2, 0), Vector2(1, 0), Vector2(0, 0), 1.0) == doctest::Approx(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), + Geometry2D::segment_intersects_circle(Vector2(-1, 0), Vector2(1, 0), Vector2(0, 0), 2.0) == doctest::Approx(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), + Geometry2D::segment_intersects_circle(Vector2(1, 0), Vector2(-1, 0), Vector2(0, 0), 2.0) == doctest::Approx(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), + Geometry2D::segment_intersects_circle(Vector2(2, 0), Vector2(3, 0), Vector2(0, 0), 1.0) == doctest::Approx(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), + Geometry2D::segment_intersects_circle(Vector2(3, 0), Vector2(2, 0), Vector2(0, 0), 1.0) == doctest::Approx(minus_one), "Segment completely outside the circle should not intersect it"); } diff --git a/tests/core/math/test_geometry_3d.h b/tests/core/math/test_geometry_3d.h index 99a4ef2d46..23bbf1e183 100644 --- a/tests/core/math/test_geometry_3d.h +++ b/tests/core/math/test_geometry_3d.h @@ -63,7 +63,7 @@ TEST_CASE("[Geometry3D] Closest Distance Between Segments") { 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), 0.0f)); + 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); diff --git a/tests/core/math/test_math_funcs.h b/tests/core/math/test_math_funcs.h new file mode 100644 index 0000000000..c9d14bbd21 --- /dev/null +++ b/tests/core/math/test_math_funcs.h @@ -0,0 +1,549 @@ +/*************************************************************************/ +/* test_math_funcs.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_MATH_FUNCS_H +#define TEST_MATH_FUNCS_H + +#include "tests/test_macros.h" + +namespace TestMath { + +TEST_CASE("[Math] C++ macros") { + CHECK(MIN(-2, 2) == -2); + CHECK(MIN(600, 2) == 2); + + CHECK(MAX(-2, 2) == 2); + CHECK(MAX(600, 2) == 600); + + CHECK(CLAMP(600, -2, 2) == 2); + CHECK(CLAMP(620, 600, 650) == 620); + // `max` is lower than `min`. + CHECK(CLAMP(620, 600, 50) == 50); + + CHECK(ABS(-5) == 5); + CHECK(ABS(0) == 0); + CHECK(ABS(5) == 5); + + CHECK(SIGN(-5) == -1.0); + CHECK(SIGN(0) == 0.0); + CHECK(SIGN(5) == 1.0); +} + +TEST_CASE("[Math] Power of two functions") { + CHECK(next_power_of_2(0) == 0); + CHECK(next_power_of_2(1) == 1); + CHECK(next_power_of_2(16) == 16); + CHECK(next_power_of_2(17) == 32); + CHECK(next_power_of_2(65535) == 65536); + + CHECK(previous_power_of_2(0) == 0); + CHECK(previous_power_of_2(1) == 1); + CHECK(previous_power_of_2(16) == 16); + CHECK(previous_power_of_2(17) == 16); + CHECK(previous_power_of_2(65535) == 32768); + + CHECK(closest_power_of_2(0) == 0); + CHECK(closest_power_of_2(1) == 1); + CHECK(closest_power_of_2(16) == 16); + CHECK(closest_power_of_2(17) == 16); + CHECK(closest_power_of_2(65535) == 65536); + + CHECK(get_shift_from_power_of_2(0) == -1); + CHECK(get_shift_from_power_of_2(1) == 0); + CHECK(get_shift_from_power_of_2(16) == 4); + CHECK(get_shift_from_power_of_2(17) == -1); + CHECK(get_shift_from_power_of_2(65535) == -1); + + CHECK(nearest_shift(0) == 0); + CHECK(nearest_shift(1) == 1); + CHECK(nearest_shift(16) == 5); + CHECK(nearest_shift(17) == 5); + CHECK(nearest_shift(65535) == 16); +} + +TEST_CASE_TEMPLATE("[Math] abs", T, int, float, double) { + CHECK(Math::abs((T)-1) == (T)1); + CHECK(Math::abs((T)0) == (T)0); + CHECK(Math::abs((T)1) == (T)1); + CHECK(Math::abs((T)0.1) == (T)0.1); +} + +TEST_CASE_TEMPLATE("[Math] round/floor/ceil", T, float, double) { + CHECK(Math::round((T)1.5) == (T)2.0); + CHECK(Math::round((T)1.6) == (T)2.0); + CHECK(Math::round((T)-1.5) == (T)-2.0); + CHECK(Math::round((T)-1.1) == (T)-1.0); + + CHECK(Math::floor((T)1.5) == (T)1.0); + CHECK(Math::floor((T)-1.5) == (T)-2.0); + + CHECK(Math::ceil((T)1.5) == (T)2.0); + CHECK(Math::ceil((T)-1.9) == (T)-1.0); +} + +TEST_CASE_TEMPLATE("[Math] sin/cos/tan", T, float, double) { + CHECK(Math::sin((T)-0.1) == doctest::Approx((T)-0.0998334166)); + CHECK(Math::sin((T)0.1) == doctest::Approx((T)0.0998334166)); + CHECK(Math::sin((T)0.5) == doctest::Approx((T)0.4794255386)); + CHECK(Math::sin((T)1.0) == doctest::Approx((T)0.8414709848)); + CHECK(Math::sin((T)1.5) == doctest::Approx((T)0.9974949866)); + CHECK(Math::sin((T)450.0) == doctest::Approx((T)-0.683283725)); + + CHECK(Math::cos((T)-0.1) == doctest::Approx((T)0.99500416530)); + CHECK(Math::cos((T)0.1) == doctest::Approx((T)0.9950041653)); + CHECK(Math::cos((T)0.5) == doctest::Approx((T)0.8775825619)); + CHECK(Math::cos((T)1.0) == doctest::Approx((T)0.5403023059)); + CHECK(Math::cos((T)1.5) == doctest::Approx((T)0.0707372017)); + CHECK(Math::cos((T)450.0) == doctest::Approx((T)-0.7301529642)); + + CHECK(Math::tan((T)-0.1) == doctest::Approx((T)-0.1003346721)); + CHECK(Math::tan((T)0.1) == doctest::Approx((T)0.1003346721)); + CHECK(Math::tan((T)0.5) == doctest::Approx((T)0.5463024898)); + CHECK(Math::tan((T)1.0) == doctest::Approx((T)1.5574077247)); + CHECK(Math::tan((T)1.5) == doctest::Approx((T)14.1014199472)); + CHECK(Math::tan((T)450.0) == doctest::Approx((T)0.9358090134)); +} + +TEST_CASE_TEMPLATE("[Math] sinh/cosh/tanh", T, float, double) { + CHECK(Math::sinh((T)-0.1) == doctest::Approx((T)-0.10016675)); + CHECK(Math::sinh((T)0.1) == doctest::Approx((T)0.10016675)); + CHECK(Math::sinh((T)0.5) == doctest::Approx((T)0.5210953055)); + CHECK(Math::sinh((T)1.0) == doctest::Approx((T)1.1752011936)); + CHECK(Math::sinh((T)1.5) == doctest::Approx((T)2.1292794551)); + + CHECK(Math::cosh((T)-0.1) == doctest::Approx((T)1.0050041681)); + CHECK(Math::cosh((T)0.1) == doctest::Approx((T)1.0050041681)); + CHECK(Math::cosh((T)0.5) == doctest::Approx((T)1.1276259652)); + CHECK(Math::cosh((T)1.0) == doctest::Approx((T)1.5430806348)); + CHECK(Math::cosh((T)1.5) == doctest::Approx((T)2.3524096152)); + + CHECK(Math::tanh((T)-0.1) == doctest::Approx((T)-0.0996679946)); + CHECK(Math::tanh((T)0.1) == doctest::Approx((T)0.0996679946)); + CHECK(Math::tanh((T)0.5) == doctest::Approx((T)0.4621171573)); + CHECK(Math::tanh((T)1.0) == doctest::Approx((T)0.761594156)); + CHECK(Math::tanh((T)1.5) == doctest::Approx((T)0.9051482536)); + CHECK(Math::tanh((T)450.0) == doctest::Approx((T)1.0)); +} + +TEST_CASE_TEMPLATE("[Math] asin/acos/atan", T, float, double) { + CHECK(Math::asin((T)-0.1) == doctest::Approx((T)-0.1001674212)); + CHECK(Math::asin((T)0.1) == doctest::Approx((T)0.1001674212)); + CHECK(Math::asin((T)0.5) == doctest::Approx((T)0.5235987756)); + CHECK(Math::asin((T)1.0) == doctest::Approx((T)1.5707963268)); + CHECK(Math::is_nan(Math::asin((T)1.5))); + CHECK(Math::is_nan(Math::asin((T)450.0))); + + CHECK(Math::acos((T)-0.1) == doctest::Approx((T)1.670963748)); + CHECK(Math::acos((T)0.1) == doctest::Approx((T)1.4706289056)); + CHECK(Math::acos((T)0.5) == doctest::Approx((T)1.0471975512)); + CHECK(Math::acos((T)1.0) == doctest::Approx((T)0.0)); + CHECK(Math::is_nan(Math::acos((T)1.5))); + CHECK(Math::is_nan(Math::acos((T)450.0))); + + CHECK(Math::atan((T)-0.1) == doctest::Approx((T)-0.0996686525)); + CHECK(Math::atan((T)0.1) == doctest::Approx((T)0.0996686525)); + CHECK(Math::atan((T)0.5) == doctest::Approx((T)0.463647609)); + CHECK(Math::atan((T)1.0) == doctest::Approx((T)0.7853981634)); + CHECK(Math::atan((T)1.5) == doctest::Approx((T)0.9827937232)); + CHECK(Math::atan((T)450.0) == doctest::Approx((T)1.5685741082)); +} + +TEST_CASE_TEMPLATE("[Math] sinc/sincn/atan2", T, float, double) { + CHECK(Math::sinc((T)-0.1) == doctest::Approx((T)0.9983341665)); + CHECK(Math::sinc((T)0.1) == doctest::Approx((T)0.9983341665)); + CHECK(Math::sinc((T)0.5) == doctest::Approx((T)0.9588510772)); + CHECK(Math::sinc((T)1.0) == doctest::Approx((T)0.8414709848)); + CHECK(Math::sinc((T)1.5) == doctest::Approx((T)0.6649966577)); + CHECK(Math::sinc((T)450.0) == doctest::Approx((T)-0.0015184083)); + + CHECK(Math::sincn((T)-0.1) == doctest::Approx((T)0.9836316431)); + CHECK(Math::sincn((T)0.1) == doctest::Approx((T)0.9836316431)); + CHECK(Math::sincn((T)0.5) == doctest::Approx((T)0.6366197724)); + CHECK(Math::sincn((T)1.0) == doctest::Approx((T)0.0)); + CHECK(Math::sincn((T)1.5) == doctest::Approx((T)-0.2122065908)); + CHECK(Math::sincn((T)450.0) == doctest::Approx((T)0.0)); + + CHECK(Math::atan2((T)-0.1, (T)0.5) == doctest::Approx((T)-0.1973955598)); + CHECK(Math::atan2((T)0.1, (T)-0.5) == doctest::Approx((T)2.9441970937)); + CHECK(Math::atan2((T)0.5, (T)1.5) == doctest::Approx((T)0.3217505544)); + CHECK(Math::atan2((T)1.0, (T)2.5) == doctest::Approx((T)0.3805063771)); + CHECK(Math::atan2((T)1.5, (T)1.0) == doctest::Approx((T)0.9827937232)); + CHECK(Math::atan2((T)450.0, (T)1.0) == doctest::Approx((T)1.5685741082)); +} + +TEST_CASE_TEMPLATE("[Math] pow/log/log2/exp/sqrt", T, float, double) { + CHECK(Math::pow((T)-0.1, (T)2.0) == doctest::Approx((T)0.01)); + CHECK(Math::pow((T)0.1, (T)2.5) == doctest::Approx((T)0.0031622777)); + CHECK(Math::pow((T)0.5, (T)0.5) == doctest::Approx((T)0.7071067812)); + CHECK(Math::pow((T)1.0, (T)1.0) == doctest::Approx((T)1.0)); + CHECK(Math::pow((T)1.5, (T)-1.0) == doctest::Approx((T)0.6666666667)); + CHECK(Math::pow((T)450.0, (T)-2.0) == doctest::Approx((T)0.0000049383)); + CHECK(Math::pow((T)450.0, (T)0.0) == doctest::Approx((T)1.0)); + + CHECK(Math::is_nan(Math::log((T)-0.1))); + CHECK(Math::log((T)0.1) == doctest::Approx((T)-2.302585093)); + CHECK(Math::log((T)0.5) == doctest::Approx((T)-0.6931471806)); + CHECK(Math::log((T)1.0) == doctest::Approx((T)0.0)); + CHECK(Math::log((T)1.5) == doctest::Approx((T)0.4054651081)); + CHECK(Math::log((T)450.0) == doctest::Approx((T)6.1092475828)); + + CHECK(Math::is_nan(Math::log2((T)-0.1))); + CHECK(Math::log2((T)0.1) == doctest::Approx((T)-3.3219280949)); + CHECK(Math::log2((T)0.5) == doctest::Approx((T)-1.0)); + CHECK(Math::log2((T)1.0) == doctest::Approx((T)0.0)); + CHECK(Math::log2((T)1.5) == doctest::Approx((T)0.5849625007)); + CHECK(Math::log2((T)450.0) == doctest::Approx((T)8.8137811912)); + + CHECK(Math::exp((T)-0.1) == doctest::Approx((T)0.904837418)); + CHECK(Math::exp((T)0.1) == doctest::Approx((T)1.1051709181)); + CHECK(Math::exp((T)0.5) == doctest::Approx((T)1.6487212707)); + CHECK(Math::exp((T)1.0) == doctest::Approx((T)2.7182818285)); + CHECK(Math::exp((T)1.5) == doctest::Approx((T)4.4816890703)); + + CHECK(Math::is_nan(Math::sqrt((T)-0.1))); + CHECK(Math::sqrt((T)0.1) == doctest::Approx((T)0.316228)); + CHECK(Math::sqrt((T)0.5) == doctest::Approx((T)0.707107)); + CHECK(Math::sqrt((T)1.0) == doctest::Approx((T)1.0)); + CHECK(Math::sqrt((T)1.5) == doctest::Approx((T)1.224745)); +} + +TEST_CASE_TEMPLATE("[Math] is_nan/is_inf", T, float, double) { + CHECK(!Math::is_nan((T)0.0)); + CHECK(Math::is_nan((T)NAN)); + + CHECK(!Math::is_inf((T)0.0)); + CHECK(Math::is_inf((T)INFINITY)); +} + +TEST_CASE_TEMPLATE("[Math] linear_to_db", T, float, double) { + CHECK(Math::linear_to_db((T)1.0) == doctest::Approx((T)0.0)); + CHECK(Math::linear_to_db((T)20.0) == doctest::Approx((T)26.0206)); + CHECK(Math::is_inf(Math::linear_to_db((T)0.0))); + CHECK(Math::is_nan(Math::linear_to_db((T)-20.0))); +} + +TEST_CASE_TEMPLATE("[Math] db_to_linear", T, float, double) { + CHECK(Math::db_to_linear((T)0.0) == doctest::Approx((T)1.0)); + CHECK(Math::db_to_linear((T)1.0) == doctest::Approx((T)1.122018)); + CHECK(Math::db_to_linear((T)20.0) == doctest::Approx((T)10.0)); + CHECK(Math::db_to_linear((T)-20.0) == doctest::Approx((T)0.1)); +} + +TEST_CASE_TEMPLATE("[Math] step_decimals", T, float, double) { + CHECK(Math::step_decimals((T)-0.5) == 1); + CHECK(Math::step_decimals((T)0) == 0); + CHECK(Math::step_decimals((T)1) == 0); + CHECK(Math::step_decimals((T)0.1) == 1); + CHECK(Math::step_decimals((T)0.01) == 2); + CHECK(Math::step_decimals((T)0.001) == 3); + CHECK(Math::step_decimals((T)0.0001) == 4); + CHECK(Math::step_decimals((T)0.00001) == 5); + CHECK(Math::step_decimals((T)0.000001) == 6); + CHECK(Math::step_decimals((T)0.0000001) == 7); + CHECK(Math::step_decimals((T)0.00000001) == 8); + CHECK(Math::step_decimals((T)0.000000001) == 9); + // Too many decimals to handle. + CHECK(Math::step_decimals((T)0.0000000001) == 0); +} + +TEST_CASE_TEMPLATE("[Math] range_step_decimals", T, float, double) { + CHECK(Math::range_step_decimals((T)0.000000001) == 9); + // Too many decimals to handle. + CHECK(Math::range_step_decimals((T)0.0000000001) == 0); + // Should be treated as a step of 0 for use by the editor. + CHECK(Math::range_step_decimals((T)0.0) == 16); + CHECK(Math::range_step_decimals((T)-0.5) == 16); +} + +TEST_CASE_TEMPLATE("[Math] lerp", T, float, double) { + CHECK(Math::lerp((T)2.0, (T)5.0, (T)-0.1) == doctest::Approx((T)1.7)); + CHECK(Math::lerp((T)2.0, (T)5.0, (T)0.0) == doctest::Approx((T)2.0)); + CHECK(Math::lerp((T)2.0, (T)5.0, (T)0.1) == doctest::Approx((T)2.3)); + CHECK(Math::lerp((T)2.0, (T)5.0, (T)1.0) == doctest::Approx((T)5.0)); + CHECK(Math::lerp((T)2.0, (T)5.0, (T)2.0) == doctest::Approx((T)8.0)); + + CHECK(Math::lerp((T)-2.0, (T)-5.0, (T)-0.1) == doctest::Approx((T)-1.7)); + CHECK(Math::lerp((T)-2.0, (T)-5.0, (T)0.0) == doctest::Approx((T)-2.0)); + CHECK(Math::lerp((T)-2.0, (T)-5.0, (T)0.1) == doctest::Approx((T)-2.3)); + CHECK(Math::lerp((T)-2.0, (T)-5.0, (T)1.0) == doctest::Approx((T)-5.0)); + CHECK(Math::lerp((T)-2.0, (T)-5.0, (T)2.0) == doctest::Approx((T)-8.0)); +} + +TEST_CASE_TEMPLATE("[Math] inverse_lerp", T, float, double) { + CHECK(Math::inverse_lerp((T)2.0, (T)5.0, (T)1.7) == doctest::Approx((T)-0.1)); + CHECK(Math::inverse_lerp((T)2.0, (T)5.0, (T)2.0) == doctest::Approx((T)0.0)); + CHECK(Math::inverse_lerp((T)2.0, (T)5.0, (T)2.3) == doctest::Approx((T)0.1)); + CHECK(Math::inverse_lerp((T)2.0, (T)5.0, (T)5.0) == doctest::Approx((T)1.0)); + CHECK(Math::inverse_lerp((T)2.0, (T)5.0, (T)8.0) == doctest::Approx((T)2.0)); + + CHECK(Math::inverse_lerp((T)-2.0, (T)-5.0, (T)-1.7) == doctest::Approx((T)-0.1)); + CHECK(Math::inverse_lerp((T)-2.0, (T)-5.0, (T)-2.0) == doctest::Approx((T)0.0)); + CHECK(Math::inverse_lerp((T)-2.0, (T)-5.0, (T)-2.3) == doctest::Approx((T)0.1)); + CHECK(Math::inverse_lerp((T)-2.0, (T)-5.0, (T)-5.0) == doctest::Approx((T)1.0)); + CHECK(Math::inverse_lerp((T)-2.0, (T)-5.0, (T)-8.0) == doctest::Approx((T)2.0)); +} + +TEST_CASE_TEMPLATE("[Math] remap", T, float, double) { + CHECK(Math::remap((T)50.0, (T)100.0, (T)200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)-500.0)); + CHECK(Math::remap((T)100.0, (T)100.0, (T)200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)0.0)); + CHECK(Math::remap((T)200.0, (T)100.0, (T)200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)1000.0)); + CHECK(Math::remap((T)250.0, (T)100.0, (T)200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)1500.0)); + + CHECK(Math::remap((T)-50.0, (T)-100.0, (T)-200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)-500.0)); + CHECK(Math::remap((T)-100.0, (T)-100.0, (T)-200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)0.0)); + CHECK(Math::remap((T)-200.0, (T)-100.0, (T)-200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)1000.0)); + CHECK(Math::remap((T)-250.0, (T)-100.0, (T)-200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)1500.0)); + + CHECK(Math::remap((T)-50.0, (T)-100.0, (T)-200.0, (T)0.0, (T)-1000.0) == doctest::Approx((T)500.0)); + CHECK(Math::remap((T)-100.0, (T)-100.0, (T)-200.0, (T)0.0, (T)-1000.0) == doctest::Approx((T)0.0)); + CHECK(Math::remap((T)-200.0, (T)-100.0, (T)-200.0, (T)0.0, (T)-1000.0) == doctest::Approx((T)-1000.0)); + CHECK(Math::remap((T)-250.0, (T)-100.0, (T)-200.0, (T)0.0, (T)-1000.0) == doctest::Approx((T)-1500.0)); +} + +TEST_CASE_TEMPLATE("[Math] lerp_angle", T, float, double) { + // Counter-clockwise rotation. + CHECK(Math::lerp_angle((T)0.24 * Math_TAU, 0.75 * Math_TAU, 0.5) == doctest::Approx((T)-0.005 * Math_TAU)); + // Counter-clockwise rotation. + CHECK(Math::lerp_angle((T)0.25 * Math_TAU, 0.75 * Math_TAU, 0.5) == doctest::Approx((T)0.0)); + // Clockwise rotation. + CHECK(Math::lerp_angle((T)0.26 * Math_TAU, 0.75 * Math_TAU, 0.5) == doctest::Approx((T)0.505 * Math_TAU)); + + CHECK(Math::lerp_angle((T)-0.25 * Math_TAU, 1.25 * Math_TAU, 0.5) == doctest::Approx((T)-0.5 * Math_TAU)); + CHECK(Math::lerp_angle((T)0.72 * Math_TAU, 1.44 * Math_TAU, 0.96) == doctest::Approx((T)0.4512 * Math_TAU)); + CHECK(Math::lerp_angle((T)0.72 * Math_TAU, 1.44 * Math_TAU, 1.04) == doctest::Approx((T)0.4288 * Math_TAU)); + + // Initial and final angles are effectively identical, so the value returned + // should always be the same regardless of the `weight` parameter. + CHECK(Math::lerp_angle((T)-4 * Math_TAU, 4 * Math_TAU, -1.0) == doctest::Approx((T)-4.0 * Math_TAU)); + CHECK(Math::lerp_angle((T)-4 * Math_TAU, 4 * Math_TAU, 0.0) == doctest::Approx((T)-4.0 * Math_TAU)); + CHECK(Math::lerp_angle((T)-4 * Math_TAU, 4 * Math_TAU, 0.5) == doctest::Approx((T)-4.0 * Math_TAU)); + CHECK(Math::lerp_angle((T)-4 * Math_TAU, 4 * Math_TAU, 1.0) == doctest::Approx((T)-4.0 * Math_TAU)); + CHECK(Math::lerp_angle((T)-4 * Math_TAU, 4 * Math_TAU, 500.0) == doctest::Approx((T)-4.0 * Math_TAU)); +} + +TEST_CASE_TEMPLATE("[Math] move_toward", T, float, double) { + CHECK(Math::move_toward(2.0, 5.0, -1.0) == doctest::Approx((T)1.0)); + CHECK(Math::move_toward(2.0, 5.0, 2.5) == doctest::Approx((T)4.5)); + CHECK(Math::move_toward(2.0, 5.0, 4.0) == doctest::Approx((T)5.0)); + CHECK(Math::move_toward(-2.0, -5.0, -1.0) == doctest::Approx((T)-1.0)); + CHECK(Math::move_toward(-2.0, -5.0, 2.5) == doctest::Approx((T)-4.5)); + CHECK(Math::move_toward(-2.0, -5.0, 4.0) == doctest::Approx((T)-5.0)); +} + +TEST_CASE_TEMPLATE("[Math] smoothstep", T, float, double) { + CHECK(Math::smoothstep((T)0.0, (T)2.0, (T)-5.0) == doctest::Approx((T)0.0)); + CHECK(Math::smoothstep((T)0.0, (T)2.0, (T)0.5) == doctest::Approx((T)0.15625)); + CHECK(Math::smoothstep((T)0.0, (T)2.0, (T)1.0) == doctest::Approx((T)0.5)); + CHECK(Math::smoothstep((T)0.0, (T)2.0, (T)2.0) == doctest::Approx((T)1.0)); +} + +TEST_CASE("[Math] ease") { + CHECK(Math::ease(0.1, 1.0) == doctest::Approx(0.1)); + CHECK(Math::ease(0.1, 2.0) == doctest::Approx(0.01)); + CHECK(Math::ease(0.1, 0.5) == doctest::Approx(0.19)); + CHECK(Math::ease(0.1, 0.0) == doctest::Approx(0)); + CHECK(Math::ease(0.1, -0.5) == doctest::Approx(0.2236067977)); + CHECK(Math::ease(0.1, -1.0) == doctest::Approx(0.1)); + CHECK(Math::ease(0.1, -2.0) == doctest::Approx(0.02)); + + CHECK(Math::ease(-1.0, 1.0) == doctest::Approx(0)); + CHECK(Math::ease(-1.0, 2.0) == doctest::Approx(0)); + CHECK(Math::ease(-1.0, 0.5) == doctest::Approx(0)); + CHECK(Math::ease(-1.0, 0.0) == doctest::Approx(0)); + CHECK(Math::ease(-1.0, -0.5) == doctest::Approx(0)); + CHECK(Math::ease(-1.0, -1.0) == doctest::Approx(0)); + CHECK(Math::ease(-1.0, -2.0) == doctest::Approx(0)); +} + +TEST_CASE("[Math] snapped") { + CHECK(Math::snapped(0.5, 0.04) == doctest::Approx(0.52)); + CHECK(Math::snapped(-0.5, 0.04) == doctest::Approx(-0.48)); + CHECK(Math::snapped(0.0, 0.04) == doctest::Approx(0)); + CHECK(Math::snapped(128'000.025, 0.04) == doctest::Approx(128'000.04)); + + CHECK(Math::snapped(0.5, 400) == doctest::Approx(0)); + CHECK(Math::snapped(-0.5, 400) == doctest::Approx(0)); + CHECK(Math::snapped(0.0, 400) == doctest::Approx(0)); + CHECK(Math::snapped(128'000.025, 400) == doctest::Approx(128'000.0)); + + CHECK(Math::snapped(0.5, 0.0) == doctest::Approx(0.5)); + CHECK(Math::snapped(-0.5, 0.0) == doctest::Approx(-0.5)); + CHECK(Math::snapped(0.0, 0.0) == doctest::Approx(0.0)); + CHECK(Math::snapped(128'000.025, 0.0) == doctest::Approx(128'000.0)); + + CHECK(Math::snapped(0.5, -1.0) == doctest::Approx(0)); + CHECK(Math::snapped(-0.5, -1.0) == doctest::Approx(-1.0)); + CHECK(Math::snapped(0.0, -1.0) == doctest::Approx(0)); + CHECK(Math::snapped(128'000.025, -1.0) == doctest::Approx(128'000.0)); +} + +TEST_CASE("[Math] larger_prime") { + CHECK(Math::larger_prime(0) == 5); + CHECK(Math::larger_prime(1) == 5); + CHECK(Math::larger_prime(2) == 5); + CHECK(Math::larger_prime(5) == 13); + CHECK(Math::larger_prime(500) == 769); + CHECK(Math::larger_prime(1'000'000) == 1'572'869); + CHECK(Math::larger_prime(1'000'000'000) == 1'610'612'741); + + // The next prime is larger than `INT32_MAX` and is not present in the built-in prime table. + ERR_PRINT_OFF; + CHECK(Math::larger_prime(2'000'000'000) == 0); + ERR_PRINT_ON; +} + +TEST_CASE_TEMPLATE("[Math] fmod", T, float, double) { + CHECK(Math::fmod((T)-2.0, (T)0.3) == doctest::Approx((T)-0.2)); + CHECK(Math::fmod((T)0.0, (T)0.3) == doctest::Approx((T)0.0)); + CHECK(Math::fmod((T)2.0, (T)0.3) == doctest::Approx((T)0.2)); + + CHECK(Math::fmod((T)-2.0, (T)-0.3) == doctest::Approx((T)-0.2)); + CHECK(Math::fmod((T)0.0, (T)-0.3) == doctest::Approx((T)0.0)); + CHECK(Math::fmod((T)2.0, (T)-0.3) == doctest::Approx((T)0.2)); +} + +TEST_CASE_TEMPLATE("[Math] fposmod", T, float, double) { + CHECK(Math::fposmod((T)-2.0, (T)0.3) == doctest::Approx((T)0.1)); + CHECK(Math::fposmod((T)0.0, (T)0.3) == doctest::Approx((T)0.0)); + CHECK(Math::fposmod((T)2.0, (T)0.3) == doctest::Approx((T)0.2)); + + CHECK(Math::fposmod((T)-2.0, (T)-0.3) == doctest::Approx((T)-0.2)); + CHECK(Math::fposmod((T)0.0, (T)-0.3) == doctest::Approx((T)0.0)); + CHECK(Math::fposmod((T)2.0, (T)-0.3) == doctest::Approx((T)-0.1)); +} + +TEST_CASE_TEMPLATE("[Math] fposmodp", T, float, double) { + CHECK(Math::fposmodp((T)-2.0, (T)0.3) == doctest::Approx((T)0.1)); + CHECK(Math::fposmodp((T)0.0, (T)0.3) == doctest::Approx((T)0.0)); + CHECK(Math::fposmodp((T)2.0, (T)0.3) == doctest::Approx((T)0.2)); + + CHECK(Math::fposmodp((T)-2.0, (T)-0.3) == doctest::Approx((T)-0.5)); + CHECK(Math::fposmodp((T)0.0, (T)-0.3) == doctest::Approx((T)0.0)); + CHECK(Math::fposmodp((T)2.0, (T)-0.3) == doctest::Approx((T)0.2)); +} + +TEST_CASE("[Math] posmod") { + CHECK(Math::posmod(-20, 3) == 1); + CHECK(Math::posmod(0, 3) == 0); + CHECK(Math::posmod(20, 3) == 2); + CHECK(Math::posmod(-20, -3) == -2); + CHECK(Math::posmod(0, -3) == 0); + CHECK(Math::posmod(20, -3) == -1); +} + +TEST_CASE("[Math] wrapi") { + CHECK(Math::wrapi(-30, -20, 160) == 150); + CHECK(Math::wrapi(30, -20, 160) == 30); + CHECK(Math::wrapi(300, -20, 160) == 120); + CHECK(Math::wrapi(300'000'000'000, -20, 160) == 120); +} + +TEST_CASE_TEMPLATE("[Math] wrapf", T, float, double) { + CHECK(Math::wrapf((T)-30.0, (T)-20.0, (T)160.0) == doctest::Approx((T)150.0)); + CHECK(Math::wrapf((T)30.0, (T)-2.0, (T)160.0) == doctest::Approx((T)30.0)); + CHECK(Math::wrapf((T)300.0, (T)-20.0, (T)160.0) == doctest::Approx((T)120.0)); + + CHECK(Math::wrapf(300'000'000'000.0, -20.0, 160.0) == doctest::Approx((T)120.0)); + // float's precision is too low for 300'000'000'000.0, so we reduce it by a factor of 1000. + CHECK(Math::wrapf((float)300'000'000.0, (float)-20.0, (float)160.0) == doctest::Approx((T)128.0)); +} + +TEST_CASE_TEMPLATE("[Math] fract", T, float, double) { + CHECK(Math::fract((T)1.0) == doctest::Approx((T)0.0)); + CHECK(Math::fract((T)77.8) == doctest::Approx((T)0.8)); + CHECK(Math::fract((T)-10.1) == doctest::Approx((T)0.9)); +} + +TEST_CASE_TEMPLATE("[Math] pingpong", T, float, double) { + CHECK(Math::pingpong((T)0.0, (T)0.0) == doctest::Approx((T)0.0)); + CHECK(Math::pingpong((T)1.0, (T)1.0) == doctest::Approx((T)1.0)); + CHECK(Math::pingpong((T)0.5, (T)2.0) == doctest::Approx((T)0.5)); + CHECK(Math::pingpong((T)3.5, (T)2.0) == doctest::Approx((T)0.5)); + CHECK(Math::pingpong((T)11.5, (T)2.0) == doctest::Approx((T)0.5)); + CHECK(Math::pingpong((T)-2.5, (T)2.0) == doctest::Approx((T)1.5)); +} + +TEST_CASE_TEMPLATE("[Math] deg_to_rad/rad_to_deg", T, float, double) { + CHECK(Math::deg_to_rad((T)180.0) == doctest::Approx((T)Math_PI)); + CHECK(Math::deg_to_rad((T)-27.0) == doctest::Approx((T)-0.471239)); + + CHECK(Math::rad_to_deg((T)Math_PI) == doctest::Approx((T)180.0)); + CHECK(Math::rad_to_deg((T)-1.5) == doctest::Approx((T)-85.94366927)); +} + +TEST_CASE_TEMPLATE("[Math] cubic_interpolate", T, float, double) { + CHECK(Math::cubic_interpolate((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.0) == doctest::Approx((T)0.2)); + CHECK(Math::cubic_interpolate((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.25) == doctest::Approx((T)0.33125)); + CHECK(Math::cubic_interpolate((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.5) == doctest::Approx((T)0.5)); + CHECK(Math::cubic_interpolate((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.75) == doctest::Approx((T)0.66875)); + CHECK(Math::cubic_interpolate((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)1.0) == doctest::Approx((T)0.8)); + + CHECK(Math::cubic_interpolate((T)20.2, (T)30.1, (T)-100.0, (T)32.0, (T)-50.0) == doctest::Approx((T)-6662732.3)); + CHECK(Math::cubic_interpolate((T)20.2, (T)30.1, (T)-100.0, (T)32.0, (T)-5.0) == doctest::Approx((T)-9356.3)); + CHECK(Math::cubic_interpolate((T)20.2, (T)30.1, (T)-100.0, (T)32.0, (T)0.0) == doctest::Approx((T)20.2)); + CHECK(Math::cubic_interpolate((T)20.2, (T)30.1, (T)-100.0, (T)32.0, (T)1.0) == doctest::Approx((T)30.1)); + CHECK(Math::cubic_interpolate((T)20.2, (T)30.1, (T)-100.0, (T)32.0, (T)4.0) == doctest::Approx((T)1853.2)); +} + +TEST_CASE_TEMPLATE("[Math] cubic_interpolate_angle", T, float, double) { + CHECK(Math::cubic_interpolate_angle((T)(Math_PI * (1.0 / 6.0)), (T)(Math_PI * (5.0 / 6.0)), (T)0.0, (T)Math_PI, (T)0.0) == doctest::Approx((T)Math_PI * (1.0 / 6.0))); + CHECK(Math::cubic_interpolate_angle((T)(Math_PI * (1.0 / 6.0)), (T)(Math_PI * (5.0 / 6.0)), (T)0.0, (T)Math_PI, (T)0.25) == doctest::Approx((T)0.973566)); + CHECK(Math::cubic_interpolate_angle((T)(Math_PI * (1.0 / 6.0)), (T)(Math_PI * (5.0 / 6.0)), (T)0.0, (T)Math_PI, (T)0.5) == doctest::Approx((T)Math_PI / 2.0)); + CHECK(Math::cubic_interpolate_angle((T)(Math_PI * (1.0 / 6.0)), (T)(Math_PI * (5.0 / 6.0)), (T)0.0, (T)Math_PI, (T)0.75) == doctest::Approx((T)2.16803)); + CHECK(Math::cubic_interpolate_angle((T)(Math_PI * (1.0 / 6.0)), (T)(Math_PI * (5.0 / 6.0)), (T)0.0, (T)Math_PI, (T)1.0) == doctest::Approx((T)Math_PI * (5.0 / 6.0))); +} + +TEST_CASE_TEMPLATE("[Math] cubic_interpolate_in_time", T, float, double) { + CHECK(Math::cubic_interpolate_in_time((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.0, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.0)); + CHECK(Math::cubic_interpolate_in_time((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.25, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.1625)); + CHECK(Math::cubic_interpolate_in_time((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.5, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.4)); + CHECK(Math::cubic_interpolate_in_time((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.75, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.6375)); + CHECK(Math::cubic_interpolate_in_time((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)1.0, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.8)); +} + +TEST_CASE_TEMPLATE("[Math] cubic_interpolate_angle_in_time", T, float, double) { + CHECK(Math::cubic_interpolate_angle_in_time((T)(Math_PI * (1.0 / 6.0)), (T)(Math_PI * (5.0 / 6.0)), (T)0.0, (T)Math_PI, (T)0.0, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.0)); + CHECK(Math::cubic_interpolate_angle_in_time((T)(Math_PI * (1.0 / 6.0)), (T)(Math_PI * (5.0 / 6.0)), (T)0.0, (T)Math_PI, (T)0.25, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.494964)); + CHECK(Math::cubic_interpolate_angle_in_time((T)(Math_PI * (1.0 / 6.0)), (T)(Math_PI * (5.0 / 6.0)), (T)0.0, (T)Math_PI, (T)0.5, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)1.27627)); + CHECK(Math::cubic_interpolate_angle_in_time((T)(Math_PI * (1.0 / 6.0)), (T)(Math_PI * (5.0 / 6.0)), (T)0.0, (T)Math_PI, (T)0.75, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)2.07394)); + CHECK(Math::cubic_interpolate_angle_in_time((T)(Math_PI * (1.0 / 6.0)), (T)(Math_PI * (5.0 / 6.0)), (T)0.0, (T)Math_PI, (T)1.0, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)Math_PI * (5.0 / 6.0))); +} + +TEST_CASE_TEMPLATE("[Math] bezier_interpolate", T, float, double) { + CHECK(Math::bezier_interpolate((T)0.0, (T)0.2, (T)0.8, (T)1.0, (T)0.0) == doctest::Approx((T)0.0)); + CHECK(Math::bezier_interpolate((T)0.0, (T)0.2, (T)0.8, (T)1.0, (T)0.25) == doctest::Approx((T)0.2125)); + CHECK(Math::bezier_interpolate((T)0.0, (T)0.2, (T)0.8, (T)1.0, (T)0.5) == doctest::Approx((T)0.5)); + CHECK(Math::bezier_interpolate((T)0.0, (T)0.2, (T)0.8, (T)1.0, (T)0.75) == doctest::Approx((T)0.7875)); + CHECK(Math::bezier_interpolate((T)0.0, (T)0.2, (T)0.8, (T)1.0, (T)1.0) == doctest::Approx((T)1.0)); +} + +} // namespace TestMath + +#endif // TEST_MATH_FUNCS_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..c3ae322991 --- /dev/null +++ b/tests/core/math/test_quaternion.h @@ -0,0 +1,463 @@ +/*************************************************************************/ +/* 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 = Quaternion::from_euler(Vector3(0.0, yaw, 0.0)); + Quaternion q_p = Quaternion::from_euler(Vector3(pitch, 0.0, 0.0)); + Quaternion q_r = Quaternion::from_euler(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 = Quaternion::from_euler(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 = Quaternion::from_euler(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 = Quaternion::from_euler(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 = Quaternion::from_euler(euler_y); + Vector3 euler_p(pitch, 0.0, 0.0); + Quaternion q_p = Quaternion::from_euler(euler_p); + Vector3 euler_r(0.0, 0.0, roll); + Quaternion q_r = Quaternion::from_euler(euler_r); + + // Instrinsically, Yaw-Y then Pitch-X then Roll-Z. + // Extrinsically, Roll-Z is followed by Pitch-X, then Yaw-Y. + Quaternion check_yxz = q_y * q_p * q_r; + + // Test construction from YXZ Euler angles. + Vector3 euler_yxz(pitch, yaw, roll); + Quaternion q = Quaternion::from_euler(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])); + + CHECK(q.is_equal_approx(check_yxz)); + CHECK(q.get_euler().is_equal_approx(euler_yxz)); + CHECK(check_yxz.get_euler().is_equal_approx(euler_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 = Quaternion::from_euler(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 = Quaternion::from_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] Get Euler Orders") { + double x = Math::deg_to_rad(30.0); + double y = Math::deg_to_rad(45.0); + double z = Math::deg_to_rad(10.0); + Vector3 euler(x, y, z); + for (int i = 0; i < 6; i++) { + EulerOrder order = (EulerOrder)i; + Basis basis = Basis::from_euler(euler, order); + Quaternion q = Quaternion(basis); + Vector3 check = q.get_euler(order); + CHECK_MESSAGE(check.is_equal_approx(euler), + "Quaternion get_euler method should return the original angles."); + CHECK_MESSAGE(check.is_equal_approx(basis.get_euler(order)), + "Quaternion get_euler method should behave the same as Basis get_euler."); + } +} + +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 = Quaternion::from_euler(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 = Quaternion::from_euler(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 = Quaternion::from_euler(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_rect2.h b/tests/core/math/test_rect2.h index 0b1106ac3c..9984823331 100644 --- a/tests/core/math/test_rect2.h +++ b/tests/core/math/test_rect2.h @@ -102,33 +102,33 @@ TEST_CASE("[Rect2] Basic setters") { TEST_CASE("[Rect2] Area getters") { CHECK_MESSAGE( - Math::is_equal_approx(Rect2(0, 100, 1280, 720).get_area(), 921'600), + Rect2(0, 100, 1280, 720).get_area() == doctest::Approx(921'600), "get_area() should return the expected value."); CHECK_MESSAGE( - Math::is_equal_approx(Rect2(0, 100, -1280, -720).get_area(), 921'600), + Rect2(0, 100, -1280, -720).get_area() == doctest::Approx(921'600), "get_area() should return the expected value."); CHECK_MESSAGE( - Math::is_equal_approx(Rect2(0, 100, 1280, -720).get_area(), -921'600), + Rect2(0, 100, 1280, -720).get_area() == doctest::Approx(-921'600), "get_area() should return the expected value."); CHECK_MESSAGE( - Math::is_equal_approx(Rect2(0, 100, -1280, 720).get_area(), -921'600), + Rect2(0, 100, -1280, 720).get_area() == doctest::Approx(-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_no_area(), - "has_no_area() should return the expected value on Rect2 with an area."); + 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_no_area(), - "has_no_area() should return the expected value on Rect2 with no area."); + !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_no_area(), - "has_no_area() should return the expected value on Rect2 with no area."); + !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_no_area(), - "has_no_area() should return the expected value on Rect2 with no area."); + !Rect2(0, 100, 0, 0).has_area(), + "has_area() should return the expected value on Rect2 with no area."); } TEST_CASE("[Rect2] Absolute coordinates") { @@ -300,6 +300,27 @@ TEST_CASE("[Rect2] Merging") { 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 index 0d1a088a66..4005300e1f 100644 --- a/tests/core/math/test_rect2i.h +++ b/tests/core/math/test_rect2i.h @@ -118,17 +118,17 @@ TEST_CASE("[Rect2i] Area getters") { "get_area() should return the expected value."); CHECK_MESSAGE( - !Rect2i(0, 100, 1280, 720).has_no_area(), - "has_no_area() should return the expected value on Rect2i with an area."); + 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_no_area(), - "has_no_area() should return the expected value on Rect2i with no area."); + !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_no_area(), - "has_no_area() should return the expected value on Rect2i with no area."); + !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_no_area(), - "has_no_area() should return the expected value on Rect2i with no area."); + !Rect2i(0, 100, 0, 0).has_area(), + "has_area() should return the expected value on Rect2i with no area."); } TEST_CASE("[Rect2i] Absolute coordinates") { 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 index 9b7800164a..8f8fccd717 100644 --- a/tests/core/math/test_vector2.h +++ b/tests/core/math/test_vector2.h @@ -37,20 +37,28 @@ 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), + vector_x.angle_to(vector_y) == doctest::Approx((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), + vector_y.angle_to(vector_x) == doctest::Approx((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), + vector_x.angle_to_point(vector_y) == doctest::Approx((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), + vector_y.angle_to_point(vector_x) == doctest::Approx((real_t)-Math_TAU / 8), "Vector2 angle_to_point should work as expected."); } @@ -102,10 +110,13 @@ TEST_CASE("[Vector2] Interpolation methods") { 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( - Math::is_equal_approx(vector1.slerp(vector2, 0.5).length(), (real_t)4.31959610746631919), + 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( + vector1.slerp(vector2, 0.5).length() == doctest::Approx((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)), + vector1.angle_to(vector1.slerp(vector2, 0.5)) * 2 == doctest::Approx(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), @@ -125,19 +136,19 @@ TEST_CASE("[Vector2] Length methods") { 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), + vector1.length() == doctest::Approx(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() == doctest::Approx((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), + vector1.distance_to(vector2) == doctest::Approx((real_t)22.36067977499789696409), "Vector2 distance_to should work as expected."); } @@ -171,6 +182,15 @@ TEST_CASE("[Vector2] Normalization methods") { 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") { @@ -274,14 +294,16 @@ TEST_CASE("[Vector2] Operators") { 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), + vector.aspect() == doctest::Approx((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."); @@ -294,10 +316,21 @@ TEST_CASE("[Vector2] Other methods") { 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( @@ -306,23 +339,57 @@ TEST_CASE("[Vector2] Other methods") { 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 X axis should only give the Y component."); + "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-normalized input."); + CHECK_MESSAGE( + vector.reflect(vector_non_normal).is_equal_approx(Vector2()), + "Vector2 reflect should return empty Vector2 with non-normalized input."); + CHECK_MESSAGE( + vector.slide(vector_non_normal).is_equal_approx(Vector2()), + "Vector2 slide should return empty Vector2 with non-normalized input."); + ERR_PRINT_ON; } TEST_CASE("[Vector2] Rounding methods") { @@ -367,12 +434,20 @@ TEST_CASE("[Vector2] Rounding methods") { 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( + a.cross(b) == doctest::Approx((real_t)-28.1), + "Vector2 cross should return expected value."); + CHECK_MESSAGE( + Vector2(-a.x, a.y).cross(Vector2(b.x, -b.y)) == doctest::Approx((real_t)-28.1), + "Vector2 cross should return expected value."); CHECK_MESSAGE( vector_x.dot(vector_y) == 0.0, @@ -383,7 +458,39 @@ TEST_CASE("[Vector2] Linear algebra methods") { CHECK_MESSAGE( (vector_x * 10).dot(vector_x * 10) == 100.0, "Vector2 dot product of same direction vectors should behave as expected."); + CHECK_MESSAGE( + a.dot(b) == doctest::Approx((real_t)57.3), + "Vector2 dot should return expected value."); + CHECK_MESSAGE( + Vector2(-a.x, a.y).dot(Vector2(b.x, -b.y)) == doctest::Approx((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 index 841bb793a4..9ee844ffa8 100644 --- a/tests/core/math/test_vector2i.h +++ b/tests/core/math/test_vector2i.h @@ -37,6 +37,14 @@ 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( @@ -71,13 +79,13 @@ TEST_CASE("[Vector2i] Length methods") { 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), + vector1.length() == doctest::Approx(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), + vector2.length() == doctest::Approx(36.05551275463989293119), "Vector2i length should work as expected."); } @@ -119,8 +127,20 @@ TEST_CASE("[Vector2i] Operators") { 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), + vector.aspect() == doctest::Approx((real_t)1.0 / (real_t)3.0), "Vector2i aspect should work as expected."); + + CHECK_MESSAGE( + vector.min(Vector2i(3, 2)) == Vector2i(1, 2), + "Vector2i min should return expected value."); + + CHECK_MESSAGE( + vector.max(Vector2i(5, 2)) == Vector2i(5, 3), + "Vector2i max should return expected value."); + + CHECK_MESSAGE( + vector.snapped(Vector2i(4, 2)) == Vector2i(0, 4), + "Vector2i snapped should work as expected."); } TEST_CASE("[Vector2i] Abs and sign methods") { diff --git a/tests/core/math/test_vector3.h b/tests/core/math/test_vector3.h index 6f99fada2b..89d73ee6de 100644 --- a/tests/core/math/test_vector3.h +++ b/tests/core/math/test_vector3.h @@ -39,31 +39,39 @@ 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), + vector_x.angle_to(vector_y) == doctest::Approx((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), + vector_x.angle_to(vector_yz) == doctest::Approx((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), + vector_yz.angle_to(vector_x) == doctest::Approx((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), + vector_y.angle_to(vector_yz) == doctest::Approx((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), + vector_x.signed_angle_to(vector_y, vector_y) == doctest::Approx((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), + vector_x.signed_angle_to(vector_yz, vector_y) == doctest::Approx((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), + vector_yz.signed_angle_to(vector_x, vector_y) == doctest::Approx((real_t)Math_TAU / 4), "Vector3 signed_angle_to should work as expected."); } @@ -76,16 +84,12 @@ TEST_CASE("[Vector3] Axis methods") { vector.min_axis_index() == Vector3::Axis::AXIS_X, "Vector3 min_axis_index should work as expected."); CHECK_MESSAGE( - vector.get_axis(vector.max_axis_index()) == (real_t)5.6, - "Vector3 get_axis should work as expected."); + 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.set_axis(Vector3::Axis::AXIS_Y, 4.7); - CHECK_MESSAGE( - vector.get_axis(Vector3::Axis::AXIS_Y) == (real_t)4.7, - "Vector3 set_axis should work as expected."); vector[Vector3::Axis::AXIS_Y] = 3.7; CHECK_MESSAGE( vector[Vector3::Axis::AXIS_Y] == (real_t)3.7, @@ -123,10 +127,13 @@ TEST_CASE("[Vector3] Interpolation methods") { 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( - Math::is_equal_approx(vector1.slerp(vector2, 0.5).length(), (real_t)6.25831088708303172), + 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( + vector1.slerp(vector2, 0.5).length() == doctest::Approx((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)), + vector1.angle_to(vector1.slerp(vector2, 0.5)) * 2 == doctest::Approx(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), @@ -146,19 +153,19 @@ TEST_CASE("[Vector3] Length methods") { 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), + vector1.length() == doctest::Approx(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), + vector2.length() == doctest::Approx((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), + vector1.distance_to(vector2) == doctest::Approx((real_t)37.41657386773941385584), "Vector3 distance_to should work as expected."); } @@ -195,6 +202,15 @@ TEST_CASE("[Vector3] Normalization methods") { 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") { @@ -318,10 +334,21 @@ TEST_CASE("[Vector3] Other methods") { 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( @@ -332,18 +359,44 @@ TEST_CASE("[Vector3] Other methods") { 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 X axis should only give the Y component."); + "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-normalized input."); + CHECK_MESSAGE( + vector.reflect(vector_non_normal).is_equal_approx(Vector3()), + "Vector3 reflect should return empty Vector3 with non-normalized input."); + CHECK_MESSAGE( + vector.slide(vector_non_normal).is_equal_approx(Vector3()), + "Vector3 slide should return empty Vector3 with non-normalized input."); + ERR_PRINT_ON; } TEST_CASE("[Vector3] Rounding methods") { @@ -389,6 +442,8 @@ 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."); @@ -401,6 +456,12 @@ TEST_CASE("[Vector3] Linear algebra methods") { 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, @@ -411,7 +472,58 @@ TEST_CASE("[Vector3] Linear algebra methods") { CHECK_MESSAGE( (vector_x * 10).dot(vector_x * 10) == 100.0, "Vector3 dot product of same direction vectors should behave as expected."); + CHECK_MESSAGE( + a.dot(b) == doctest::Approx((real_t)75.24), + "Vector3 dot should return expected value."); + CHECK_MESSAGE( + Vector3(-a.x, a.y, -a.z).dot(Vector3(b.x, -b.y, b.z)) == doctest::Approx((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 index b1c6944eba..45240bd2ff 100644 --- a/tests/core/math/test_vector3i.h +++ b/tests/core/math/test_vector3i.h @@ -36,6 +36,14 @@ 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( @@ -45,16 +53,12 @@ TEST_CASE("[Vector3i] Axis methods") { vector.min_axis_index() == Vector3i::Axis::AXIS_X, "Vector3i min_axis_index should work as expected."); CHECK_MESSAGE( - vector.get_axis(vector.max_axis_index()) == 3, - "Vector3i get_axis should work as expected."); + 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.set_axis(Vector3i::Axis::AXIS_Y, 4); - CHECK_MESSAGE( - vector.get_axis(Vector3i::Axis::AXIS_Y) == 4, - "Vector3i set_axis should work as expected."); vector[Vector3i::Axis::AXIS_Y] = 5; CHECK_MESSAGE( vector[Vector3i::Axis::AXIS_Y] == 5, @@ -78,13 +82,13 @@ TEST_CASE("[Vector3i] Length methods") { 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), + vector1.length() == doctest::Approx(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), + vector2.length() == doctest::Approx(53.8516480713450403125), "Vector3i length should work as expected."); } @@ -123,6 +127,14 @@ TEST_CASE("[Vector3i] Operators") { "Vector3i constructed from Vector3 should work as expected."); } +TEST_CASE("[Vector3i] Other methods") { + const Vector3i vector = Vector3i(1, 3, -7); + + CHECK_MESSAGE( + vector.snapped(Vector3i(4, 2, 5)) == Vector3i(0, 4, -5), + "Vector3i snapped 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); diff --git a/tests/core/math/test_vector4.h b/tests/core/math/test_vector4.h new file mode 100644 index 0000000000..6ed85661cb --- /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( + vector1.length() == doctest::Approx(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( + vector2.length() == doctest::Approx((real_t)73.484692283495), + "Vector4 length should work as expected."); + CHECK_MESSAGE( + vector1.distance_to(vector2) == doctest::Approx((real_t)54.772255750517), + "Vector4 distance_to should work as expected."); + CHECK_MESSAGE( + vector1.distance_squared_to(vector2) == doctest::Approx(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( + (vector1 * 2).dot(vector2 * 4) == doctest::Approx((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..8a9522f9cc --- /dev/null +++ b/tests/core/math/test_vector4i.h @@ -0,0 +1,160 @@ +/*************************************************************************/ +/* 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( + vector1.length() == doctest::Approx(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( + vector2.length() == doctest::Approx(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("[Vector3i] Other methods") { + const Vector4i vector = Vector4i(1, 3, -7, 13); + + CHECK_MESSAGE( + vector.snapped(Vector4i(4, 2, 5, 8)) == Vector4i(0, 4, -5, 16), + "Vector4i snapped 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/core/object/test_class_db.h b/tests/core/object/test_class_db.h index 8aaca69d13..b0375c63b9 100644 --- a/tests/core/object/test_class_db.h +++ b/tests/core/object/test_class_db.h @@ -46,7 +46,7 @@ struct TypeReference { struct ConstantData { String name; - int value = 0; + int64_t value = 0; }; struct EnumData { @@ -71,6 +71,7 @@ struct ArgumentData { String name; bool has_defval = false; Variant defval; + int position; }; struct MethodData { @@ -371,6 +372,39 @@ void validate_property(const Context &p_context, const ExposedClass &p_class, co } } +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)); + } + + 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); + + 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; + } + + 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); @@ -392,54 +426,14 @@ void validate_method(const Context &p_context, const ExposedClass &p_class, cons for (const ArgumentData &F : p_method.arguments) { const ArgumentData &arg = F; - - 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, "'."); - - if (p_class.api_type == ClassDB::API_CORE) { - TEST_COND(arg_class->api_type == ClassDB::API_EDITOR, - "Argument '", arg.name, "' of method '", p_class.name, ".", p_method.name, "' has type '", - arg_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(arg.type), - "Argument type '", arg.type.name, "' not found: '", arg.name, "' of method", p_class.name, ".", p_method.name, "'."); - } - - 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.is_empty()) { - err_msg += " " + type_error_msg; - } - TEST_COND(!arg_defval_assignable_to_type, err_msg.utf8().get_data()); - } + 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 ArgumentData &F : p_signal.arguments) { const ArgumentData &arg = F; - - 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, "'."); - - if (p_class.api_type == ClassDB::API_CORE) { - TEST_COND(arg_class->api_type == ClassDB::API_EDITOR, - "Argument '", arg.name, "' of signal '", p_class.name, ".", p_signal.name, "' has type '", - arg_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(arg.type), - "Argument type '", arg.type.name, "' not found: '", arg.name, "' of signal", p_class.name, ".", p_signal.name, "'."); - } + validate_argument(p_context, p_class, p_signal.name, "signal", arg); } } @@ -493,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; } @@ -597,7 +591,7 @@ 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()) { @@ -625,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()) { @@ -671,6 +666,10 @@ 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 @@ -693,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()) { @@ -732,18 +732,18 @@ 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; + const HashMap<StringName, ClassDB::ClassInfo::EnumInfo> &enum_map = class_info->enum_map; - for (const KeyValue<StringName, List<StringName>> &K : enum_map) { + for (const KeyValue<StringName, ClassDB::ClassInfo::EnumInfo> &K : enum_map) { EnumData enum_; enum_.name = K.key; - for (const StringName &E : K.value) { + 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), "'."); - int *value = class_info->constant_map.getptr(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); @@ -765,7 +765,7 @@ void add_exposed_classes(Context &r_context) { TEST_FAIL_COND(constant_name.find("::") != -1, "Constant contains '::', check bindings to remove the scope: '", String(class_name), ".", constant_name, "'."); - int *value = class_info->constant_map.getptr(StringName(E)); + 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; @@ -841,7 +841,7 @@ 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(), diff --git a/tests/core/object/test_method_bind.h b/tests/core/object/test_method_bind.h index 350a08b6e2..2fe0c264a1 100644 --- a/tests/core/object/test_method_bind.h +++ b/tests/core/object/test_method_bind.h @@ -155,7 +155,6 @@ public: TEST_CASE("[MethodBind] check all method binds") { MethodBindTester *mbt = memnew(MethodBindTester); - print_line("testing method bind"); mbt->run_tests(); CHECK(mbt->test_valid[MethodBindTester::TEST_METHOD]); diff --git a/tests/core/object/test_object.h b/tests/core/object/test_object.h index 5b9d9cab53..f5c5de7fdf 100644 --- a/tests/core/object/test_object.h +++ b/tests/core/object/test_object.h @@ -82,6 +82,12 @@ public: 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 { @@ -95,8 +101,8 @@ public: Ref<Script> get_script() const override { return Ref<Script>(); } - const Vector<Multiplayer::RPCConfig> get_rpc_methods() const override { - return Vector<Multiplayer::RPCConfig>(); + const Variant get_rpc_config() const override { + return Variant(); } ScriptLanguage *get_language() override { return nullptr; diff --git a/tests/core/os/test_os.h b/tests/core/os/test_os.h new file mode 100644 index 0000000000..086f0b9b0c --- /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("template"), + "The binary does not have the \"template\" feature tag."); + CHECK_MESSAGE( + !OS::get_singleton()->has_feature("template_debug"), + "The binary does not have the \"template_debug\" feature tag."); + CHECK_MESSAGE( + !OS::get_singleton()->has_feature("template_release"), + "The binary does not have the \"template_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_string.h b/tests/core/string/test_string.h index 0b191d2d94..659d451d76 100644 --- a/tests/core/string/test_string.h +++ b/tests/core/string/test_string.h @@ -89,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; @@ -106,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; @@ -123,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; @@ -137,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; @@ -152,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.is_empty()); + 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).is_empty()); + 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.is_empty()); + Error err = s.parse_utf16(u16str); + CHECK(err == ERR_PARSE_ERROR); + CHECK(s == u32str); Char16String cs = u16str; - CHECK(String::utf16(cs).is_empty()); + CHECK(String::utf16(cs) == s); ERR_PRINT_ON } @@ -193,6 +226,12 @@ TEST_CASE("[String] Comparisons (equal)") { CHECK(s == U"Test Compare"); CHECK(s == L"Test Compare"); CHECK(s == String("Test Compare")); + + CharString empty = ""; + CharString cs = "Test Compare"; + CHECK(!(empty == cs)); + CHECK(!(cs == empty)); + CHECK(cs == CharString("Test Compare")); } TEST_CASE("[String] Comparisons (not equal)") { @@ -262,8 +301,8 @@ 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 } @@ -378,9 +417,13 @@ TEST_CASE("[String] Number to string") { 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 @@ -433,11 +476,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"; @@ -453,6 +491,7 @@ TEST_CASE("[String] Splitting") { const char *slices_l[3] = { "Mars", "Jupiter", "Saturn,Uranus" }; const char *slices_r[3] = { "Mars,Jupiter", "Saturn", "Uranus" }; + const char *slices_3[4] = { "t", "e", "s", "t" }; l = s.split(",", true, 2); CHECK(l.size() == 3); @@ -466,6 +505,13 @@ TEST_CASE("[String] Splitting") { CHECK(l[i] == slices_r[i]); } + s = "test"; + l = s.split(); + CHECK(l.size() == 4); + for (int i = 0; i < l.size(); i++) { + CHECK(l[i] == slices_3[i]); + } + s = "Mars Jupiter Saturn Uranus"; const char *slices_s[4] = { "Mars", "Jupiter", "Saturn", "Uranus" }; l = s.split_spaces(); @@ -476,21 +522,22 @@ TEST_CASE("[String] Splitting") { s = "1.2;2.3 4.5"; const double slices_d[3] = { 1.2, 2.3, 4.5 }; - Vector<float> f; - f = s.split_floats(";"); - CHECK(f.size() == 2); - for (int i = 0; i < f.size(); i++) { - CHECK(ABS(f[i] - slices_d[i]) <= 0.00001); + Vector<double> d_arr; + d_arr = s.split_floats(";"); + CHECK(d_arr.size() == 2); + for (int i = 0; i < d_arr.size(); i++) { + CHECK(ABS(d_arr[i] - slices_d[i]) <= 0.00001); } Vector<String> keys; keys.push_back(";"); keys.push_back(" "); - f = s.split_floats_mk(keys); - CHECK(f.size() == 3); - for (int i = 0; i < f.size(); i++) { - CHECK(ABS(f[i] - slices_d[i]) <= 0.00001); + Vector<float> f_arr; + f_arr = s.split_floats_mk(keys); + CHECK(f_arr.size() == 3); + for (int i = 0; i < f_arr.size(); i++) { + CHECK(ABS(f_arr[i] - slices_d[i]) <= 0.00001); } s = "1;2 4"; @@ -586,7 +633,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"; @@ -664,7 +711,9 @@ TEST_CASE("[String] sprintf") { 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")); @@ -692,7 +741,7 @@ TEST_CASE("[String] sprintf") { REQUIRE(error == false); CHECK(output == String("fish 143 frog")); - ////// REALS + ///// Reals // Real format = "fish %f frog"; @@ -702,7 +751,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); @@ -710,7 +759,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); @@ -734,7 +791,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); @@ -762,11 +819,111 @@ TEST_CASE("[String] sprintf") { 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")); - /////// Strings. + ///// 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"; @@ -776,7 +933,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"); @@ -784,7 +941,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"); @@ -812,7 +969,7 @@ TEST_CASE("[String] sprintf") { ///// Dynamic width - // String dynamic width + // String dynamic width. format = "fish %*s frog"; args.clear(); args.push_back(10); @@ -821,7 +978,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); @@ -830,7 +987,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); @@ -867,7 +1024,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"); @@ -883,14 +1040,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"; @@ -969,8 +1126,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"; @@ -985,10 +1170,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); @@ -1026,6 +1207,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; @@ -1125,9 +1350,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(nullptr, -1)); + CHECK(empty.parse_utf8(nullptr, -1) == ERR_INVALID_DATA); } TEST_CASE("[String] Cyrillic to_lower()") { @@ -1218,21 +1443,23 @@ TEST_CASE("[String] dedent") { } TEST_CASE("[String] Path functions") { - static const char *path[7] = { "C:\\Godot\\project\\test.tscn", "/Godot/project/test.xscn", "../Godot/project/test.scn", "Godot\\test.doc", "C:\\test.", "res://test", "/.test" }; - static const char *base_dir[7] = { "C:\\Godot\\project", "/Godot/project", "../Godot/project", "Godot", "C:\\", "res://", "/" }; - static const char *base_name[7] = { "C:\\Godot\\project\\test", "/Godot/project/test", "../Godot/project/test", "Godot\\test", "C:\\test", "res://test", "/" }; - static const char *ext[7] = { "tscn", "xscn", "scn", "doc", "", "", "test" }; - static const char *file[7] = { "test.tscn", "test.xscn", "test.scn", "test.doc", "test.", "test", ".test" }; - static const bool abs[7] = { true, true, false, false, true, true, true }; - - for (int i = 0; i < 7; 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_absolute_path() == abs[i]); CHECK(String(path[i]).is_relative_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]).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" }; @@ -1440,8 +1667,8 @@ TEST_CASE("[String] validate_node_name") { String name_with_spaces = "Name with spaces"; CHECK(name_with_spaces.validate_node_name() == "Name with spaces"); - String name_with_kana = "Name with kana ゴドツ"; - CHECK(name_with_kana.validate_node_name() == "Name with kana ゴドツ"); + 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!"); @@ -1536,7 +1763,7 @@ TEST_CASE("[String] Variant ptr indexed set") { TEST_CASE("[Stress][String] Empty via ' == String()'") { for (int i = 0; i < 100000; ++i) { String str = "Hello World!"; - if (str.is_empty()) { + if (str == String()) { continue; } } diff --git a/tests/core/templates/test_command_queue.h b/tests/core/templates/test_command_queue.h index 0d016f5d06..db1e436ed9 100644 --- a/tests/core/templates/test_command_queue.h +++ b/tests/core/templates/test_command_queue.h @@ -38,8 +38,6 @@ #include "core/templates/command_queue_mt.h" #include "tests/test_macros.h" -#if !defined(NO_THREADS) - namespace TestCommandQueue { class ThreadWork { @@ -426,6 +424,4 @@ TEST_CASE("[Stress][CommandQueue] Stress test command queue") { } } // namespace TestCommandQueue -#endif // !defined(NO_THREADS) - #endif // TEST_COMMAND_QUEUE_H diff --git a/tests/core/templates/test_hash_set.h b/tests/core/templates/test_hash_set.h index 93fc0b26a3..dad149604b 100644 --- a/tests/core/templates/test_hash_set.h +++ b/tests/core/templates/test_hash_set.h @@ -38,7 +38,6 @@ namespace TestHashSet { TEST_CASE("[HashSet] Insert element") { - print_line("SMALL BEGIN MEM: ", Memory::get_mem_usage()); HashSet<int> set; HashSet<int>::Iterator e = set.insert(42); @@ -47,7 +46,6 @@ TEST_CASE("[HashSet] Insert element") { CHECK(set.has(42)); CHECK(set.find(42)); set.reset(); - print_line("SMALL END MEM: ", Memory::get_mem_usage()); } TEST_CASE("[HashSet] Insert existing element") { @@ -225,4 +223,4 @@ TEST_CASE("[HashSet] Copy") { } // namespace TestHashSet -#endif // TEST_HASH_MAP_H +#endif // TEST_HASH_SET_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 index f27d6a332e..3fc9264f5a 100644 --- a/tests/core/templates/test_vector.h +++ b/tests/core/templates/test_vector.h @@ -291,8 +291,10 @@ TEST_CASE("[Vector] Slice") { CHECK(slice6[1] == 3); CHECK(slice6[2] == 4); + ERR_PRINT_OFF; Vector<int> slice7 = vector.slice(5, 1); - CHECK(slice7.size() == 0); + CHECK(slice7.size() == 0); // Expected to fail. + ERR_PRINT_ON; } TEST_CASE("[Vector] Find, has") { 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_dictionary.h b/tests/core/variant/test_dictionary.h index 729035919d..0c87f11ed7 100644 --- a/tests/core/variant/test_dictionary.h +++ b/tests/core/variant/test_dictionary.h @@ -64,6 +64,19 @@ TEST_CASE("[Dictionary] Assignment using bracket notation ([])") { map["World!"] = 4; CHECK(int(map["World!"]) == 4); + map[StringName("HelloName")] = 6; + CHECK(int(map[StringName("HelloName")]) == 6); + // Check that StringName key is converted to String. + CHECK(int(map.find_key(6).get_type()) == Variant::STRING); + map[StringName("HelloName")] = 7; + CHECK(int(map[StringName("HelloName")]) == 7); + + // Test String and StringName are equivalent. + map[StringName("Hello")] = 8; + CHECK(int(map["Hello"]) == 8); + map["Hello"] = 9; + CHECK(int(map[StringName("Hello")]) == 9); + // Test non-string keys, since keys can be of any Variant type. map[12345] = -5; CHECK(int(map[12345]) == -5); @@ -500,6 +513,24 @@ TEST_CASE("[Dictionary] Recursive self comparison") { 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 index 916686d7c1..d6799928b4 100644 --- a/tests/core/variant/test_variant.h +++ b/tests/core/variant/test_variant.h @@ -719,6 +719,7 @@ TEST_CASE("[Variant] Assignment To Color from Bool,Int,Float,String,Vec2,Vec2i,V 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; @@ -911,6 +912,67 @@ TEST_CASE("[Variant] Nested dictionary comparison") { 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/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/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..58ddd08cdd --- /dev/null +++ b/tests/python_build/fixtures/gles3/vertex_fragment_expected_full.glsl @@ -0,0 +1,51 @@ +/* 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_ bool version_bind_shader(RID p_version,ShaderVariant p_variant,uint64_t p_specialization=0) { return _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 Feedback* _feedbacks=nullptr; + 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,_feedbacks,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..5ac8092ad0 --- /dev/null +++ b/tests/python_build/fixtures/gles3/vertex_fragment_expected_parts.json @@ -0,0 +1,53 @@ +{ + "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": [], + "feedbacks": [], + "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 index 9199713fd9..e921779c23 100644 --- a/tests/scene/test_animation.h +++ b/tests/scene/test_animation.h @@ -40,8 +40,8 @@ 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))); + CHECK(animation->get_length() == doctest::Approx(real_t(1.0))); + CHECK(animation->get_step() == doctest::Approx(real_t(0.1))); } TEST_CASE("[Animation] Create value track") { @@ -59,33 +59,33 @@ TEST_CASE("[Animation] Create value track") { 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(animation->value_track_interpolate(0, -0.2) == doctest::Approx(0.0)); + CHECK(animation->value_track_interpolate(0, 0.0) == doctest::Approx(0.0)); + CHECK(animation->value_track_interpolate(0, 0.2) == doctest::Approx(40.0)); + CHECK(animation->value_track_interpolate(0, 0.4) == doctest::Approx(80.0)); + CHECK(animation->value_track_interpolate(0, 0.5) == doctest::Approx(100.0)); + CHECK(animation->value_track_interpolate(0, 0.6) == doctest::Approx(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))); + CHECK(animation->track_get_key_transition(0, 0) == doctest::Approx(real_t(1.0))); + CHECK(animation->track_get_key_transition(0, 1) == doctest::Approx(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))); + CHECK(animation->track_get_key_transition(0, 2) == doctest::Approx(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))); + CHECK(animation->track_get_key_transition(1, 0) == doctest::Approx(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->bezier_track_interpolate(0, 0.0) == doctest::Approx(0.0)); CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); ERR_PRINT_ON; } @@ -123,15 +123,15 @@ TEST_CASE("[Animation] Create 3D position track") { 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))); + CHECK(animation->track_get_key_transition(0, 0) == doctest::Approx(real_t(1.0))); + CHECK(animation->track_get_key_transition(0, 1) == doctest::Approx(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->bezier_track_interpolate(0, 0.0) == doctest::Approx(0.0)); CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); ERR_PRINT_ON; } @@ -140,13 +140,13 @@ 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))); + animation->rotation_track_insert_key(track_index, 0.0, Quaternion::from_euler(Vector3(0, 1, 2))); + animation->rotation_track_insert_key(track_index, 0.5, Quaternion::from_euler(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)))); + CHECK(Quaternion(animation->track_get_key_value(0, 0)).is_equal_approx(Quaternion::from_euler(Vector3(0, 1, 2)))); + CHECK(Quaternion(animation->track_get_key_value(0, 1)).is_equal_approx(Quaternion::from_euler(Vector3(3.5, 4, 5)))); Quaternion r_interpolation; @@ -169,15 +169,15 @@ TEST_CASE("[Animation] Create 3D rotation track") { 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))); + CHECK(animation->track_get_key_transition(0, 0) == doctest::Approx(real_t(1.0))); + CHECK(animation->track_get_key_transition(0, 1) == doctest::Approx(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->bezier_track_interpolate(0, 0.0) == doctest::Approx(real_t(0.0))); CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); ERR_PRINT_ON; } @@ -215,15 +215,15 @@ TEST_CASE("[Animation] Create 3D scale track") { 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))); + CHECK(animation->track_get_key_transition(0, 0) == doctest::Approx(1.0)); + CHECK(animation->track_get_key_transition(0, 1) == doctest::Approx(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->bezier_track_interpolate(0, 0.0) == doctest::Approx(0.0)); CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); ERR_PRINT_ON; } @@ -242,32 +242,32 @@ TEST_CASE("[Animation] Create blend shape track") { 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(r_blend == doctest::Approx(-1.0f)); CHECK(animation->blend_shape_track_get_key(0, 1, &r_blend) == OK); - CHECK(Math::is_equal_approx(r_blend, 1.0f)); + CHECK(r_blend == doctest::Approx(1.0f)); CHECK(animation->blend_shape_track_interpolate(0, -0.2, &r_blend) == OK); - CHECK(Math::is_equal_approx(r_blend, -1.0f)); + CHECK(r_blend == doctest::Approx(-1.0f)); CHECK(animation->blend_shape_track_interpolate(0, 0.0, &r_blend) == OK); - CHECK(Math::is_equal_approx(r_blend, -1.0f)); + CHECK(r_blend == doctest::Approx(-1.0f)); CHECK(animation->blend_shape_track_interpolate(0, 0.2, &r_blend) == OK); - CHECK(Math::is_equal_approx(r_blend, -0.2f)); + CHECK(r_blend == doctest::Approx(-0.2f)); CHECK(animation->blend_shape_track_interpolate(0, 0.4, &r_blend) == OK); - CHECK(Math::is_equal_approx(r_blend, 0.6f)); + CHECK(r_blend == doctest::Approx(0.6f)); CHECK(animation->blend_shape_track_interpolate(0, 0.5, &r_blend) == OK); - CHECK(Math::is_equal_approx(r_blend, 1.0f)); + CHECK(r_blend == doctest::Approx(1.0f)); CHECK(animation->blend_shape_track_interpolate(0, 0.6, &r_blend) == OK); - CHECK(Math::is_equal_approx(r_blend, 1.0f)); + CHECK(r_blend == doctest::Approx(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))); + CHECK(animation->track_get_key_transition(0, 0) == doctest::Approx(real_t(1.0))); + CHECK(animation->track_get_key_transition(0, 1) == doctest::Approx(real_t(1.0))); // This is a blend shape track, so the methods below should return errors. ERR_PRINT_OFF; @@ -275,7 +275,7 @@ TEST_CASE("[Animation] Create blend shape track") { 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->bezier_track_interpolate(0, 0.0) == doctest::Approx(0.0)); ERR_PRINT_ON; } @@ -289,15 +289,15 @@ TEST_CASE("[Animation] Create Bezier track") { 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(animation->bezier_track_get_key_value(0, 0) == doctest::Approx(real_t(-1.0))); + CHECK(animation->bezier_track_get_key_value(0, 1) == doctest::Approx(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))); + CHECK(animation->bezier_track_interpolate(0, -0.2) == doctest::Approx(real_t(-1.0))); + CHECK(animation->bezier_track_interpolate(0, 0.0) == doctest::Approx(real_t(-1.0))); + CHECK(animation->bezier_track_interpolate(0, 0.2) == doctest::Approx(real_t(-0.76057207584381))); + CHECK(animation->bezier_track_interpolate(0, 0.4) == doctest::Approx(real_t(-0.39975279569626))); + CHECK(animation->bezier_track_interpolate(0, 0.5) == doctest::Approx(real_t(1.0))); + CHECK(animation->bezier_track_interpolate(0, 0.6) == doctest::Approx(real_t(1.0))); // This is a bezier track, so the methods below should return errors. ERR_PRINT_OFF; 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..c84c66b0e6 --- /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(stream->get_length() == doctest::Approx(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..dc47bd7863 --- /dev/null +++ b/tests/scene/test_bit_map.h @@ -0,0 +1,472 @@ +/*************************************************************************/ +/* 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 uninitialized 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 uninitialized blit bit map and uninitialized bit map does not crash + bit_map.blit(blit_pos, blit_bit_map); + + // Testing if uninitialized bit map does not crash + blit_bit_map->create(blit_size); + bit_map.blit(blit_pos, blit_bit_map); + + // Testing if uninitialized 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 initialized 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"); + + reset_bit_map(bit_map); + bit_map.set_bit_rect(Rect2i(0, 0, 64, 64), true); + bit_map.set_bit_rect(Rect2i(64, 64, 64, 64), true); + bit_map.set_bit_rect(Rect2i(192, 128, 64, 64), true); + bit_map.set_bit_rect(Rect2i(128, 192, 64, 64), true); + polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 256, 256)); + CHECK_MESSAGE(polygons.size() == 4, "We should have exactly 4 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"); + CHECK_MESSAGE(polygons[2].size() == 4, "The polygon should have exactly 4 points"); + CHECK_MESSAGE(polygons[3].size() == 4, "The polygon should have exactly 4 points"); + + reset_bit_map(bit_map); + bit_map.set_bit(0, 0, true); + bit_map.set_bit(2, 0, true); + bit_map.set_bit_rect(Rect2i(1, 1, 1, 2), true); + polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 3, 3)); + CHECK_MESSAGE(polygons.size() == 3, "We should have exactly 3 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"); + CHECK_MESSAGE(polygons[2].size() == 4, "The polygon should have exactly 4 points"); + + reset_bit_map(bit_map); + bit_map.set_bit_rect(Rect2i(0, 0, 2, 1), true); + bit_map.set_bit_rect(Rect2i(0, 2, 3, 1), true); + bit_map.set_bit(0, 1, true); + bit_map.set_bit(2, 1, true); + polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 4, 4)); + 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 index d28380d056..3940bdb37a 100644 --- a/tests/scene/test_code_edit.h +++ b/tests/scene/test_code_edit.h @@ -74,7 +74,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { code_edit->set_line_as_breakpoint(0, true); CHECK(code_edit->is_line_breakpointed(0)); - CHECK(code_edit->get_breakpointed_lines()[0] == Variant(0)); + CHECK(code_edit->get_breakpointed_lines()[0] == 0); SIGNAL_CHECK("breakpoint_toggled", args); code_edit->set_line_as_breakpoint(0, false); @@ -451,7 +451,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { ERR_PRINT_ON; code_edit->set_line_as_bookmarked(0, true); - CHECK(code_edit->get_bookmarked_lines()[0] == Variant(0)); + CHECK(code_edit->get_bookmarked_lines()[0] == 0); CHECK(code_edit->is_line_bookmarked(0)); code_edit->set_line_as_bookmarked(0, false); @@ -657,7 +657,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { ERR_PRINT_ON; code_edit->set_line_as_executing(0, true); - CHECK(code_edit->get_executing_lines()[0] == Variant(0)); + CHECK(code_edit->get_executing_lines()[0] == 0); CHECK(code_edit->is_line_executing(0)); code_edit->set_line_as_executing(0, false); @@ -3245,13 +3245,13 @@ TEST_CASE("[SceneTree][CodeEdit] symbol lookup") { code_edit->set_text("this is some text"); Point2 caret_pos = code_edit->get_caret_draw_pos(); - caret_pos.x += 58; + 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 OSX_ENABLED +#ifdef MACOS_ENABLED SEND_GUI_KEY_EVENT(code_edit, Key::META); #else SEND_GUI_KEY_EVENT(code_edit, Key::CTRL); diff --git a/tests/scene/test_curve.h b/tests/scene/test_curve.h index 0370ab15fd..4afd5b293f 100644 --- a/tests/scene/test_curve.h +++ b/tests/scene/test_curve.h @@ -31,6 +31,7 @@ #ifndef TEST_CURVE_H #define TEST_CURVE_H +#include "core/math/math_funcs.h" #include "scene/resources/curve.h" #include "tests/test_macros.h" @@ -44,13 +45,13 @@ TEST_CASE("[Curve] Default curve") { curve->get_point_count() == 0, "Default curve should contain the expected number of points."); CHECK_MESSAGE( - Math::is_zero_approx(curve->interpolate(0)), + 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->interpolate(0.5)), + 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->interpolate(1)), + Math::is_zero_approx(curve->sample(1)), "Default curve should return the expected value at offset 1.0."); } @@ -80,57 +81,57 @@ TEST_CASE("[Curve] Custom curve with free tangents") { "Custom free curve should contain the expected number of points."); CHECK_MESSAGE( - Math::is_zero_approx(curve->interpolate(-0.1)), + 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->interpolate(0.1), (real_t)0.352), + curve->sample(0.1) == doctest::Approx((real_t)0.352), "Custom free curve should return the expected value at offset 0.1."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate(0.4), (real_t)0.352), + curve->sample(0.4) == doctest::Approx((real_t)0.352), "Custom free curve should return the expected value at offset 0.1."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate(0.7), (real_t)0.896), + curve->sample(0.7) == doctest::Approx((real_t)0.896), "Custom free curve should return the expected value at offset 0.1."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate(1), 1), + curve->sample(1) == doctest::Approx(1), "Custom free curve should return the expected value at offset 0.1."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate(2), 1), + curve->sample(2) == doctest::Approx(1), "Custom free curve should return the expected value at offset 0.1."); CHECK_MESSAGE( - Math::is_zero_approx(curve->interpolate_baked(-0.1)), + 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->interpolate_baked(0.1), (real_t)0.352), + curve->sample_baked(0.1) == doctest::Approx((real_t)0.352), "Custom free curve should return the expected baked value at offset 0.1."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate_baked(0.4), (real_t)0.352), + curve->sample_baked(0.4) == doctest::Approx((real_t)0.352), "Custom free curve should return the expected baked value at offset 0.1."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate_baked(0.7), (real_t)0.896), + curve->sample_baked(0.7) == doctest::Approx((real_t)0.896), "Custom free curve should return the expected baked value at offset 0.1."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate_baked(1), 1), + curve->sample_baked(1) == doctest::Approx(1), "Custom free curve should return the expected baked value at offset 0.1."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate_baked(2), 1), + curve->sample_baked(2) == doctest::Approx(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->interpolate(0.1), 0), + curve->sample(0.1) == doctest::Approx(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->interpolate_baked(0.1), 0), + curve->sample_baked(0.1) == doctest::Approx(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->interpolate(0.6), 0), + curve->sample(0.6) == doctest::Approx(0), "Custom free curve should return the expected value at offset 0.6 after clearing all points."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate_baked(0.6), 0), + curve->sample_baked(0.6) == doctest::Approx(0), "Custom free curve should return the expected baked value at offset 0.6 after clearing all points."); } @@ -143,7 +144,7 @@ TEST_CASE("[Curve] Custom curve with linear tangents") { 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), + curve->get_point_left_tangent(3) == doctest::Approx(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)), @@ -169,51 +170,51 @@ TEST_CASE("[Curve] Custom curve with linear tangents") { "Custom linear curve should contain the expected number of points."); CHECK_MESSAGE( - Math::is_zero_approx(curve->interpolate(-0.1)), + 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->interpolate(0.1), (real_t)0.4), + curve->sample(0.1) == doctest::Approx((real_t)0.4), "Custom linear curve should return the expected value at offset 0.1."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate(0.4), (real_t)0.4), + curve->sample(0.4) == doctest::Approx((real_t)0.4), "Custom linear curve should return the expected value at offset 0.4."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate(0.7), (real_t)0.8), + curve->sample(0.7) == doctest::Approx((real_t)0.8), "Custom linear curve should return the expected value at offset 0.7."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate(1), 1), + curve->sample(1) == doctest::Approx(1), "Custom linear curve should return the expected value at offset 1.0."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate(2), 1), + curve->sample(2) == doctest::Approx(1), "Custom linear curve should return the expected value at offset 2.0."); CHECK_MESSAGE( - Math::is_zero_approx(curve->interpolate_baked(-0.1)), + 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->interpolate_baked(0.1), (real_t)0.4), + curve->sample_baked(0.1) == doctest::Approx((real_t)0.4), "Custom linear curve should return the expected baked value at offset 0.1."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate_baked(0.4), (real_t)0.4), + curve->sample_baked(0.4) == doctest::Approx((real_t)0.4), "Custom linear curve should return the expected baked value at offset 0.4."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate_baked(0.7), (real_t)0.8), + curve->sample_baked(0.7) == doctest::Approx((real_t)0.8), "Custom linear curve should return the expected baked value at offset 0.7."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate_baked(1), 1), + curve->sample_baked(1) == doctest::Approx(1), "Custom linear curve should return the expected baked value at offset 1.0."); CHECK_MESSAGE( - Math::is_equal_approx(curve->interpolate_baked(2), 1), + curve->sample_baked(2) == doctest::Approx(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->interpolate(0.7), (real_t)0.8), + curve->sample(0.7) == doctest::Approx((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->interpolate_baked(0.7), (real_t)0.8), + curve->sample_baked(0.7) == doctest::Approx((real_t)0.8), "Custom free curve should return the expected baked value at offset 0.7 after removing point at invalid index 10."); } @@ -228,8 +229,8 @@ TEST_CASE("[Curve2D] Linear sampling should return exact value") { CHECK(len == baked_length); for (int i = 0; i < len; i++) { - Vector2 pos = curve->interpolate_baked(i); - CHECK_MESSAGE(pos.x == i, "interpolate_baked should return exact value"); + Vector2 pos = curve->sample_baked(i); + CHECK_MESSAGE(Math::is_equal_approx(pos.x, i), "sample_baked should return exact value"); } } @@ -244,8 +245,8 @@ TEST_CASE("[Curve3D] Linear sampling should return exact value") { CHECK(len == baked_length); for (int i = 0; i < len; i++) { - Vector3 pos = curve->interpolate_baked(i); - CHECK_MESSAGE(pos.x == i, "interpolate_baked should return exact value"); + Vector3 pos = curve->sample_baked(i); + CHECK_MESSAGE(Math::is_equal_approx(pos.x, i), "sample_baked should return exact value"); } } diff --git a/tests/scene/test_path_2d.h b/tests/scene/test_path_2d.h new file mode 100644 index 0000000000..dc5eee4012 --- /dev/null +++ b/tests/scene/test_path_2d.h @@ -0,0 +1,109 @@ +/*************************************************************************/ +/* test_path_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_2D_H +#define TEST_PATH_2D_H + +#include "scene/2d/path_2d.h" + +#include "tests/test_macros.h" + +namespace TestPath2D { + +TEST_CASE("[SceneTree][Path2D] Initialization") { + SUBCASE("Path should be empty right after initialization") { + Path2D *test_path = memnew(Path2D); + CHECK(test_path->get_curve() == nullptr); + memdelete(test_path); + } +} + +TEST_CASE("[SceneTree][Path2D] Curve setter and getter") { + Path2D *test_path = memnew(Path2D); + const Ref<Curve2D> &curve = memnew(Curve2D); + + SUBCASE("Curve passed to the class should remain the same") { + test_path->set_curve(curve); + CHECK(test_path->get_curve() == curve); + } + SUBCASE("Curve passed many times to the class should remain the same") { + test_path->set_curve(curve); + test_path->set_curve(curve); + test_path->set_curve(curve); + CHECK(test_path->get_curve() == curve); + } + SUBCASE("Curve rewrite testing") { + const Ref<Curve2D> &curve1 = memnew(Curve2D); + const Ref<Curve2D> &curve2 = memnew(Curve2D); + + 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"); + } + + SUBCASE("Assign same curve to two paths") { + Path2D *path2 = memnew(Path2D); + + test_path->set_curve(curve); + path2->set_curve(curve); + CHECK_MESSAGE(test_path->get_curve() == path2->get_curve(), + "Both paths have the same curve."); + memdelete(path2); + } + + SUBCASE("Swapping curves between two paths") { + Path2D *path2 = memnew(Path2D); + const Ref<Curve2D> &curve1 = memnew(Curve2D); + const Ref<Curve2D> &curve2 = memnew(Curve2D); + + test_path->set_curve(curve1); + path2->set_curve(curve2); + CHECK(test_path->get_curve() == curve1); + CHECK(path2->get_curve() == curve2); + + // Do the swap + Ref<Curve2D> temp = test_path->get_curve(); + test_path->set_curve(path2->get_curve()); + path2->set_curve(temp); + + CHECK(test_path->get_curve() == curve2); + CHECK(path2->get_curve() == curve1); + memdelete(path2); + } + + memdelete(test_path); +} + +} // namespace TestPath2D + +#endif // TEST_PATH_2D_H diff --git a/tests/scene/test_path_3d.h b/tests/scene/test_path_3d.h index 78f4e97f03..8ac3d7b5b4 100644 --- a/tests/scene/test_path_3d.h +++ b/tests/scene/test_path_3d.h @@ -81,4 +81,4 @@ TEST_CASE("[Path3D] Curve setter and getter") { } // namespace TestPath3D -#endif // TEST_PATH_3D +#endif // TEST_PATH_3D_H diff --git a/tests/scene/test_path_follow_2d.h b/tests/scene/test_path_follow_2d.h index abd12fe862..57261116a2 100644 --- a/tests/scene/test_path_follow_2d.h +++ b/tests/scene/test_path_follow_2d.h @@ -37,7 +37,7 @@ namespace TestPathFollow2D { -TEST_CASE("[PathFollow2D] Sampling with unit offset") { +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)); @@ -49,37 +49,37 @@ TEST_CASE("[PathFollow2D] Sampling with unit offset") { const PathFollow2D *path_follow_2d = memnew(PathFollow2D); path->add_child(path_follow_2d); - path_follow_2d->set_unit_offset(0); + 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_unit_offset(0.125); + 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_unit_offset(0.25); + 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_unit_offset(0.375); + 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_unit_offset(0.5); + 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_unit_offset(0.625); + 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_unit_offset(0.75); + 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_unit_offset(0.875); + 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_unit_offset(1); + 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 offset") { +TEST_CASE("[PathFollow2D] Sampling with progress") { const Ref<Curve2D> &curve = memnew(Curve2D()); curve->add_point(Vector2(0, 0)); curve->add_point(Vector2(100, 0)); @@ -91,31 +91,31 @@ TEST_CASE("[PathFollow2D] Sampling with offset") { const PathFollow2D *path_follow_2d = memnew(PathFollow2D); path->add_child(path_follow_2d); - path_follow_2d->set_offset(0); + path_follow_2d->set_progress(0); CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0))); - path_follow_2d->set_offset(50); + path_follow_2d->set_progress(50); CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 0))); - path_follow_2d->set_offset(100); + path_follow_2d->set_progress(100); CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 0))); - path_follow_2d->set_offset(150); + path_follow_2d->set_progress(150); CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 50))); - path_follow_2d->set_offset(200); + path_follow_2d->set_progress(200); CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 100))); - path_follow_2d->set_offset(250); + path_follow_2d->set_progress(250); CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 100))); - path_follow_2d->set_offset(300); + path_follow_2d->set_progress(300); CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 100))); - path_follow_2d->set_offset(350); + path_follow_2d->set_progress(350); CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 50))); - path_follow_2d->set_offset(400); + path_follow_2d->set_progress(400); CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0))); memdelete(path); @@ -131,7 +131,7 @@ TEST_CASE("[PathFollow2D] Removal of a point in curve") { const PathFollow2D *path_follow_2d = memnew(PathFollow2D); path->add_child(path_follow_2d); - path_follow_2d->set_unit_offset(0.5); + 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); @@ -152,7 +152,7 @@ TEST_CASE("[PathFollow2D] Setting h_offset and v_offset") { const PathFollow2D *path_follow_2d = memnew(PathFollow2D); path->add_child(path_follow_2d); - path_follow_2d->set_unit_offset(0.5); + 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); @@ -175,32 +175,32 @@ TEST_CASE("[PathFollow2D] Unit offset out of range") { path_follow_2d->set_loop(true); - path_follow_2d->set_unit_offset(-0.3); + path_follow_2d->set_progress_ratio(-0.3); CHECK_MESSAGE( - path_follow_2d->get_unit_offset() == 0.7, - "Unit Offset should loop back from the end in the opposite direction"); + path_follow_2d->get_progress_ratio() == 0.7, + "Progress Ratio should loop back from the end in the opposite direction"); - path_follow_2d->set_unit_offset(1.3); + path_follow_2d->set_progress_ratio(1.3); CHECK_MESSAGE( - path_follow_2d->get_unit_offset() == 0.3, - "Unit Offset should loop back from the end in the opposite direction"); + 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_unit_offset(-0.3); + path_follow_2d->set_progress_ratio(-0.3); CHECK_MESSAGE( - path_follow_2d->get_unit_offset() == 0, - "Unit Offset should be clamped at 0"); + path_follow_2d->get_progress_ratio() == 0, + "Progress Ratio should be clamped at 0"); - path_follow_2d->set_unit_offset(1.3); + path_follow_2d->set_progress_ratio(1.3); CHECK_MESSAGE( - path_follow_2d->get_unit_offset() == 1, - "Unit Offset should be clamped at 1"); + path_follow_2d->get_progress_ratio() == 1, + "Progress Ratio should be clamped at 1"); memdelete(path); } -TEST_CASE("[PathFollow2D] Offset out of range") { +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)); @@ -211,27 +211,27 @@ TEST_CASE("[PathFollow2D] Offset out of range") { path_follow_2d->set_loop(true); - path_follow_2d->set_offset(-50); + path_follow_2d->set_progress(-50); CHECK_MESSAGE( - path_follow_2d->get_offset() == 50, - "Offset should loop back from the end in the opposite direction"); + path_follow_2d->get_progress() == 50, + "Progress should loop back from the end in the opposite direction"); - path_follow_2d->set_offset(150); + path_follow_2d->set_progress(150); CHECK_MESSAGE( - path_follow_2d->get_offset() == 50, - "Offset should loop back from the end in the opposite direction"); + 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_offset(-50); + path_follow_2d->set_progress(-50); CHECK_MESSAGE( - path_follow_2d->get_offset() == 0, - "Offset should be clamped at 0"); + path_follow_2d->get_progress() == 0, + "Progress should be clamped at 0"); - path_follow_2d->set_offset(150); + path_follow_2d->set_progress(150); CHECK_MESSAGE( - path_follow_2d->get_offset() == 100, - "Offset should be clamped at 1"); + path_follow_2d->get_progress() == 100, + "Progress should be clamped at 1"); memdelete(path); } diff --git a/tests/scene/test_path_follow_3d.h b/tests/scene/test_path_follow_3d.h index 9ffe49e3d6..6334fa56de 100644 --- a/tests/scene/test_path_follow_3d.h +++ b/tests/scene/test_path_follow_3d.h @@ -37,7 +37,7 @@ namespace TestPathFollow3D { -TEST_CASE("[PathFollow3D] Sampling with unit offset") { +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)); @@ -49,37 +49,37 @@ TEST_CASE("[PathFollow3D] Sampling with unit offset") { const PathFollow3D *path_follow_3d = memnew(PathFollow3D); path->add_child(path_follow_3d); - path_follow_3d->set_unit_offset(0); + 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_unit_offset(0.125); + 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_unit_offset(0.25); + 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_unit_offset(0.375); + 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_unit_offset(0.5); + 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_unit_offset(0.625); + 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_unit_offset(0.75); + 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_unit_offset(0.875); + 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_unit_offset(1); + 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 offset") { +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)); @@ -91,31 +91,31 @@ TEST_CASE("[PathFollow3D] Sampling with offset") { const PathFollow3D *path_follow_3d = memnew(PathFollow3D); path->add_child(path_follow_3d); - path_follow_3d->set_offset(0); + 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_offset(50); + 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_offset(100); + 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_offset(150); + 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_offset(200); + 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_offset(250); + 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_offset(300); + 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_offset(350); + 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_offset(400); + path_follow_3d->set_progress(400); CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 0, 100))); memdelete(path); @@ -131,7 +131,7 @@ TEST_CASE("[PathFollow3D] Removal of a point in curve") { const PathFollow3D *path_follow_3d = memnew(PathFollow3D); path->add_child(path_follow_3d); - path_follow_3d->set_unit_offset(0.5); + 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); @@ -143,7 +143,7 @@ TEST_CASE("[PathFollow3D] Removal of a point in curve") { memdelete(path); } -TEST_CASE("[PathFollow3D] Unit offset out of range") { +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)); @@ -154,32 +154,32 @@ TEST_CASE("[PathFollow3D] Unit offset out of range") { path_follow_3d->set_loop(true); - path_follow_3d->set_unit_offset(-0.3); + path_follow_3d->set_progress_ratio(-0.3); CHECK_MESSAGE( - path_follow_3d->get_unit_offset() == 0.7, - "Unit Offset should loop back from the end in the opposite direction"); + path_follow_3d->get_progress_ratio() == 0.7, + "Progress Ratio should loop back from the end in the opposite direction"); - path_follow_3d->set_unit_offset(1.3); + path_follow_3d->set_progress_ratio(1.3); CHECK_MESSAGE( - path_follow_3d->get_unit_offset() == 0.3, - "Unit Offset should loop back from the end in the opposite direction"); + 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_unit_offset(-0.3); + path_follow_3d->set_progress_ratio(-0.3); CHECK_MESSAGE( - path_follow_3d->get_unit_offset() == 0, - "Unit Offset should be clamped at 0"); + path_follow_3d->get_progress_ratio() == 0, + "Progress Ratio should be clamped at 0"); - path_follow_3d->set_unit_offset(1.3); + path_follow_3d->set_progress_ratio(1.3); CHECK_MESSAGE( - path_follow_3d->get_unit_offset() == 1, - "Unit Offset should be clamped at 1"); + path_follow_3d->get_progress_ratio() == 1, + "Progress Ratio should be clamped at 1"); memdelete(path); } -TEST_CASE("[PathFollow3D] Offset out of range") { +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)); @@ -190,27 +190,27 @@ TEST_CASE("[PathFollow3D] Offset out of range") { path_follow_3d->set_loop(true); - path_follow_3d->set_offset(-50); + path_follow_3d->set_progress(-50); CHECK_MESSAGE( - path_follow_3d->get_offset() == 50, - "Offset should loop back from the end in the opposite direction"); + path_follow_3d->get_progress() == 50, + "Progress should loop back from the end in the opposite direction"); - path_follow_3d->set_offset(150); + path_follow_3d->set_progress(150); CHECK_MESSAGE( - path_follow_3d->get_offset() == 50, - "Offset should loop back from the end in the opposite direction"); + 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_offset(-50); + path_follow_3d->set_progress(-50); CHECK_MESSAGE( - path_follow_3d->get_offset() == 0, - "Offset should be clamped at 0"); + path_follow_3d->get_progress() == 0, + "Progress should be clamped at 0"); - path_follow_3d->set_offset(150); + path_follow_3d->set_progress(150); CHECK_MESSAGE( - path_follow_3d->get_offset() == 100, - "Offset should be clamped at max value of curve"); + path_follow_3d->get_progress() == 100, + "Progress should be clamped at max value of curve"); memdelete(path); } diff --git a/tests/scene/test_primitives.h b/tests/scene/test_primitives.h new file mode 100644 index 0000000000..8bac903d93 --- /dev/null +++ b/tests/scene/test_primitives.h @@ -0,0 +1,850 @@ +/*************************************************************************/ +/* test_primitives.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_PRIMITIVES_H +#define TEST_PRIMITIVES_H + +#include "scene/resources/primitive_meshes.h" + +#include "tests/test_macros.h" + +namespace TestPrimitives { + +TEST_CASE("[SceneTree][Primitive][Capsule] Capsule Primitive") { + Ref<CapsuleMesh> capsule = memnew(CapsuleMesh); + + SUBCASE("[SceneTree][Primitive][Capsule] Default values should be valid") { + CHECK_MESSAGE(capsule->get_radius() > 0, + "Radius of default capsule positive."); + CHECK_MESSAGE(capsule->get_height() > 0, + "Height of default capsule positive."); + CHECK_MESSAGE(capsule->get_radial_segments() >= 0, + "Radius Segments of default capsule positive."); + CHECK_MESSAGE(capsule->get_rings() >= 0, + "Number of rings of default capsule positive."); + } + + SUBCASE("[SceneTree][Primitive][Capsule] Set properties of the capsule and get them with accessor methods") { + capsule->set_height(7.1f); + capsule->set_radius(1.3f); + capsule->set_radial_segments(16); + capsule->set_rings(32); + + CHECK_MESSAGE(capsule->get_radius() == doctest::Approx(1.3f), + "Get/Set radius work with one set."); + CHECK_MESSAGE(capsule->get_height() == doctest::Approx(7.1f), + "Get/Set radius work with one set."); + CHECK_MESSAGE(capsule->get_radial_segments() == 16, + "Get/Set radius work with one set."); + CHECK_MESSAGE(capsule->get_rings() == 32, + "Get/Set radius work with one set."); + } + + SUBCASE("[SceneTree][Primitive][Capsule] If set segments negative, default to at least 0") { + capsule->set_radial_segments(-5); + capsule->set_rings(-17); + + CHECK_MESSAGE(capsule->get_radial_segments() >= 0, + "Ensure number of radial segments is >= 0."); + CHECK_MESSAGE(capsule->get_rings() >= 0, + "Ensure number of rings is >= 0."); + } + + SUBCASE("[SceneTree][Primitive][Capsule] If set height < 2*radius, adjust radius and height to radius=height*0.5") { + capsule->set_radius(1.f); + capsule->set_height(0.5f); + + CHECK_MESSAGE(capsule->get_radius() >= capsule->get_height() * 0.5, + "Ensure radius >= height * 0.5 (needed for capsule to exist)."); + } + + SUBCASE("[Primitive][Capsule] Check mesh is correct") { + Array data{}; + data.resize(RS::ARRAY_MAX); + float radius{ 0.5f }; + float height{ 4.f }; + int num_radial_segments{ 4 }; + int num_rings{ 8 }; + CapsuleMesh::create_mesh_array(data, radius, height, num_radial_segments, num_rings); + Vector<Vector3> points = data[RS::ARRAY_VERTEX]; + + SUBCASE("[Primitive][Capsule] Ensure all vertices positions are within bounding radius and height") { + // Get mesh data + + // Check all points within radius of capsule + float dist_to_yaxis = 0.f; + for (Vector3 point : points) { + float new_dist_to_y = point.x * point.x + point.z * point.z; + if (new_dist_to_y > dist_to_yaxis) + dist_to_yaxis = new_dist_to_y; + } + + CHECK(dist_to_yaxis <= radius * radius); + + // Check highest point and lowest point are within height of each other + float max_y{ 0.f }; + float min_y{ 0.f }; + for (Vector3 point : points) { + if (point.y > max_y) + max_y = point.y; + if (point.y < min_y) + min_y = point.y; + } + + CHECK(max_y - min_y <= height); + } + + SUBCASE("[Primitive][Capsule] If normal.y == 0, then mesh makes a cylinder.") { + Vector<Vector3> normals = data[RS::ARRAY_NORMAL]; + for (int ii = 0; ii < points.size(); ++ii) { + float point_dist_from_yaxis = Math::sqrt(points[ii].x * points[ii].x + points[ii].z * points[ii].z); + Vector3 yaxis_to_point{ points[ii].x / point_dist_from_yaxis, 0.f, points[ii].z / point_dist_from_yaxis }; + if (normals[ii].y == 0.f) { + float mag_of_normal = Math::sqrt(normals[ii].x * normals[ii].x + normals[ii].z * normals[ii].z); + Vector3 normalized_normal = normals[ii] / mag_of_normal; + CHECK_MESSAGE(point_dist_from_yaxis == doctest::Approx(radius), + "Points on the tube of the capsule are radius away from y-axis."); + CHECK_MESSAGE(normalized_normal.is_equal_approx(yaxis_to_point), + "Normal points orthogonal from mid cylinder."); + } + } + } + } +} // End capsule tests + +TEST_CASE("[SceneTree][Primitive][Box] Box Primitive") { + Ref<BoxMesh> box = memnew(BoxMesh); + + SUBCASE("[SceneTree][Primitive][Box] Default values should be valid") { + CHECK(box->get_size().x > 0); + CHECK(box->get_size().y > 0); + CHECK(box->get_size().z > 0); + CHECK(box->get_subdivide_width() >= 0); + CHECK(box->get_subdivide_height() >= 0); + CHECK(box->get_subdivide_depth() >= 0); + } + + SUBCASE("[SceneTree][Primitive][Box] Set properties and get them with accessor methods") { + Vector3 size{ 2.1, 3.3, 1.7 }; + box->set_size(size); + box->set_subdivide_width(3); + box->set_subdivide_height(2); + box->set_subdivide_depth(4); + + CHECK(box->get_size().is_equal_approx(size)); + CHECK(box->get_subdivide_width() == 3); + CHECK(box->get_subdivide_height() == 2); + CHECK(box->get_subdivide_depth() == 4); + } + + SUBCASE("[SceneTree][Primitive][Box] Set subdivides to negative and ensure they are >= 0") { + box->set_subdivide_width(-2); + box->set_subdivide_height(-2); + box->set_subdivide_depth(-2); + + CHECK(box->get_subdivide_width() >= 0); + CHECK(box->get_subdivide_height() >= 0); + CHECK(box->get_subdivide_depth() >= 0); + } + + SUBCASE("[Primitive][Box] Check mesh is correct.") { + Array data{}; + data.resize(RS::ARRAY_MAX); + Vector3 size{ 0.5f, 1.2f, .9f }; + int subdivide_width{ 3 }; + int subdivide_height{ 2 }; + int subdivide_depth{ 8 }; + BoxMesh::create_mesh_array(data, size, subdivide_width, subdivide_height, subdivide_depth); + Vector<Vector3> points = data[RS::ARRAY_VERTEX]; + Vector<Vector3> normals = data[RS::ARRAY_NORMAL]; + + SUBCASE("Only 6 distinct normals.") { + Vector<Vector3> distinct_normals{}; + distinct_normals.push_back(normals[0]); + + for (const Vector3 &normal : normals) { + bool add_normal{ true }; + for (const Vector3 &vec : distinct_normals) { + if (vec.is_equal_approx(normal)) + add_normal = false; + } + + if (add_normal) + distinct_normals.push_back(normal); + } + + CHECK_MESSAGE(distinct_normals.size() == 6, + "There are exactly 6 distinct normals in the mesh data."); + + // All normals are orthogonal, or pointing in same direction. + bool normal_correct_direction{ true }; + for (int rowIndex = 0; rowIndex < distinct_normals.size(); ++rowIndex) { + for (int colIndex = rowIndex + 1; colIndex < distinct_normals.size(); ++colIndex) { + if (!Math::is_equal_approx(distinct_normals[rowIndex].normalized().dot(distinct_normals[colIndex].normalized()), 0) && + !Math::is_equal_approx(distinct_normals[rowIndex].normalized().dot(distinct_normals[colIndex].normalized()), 1) && + !Math::is_equal_approx(distinct_normals[rowIndex].normalized().dot(distinct_normals[colIndex].normalized()), -1)) { + normal_correct_direction = false; + break; + } + } + if (!normal_correct_direction) + break; + } + + CHECK_MESSAGE(normal_correct_direction, + "All normals are either orthogonal or colinear."); + } + } +} // End box tests + +TEST_CASE("[SceneTree][Primitive][Cylinder] Cylinder Primitive") { + Ref<CylinderMesh> cylinder = memnew(CylinderMesh); + + SUBCASE("[SceneTree][Primitive][Cylinder] Default values should be valid") { + CHECK(cylinder->get_top_radius() > 0); + CHECK(cylinder->get_bottom_radius() > 0); + CHECK(cylinder->get_height() > 0); + CHECK(cylinder->get_radial_segments() > 0); + CHECK(cylinder->get_rings() > 0); + } + + SUBCASE("[SceneTree][Primitive][Cylinder] Set properties and get them") { + cylinder->set_top_radius(4.3f); + cylinder->set_bottom_radius(1.2f); + cylinder->set_height(9.77f); + cylinder->set_radial_segments(12); + cylinder->set_rings(16); + cylinder->set_cap_top(false); + cylinder->set_cap_bottom(false); + + CHECK(cylinder->get_top_radius() == doctest::Approx(4.3f)); + CHECK(cylinder->get_bottom_radius() == doctest::Approx(1.2f)); + CHECK(cylinder->get_height() == doctest::Approx(9.77f)); + CHECK(cylinder->get_radial_segments() == 12); + CHECK(cylinder->get_rings() == 16); + CHECK(!cylinder->is_cap_top()); + CHECK(!cylinder->is_cap_bottom()); + } + + SUBCASE("[SceneTree][Primitive][Cylinder] Ensure num segments is >= 0") { + cylinder->set_radial_segments(-12); + cylinder->set_rings(-16); + + CHECK(cylinder->get_radial_segments() >= 0); + CHECK(cylinder->get_rings() >= 0); + } + + SUBCASE("[Primitive][Cylinder] Actual cylinder mesh tests (top and bottom radius the same).") { + Array data{}; + data.resize(RS::ARRAY_MAX); + real_t radius = .9f; + real_t height = 3.2f; + int radial_segments = 8; + int rings = 5; + bool top_cap = true; + bool bottom_cap = true; + CylinderMesh::create_mesh_array(data, radius, radius, height, radial_segments, rings, top_cap, bottom_cap); + Vector<Vector3> points = data[RS::ARRAY_VERTEX]; + Vector<Vector3> normals = data[RS::ARRAY_NORMAL]; + + SUBCASE("[Primitive][Cylinder] Side points are radius away from y-axis.") { + bool is_radius_correct{ true }; + for (int index = 0; index < normals.size(); ++index) { + if (Math::is_equal_approx(normals[index].y, 0)) { + if (!Math::is_equal_approx((points[index] - Vector3(0, points[index].y, 0)).length_squared(), radius * radius)) { + is_radius_correct = false; + break; + } + } + } + + CHECK(is_radius_correct); + } + + SUBCASE("[Primitive][Cylinder] Only possible normals point in direction of point or in positive/negative y direction.") { + bool is_correct_normals{ true }; + for (int index = 0; index < normals.size(); ++index) { + Vector3 yaxis_to_point = points[index] - Vector3(0.f, points[index].y, 0.f); + Vector3 point_to_normal = normals[index].normalized() - yaxis_to_point.normalized(); + // std::cout << "<" << point_to_normal.x << ", " << point_to_normal.y << ", " << point_to_normal.z << ">\n"; + if (!(point_to_normal.is_equal_approx(Vector3(0, 0, 0))) && + (!Math::is_equal_approx(Math::abs(normals[index].normalized().y), 1))) { + is_correct_normals = false; + break; + } + } + + CHECK(is_correct_normals); + } + + SUBCASE("[Primitive][Cylinder] Points on top and bottom are height/2 away from origin.") { + bool is_height_correct{ true }; + real_t half_height = 0.5 * height; + for (int index = 0; index < normals.size(); ++index) { + if (Math::is_equal_approx(normals[index].x, 0) && + Math::is_equal_approx(normals[index].z, 0) && + normals[index].y > 0) { + if (!Math::is_equal_approx(points[index].y, half_height)) { + is_height_correct = false; + break; + } + } + if (Math::is_equal_approx(normals[index].x, 0) && + Math::is_equal_approx(normals[index].z, 0) && + normals[index].y < 0) { + if (!Math::is_equal_approx(points[index].y, -half_height)) { + is_height_correct = false; + break; + } + } + } + + CHECK(is_height_correct); + } + + SUBCASE("[Primitive][Cylinder] Does mesh obey cap parameters?") { + CylinderMesh::create_mesh_array(data, radius, radius, height, radial_segments, rings, top_cap, false); + points = data[RS::ARRAY_VERTEX]; + normals = data[RS::ARRAY_NORMAL]; + bool no_bottom_cap{ true }; + + for (int index = 0; index < normals.size(); ++index) { + if (Math::is_equal_approx(normals[index].x, 0) && + Math::is_equal_approx(normals[index].z, 0) && + normals[index].y < 0) { + no_bottom_cap = false; + break; + } + } + + CHECK_MESSAGE(no_bottom_cap, + "Check there is no bottom cap."); + + CylinderMesh::create_mesh_array(data, radius, radius, height, radial_segments, rings, false, bottom_cap); + points = data[RS::ARRAY_VERTEX]; + normals = data[RS::ARRAY_NORMAL]; + bool no_top_cap{ true }; + + for (int index = 0; index < normals.size(); ++index) { + if (Math::is_equal_approx(normals[index].x, 0) && + Math::is_equal_approx(normals[index].z, 0) && + normals[index].y > 0) { + no_top_cap = false; + break; + } + } + + CHECK_MESSAGE(no_top_cap, + "Check there is no top cap."); + } + } + + SUBCASE("[Primitive][Cylinder] Slanted cylinder mesh (top and bottom radius different).") { + Array data{}; + data.resize(RS::ARRAY_MAX); + real_t top_radius = 2.f; + real_t bottom_radius = 1.f; + real_t height = 1.f; + int radial_segments = 8; + int rings = 5; + CylinderMesh::create_mesh_array(data, top_radius, bottom_radius, height, radial_segments, rings, false, false); + Vector<Vector3> points = data[RS::ARRAY_VERTEX]; + Vector<Vector3> normals = data[RS::ARRAY_NORMAL]; + + SUBCASE("[Primitive][Cylinder] Side points lie correct distance from y-axis") { + bool is_radius_correct{ true }; + for (int index = 0; index < points.size(); ++index) { + real_t radius = ((top_radius - bottom_radius) / height) * (points[index].y - 0.5 * height) + top_radius; + Vector3 distance_to_yaxis = points[index] - Vector3(0.f, points[index].y, 0.f); + if (!Math::is_equal_approx(distance_to_yaxis.length_squared(), radius * radius)) { + is_radius_correct = false; + break; + } + } + + CHECK(is_radius_correct); + } + + SUBCASE("[Primitive][Cylinder] Normal on side is orthogonal to side tangent vector") { + bool is_normal_correct{ true }; + for (int index = 0; index < points.size(); ++index) { + Vector3 yaxis_to_point = points[index] - Vector3(0.f, points[index].y, 0.f); + Vector3 yaxis_to_rb = yaxis_to_point.normalized() * bottom_radius; + Vector3 rb_to_point = yaxis_to_point - yaxis_to_rb; + Vector3 y_to_bottom = -Vector3(0.f, points[index].y + 0.5 * height, 0.f); + Vector3 side_tangent = rb_to_point - y_to_bottom; + + if (!Math::is_equal_approx(normals[index].dot(side_tangent), 0)) { + is_normal_correct = false; + break; + } + } + + CHECK(is_normal_correct); + } + } + +} // End cylinder tests + +TEST_CASE("[SceneTree][Primitive][Plane] Plane Primitive") { + Ref<PlaneMesh> plane = memnew(PlaneMesh); + + SUBCASE("[SceneTree][Primitive][Plane] Default values should be valid") { + CHECK(plane->get_size().x > 0); + CHECK(plane->get_size().y > 0); + CHECK(plane->get_subdivide_width() >= 0); + CHECK(plane->get_subdivide_depth() >= 0); + CHECK((plane->get_orientation() == PlaneMesh::FACE_X || plane->get_orientation() == PlaneMesh::FACE_Y || plane->get_orientation() == PlaneMesh::FACE_Z)); + } + + SUBCASE("[SceneTree][Primitive][Plane] Set properties and get them.") { + Size2 size{ 3.2, 1.8 }; + Vector3 offset{ -7.3, 0.4, -1.7 }; + plane->set_size(size); + plane->set_subdivide_width(15); + plane->set_subdivide_depth(29); + plane->set_center_offset(offset); + plane->set_orientation(PlaneMesh::FACE_X); + + CHECK(plane->get_size().is_equal_approx(size)); + CHECK(plane->get_subdivide_width() == 15); + CHECK(plane->get_subdivide_depth() == 29); + CHECK(plane->get_center_offset().is_equal_approx(offset)); + CHECK(plane->get_orientation() == PlaneMesh::FACE_X); + } + + SUBCASE("[SceneTree][Primitive][Plane] Ensure number of segments is >= 0.") { + plane->set_subdivide_width(-15); + plane->set_subdivide_depth(-29); + + CHECK(plane->get_subdivide_width() >= 0); + CHECK(plane->get_subdivide_depth() >= 0); + } +} + +TEST_CASE("[SceneTree][Primitive][Quad] QuadMesh Primitive") { + Ref<QuadMesh> quad = memnew(QuadMesh); + + SUBCASE("[Primitive][Quad] Orientation on initialization is in z direction") { + CHECK(quad->get_orientation() == PlaneMesh::FACE_Z); + } +} + +TEST_CASE("[SceneTree][Primitive][Prism] Prism Primitive") { + Ref<PrismMesh> prism = memnew(PrismMesh); + + SUBCASE("[Primitive][Prism] There are valid values of properties on initialization.") { + CHECK(prism->get_left_to_right() >= 0); + CHECK(prism->get_size().x >= 0); + CHECK(prism->get_size().y >= 0); + CHECK(prism->get_size().z >= 0); + CHECK(prism->get_subdivide_width() >= 0); + CHECK(prism->get_subdivide_height() >= 0); + CHECK(prism->get_subdivide_depth() >= 0); + } + + SUBCASE("[Primitive][Prism] Are able to change prism properties.") { + Vector3 size{ 4.3, 9.1, 0.43 }; + prism->set_left_to_right(3.4f); + prism->set_size(size); + prism->set_subdivide_width(36); + prism->set_subdivide_height(5); + prism->set_subdivide_depth(64); + + CHECK(prism->get_left_to_right() == doctest::Approx(3.4f)); + CHECK(prism->get_size().is_equal_approx(size)); + CHECK(prism->get_subdivide_width() == 36); + CHECK(prism->get_subdivide_height() == 5); + CHECK(prism->get_subdivide_depth() == 64); + } + + SUBCASE("[Primitive][Prism] Ensure number of segments always >= 0") { + prism->set_subdivide_width(-36); + prism->set_subdivide_height(-5); + prism->set_subdivide_depth(-64); + + CHECK(prism->get_subdivide_width() >= 0); + CHECK(prism->get_subdivide_height() >= 0); + CHECK(prism->get_subdivide_depth() >= 0); + } +} + +TEST_CASE("[SceneTree][Primitive][Sphere] Sphere Primitive") { + Ref<SphereMesh> sphere = memnew(SphereMesh); + + SUBCASE("[Primitive][Sphere] There are valid values of properties on initialization.") { + CHECK(sphere->get_radius() >= 0); + CHECK(sphere->get_height() >= 0); + CHECK(sphere->get_radial_segments() >= 0); + CHECK(sphere->get_rings() >= 0); + } + + SUBCASE("[Primitive][Sphere] Are able to change prism properties.") { + sphere->set_radius(3.4f); + sphere->set_height(2.2f); + sphere->set_radial_segments(36); + sphere->set_rings(5); + sphere->set_is_hemisphere(true); + + CHECK(sphere->get_radius() == doctest::Approx(3.4f)); + CHECK(sphere->get_height() == doctest::Approx(2.2f)); + CHECK(sphere->get_radial_segments() == 36); + CHECK(sphere->get_rings() == 5); + CHECK(sphere->get_is_hemisphere()); + } + + SUBCASE("[Primitive][Sphere] Ensure number of segments always >= 0") { + sphere->set_radial_segments(-36); + sphere->set_rings(-5); + + CHECK(sphere->get_radial_segments() >= 0); + CHECK(sphere->get_rings() >= 0); + } + + SUBCASE("[Primitive][Sphere] Sphere mesh tests.") { + Array data{}; + data.resize(RS::ARRAY_MAX); + real_t radius = 1.1f; + int radial_segments = 8; + int rings = 5; + SphereMesh::create_mesh_array(data, radius, 2 * radius, radial_segments, rings); + Vector<Vector3> points = data[RS::ARRAY_VERTEX]; + Vector<Vector3> normals = data[RS::ARRAY_NORMAL]; + + SUBCASE("[Primitive][Sphere] All points lie radius away from origin.") { + bool is_radius_correct = true; + for (Vector3 point : points) { + if (!Math::is_equal_approx(point.length_squared(), radius * radius)) { + is_radius_correct = false; + break; + } + } + + CHECK(is_radius_correct); + } + + SUBCASE("[Primitive][Sphere] All normals lie in direction of corresponding point.") { + bool is_normals_correct = true; + for (int index = 0; index < points.size(); ++index) { + if (!Math::is_equal_approx(normals[index].normalized().dot(points[index].normalized()), 1)) { + is_normals_correct = false; + break; + } + } + + CHECK(is_normals_correct); + } + } +} + +TEST_CASE("[SceneTree][Primitive][Torus] Torus Primitive") { + Ref<TorusMesh> torus = memnew(TorusMesh); + Ref<PrimitiveMesh> prim = memnew(PrimitiveMesh); + + SUBCASE("[Primitive][Torus] There are valid values of properties on initialization.") { + CHECK(torus->get_inner_radius() > 0); + CHECK(torus->get_outer_radius() > 0); + CHECK(torus->get_rings() >= 0); + CHECK(torus->get_ring_segments() >= 0); + } + + SUBCASE("[Primitive][Torus] Are able to change properties.") { + torus->set_inner_radius(3.2f); + torus->set_outer_radius(9.5f); + torus->set_rings(19); + torus->set_ring_segments(43); + + CHECK(torus->get_inner_radius() == doctest::Approx(3.2f)); + CHECK(torus->get_outer_radius() == doctest::Approx(9.5f)); + CHECK(torus->get_rings() == 19); + CHECK(torus->get_ring_segments() == 43); + } +} + +TEST_CASE("[SceneTree][Primitive][TubeTrail] TubeTrail Primitive") { + Ref<TubeTrailMesh> tube = memnew(TubeTrailMesh); + + SUBCASE("[Primitive][TubeTrail] There are valid values of properties on initialization.") { + CHECK(tube->get_radius() > 0); + CHECK(tube->get_radial_steps() >= 0); + CHECK(tube->get_sections() >= 0); + CHECK(tube->get_section_length() > 0); + CHECK(tube->get_section_rings() >= 0); + CHECK(tube->get_curve() == nullptr); + CHECK(tube->get_builtin_bind_pose_count() >= 0); + } + + SUBCASE("[Primitive][TubeTrail] Are able to change properties.") { + tube->set_radius(7.2f); + tube->set_radial_steps(9); + tube->set_sections(33); + tube->set_section_length(5.5f); + tube->set_section_rings(12); + Ref<Curve> curve = memnew(Curve); + tube->set_curve(curve); + + CHECK(tube->get_radius() == doctest::Approx(7.2f)); + CHECK(tube->get_section_length() == doctest::Approx(5.5f)); + CHECK(tube->get_radial_steps() == 9); + CHECK(tube->get_sections() == 33); + CHECK(tube->get_section_rings() == 12); + CHECK(tube->get_curve() == curve); + } + + SUBCASE("[Primitive][TubeTrail] Setting same curve more than once, it remains the same.") { + Ref<Curve> curve = memnew(Curve); + tube->set_curve(curve); + tube->set_curve(curve); + tube->set_curve(curve); + + CHECK(tube->get_curve() == curve); + } + + SUBCASE("[Primitive][TubeTrail] Setting curve, then changing to different curve.") { + Ref<Curve> curve1 = memnew(Curve); + Ref<Curve> curve2 = memnew(Curve); + tube->set_curve(curve1); + CHECK(tube->get_curve() == curve1); + + tube->set_curve(curve2); + CHECK(tube->get_curve() == curve2); + } + + SUBCASE("[Primitive][TubeTrail] Assign same curve to two different tube trails") { + Ref<TubeTrailMesh> tube2 = memnew(TubeTrailMesh); + Ref<Curve> curve = memnew(Curve); + tube->set_curve(curve); + tube2->set_curve(curve); + + CHECK(tube->get_curve() == curve); + CHECK(tube2->get_curve() == curve); + } +} + +TEST_CASE("[SceneTree][Primitive][RibbonTrail] RibbonTrail Primitive") { + Ref<RibbonTrailMesh> ribbon = memnew(RibbonTrailMesh); + + SUBCASE("[Primitive][RibbonTrail] There are valid values of properties on initialization.") { + CHECK(ribbon->get_size() > 0); + CHECK(ribbon->get_sections() >= 0); + CHECK(ribbon->get_section_length() > 0); + CHECK(ribbon->get_section_segments() >= 0); + CHECK(ribbon->get_builtin_bind_pose_count() >= 0); + CHECK(ribbon->get_curve() == nullptr); + CHECK((ribbon->get_shape() == RibbonTrailMesh::SHAPE_CROSS || + ribbon->get_shape() == RibbonTrailMesh::SHAPE_FLAT)); + } + + SUBCASE("[Primitive][RibbonTrail] Able to change properties.") { + Ref<Curve> curve = memnew(Curve); + ribbon->set_size(4.3f); + ribbon->set_sections(16); + ribbon->set_section_length(1.3f); + ribbon->set_section_segments(9); + ribbon->set_curve(curve); + + CHECK(ribbon->get_size() == doctest::Approx(4.3f)); + CHECK(ribbon->get_section_length() == doctest::Approx(1.3f)); + CHECK(ribbon->get_sections() == 16); + CHECK(ribbon->get_section_segments() == 9); + CHECK(ribbon->get_curve() == curve); + } + + SUBCASE("[Primitive][RibbonTrail] Setting same curve more than once, it remains the same.") { + Ref<Curve> curve = memnew(Curve); + ribbon->set_curve(curve); + ribbon->set_curve(curve); + ribbon->set_curve(curve); + + CHECK(ribbon->get_curve() == curve); + } + + SUBCASE("[Primitive][RibbonTrail] Setting curve, then changing to different curve.") { + Ref<Curve> curve1 = memnew(Curve); + Ref<Curve> curve2 = memnew(Curve); + ribbon->set_curve(curve1); + CHECK(ribbon->get_curve() == curve1); + + ribbon->set_curve(curve2); + CHECK(ribbon->get_curve() == curve2); + } + + SUBCASE("[Primitive][RibbonTrail] Assign same curve to two different ribbon trails") { + Ref<RibbonTrailMesh> ribbon2 = memnew(RibbonTrailMesh); + Ref<Curve> curve = memnew(Curve); + ribbon->set_curve(curve); + ribbon2->set_curve(curve); + + CHECK(ribbon->get_curve() == curve); + CHECK(ribbon2->get_curve() == curve); + } +} + +TEST_CASE("[SceneTree][Primitive][Text] Text Primitive") { + Ref<TextMesh> text = memnew(TextMesh); + + SUBCASE("[Primitive][Text] There are valid values of properties on initialization.") { + CHECK((text->get_horizontal_alignment() == HORIZONTAL_ALIGNMENT_CENTER || + text->get_horizontal_alignment() == HORIZONTAL_ALIGNMENT_LEFT || + text->get_horizontal_alignment() == HORIZONTAL_ALIGNMENT_RIGHT || + text->get_horizontal_alignment() == HORIZONTAL_ALIGNMENT_FILL)); + CHECK((text->get_vertical_alignment() == VERTICAL_ALIGNMENT_BOTTOM || + text->get_vertical_alignment() == VERTICAL_ALIGNMENT_TOP || + text->get_vertical_alignment() == VERTICAL_ALIGNMENT_CENTER || + text->get_vertical_alignment() == VERTICAL_ALIGNMENT_FILL)); + CHECK(text->get_font() == nullptr); + CHECK(text->get_font_size() > 0); + CHECK(text->get_line_spacing() >= 0); + CHECK((text->get_autowrap_mode() == TextServer::AUTOWRAP_OFF || + text->get_autowrap_mode() == TextServer::AUTOWRAP_ARBITRARY || + text->get_autowrap_mode() == TextServer::AUTOWRAP_WORD || + text->get_autowrap_mode() == TextServer::AUTOWRAP_WORD_SMART)); + CHECK((text->get_text_direction() == TextServer::DIRECTION_AUTO || + text->get_text_direction() == TextServer::DIRECTION_LTR || + text->get_text_direction() == TextServer::DIRECTION_RTL)); + CHECK((text->get_structured_text_bidi_override() == TextServer::STRUCTURED_TEXT_DEFAULT || + text->get_structured_text_bidi_override() == TextServer::STRUCTURED_TEXT_URI || + text->get_structured_text_bidi_override() == TextServer::STRUCTURED_TEXT_FILE || + text->get_structured_text_bidi_override() == TextServer::STRUCTURED_TEXT_EMAIL || + text->get_structured_text_bidi_override() == TextServer::STRUCTURED_TEXT_LIST || + text->get_structured_text_bidi_override() == TextServer::STRUCTURED_TEXT_NONE || + text->get_structured_text_bidi_override() == TextServer::STRUCTURED_TEXT_CUSTOM)); + CHECK(text->get_structured_text_bidi_override_options().size() >= 0); + CHECK(text->get_width() > 0); + CHECK(text->get_depth() > 0); + CHECK(text->get_curve_step() > 0); + CHECK(text->get_pixel_size() > 0); + } + + SUBCASE("[Primitive][Text] Change the properties of the mesh.") { + Ref<Font> font = memnew(Font); + Array options{}; + Point2 offset{ 30.8, 104.23 }; + text->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + text->set_vertical_alignment(VERTICAL_ALIGNMENT_BOTTOM); + text->set_text("Hello"); + text->set_font(font); + text->set_font_size(12); + text->set_line_spacing(1.7f); + text->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART); + text->set_text_direction(TextServer::DIRECTION_RTL); + text->set_language("French"); + text->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_EMAIL); + text->set_structured_text_bidi_override_options(options); + text->set_uppercase(true); + real_t width{ 0.6 }; + real_t depth{ 1.7 }; + real_t pixel_size{ 2.8 }; + real_t curve_step{ 4.8 }; + text->set_width(width); + text->set_depth(depth); + text->set_curve_step(curve_step); + text->set_pixel_size(pixel_size); + text->set_offset(offset); + + CHECK(text->get_horizontal_alignment() == HORIZONTAL_ALIGNMENT_RIGHT); + CHECK(text->get_vertical_alignment() == VERTICAL_ALIGNMENT_BOTTOM); + CHECK(text->get_text_direction() == TextServer::DIRECTION_RTL); + CHECK(text->get_text() == "Hello"); + CHECK(text->get_font() == font); + CHECK(text->get_font_size() == 12); + CHECK(text->get_autowrap_mode() == TextServer::AUTOWRAP_WORD_SMART); + CHECK(text->get_language() == "French"); + CHECK(text->get_structured_text_bidi_override() == TextServer::STRUCTURED_TEXT_EMAIL); + CHECK(text->get_structured_text_bidi_override_options() == options); + CHECK(text->is_uppercase() == true); + CHECK(text->get_offset() == offset); + CHECK(text->get_line_spacing() == doctest::Approx(1.7f)); + CHECK(text->get_width() == doctest::Approx(width)); + CHECK(text->get_depth() == doctest::Approx(depth)); + CHECK(text->get_curve_step() == doctest::Approx(curve_step)); + CHECK(text->get_pixel_size() == doctest::Approx(pixel_size)); + } + + SUBCASE("[Primitive][Text] Set objects multiple times.") { + Ref<Font> font = memnew(Font); + Array options{}; + Point2 offset{ 30.8, 104.23 }; + + text->set_font(font); + text->set_font(font); + text->set_font(font); + text->set_structured_text_bidi_override_options(options); + text->set_structured_text_bidi_override_options(options); + text->set_structured_text_bidi_override_options(options); + text->set_offset(offset); + text->set_offset(offset); + text->set_offset(offset); + + CHECK(text->get_font() == font); + CHECK(text->get_structured_text_bidi_override_options() == options); + CHECK(text->get_offset() == offset); + } + + SUBCASE("[Primitive][Text] Set then change objects.") { + Ref<Font> font1 = memnew(Font); + Ref<Font> font2 = memnew(Font); + Array options1{}; + Array options2{}; + Point2 offset1{ 30.8, 104.23 }; + Point2 offset2{ -30.8, -104.23 }; + + text->set_font(font1); + text->set_structured_text_bidi_override_options(options1); + text->set_offset(offset1); + + CHECK(text->get_font() == font1); + CHECK(text->get_structured_text_bidi_override_options() == options1); + CHECK(text->get_offset() == offset1); + + text->set_font(font2); + text->set_structured_text_bidi_override_options(options2); + text->set_offset(offset2); + + CHECK(text->get_font() == font2); + CHECK(text->get_structured_text_bidi_override_options() == options2); + CHECK(text->get_offset() == offset2); + } + + SUBCASE("[Primitive][Text] Assign same font to two Textmeshes.") { + Ref<TextMesh> text2 = memnew(TextMesh); + Ref<Font> font = memnew(Font); + + text->set_font(font); + text2->set_font(font); + + CHECK(text->get_font() == font); + CHECK(text2->get_font() == font); + } +} + +} // namespace TestPrimitives + +#endif // TEST_PRIMITIVES_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 index 4098dd7ace..6d1c2f4f2e 100644 --- a/tests/scene/test_text_edit.h +++ b/tests/scene/test_text_edit.h @@ -93,10 +93,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "test text"); - CHECK(text_edit->get_caret_column() == 9); + CHECK(text_edit->get_caret_column() == 0); 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("caret_changed"); SIGNAL_CHECK_FALSE("text_set"); text_edit->redo(); @@ -104,18 +104,18 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_text() == ""); CHECK(text_edit->get_caret_column() == 0); 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("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() == 9); + CHECK(text_edit->get_caret_column() == 0); 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("caret_changed"); SIGNAL_CHECK_FALSE("text_set"); // Clear. @@ -131,8 +131,8 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_text() == ""); CHECK(text_edit->get_caret_column() == 0); SIGNAL_CHECK("text_set", empty_signal_args); - SIGNAL_CHECK("caret_changed", 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); @@ -252,6 +252,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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); @@ -263,6 +264,8 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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); @@ -271,7 +274,8 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->redo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_line(0) == "te"); - CHECK_FALSE(text_edit->has_selection()); // Currently not handled. + 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); @@ -425,7 +429,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "testing\nswap"); - CHECK_FALSE(text_edit->has_selection()); // Not currently handled. + 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); @@ -436,7 +440,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->redo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "new\ntesting\nswap"); - CHECK_FALSE(text_edit->has_selection()); // Not currently handled. + 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); @@ -448,7 +452,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->has_selection()); CHECK(text_edit->get_selection_from_line() == 0); CHECK(text_edit->get_selection_to_line() == 2); - SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("caret_changed"); ((Array)lines_edited_args[0])[0] = 2; ((Array)lines_edited_args[0])[1] = 3; @@ -533,7 +537,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "temidsting\nswap"); CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 10); + 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); @@ -633,17 +637,42 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] select word under caret") { - text_edit->set_text("test test"); + 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->get_selected_text() == "test"); - 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() == 0); - CHECK(text_edit->get_selection_to_column() == 4); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 4); + 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()); @@ -652,27 +681,44 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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()); - CHECK(text_edit->get_selected_text() == "test"); - CHECK(text_edit->get_selection_from_line() == 0); - CHECK(text_edit->get_selection_from_column() == 0); - CHECK(text_edit->get_selection_to_line() == 0); - CHECK(text_edit->get_selection_to_column() == 4); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 4); + 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); - CHECK(text_edit->get_caret_column() == 4); + 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(0); - text_edit->set_caret_column(5); + 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() == ""); @@ -680,11 +726,81 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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); - CHECK(text_edit->get_caret_column() == 5); + 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 nonrandom"); + text_edit->set_caret_column(0); + text_edit->set_caret_line(1); + + // First selection made by the implicit select_word_under_caret call + text_edit->add_selection_for_next_occurrence(); + CHECK(text_edit->get_caret_count() == 1); + 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); + + 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); + + // A different word with a new manually added caret + text_edit->add_caret(2, 1); + text_edit->select(2, 0, 2, 4, 4); + CHECK(text_edit->get_selected_text(4) == "rand"); + + text_edit->add_selection_for_next_occurrence(); + CHECK(text_edit->get_caret_count() == 6); + CHECK(text_edit->get_selected_text(5) == "rand"); + CHECK(text_edit->get_selection_from_line(5) == 3); + CHECK(text_edit->get_selection_from_column(5) == 18); + CHECK(text_edit->get_selection_to_line(5) == 3); + CHECK(text_edit->get_selection_to_column(5) == 22); + CHECK(text_edit->get_caret_line(5) == 3); + CHECK(text_edit->get_caret_column(5) == 22); + + // Make sure the previous selections are still active + CHECK(text_edit->get_selected_text(0) == "test"); + CHECK(text_edit->get_selected_text(1) == "test"); + CHECK(text_edit->get_selected_text(2) == "test"); + CHECK(text_edit->get_selected_text(3) == "test"); + } + SUBCASE("[TextEdit] deselect on focus loss") { text_edit->set_text("test"); @@ -724,10 +840,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "t"); -#ifdef OSX_ENABLED +#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) + 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"); @@ -736,10 +852,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "tes"); -#ifdef OSX_ENABLED +#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) + 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() == ""); @@ -950,11 +1066,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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()); @@ -966,7 +1086,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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() == 8); + CHECK(text_edit->get_caret_column() == 4); text_edit->undo(); CHECK(text_edit->has_selection()); @@ -991,7 +1111,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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() == 8); + CHECK(text_edit->get_caret_column() == 4); text_edit->undo(); CHECK(text_edit->has_selection()); @@ -1176,8 +1296,8 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "this is\nsome\n"); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); + 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); @@ -1187,9 +1307,9 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->redo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "some\n"); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); - SIGNAL_CHECK_FALSE("caret_changed"); + 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); @@ -1259,9 +1379,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { lines_edited_args.push_back(args1); SUBCASE("[TextEdit] ui_text_newline_above") { - text_edit->set_text("this is some test text."); + 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"); @@ -1269,50 +1394,78 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - ((Array)lines_edited_args[0])[1] = 1; + // 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."); + 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()); + 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->select(0, 0, 0, 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."); + 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()); + 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."); + 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()); + 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."); + 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"); @@ -1320,13 +1473,23 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - ((Array)lines_edited_args[0])[1] = 1; + // 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"); + 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()); + 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); @@ -1334,10 +1497,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); + 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()); + 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"); @@ -1345,9 +1512,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] ui_text_newline") { - text_edit->set_text("this is some test text."); + 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"); @@ -1355,14 +1527,27 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - lines_edited_args.push_back(lines_edited_args[0].duplicate()); - ((Array)lines_edited_args[1])[1] = 1; + // 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."); + 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()); + 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); @@ -1370,10 +1555,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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."); + 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()); + 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"); @@ -1381,13 +1570,18 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] ui_text_backspace_all_to_left") { - text_edit->set_text("\nthis is some test text."); + 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); + 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"); @@ -1395,34 +1589,50 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; + ((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."); + 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()); + 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] = 0; + ((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."); + 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()); + 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"); @@ -1433,23 +1643,33 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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."); + 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()); + 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] = 0; + ((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() == ""); + 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()); + 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); @@ -1458,10 +1678,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] ui_text_backspace_word") { - text_edit->set_text("\nthis is some test text."); + 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"); @@ -1469,30 +1693,45 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; + ((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."); + 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()); + 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] = 0; + ((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."); + 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()); + 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); @@ -1500,16 +1739,21 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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."); + 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()); + 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"); @@ -1517,24 +1761,35 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - ((Array)lines_edited_args[0])[0] = 0; + ((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 "); + 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()); + 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."); + 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"); @@ -1542,34 +1797,50 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; + ((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."); + 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()); + 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] = 0; + ((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."); + 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()); + 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"); @@ -1580,23 +1851,33 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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."); + 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()); + 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] = 0; + ((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"); + 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()); + 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); @@ -1605,6 +1886,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); @@ -1612,25 +1897,30 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - ((Array)lines_edited_args[0])[0] = 0; - SEND_GUI_ACTION(text_edit, "ui_text_backspace"); - CHECK(text_edit->get_text() == ""); + 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); + 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.\n"); + 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"); @@ -1638,19 +1928,30 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); + 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()); + 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"); @@ -1660,15 +1961,20 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); + 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()); + 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"); @@ -1679,10 +1985,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); + 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()); + 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"); @@ -1690,10 +2000,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); + 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()); + 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); @@ -1705,10 +2019,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); + 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"); @@ -1716,20 +2035,32 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); + 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()); + 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] = 1; + ((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"); @@ -1739,16 +2070,23 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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."); + 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()); - SIGNAL_CHECK_FALSE("caret_changed"); + + 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[0])[0] = 0; + ((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"); @@ -1759,10 +2097,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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."); + 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()); + 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"); @@ -1770,10 +2112,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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."); + 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()); + 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); @@ -1783,10 +2129,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); + 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"); @@ -1794,19 +2145,31 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); + 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()); + 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(); @@ -1817,16 +2180,25 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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."); + 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() == text_edit->get_line(0).length()); - CHECK_FALSE(text_edit->has_selection()); - SIGNAL_CHECK_FALSE("caret_changed"); + 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); - ((Array)lines_edited_args[0])[0] = 0; + // 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"); @@ -1837,43 +2209,59 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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."); + 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()); + 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."); + 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()); - SIGNAL_CHECK_FALSE("caret_changed"); + 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."); + 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()); - SIGNAL_CHECK_FALSE("caret_changed"); + 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(); - text_edit->set_caret_line(0); - text_edit->set_caret_column(0); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "ffi some test text."); + CHECK(text_edit->get_text() == "ffi some test text.ffi some test text."); SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); @@ -1882,19 +2270,26 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_ACTION(text_edit, "ui_text_delete"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " some test text."); + 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()); - SIGNAL_CHECK_FALSE("caret_changed"); + 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."); + 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"); @@ -1902,47 +2297,67 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); -#ifdef OSX_ENABLED + // 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 | KeyModifierMask::SHIFT); + 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_text() == "\nthis is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 5); - CHECK(text_edit->get_selected_text() == "is"); - CHECK(text_edit->has_selection()); + 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_text() == "\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()); + 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_text() == "\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()); + 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."); + 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"); @@ -1950,62 +2365,91 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Normal left should 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_text() == "\nthis is some test text."); + CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 2); - CHECK_FALSE(text_edit->has_selection()); + 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_text() == "\nthis is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 1); - CHECK(text_edit->get_selected_text() == "h"); - CHECK(text_edit->has_selection()); + 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_text() == "\nthis is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 1); - CHECK_FALSE(text_edit->has_selection()); + 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_text() == "\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) == 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_text() == "\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()); + 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"); + 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"); @@ -2013,47 +2457,67 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); -#ifdef OSX_ENABLED + // 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 | KeyModifierMask::SHIFT); + 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_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 17); - CHECK(text_edit->get_selected_text() == "test"); - CHECK(text_edit->has_selection()); + 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_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 22); - CHECK_FALSE(text_edit->has_selection()); + 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_text() == "this 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()); + 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"); + 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"); @@ -2061,53 +2525,76 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Normal right should 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_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 20); - CHECK_FALSE(text_edit->has_selection()); + 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_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 21); - CHECK(text_edit->get_selected_text() == "x"); - CHECK(text_edit->has_selection()); + 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_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 21); - CHECK_FALSE(text_edit->has_selection()); + 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_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 22); - CHECK_FALSE(text_edit->has_selection()); + 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_text() == "this 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()); + 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"); @@ -2117,9 +2604,13 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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_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)); @@ -2128,44 +2619,61 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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_text() == "this is some\nother test\nlines\ngo here"); CHECK(text_edit->get_caret_line() == 2); CHECK(text_edit->get_caret_column() == 5); - CHECK(text_edit->get_selected_text() == "\ngo here"); - CHECK(text_edit->has_selection()); + 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_text() == "this is some\nother test\nlines\ngo here"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 8); - CHECK_FALSE(text_edit->has_selection()); + 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_text() == "this is some\nother test\nlines\ngo here"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 12); - CHECK_FALSE(text_edit->has_selection()); + 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_text() == "this is some\nother test\nlines\ngo here"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 7); - CHECK_FALSE(text_edit->has_selection()); + 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"); @@ -2175,9 +2683,13 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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_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)); @@ -2186,44 +2698,61 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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_text() == "go here\nlines\nother test\nthis is some"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 5); - CHECK(text_edit->get_selected_text() == "\nlines"); - CHECK(text_edit->has_selection()); + 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_text() == "go here\nlines\nother test\nthis is some"); CHECK(text_edit->get_caret_line() == 2); CHECK(text_edit->get_caret_column() == 8); - CHECK_FALSE(text_edit->has_selection()); + 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_text() == "go here\nlines\nother test\nthis is some"); CHECK(text_edit->get_caret_line() == 3); CHECK(text_edit->get_caret_column() == 7); - CHECK_FALSE(text_edit->has_selection()); + 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_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()); + 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"); @@ -2236,6 +2765,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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)); @@ -2244,10 +2777,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); -#ifdef OSX_ENABLED - SEND_GUI_KEY_EVENT(text_edit, Key::UP | KeyModifierMask::CMD | KeyModifierMask::SHIFT); +#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 | KeyModifierMask::SHIFT); + 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"); @@ -2258,6 +2791,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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()); @@ -2277,6 +2811,9 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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)); @@ -2285,10 +2822,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); -#ifdef OSX_ENABLED - SEND_GUI_KEY_EVENT(text_edit, Key::DOWN | KeyModifierMask::CMD | KeyModifierMask::SHIFT); +#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 | KeyModifierMask::SHIFT); + 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"); @@ -2299,6 +2836,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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()); @@ -2315,9 +2853,12 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); + 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)); @@ -2326,16 +2867,21 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); -#ifdef OSX_ENABLED - SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::CMD | KeyModifierMask::SHIFT); +#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()); - CHECK(text_edit->get_selected_text() == "some"); + 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"); @@ -2344,7 +2890,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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()); + 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"); @@ -2353,7 +2903,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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()); + 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"); @@ -2362,7 +2916,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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()); + 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"); @@ -2372,9 +2930,12 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); + 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)); @@ -2383,16 +2944,21 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); -#ifdef OSX_ENABLED - SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::CMD | KeyModifierMask::SHIFT); +#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()); - CHECK(text_edit->get_selected_text() == " this is"); + 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"); @@ -2401,13 +2967,24 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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()); + 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(); @@ -2416,10 +2993,17 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); + 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); @@ -2427,20 +3011,24 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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"); + 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[0].duplicate()); + 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"); + 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); @@ -2450,22 +3038,36 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_KEY_EVENT(text_edit, Key::B); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "BB"); + 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"); + 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"); @@ -2558,7 +3160,7 @@ TEST_CASE("[SceneTree][TextEdit] versioning") { CHECK(text_edit->has_redo()); text_edit->redo(); - CHECK(text_edit->get_line(0) == "test nested ops"); + 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()); @@ -2659,8 +3261,8 @@ TEST_CASE("[SceneTree][TextEdit] mouse") { 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(140, 0)); + CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x - 100, end_pos.y + 100)) == Point2i(140, 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)); @@ -2714,15 +3316,15 @@ TEST_CASE("[SceneTree][TextEdit] caret") { text_edit->set_caret_blink_enabled(true); CHECK(text_edit->is_caret_blink_enabled()); - text_edit->set_caret_blink_speed(10); - CHECK(text_edit->get_caret_blink_speed() == 10); + text_edit->set_caret_blink_interval(10); + CHECK(text_edit->get_caret_blink_interval() == 10); ERR_PRINT_OFF; - text_edit->set_caret_blink_speed(-1); - CHECK(text_edit->get_caret_blink_speed() == 10); + text_edit->set_caret_blink_interval(-1); + CHECK(text_edit->get_caret_blink_interval() == 10); - text_edit->set_caret_blink_speed(0); - CHECK(text_edit->get_caret_blink_speed() == 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); @@ -2779,6 +3381,136 @@ TEST_CASE("[SceneTree][TextEdit] caret") { 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); @@ -3139,7 +3871,7 @@ TEST_CASE("[SceneTree][TextEdit] viewport") { 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); + 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); @@ -3148,7 +3880,7 @@ TEST_CASE("[SceneTree][TextEdit] viewport") { 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); + 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); @@ -3291,6 +4023,32 @@ TEST_CASE("[SceneTree][TextEdit] viewport") { 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); } @@ -3388,6 +4146,8 @@ TEST_CASE("[SceneTree][TextEdit] gutters") { 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"); @@ -3395,39 +4155,43 @@ TEST_CASE("[SceneTree][TextEdit] gutters") { text_edit->set_gutter_width(0, 10); CHECK(text_edit->get_gutter_width(0) == 10); - CHECK(text_edit->get_total_gutter_width() > 10); - CHECK(text_edit->get_total_gutter_width() < 20); + 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_total_gutter_width() > 20); - CHECK(text_edit->get_total_gutter_width() < 30); + 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); - CHECK(text_edit->get_total_gutter_width() < 20); + 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; diff --git a/tests/scene/test_theme.h b/tests/scene/test_theme.h index fedffc8449..f5b21eec32 100644 --- a/tests/scene/test_theme.h +++ b/tests/scene/test_theme.h @@ -46,7 +46,7 @@ public: } const valid_data[Theme::DATA_TYPE_MAX] = { { Theme::DATA_TYPE_COLOR, Color() }, { Theme::DATA_TYPE_CONSTANT, 42 }, - { Theme::DATA_TYPE_FONT, Ref<Font>(memnew(Font)) }, + { 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)) }, @@ -101,18 +101,24 @@ TEST_CASE_FIXTURE(Fixture, "[Theme] Good theme type names") { 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(ed.has_error == (name == StringName())); + 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(ed.has_error == (name == StringName())); + CHECK_FALSE(ed.has_error); } } } @@ -125,6 +131,8 @@ TEST_CASE_FIXTURE(Fixture, "[Theme] Bad theme type names") { String::utf8("contains_汉字"), }; + ERR_PRINT_OFF; // All these rightfully print errors. + SUBCASE("add_type") { for (const StringName &name : names) { Ref<Theme> theme = memnew(Theme); @@ -175,6 +183,8 @@ TEST_CASE_FIXTURE(Fixture, "[Theme] Bad theme type names") { CHECK(ed.has_error); } } + + ERR_PRINT_ON; } TEST_CASE_FIXTURE(Fixture, "[Theme] Good theme item names") { @@ -223,6 +233,8 @@ TEST_CASE_FIXTURE(Fixture, "[Theme] Bad theme item names") { 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) { @@ -250,6 +262,8 @@ TEST_CASE_FIXTURE(Fixture, "[Theme] Bad theme item names") { } } } + + ERR_PRINT_ON; } } // namespace TestTheme diff --git a/tests/servers/test_text_server.h b/tests/servers/test_text_server.h index 066c280fd5..297f7d2068 100644 --- a/tests/servers/test_text_server.h +++ b/tests/servers/test_text_server.h @@ -28,23 +28,23 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifdef TOOLS_ENABLED - #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_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); - TEST_FAIL_COND(ts.is_null(), "Invalid TS interface."); + CHECK_FALSE_MESSAGE(ts.is_null(), "Invalid TS interface."); if (!ts->has_feature(TextServer::FEATURE_FONT_DYNAMIC)) { continue; @@ -52,7 +52,7 @@ TEST_SUITE("[[TextServer]") { RID font = ts->create_font(); ts->font_set_data_ptr(font, _font_NotoSans_Regular, _font_NotoSans_Regular_size); - TEST_FAIL_COND(font == RID(), "Loading font failed."); + CHECK_FALSE_MESSAGE(font == RID(), "Loading font failed."); ts->free_rid(font); } } @@ -60,7 +60,7 @@ TEST_SUITE("[[TextServer]") { 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); - TEST_FAIL_COND(ts.is_null(), "Invalid TS interface."); + 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; @@ -68,8 +68,10 @@ TEST_SUITE("[[TextServer]") { RID font1 = ts->create_font(); ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size); + ts->font_set_allow_system_fallback(font1, false); RID font2 = ts->create_font(); ts->font_set_data_ptr(font2, _font_NotoSansThaiUI_Regular, _font_NotoSansThaiUI_Regular_size); + ts->font_set_allow_system_fallback(font2, false); Array font; font.push_back(font1); @@ -79,26 +81,26 @@ TEST_SUITE("[[TextServer]") { // 6^ 17^ RID ctx = ts->create_shaped_text(); - TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed."); + CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed."); bool ok = ts->shaped_text_add_string(ctx, test, font, 16); - TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); + 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); - TEST_FAIL_COND(gl_size == 0, "Shaping failed"); + CHECK_FALSE_MESSAGE(gl_size == 0, "Shaping failed"); for (int j = 0; j < gl_size; j++) { if (glyphs[j].start < 6) { - TEST_FAIL_COND(glyphs[j].font_rid != font[1], "Incorrect font selected."); + CHECK_FALSE_MESSAGE(glyphs[j].font_rid != font[1], "Incorrect font selected."); } if ((glyphs[j].start > 6) && (glyphs[j].start < 16)) { - TEST_FAIL_COND(glyphs[j].font_rid != font[0], "Incorrect font selected."); + CHECK_FALSE_MESSAGE(glyphs[j].font_rid != font[0], "Incorrect font selected."); } if (glyphs[j].start > 16) { - TEST_FAIL_COND(glyphs[j].font_rid != RID(), "Incorrect font selected."); - TEST_FAIL_COND(glyphs[j].index != test[glyphs[j].start], "Incorrect glyph index."); + CHECK_FALSE_MESSAGE(glyphs[j].font_rid != RID(), "Incorrect font selected."); + CHECK_FALSE_MESSAGE(glyphs[j].index != test[glyphs[j].start], "Incorrect glyph index."); } - TEST_FAIL_COND((glyphs[j].start < 0 || glyphs[j].end > test.length()), "Incorrect glyph range."); - TEST_FAIL_COND(glyphs[j].font_size != 16, "Incorrect glyph font size."); + 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); @@ -113,7 +115,7 @@ TEST_SUITE("[[TextServer]") { 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); - TEST_FAIL_COND(ts.is_null(), "Invalid TS interface."); + 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; @@ -132,23 +134,23 @@ TEST_SUITE("[[TextServer]") { // 7^ 26^ RID ctx = ts->create_shaped_text(); - TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed."); + CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed."); bool ok = ts->shaped_text_add_string(ctx, test, font, 16); - TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); + 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); - TEST_FAIL_COND(gl_size == 0, "Shaping failed"); + 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) { - TEST_FAIL_COND(((glyphs[j].flags & TextServer::GRAPHEME_IS_RTL) == TextServer::GRAPHEME_IS_RTL), "Incorrect direction."); + 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)) { - TEST_FAIL_COND(((glyphs[j].flags & TextServer::GRAPHEME_IS_RTL) != TextServer::GRAPHEME_IS_RTL), "Incorrect direction."); + CHECK_FALSE_MESSAGE(((glyphs[j].flags & TextServer::GRAPHEME_IS_RTL) != TextServer::GRAPHEME_IS_RTL), "Incorrect direction."); } if (glyphs[j].start > 26) { - TEST_FAIL_COND(((glyphs[j].flags & TextServer::GRAPHEME_IS_RTL) == TextServer::GRAPHEME_IS_RTL), "Incorrect direction."); + CHECK_FALSE_MESSAGE(((glyphs[j].flags & TextServer::GRAPHEME_IS_RTL) == TextServer::GRAPHEME_IS_RTL), "Incorrect direction."); } } } @@ -165,7 +167,7 @@ TEST_SUITE("[[TextServer]") { 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); - TEST_FAIL_COND(ts.is_null(), "Invalid TS interface."); + 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; @@ -186,16 +188,16 @@ TEST_SUITE("[[TextServer]") { { String test = U"Test test long text long text\n"; RID ctx = ts->create_shaped_text(); - TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed."); + CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed."); bool ok = ts->shaped_text_add_string(ctx, test, font, 16); - TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); + 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); - TEST_FAIL_COND(gl_size != 30, "Invalid glyph count."); + 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; @@ -203,11 +205,11 @@ TEST_SUITE("[[TextServer]") { 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) { - TEST_FAIL_COND((!soft || !space || hard || virt || elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((!soft || !space || hard || virt || elo), "Invalid glyph flags."); } else if (j == 29) { - TEST_FAIL_COND((soft || !space || !hard || virt || elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((soft || !space || !hard || virt || elo), "Invalid glyph flags."); } else { - TEST_FAIL_COND((soft || space || hard || virt || elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags."); } } ts->free_rid(ctx); @@ -216,21 +218,63 @@ TEST_SUITE("[[TextServer]") { { String test = U"الحمـد"; RID ctx = ts->create_shaped_text(); - TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed."); + 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); - TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); + 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); - TEST_FAIL_COND(gl_size != 6, "Invalid glyph count."); + 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; - TEST_FAIL_COND((soft || space || hard || virt || elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags."); } if (ts->has_feature(TextServer::FEATURE_KASHIDA_JUSTIFICATION)) { @@ -239,7 +283,7 @@ TEST_SUITE("[[TextServer]") { glyphs = ts->shaped_text_get_glyphs(ctx); gl_size = ts->shaped_text_get_glyph_count(ctx); - TEST_FAIL_COND(gl_size != 6, "Invalid glyph count."); + 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; @@ -247,9 +291,9 @@ TEST_SUITE("[[TextServer]") { 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) { - TEST_FAIL_COND((soft || space || hard || virt || !elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((soft || space || hard || !virt || !elo), "Invalid glyph flags."); } else { - TEST_FAIL_COND((soft || space || hard || virt || elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags."); } } } @@ -259,15 +303,15 @@ TEST_SUITE("[[TextServer]") { { String test = U"الحمـد الرياضي العربي"; RID ctx = ts->create_shaped_text(); - TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed."); + CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed."); bool ok = ts->shaped_text_add_string(ctx, test, font, 16); - TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); + 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); - TEST_FAIL_COND(gl_size != 21, "Invalid glyph count."); + 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; @@ -275,9 +319,9 @@ TEST_SUITE("[[TextServer]") { 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) { - TEST_FAIL_COND((!soft || !space || hard || virt || elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((!soft || !space || hard || virt || elo), "Invalid glyph flags."); } else { - TEST_FAIL_COND((soft || space || hard || virt || elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags."); } } @@ -287,7 +331,7 @@ TEST_SUITE("[[TextServer]") { glyphs = ts->shaped_text_get_glyphs(ctx); gl_size = ts->shaped_text_get_glyph_count(ctx); - TEST_FAIL_COND(gl_size != 23, "Invalid glyph count."); + 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; @@ -295,13 +339,13 @@ TEST_SUITE("[[TextServer]") { 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) { - TEST_FAIL_COND((!soft || !space || hard || virt || elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((!soft || !space || hard || virt || elo), "Invalid glyph flags."); } else if (j == 3 || j == 9) { - TEST_FAIL_COND((soft || space || hard || !virt || !elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((soft || space || hard || !virt || !elo), "Invalid glyph flags."); } else if (j == 18) { - TEST_FAIL_COND((soft || space || hard || virt || !elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((soft || space || hard || virt || !elo), "Invalid glyph flags."); } else { - TEST_FAIL_COND((soft || space || hard || virt || elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags."); } } } @@ -312,16 +356,16 @@ TEST_SUITE("[[TextServer]") { { String test = U"เป็น ภาษา ราชการ และ ภาษา"; RID ctx = ts->create_shaped_text(); - TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed."); + CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed."); bool ok = ts->shaped_text_add_string(ctx, test, font, 16); - TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); + 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); - TEST_FAIL_COND(gl_size != 25, "Invalid glyph count."); + 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; @@ -329,9 +373,9 @@ TEST_SUITE("[[TextServer]") { 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) { - TEST_FAIL_COND((!soft || !space || hard || virt || elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((!soft || !space || hard || virt || elo), "Invalid glyph flags."); } else { - TEST_FAIL_COND((soft || space || hard || virt || elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags."); } } ts->free_rid(ctx); @@ -340,16 +384,16 @@ TEST_SUITE("[[TextServer]") { if (ts->has_feature(TextServer::FEATURE_BREAK_ITERATORS)) { String test = U"เป็นภาษาราชการและภาษา"; RID ctx = ts->create_shaped_text(); - TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed."); + CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed."); bool ok = ts->shaped_text_add_string(ctx, test, font, 16); - TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); + 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); - TEST_FAIL_COND(gl_size != 25, "Invalid glyph count."); + 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; @@ -357,9 +401,9 @@ TEST_SUITE("[[TextServer]") { 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) { - TEST_FAIL_COND((!soft || !space || hard || !virt || elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((!soft || !space || hard || !virt || elo), "Invalid glyph flags."); } else { - TEST_FAIL_COND((soft || space || hard || virt || elo), "Invalid glyph flags."); + CHECK_FALSE_MESSAGE((soft || space || hard || virt || elo), "Invalid glyph flags."); } } ts->free_rid(ctx); @@ -375,7 +419,7 @@ TEST_SUITE("[[TextServer]") { 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); - TEST_FAIL_COND(ts.is_null(), "Invalid TS interface."); + 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; @@ -394,21 +438,21 @@ TEST_SUITE("[[TextServer]") { font.push_back(font2); RID ctx = ts->create_shaped_text(); - TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed."); + CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed."); bool ok = ts->shaped_text_add_string(ctx, test_1, font, 16); - TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); + CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed."); PackedInt32Array brks = ts->shaped_text_get_line_breaks(ctx, 1); - TEST_FAIL_COND(brks.size() != 6, "Invalid line breaks number."); + CHECK_FALSE_MESSAGE(brks.size() != 6, "Invalid line breaks number."); if (brks.size() == 6) { - TEST_FAIL_COND(brks[0] != 0, "Invalid line break position."); - TEST_FAIL_COND(brks[1] != 5, "Invalid line break position."); + CHECK_FALSE_MESSAGE(brks[0] != 0, "Invalid line break position."); + CHECK_FALSE_MESSAGE(brks[1] != 5, "Invalid line break position."); - TEST_FAIL_COND(brks[2] != 5, "Invalid line break position."); - TEST_FAIL_COND(brks[3] != 10, "Invalid line break position."); + CHECK_FALSE_MESSAGE(brks[2] != 5, "Invalid line break position."); + CHECK_FALSE_MESSAGE(brks[3] != 10, "Invalid line break position."); - TEST_FAIL_COND(brks[4] != 10, "Invalid line break position."); - TEST_FAIL_COND(brks[5] != 14, "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); @@ -423,7 +467,7 @@ TEST_SUITE("[[TextServer]") { 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); - TEST_FAIL_COND(ts.is_null(), "Invalid TS interface."); + 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; @@ -448,40 +492,40 @@ TEST_SUITE("[[TextServer]") { float width_old, width; if (ts->has_feature(TextServer::FEATURE_KASHIDA_JUSTIFICATION)) { ctx = ts->create_shaped_text(); - TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed."); + CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed."); ok = ts->shaped_text_add_string(ctx, test_1, font, 16); - TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); + 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); - TEST_FAIL_COND((width != width_old), "Invalid fill width."); + 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); - TEST_FAIL_COND((width <= width_old || width > 100), "Invalid fill width."); + CHECK_FALSE_MESSAGE((width <= width_old || width > 100), "Invalid fill width."); ts->free_rid(ctx); ctx = ts->create_shaped_text(); - TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed."); + CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed."); ok = ts->shaped_text_add_string(ctx, test_2, font, 16); - TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); + 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); - TEST_FAIL_COND((width <= width_old || width > 100), "Invalid fill width."); + 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); - TEST_FAIL_COND((width <= width_old || width > 100), "Invalid fill width."); + CHECK_FALSE_MESSAGE((width <= width_old || width > 100), "Invalid fill width."); ts->free_rid(ctx); } ctx = ts->create_shaped_text(); - TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed."); + CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed."); ok = ts->shaped_text_add_string(ctx, test_3, font, 16); - TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); + 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); - TEST_FAIL_COND((width <= width_old || width > 100), "Invalid fill width."); + CHECK_FALSE_MESSAGE((width <= width_old || width > 100), "Invalid fill width."); ts->free_rid(ctx); @@ -492,10 +536,31 @@ TEST_SUITE("[[TextServer]") { } } + 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); - TEST_FAIL_COND(ts.is_null(), "Invalid TS interface."); + CHECK_FALSE_MESSAGE(ts.is_null(), "Invalid TS interface."); if (ts->has_feature(TextServer::FEATURE_SHAPING)) { CHECK(ts->strip_diacritics(U"ٱلسَّلَامُ عَلَيْكُمْ") == U"ٱلسلام عليكم"); @@ -523,7 +588,7 @@ TEST_SUITE("[[TextServer]") { continue; } - TEST_FAIL_COND(ts.is_null(), "Invalid TS interface."); + CHECK_FALSE_MESSAGE(ts.is_null(), "Invalid TS interface."); { String text1 = U"linguistically similar and effectively form"; // 14^ 22^ 26^ 38^ @@ -561,5 +626,6 @@ TEST_SUITE("[[TextServer]") { } }; // namespace TestTextServer -#endif // TEST_TEXT_SERVER_H #endif // TOOLS_ENABLED + +#endif // TEST_TEXT_SERVER_H diff --git a/tests/test_macros.h b/tests/test_macros.h index 6029a9cfc7..3b734b9699 100644 --- a/tests/test_macros.h +++ b/tests/test_macros.h @@ -31,6 +31,7 @@ #ifndef TEST_MACROS_H #define TEST_MACROS_H +#include "core/core_globals.h" #include "core/input/input_map.h" #include "core/object/message_queue.h" #include "core/variant/variant.h" @@ -53,12 +54,12 @@ // 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 @@ -132,11 +133,11 @@ int register_test_command(String p_command, TestFunc p_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::CMD). +// 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::CMD); -// SEND_GUI_DOUBLE_CLICK - takes an object, position and modifiers. e.g SEND_GUI_DOUBLE_CLICK(code_edit, Vector2(50, 50), KeyModifierMask::CMD); +// 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) \ { \ @@ -160,7 +161,6 @@ int register_test_command(String p_command, TestFunc p_function); 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_command_pressed(((m_modifers)&KeyModifierMask::CMD) != 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) \ @@ -199,8 +199,8 @@ int register_test_command(String p_command, TestFunc p_function); // 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 = _print_error_enabled; \ - _print_error_enabled = false; \ + bool errors_enabled = CoreGlobals::print_error_enabled; \ + CoreGlobals::print_error_enabled = false; \ Ref<InputEventMouseMotion> event; \ event.instantiate(); \ event->set_position(m_local_pos); \ @@ -209,7 +209,7 @@ int register_test_command(String p_command, TestFunc p_function); _UPDATE_EVENT_MODIFERS(event, m_modifers); \ m_object->get_viewport()->push_input(event); \ MessageQueue::get_singleton()->flush(); \ - _print_error_enabled = errors_enabled; \ + CoreGlobals::print_error_enabled = errors_enabled; \ } // Utility class / macros for testing signals @@ -271,22 +271,20 @@ public: static SignalWatcher *get_singleton() { return singleton; } void watch_signal(Object *p_object, const String &p_signal) { - Vector<Variant> args; - args.push_back(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), args); + 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), args); + 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), args); + 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), args); + 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."); diff --git a/tests/test_main.cpp b/tests/test_main.cpp index ac0cdf0cc1..d58c19ac32 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -30,6 +30,8 @@ #include "test_main.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" @@ -45,16 +47,24 @@ #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_math_funcs.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" @@ -65,18 +75,26 @@ #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_2d.h" #include "tests/scene/test_path_3d.h" +#include "tests/scene/test_primitives.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" @@ -86,7 +104,7 @@ #include "tests/test_macros.h" -#include "scene/resources/default_theme/default_theme.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" @@ -102,7 +120,7 @@ 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) { @@ -166,6 +184,7 @@ struct GodotTestCaseListener : public doctest::IReporter { 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(); @@ -173,18 +192,15 @@ struct GodotTestCaseListener : public doctest::IReporter { 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, Vector2i(0, 0), err); + DisplayServer::create(i, "", DisplayServer::WindowMode::WINDOW_MODE_MINIMIZED, DisplayServer::VSyncMode::VSYNC_ENABLED, 0, nullptr, Vector2i(0, 0), err); break; } } @@ -192,10 +208,10 @@ struct GodotTestCaseListener : public doctest::IReporter { RenderingServerDefault::get_singleton()->init(); RenderingServerDefault::get_singleton()->set_render_loop_enabled(false); - physics_server_3d = PhysicsServer3DManager::new_default_server(); + physics_server_3d = PhysicsServer3DManager::get_singleton()->new_default_server(); physics_server_3d->init(); - physics_server_2d = PhysicsServer2DManager::new_default_server(); + physics_server_2d = PhysicsServer2DManager::get_singleton()->new_default_server(); physics_server_2d->init(); navigation_server_3d = NavigationServer3DManager::new_default_server(); @@ -204,12 +220,22 @@ struct GodotTestCaseListener : public doctest::IReporter { memnew(InputMap); InputMap::get_singleton()->load_default(); - make_default_theme(1.0, Ref<Font>()); + 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 { @@ -225,7 +251,10 @@ struct GodotTestCaseListener : public doctest::IReporter { memdelete(SceneTree::get_singleton()); } - clear_default_theme(); + if (theme_db) { + memdelete(theme_db); + theme_db = nullptr; + } if (navigation_server_3d) { memdelete(navigation_server_3d); @@ -255,7 +284,7 @@ struct GodotTestCaseListener : public doctest::IReporter { if (RenderingServer::get_singleton()) { RenderingServer::get_singleton()->sync(); - RenderingServer::get_singleton()->global_variables_clear(); + RenderingServer::get_singleton()->global_shader_parameters_clear(); RenderingServer::get_singleton()->finish(); memdelete(RenderingServer::get_singleton()); } @@ -272,6 +301,11 @@ struct GodotTestCaseListener : public doctest::IReporter { 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 { diff --git a/tests/test_utils.cpp b/tests/test_utils.cpp index 11cb6398aa..ce12a30381 100644 --- a/tests/test_utils.cpp +++ b/tests/test_utils.cpp @@ -34,7 +34,7 @@ String TestUtils::get_data_path(const String &p_file) { String data_path = "../tests/data"; - return get_executable_dir().plus_file(data_path.plus_file(p_file)); + return get_executable_dir().path_join(data_path.path_join(p_file)); } String TestUtils::get_executable_dir() { diff --git a/tests/test_validate_testing.h b/tests/test_validate_testing.h index 413a7e351d..41185a7d16 100644 --- a/tests/test_validate_testing.h +++ b/tests/test_validate_testing.h @@ -31,6 +31,7 @@ #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" @@ -49,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; @@ -85,7 +86,7 @@ TEST_SUITE("Validate tests") { Plane plane(Vector3(1, 1, 1), 1.0); INFO(plane); - Quaternion quat(Vector3(0.5, 1.0, 2.0)); + Quaternion quat = Quaternion::from_euler(Vector3(0.5, 1.0, 2.0)); INFO(quat); AABB aabb(Vector3(), Vector3(100, 100, 100)); |