diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/core/config/test_project_settings.h | 60 | ||||
-rw-r--r-- | tests/core/input/test_input_event.h | 115 | ||||
-rw-r--r-- | tests/core/io/test_image.h | 90 | ||||
-rw-r--r-- | tests/core/io/test_json.h | 86 | ||||
-rw-r--r-- | tests/core/io/test_resource.h | 52 | ||||
-rw-r--r-- | tests/core/object/test_class_db.h | 3 | ||||
-rw-r--r-- | tests/core/object/test_object.h | 23 | ||||
-rw-r--r-- | tests/core/string/test_string.h | 18 | ||||
-rw-r--r-- | tests/core/string/test_translation_server.h | 136 | ||||
-rw-r--r-- | tests/core/variant/test_array.h | 26 | ||||
-rw-r--r-- | tests/data/images/icon.dds | bin | 0 -> 87536 bytes | |||
-rw-r--r-- | tests/scene/test_packed_scene.h | 155 | ||||
-rw-r--r-- | tests/scene/test_text_edit.h | 4 | ||||
-rw-r--r-- | tests/scene/test_theme.h | 2 | ||||
-rw-r--r-- | tests/scene/test_viewport.h | 463 | ||||
-rw-r--r-- | tests/scene/test_window.h | 96 | ||||
-rw-r--r-- | tests/servers/rendering/test_shader_preprocessor.h | 333 | ||||
-rw-r--r-- | tests/servers/test_navigation_server_3d.h | 483 | ||||
-rw-r--r-- | tests/test_macros.h | 14 | ||||
-rw-r--r-- | tests/test_main.cpp | 5 |
20 files changed, 2139 insertions, 25 deletions
diff --git a/tests/core/config/test_project_settings.h b/tests/core/config/test_project_settings.h index 1deafccde1..9bd072f511 100644 --- a/tests/core/config/test_project_settings.h +++ b/tests/core/config/test_project_settings.h @@ -32,9 +32,17 @@ #define TEST_PROJECT_SETTINGS_H #include "core/config/project_settings.h" +#include "core/io/dir_access.h" #include "core/variant/variant.h" #include "tests/test_macros.h" +class TestProjectSettingsInternalsAccessor { +public: + static String &resource_path() { + return ProjectSettings::get_singleton()->resource_path; + }; +}; + namespace TestProjectSettings { TEST_CASE("[ProjectSettings] Get existing setting") { @@ -97,6 +105,58 @@ TEST_CASE("[ProjectSettings] Set value should be returned when retrieved") { CHECK(ProjectSettings::get_singleton()->has_setting("my_custom_setting")); } +TEST_CASE("[ProjectSettings] localize_path") { + String old_resource_path = TestProjectSettingsInternalsAccessor::resource_path(); + TestProjectSettingsInternalsAccessor::resource_path() = DirAccess::create(DirAccess::ACCESS_FILESYSTEM)->get_current_dir(); + String root_path = ProjectSettings::get_singleton()->get_resource_path(); +#ifdef WINDOWS_ENABLED + String root_path_win = ProjectSettings::get_singleton()->get_resource_path().replace("/", "\\"); +#endif + + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("filename"), "res://filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("path/filename"), "res://path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("path/something/../filename"), "res://path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("path/./filename"), "res://path/filename"); +#ifdef WINDOWS_ENABLED + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("path\\filename"), "res://path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("path\\something\\..\\filename"), "res://path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("path\\.\\filename"), "res://path/filename"); +#endif + + // FIXME?: These checks pass, but that doesn't seems correct + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("../filename"), "res://filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("../path/filename"), "res://path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("..\\path\\filename"), "res://path/filename"); + + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("/testroot/filename"), "/testroot/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("/testroot/path/filename"), "/testroot/path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("/testroot/path/something/../filename"), "/testroot/path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("/testroot/path/./filename"), "/testroot/path/filename"); +#ifdef WINDOWS_ENABLED + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:/testroot/filename"), "C:/testroot/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:/testroot/path/filename"), "C:/testroot/path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:/testroot/path/something/../filename"), "C:/testroot/path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:/testroot/path/./filename"), "C:/testroot/path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:\\testroot\\filename"), "C:/testroot/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:\\testroot\\path\\filename"), "C:/testroot/path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:\\testroot\\path\\something\\..\\filename"), "C:/testroot/path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:\\testroot\\path\\.\\filename"), "C:/testroot/path/filename"); +#endif + + CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path + "/filename"), "res://filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path + "/path/filename"), "res://path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path + "/path/something/../filename"), "res://path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path + "/path/./filename"), "res://path/filename"); +#ifdef WINDOWS_ENABLED + CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path_win + "\\filename"), "res://filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path_win + "\\path\\filename"), "res://path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path_win + "\\path\\something\\..\\filename"), "res://path/filename"); + CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path_win + "\\path\\.\\filename"), "res://path/filename"); +#endif + + TestProjectSettingsInternalsAccessor::resource_path() = old_resource_path; +} + } // namespace TestProjectSettings #endif // TEST_PROJECT_SETTINGS_H diff --git a/tests/core/input/test_input_event.h b/tests/core/input/test_input_event.h new file mode 100644 index 0000000000..6b4b80486c --- /dev/null +++ b/tests/core/input/test_input_event.h @@ -0,0 +1,115 @@ +/**************************************************************************/ +/* test_input_event.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_INPUT_EVENT_H +#define TEST_INPUT_EVENT_H + +#include "core/input/input_event.h" +#include "core/math/rect2.h" +#include "core/os/memory.h" +#include "core/variant/array.h" + +#include "tests/test_macros.h" + +namespace TestInputEvent { +TEST_CASE("[InputEvent] Signal is emitted when device is changed") { + Ref<InputEventKey> input_event; + input_event.instantiate(); + + SIGNAL_WATCH(*input_event, SNAME("changed")); + Array args1; + Array empty_args; + empty_args.push_back(args1); + + input_event->set_device(1); + + SIGNAL_CHECK("changed", empty_args); + CHECK(input_event->get_device() == 1); + + SIGNAL_UNWATCH(*input_event, SNAME("changed")); +} + +TEST_CASE("[InputEvent] Test accumulate") { + Ref<InputEventMouseMotion> iemm1, iemm2; + Ref<InputEventKey> iek; + + iemm1.instantiate(), iemm2.instantiate(); + iek.instantiate(); + + iemm1->set_button_mask(MouseButtonMask::LEFT); + + CHECK_FALSE(iemm1->accumulate(iemm2)); + + iemm2->set_button_mask(MouseButtonMask::LEFT); + + CHECK(iemm1->accumulate(iemm2)); + + CHECK_FALSE(iemm1->accumulate(iek)); + CHECK_FALSE(iemm2->accumulate(iek)); +} + +TEST_CASE("[InputEvent][SceneTree] Test methods that interact with the InputMap") { + const String mock_action = "mock_action"; + Ref<InputEventJoypadMotion> iejm; + iejm.instantiate(); + + InputMap::get_singleton()->add_action(mock_action, 0.5); + InputMap::get_singleton()->action_add_event(mock_action, iejm); + + CHECK(iejm->is_action_type()); + CHECK(iejm->is_action(mock_action)); + + CHECK(iejm->is_action_released(mock_action)); + CHECK(Math::is_equal_approx(iejm->get_action_strength(mock_action), 0.0f)); + + iejm->set_axis_value(0.8f); + // Since deadzone is 0.5, action_strength grows linearly from 0.5 to 1.0. + CHECK(Math::is_equal_approx(iejm->get_action_strength(mock_action), 0.6f)); + CHECK(Math::is_equal_approx(iejm->get_action_raw_strength(mock_action), 0.8f)); + CHECK(iejm->is_action_pressed(mock_action)); + + InputMap::get_singleton()->erase_action(mock_action); +} + +TEST_CASE("[InputEvent] Test xformed_by") { + Ref<InputEventMouseMotion> iemm1; + iemm1.instantiate(); + + iemm1->set_position(Vector2(0.0f, 0.0f)); + Transform2D transform; + transform = transform.translated(Vector2(2.0f, 3.0f)); + + Ref<InputEventMouseMotion> iemm2 = iemm1->xformed_by(transform); + + CHECK(iemm2->get_position().is_equal_approx(Vector2(2.0f, 3.0f))); +} +} // namespace TestInputEvent + +#endif // TEST_INPUT_EVENT_H diff --git a/tests/core/io/test_image.h b/tests/core/io/test_image.h index d6eaa8bd77..92ab166ae8 100644 --- a/tests/core/io/test_image.h +++ b/tests/core/io/test_image.h @@ -115,6 +115,16 @@ TEST_CASE("[Image] Saving and loading") { image_bmp->load_bmp_from_buffer(data_bmp) == OK, "The BMP image should load successfully."); + // Load DDS + Ref<Image> image_dds = memnew(Image()); + Ref<FileAccess> f_dds = FileAccess::open(TestUtils::get_data_path("images/icon.dds"), FileAccess::READ, &err); + PackedByteArray data_dds; + data_dds.resize(f_dds->get_length() + 1); + f_dds->get_buffer(data_dds.ptrw(), f_dds->get_length()); + CHECK_MESSAGE( + image_dds->load_dds_from_buffer(data_dds) == OK, + "The DDS image should load successfully."); + // Load JPG Ref<Image> image_jpg = memnew(Image()); Ref<FileAccess> f_jpg = FileAccess::open(TestUtils::get_data_path("images/icon.jpg"), FileAccess::READ, &err); @@ -325,6 +335,86 @@ TEST_CASE("[Image] Modifying pixels of an image") { CHECK_MESSAGE(gray_image->get_pixel(2, 2).is_equal_approx(Color(0.266666681, 0.266666681, 0.266666681, 1)), "convert() RGBA to L8 should be around 0.266666681 (68)."); } } + +TEST_CASE("[Image] Custom mipmaps") { + Ref<Image> image = memnew(Image(100, 100, false, Image::FORMAT_RGBA8)); + + REQUIRE(!image->has_mipmaps()); + image->generate_mipmaps(); + REQUIRE(image->has_mipmaps()); + + const int mipmaps = image->get_mipmap_count() + 1; + REQUIRE(mipmaps == 7); + + // Initialize reference mipmap data. + // Each byte is given value "mipmap_index * 5". + + { + PackedByteArray data = image->get_data(); + uint8_t *data_ptr = data.ptrw(); + + for (int mip = 0; mip < mipmaps; mip++) { + int mip_offset = 0; + int mip_size = 0; + image->get_mipmap_offset_and_size(mip, mip_offset, mip_size); + + for (int i = 0; i < mip_size; i++) { + data_ptr[mip_offset + i] = mip * 5; + } + } + image->set_data(image->get_width(), image->get_height(), image->has_mipmaps(), image->get_format(), data); + } + + // Byte format conversion. + + for (int format = Image::FORMAT_L8; format <= Image::FORMAT_RGBA8; format++) { + Ref<Image> image_bytes = memnew(Image()); + image_bytes->copy_internals_from(image); + image_bytes->convert((Image::Format)format); + REQUIRE(image_bytes->has_mipmaps()); + + PackedByteArray data = image_bytes->get_data(); + const uint8_t *data_ptr = data.ptr(); + + for (int mip = 0; mip < mipmaps; mip++) { + int mip_offset = 0; + int mip_size = 0; + image_bytes->get_mipmap_offset_and_size(mip, mip_offset, mip_size); + + for (int i = 0; i < mip_size; i++) { + if (data_ptr[mip_offset + i] != mip * 5) { + REQUIRE_MESSAGE(false, "Byte format conversion error."); + } + } + } + } + + // Floating point format conversion. + + for (int format = Image::FORMAT_RF; format <= Image::FORMAT_RGBAF; format++) { + Ref<Image> image_rgbaf = memnew(Image()); + image_rgbaf->copy_internals_from(image); + image_rgbaf->convert((Image::Format)format); + REQUIRE(image_rgbaf->has_mipmaps()); + + PackedByteArray data = image_rgbaf->get_data(); + const uint8_t *data_ptr = data.ptr(); + + for (int mip = 0; mip < mipmaps; mip++) { + int mip_offset = 0; + int mip_size = 0; + image_rgbaf->get_mipmap_offset_and_size(mip, mip_offset, mip_size); + + for (int i = 0; i < mip_size; i += 4) { + float value = *(float *)(data_ptr + mip_offset + i); + if (!Math::is_equal_approx(value * 255.0f, mip * 5)) { + REQUIRE_MESSAGE(false, "Floating point conversion error."); + } + } + } + } +} + } // namespace TestImage #endif // TEST_IMAGE_H diff --git a/tests/core/io/test_json.h b/tests/core/io/test_json.h index 34a66ab6b5..bf2ed42740 100644 --- a/tests/core/io/test_json.h +++ b/tests/core/io/test_json.h @@ -146,6 +146,92 @@ TEST_CASE("[JSON] Parsing objects (dictionaries)") { dictionary["empty_object"].hash() == Dictionary().hash(), "The parsed JSON should contain the expected values."); } + +TEST_CASE("[JSON] Parsing escape sequences") { + // Only certain escape sequences are valid according to the JSON specification. + // Others must result in a parsing error instead. + + JSON json; + + TypedArray<String> valid_escapes; + valid_escapes.push_back("\";\""); + valid_escapes.push_back("\\;\\"); + valid_escapes.push_back("/;/"); + valid_escapes.push_back("b;\b"); + valid_escapes.push_back("f;\f"); + valid_escapes.push_back("n;\n"); + valid_escapes.push_back("r;\r"); + valid_escapes.push_back("t;\t"); + + SUBCASE("Basic valid escape sequences") { + for (int i = 0; i < valid_escapes.size(); i++) { + String valid_escape = valid_escapes[i]; + String valid_escape_string = valid_escape.get_slice(";", 0); + String valid_escape_value = valid_escape.get_slice(";", 1); + + String json_string = "\"\\"; + json_string += valid_escape_string; + json_string += "\""; + json.parse(json_string); + + CHECK_MESSAGE( + json.get_error_line() == 0, + vformat("Parsing valid escape sequence `%s` as JSON should parse successfully.", valid_escape_string)); + + String json_value = json.get_data(); + CHECK_MESSAGE( + json_value == valid_escape_value, + vformat("Parsing valid escape sequence `%s` as JSON should return the expected value.", valid_escape_string)); + } + } + + SUBCASE("Valid unicode escape sequences") { + String json_string = "\"\\u0020\""; + json.parse(json_string); + + CHECK_MESSAGE( + json.get_error_line() == 0, + vformat("Parsing valid unicode escape sequence with value `0020` as JSON should parse successfully.")); + + String json_value = json.get_data(); + CHECK_MESSAGE( + json_value == " ", + vformat("Parsing valid unicode escape sequence with value `0020` as JSON should return the expected value.")); + } + + SUBCASE("Invalid escape sequences") { + ERR_PRINT_OFF + for (char32_t i = 0; i < 128; i++) { + bool skip = false; + for (int j = 0; j < valid_escapes.size(); j++) { + String valid_escape = valid_escapes[j]; + String valid_escape_string = valid_escape.get_slice(";", 0); + if (valid_escape_string[0] == i) { + skip = true; + break; + } + } + + if (skip) { + continue; + } + + String json_string = "\"\\"; + json_string += i; + json_string += "\""; + Error err = json.parse(json_string); + + // TODO: Line number is currently kept on 0, despite an error occurring. This should be fixed in the JSON parser. + // CHECK_MESSAGE( + // json.get_error_line() != 0, + // vformat("Parsing invalid escape sequence with ASCII value `%d` as JSON should fail to parse.", i)); + CHECK_MESSAGE( + err == ERR_PARSE_ERROR, + vformat("Parsing invalid escape sequence with ASCII value `%d` as JSON should fail to parse with ERR_PARSE_ERROR.", i)); + } + ERR_PRINT_ON + } +} } // namespace TestJSON #endif // TEST_JSON_H diff --git a/tests/core/io/test_resource.h b/tests/core/io/test_resource.h index 20d76c8894..8fc2a2f040 100644 --- a/tests/core/io/test_resource.h +++ b/tests/core/io/test_resource.h @@ -109,6 +109,58 @@ TEST_CASE("[Resource] Saving and loading") { loaded_child_resource_text->get_name() == "I'm a child resource", "The loaded child resource name should be equal to the expected value."); } + +TEST_CASE("[Resource] Breaking circular references on save") { + Ref<Resource> resource_a = memnew(Resource); + resource_a->set_name("A"); + Ref<Resource> resource_b = memnew(Resource); + resource_b->set_name("B"); + Ref<Resource> resource_c = memnew(Resource); + resource_c->set_name("C"); + resource_a->set_meta("next", resource_b); + resource_b->set_meta("next", resource_c); + resource_c->set_meta("next", resource_b); + + 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_a, save_path_binary); + ResourceSaver::save(resource_a, save_path_text); + + const Ref<Resource> &loaded_resource_a_binary = ResourceLoader::load(save_path_binary); + CHECK_MESSAGE( + loaded_resource_a_binary->get_name() == "A", + "The loaded resource name should be equal to the expected value."); + const Ref<Resource> &loaded_resource_b_binary = loaded_resource_a_binary->get_meta("next"); + CHECK_MESSAGE( + loaded_resource_b_binary->get_name() == "B", + "The loaded child resource name should be equal to the expected value."); + const Ref<Resource> &loaded_resource_c_binary = loaded_resource_b_binary->get_meta("next"); + CHECK_MESSAGE( + loaded_resource_c_binary->get_name() == "C", + "The loaded child resource name should be equal to the expected value."); + CHECK_MESSAGE( + !loaded_resource_c_binary->get_meta("next"), + "The loaded child resource circular reference should be NULL."); + + const Ref<Resource> &loaded_resource_a_text = ResourceLoader::load(save_path_text); + CHECK_MESSAGE( + loaded_resource_a_text->get_name() == "A", + "The loaded resource name should be equal to the expected value."); + const Ref<Resource> &loaded_resource_b_text = loaded_resource_a_text->get_meta("next"); + CHECK_MESSAGE( + loaded_resource_b_text->get_name() == "B", + "The loaded child resource name should be equal to the expected value."); + const Ref<Resource> &loaded_resource_c_text = loaded_resource_b_text->get_meta("next"); + CHECK_MESSAGE( + loaded_resource_c_text->get_name() == "C", + "The loaded child resource name should be equal to the expected value."); + CHECK_MESSAGE( + !loaded_resource_c_text->get_meta("next"), + "The loaded child resource circular reference should be NULL."); + + // Break circular reference to avoid memory leak + resource_c->remove_meta("next"); +} } // namespace TestResource #endif // TEST_RESOURCE_H diff --git a/tests/core/object/test_class_db.h b/tests/core/object/test_class_db.h index 3f091fd2fe..5f7de11c71 100644 --- a/tests/core/object/test_class_db.h +++ b/tests/core/object/test_class_db.h @@ -409,9 +409,6 @@ void validate_method(const Context &p_context, const ExposedClass &p_class, cons if (p_method.return_type.name != StringName()) { const ExposedClass *return_class = p_context.find_exposed_class(p_method.return_type); if (return_class) { - TEST_COND(return_class->is_singleton, - "Method return type is a singleton: '", p_class.name, ".", p_method.name, "'."); - if (p_class.api_type == ClassDB::API_CORE) { TEST_COND(return_class->api_type == ClassDB::API_EDITOR, "Method '", p_class.name, ".", p_method.name, "' has return type '", return_class->name, diff --git a/tests/core/object/test_object.h b/tests/core/object/test_object.h index 98f9b3da65..8ab6221a1c 100644 --- a/tests/core/object/test_object.h +++ b/tests/core/object/test_object.h @@ -399,6 +399,29 @@ TEST_CASE("[Object] Signals") { SIGNAL_CHECK("my_custom_signal", empty_signal_args); SIGNAL_UNWATCH(&object, "my_custom_signal"); } + + SUBCASE("Connecting and then disconnecting many signals should not leave anything behind") { + List<Object::Connection> signal_connections; + Object targets[100]; + + for (int i = 0; i < 10; i++) { + ERR_PRINT_OFF; + for (Object &target : targets) { + object.connect("my_custom_signal", callable_mp(&target, &Object::notify_property_list_changed)); + } + ERR_PRINT_ON; + signal_connections.clear(); + object.get_all_signal_connections(&signal_connections); + CHECK(signal_connections.size() == 100); + } + + for (Object &target : targets) { + object.disconnect("my_custom_signal", callable_mp(&target, &Object::notify_property_list_changed)); + } + signal_connections.clear(); + object.get_all_signal_connections(&signal_connections); + CHECK(signal_connections.size() == 0); + } } } // namespace TestObject diff --git a/tests/core/string/test_string.h b/tests/core/string/test_string.h index afe6b8a7ed..659fb003d3 100644 --- a/tests/core/string/test_string.h +++ b/tests/core/string/test_string.h @@ -170,10 +170,10 @@ TEST_CASE("[String] Invalid UTF8 (non-standard)") { ERR_PRINT_OFF 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 }; + static const char32_t u32str[] = { 0x45, 0x304A, 0x3088, 0x3046, 0x1F3A4, 0x20AC, 0xFFFD, 0 }; String s; Error err = s.parse_utf8((const char *)u8str); - CHECK(err == ERR_PARSE_ERROR); + CHECK(err == ERR_INVALID_DATA); CHECK(s == u32str); CharString cs = (const char *)u8str; @@ -185,7 +185,7 @@ 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 }; + static const char32_t u32str[] = { 0x45, 0x304A, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x3088, 0x3046, 0xFFFD, 0x1F3A4, 0x20AC, 0xFFFD, 0 }; String s; Error err = s.parse_utf8((const char *)u8str); CHECK(err == ERR_INVALID_DATA); @@ -301,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] == 0xd812); // Unpaired UTF-16 surrogate - CHECK(String::chr(0x20d812)[0] == 0x20d812); // Outside UTF-32 range + CHECK(String::chr(0xd812)[0] == 0xfffd); // Unpaired UTF-16 surrogate + CHECK(String::chr(0x20d812)[0] == 0xfffd); // Outside UTF-32 range ERR_PRINT_ON } @@ -1095,7 +1095,9 @@ TEST_CASE("[String] pad") { s = String("10.10"); CHECK(s.pad_decimals(4) == U"10.1000"); + CHECK(s.pad_decimals(1) == U"10.1"); CHECK(s.pad_zeros(4) == U"0010.10"); + CHECK(s.pad_zeros(1) == U"10.10"); } TEST_CASE("[String] is_subsequence_of") { @@ -1370,8 +1372,8 @@ TEST_CASE("[String] Ensuring empty string into parse_utf8 passes empty string") } TEST_CASE("[String] Cyrillic to_lower()") { - String upper = String::utf8("АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ"); - String lower = String::utf8("абвгдеёжзийклмнопрстуфхцчшщъыьэюя"); + String upper = U"АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ"; + String lower = U"абвгдеёжзийклмнопрстуфхцчшщъыьэюя"; String test = upper.to_lower(); @@ -1703,7 +1705,7 @@ TEST_CASE("[String] validate_identifier") { String name_with_spaces = "Name with spaces"; CHECK(name_with_spaces.validate_identifier() == "Name_with_spaces"); - String name_with_invalid_chars = String::utf8("Invalid characters:@*#&世界"); + String name_with_invalid_chars = U"Invalid characters:@*#&世界"; CHECK(name_with_invalid_chars.validate_identifier() == "Invalid_characters_______"); } diff --git a/tests/core/string/test_translation_server.h b/tests/core/string/test_translation_server.h new file mode 100644 index 0000000000..2c20574309 --- /dev/null +++ b/tests/core/string/test_translation_server.h @@ -0,0 +1,136 @@ +/**************************************************************************/ +/* test_translation_server.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_TRANSLATION_SERVER_H +#define TEST_TRANSLATION_SERVER_H + +#include "core/string/translation.h" + +#include "tests/test_macros.h" + +namespace TestTranslationServer { +TEST_CASE("[TranslationServer] Translation operations") { + Ref<Translation> t = memnew(Translation); + t->set_locale("uk"); + t->add_message("Good Morning", String::utf8("Добрий ранок")); + + TranslationServer *ts = TranslationServer::get_singleton(); + + int l_count_before = ts->get_loaded_locales().size(); + ts->add_translation(t); + int l_count_after = ts->get_loaded_locales().size(); + // Newly created Translation object should be added to the list, so the counter should increase, too. + CHECK(l_count_after > l_count_before); + + Ref<Translation> trans = ts->get_translation_object("uk"); + CHECK(trans.is_valid()); + + ts->set_locale("uk"); + CHECK(ts->translate("Good Morning") == String::utf8("Добрий ранок")); + + ts->remove_translation(t); + trans = ts->get_translation_object("uk"); + CHECK(trans.is_null()); + // If no suitable Translation object has been found - the original message should be returned. + CHECK(ts->translate("Good Morning") == "Good Morning"); +} + +TEST_CASE("[TranslationServer] Locale operations") { + TranslationServer *ts = TranslationServer::get_singleton(); + + // Language variant test; we supplied the variant of Español and the result should be the same string. + String loc = "es_Hani_ES_tradnl"; + String res = ts->standardize_locale(loc); + + CHECK(res == loc); + + // No such variant in variant_map; should return everything except the variant. + loc = "es_Hani_ES_missing"; + res = ts->standardize_locale(loc); + + CHECK(res == "es_Hani_ES"); + + // Non-ISO language name check (Windows issue). + loc = "iw_Hani_IL"; + res = ts->standardize_locale(loc); + + CHECK(res == "he_Hani_IL"); + + // Country rename check. + loc = "uk_Hani_UK"; + res = ts->standardize_locale(loc); + + CHECK(res == "uk_Hani_GB"); + + // Supplying a script name that is not in the list. + loc = "de_Wrong_DE"; + res = ts->standardize_locale(loc); + + CHECK(res == "de_DE"); +} + +TEST_CASE("[TranslationServer] Comparing locales") { + TranslationServer *ts = TranslationServer::get_singleton(); + + String locale_a = "es"; + String locale_b = "es"; + + // Exact match check. + int res = ts->compare_locales(locale_a, locale_b); + + CHECK(res == 10); + + locale_a = "sr-Latn-CS"; + locale_b = "sr-Latn-RS"; + + // Two elements from locales match. + res = ts->compare_locales(locale_a, locale_b); + + CHECK(res == 2); + + locale_a = "uz-Cyrl-UZ"; + locale_b = "uz-Latn-UZ"; + + // Two elements match, but they are not sequentual. + res = ts->compare_locales(locale_a, locale_b); + + CHECK(res == 2); + + locale_a = "es-EC"; + locale_b = "fr-LU"; + + // No match. + res = ts->compare_locales(locale_a, locale_b); + + CHECK(res == 0); +} +} // namespace TestTranslationServer + +#endif // TEST_TRANSLATION_SERVER_H diff --git a/tests/core/variant/test_array.h b/tests/core/variant/test_array.h index ccb02ed5fa..228d77b3b5 100644 --- a/tests/core/variant/test_array.h +++ b/tests/core/variant/test_array.h @@ -304,13 +304,31 @@ TEST_CASE("[Array] slice()") { CHECK(slice8[1] == Variant(3)); CHECK(slice8[2] == Variant(1)); + Array slice9 = array.slice(10, 0, -2); + CHECK(slice9.size() == 3); + CHECK(slice9[0] == Variant(5)); + CHECK(slice9[1] == Variant(3)); + CHECK(slice9[2] == Variant(1)); + + Array slice10 = array.slice(2, -10, -1); + CHECK(slice10.size() == 3); + CHECK(slice10[0] == Variant(2)); + CHECK(slice10[1] == Variant(1)); + CHECK(slice10[2] == Variant(0)); + ERR_PRINT_OFF; - Array slice9 = array.slice(4, 1); - CHECK(slice9.size() == 0); + Array slice11 = array.slice(4, 1); + CHECK(slice11.size() == 0); - Array slice10 = array.slice(3, -4); - CHECK(slice10.size() == 0); + Array slice12 = array.slice(3, -4); + CHECK(slice12.size() == 0); ERR_PRINT_ON; + + Array slice13 = Array().slice(1); + CHECK(slice13.size() == 0); + + Array slice14 = array.slice(6); + CHECK(slice14.size() == 0); } TEST_CASE("[Array] Duplicate array") { diff --git a/tests/data/images/icon.dds b/tests/data/images/icon.dds Binary files differnew file mode 100644 index 0000000000..8a9de402cb --- /dev/null +++ b/tests/data/images/icon.dds diff --git a/tests/scene/test_packed_scene.h b/tests/scene/test_packed_scene.h new file mode 100644 index 0000000000..3517aba31f --- /dev/null +++ b/tests/scene/test_packed_scene.h @@ -0,0 +1,155 @@ +/**************************************************************************/ +/* test_packed_scene.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_PACKED_SCENE_H +#define TEST_PACKED_SCENE_H + +#include "scene/resources/packed_scene.h" + +#include "tests/test_macros.h" + +namespace TestPackedScene { + +TEST_CASE("[PackedScene] Pack Scene and Retrieve State") { + // Create a scene to pack. + Node *scene = memnew(Node); + scene->set_name("TestScene"); + + // Pack the scene. + PackedScene packed_scene; + const Error err = packed_scene.pack(scene); + CHECK(err == OK); + + // Retrieve the packed state. + Ref<SceneState> state = packed_scene.get_state(); + CHECK(state.is_valid()); + CHECK(state->get_node_count() == 1); + CHECK(state->get_node_name(0) == "TestScene"); + + memdelete(scene); +} + +TEST_CASE("[PackedScene] Clear Packed Scene") { + // Create a scene to pack. + Node *scene = memnew(Node); + scene->set_name("TestScene"); + + // Pack the scene. + PackedScene packed_scene; + packed_scene.pack(scene); + + // Clear the packed scene. + packed_scene.clear(); + + // Check if it has been cleared. + Ref<SceneState> state = packed_scene.get_state(); + CHECK_FALSE(state->get_node_count() == 1); + + memdelete(scene); +} + +TEST_CASE("[PackedScene] Can Instantiate Packed Scene") { + // Create a scene to pack. + Node *scene = memnew(Node); + scene->set_name("TestScene"); + + // Pack the scene. + PackedScene packed_scene; + packed_scene.pack(scene); + + // Check if the packed scene can be instantiated. + const bool can_instantiate = packed_scene.can_instantiate(); + CHECK(can_instantiate == true); + + memdelete(scene); +} + +TEST_CASE("[PackedScene] Instantiate Packed Scene") { + // Create a scene to pack. + Node *scene = memnew(Node); + scene->set_name("TestScene"); + + // Pack the scene. + PackedScene packed_scene; + packed_scene.pack(scene); + + // Instantiate the packed scene. + Node *instance = packed_scene.instantiate(); + CHECK(instance != nullptr); + CHECK(instance->get_name() == "TestScene"); + + memdelete(scene); + memdelete(instance); +} + +TEST_CASE("[PackedScene] Instantiate Packed Scene With Children") { + // Create a scene to pack. + Node *scene = memnew(Node); + scene->set_name("TestScene"); + + // Add persisting child nodes to the scene. + Node *child1 = memnew(Node); + child1->set_name("Child1"); + scene->add_child(child1); + child1->set_owner(scene); + + Node *child2 = memnew(Node); + child2->set_name("Child2"); + scene->add_child(child2); + child2->set_owner(scene); + + // Add non persisting child node to the scene. + Node *child3 = memnew(Node); + child3->set_name("Child3"); + scene->add_child(child3); + + // Pack the scene. + PackedScene packed_scene; + packed_scene.pack(scene); + + // Instantiate the packed scene. + Node *instance = packed_scene.instantiate(); + CHECK(instance != nullptr); + CHECK(instance->get_name() == "TestScene"); + + // Validate the child nodes of the instantiated scene. + CHECK(instance->get_child_count() == 2); + CHECK(instance->get_child(0)->get_name() == "Child1"); + CHECK(instance->get_child(1)->get_name() == "Child2"); + CHECK(instance->get_child(0)->get_owner() == instance); + CHECK(instance->get_child(1)->get_owner() == instance); + + memdelete(scene); + memdelete(instance); +} + +} // namespace TestPackedScene + +#endif // TEST_PACKED_SCENE_H diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h index 345e617285..8cfb189370 100644 --- a/tests/scene/test_text_edit.h +++ b/tests/scene/test_text_edit.h @@ -407,7 +407,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_selection_to_line() == 1); SIGNAL_CHECK("caret_changed", empty_signal_args); - // insert before should move caret and selecion, and works when not editable. + // Insert before should move caret and selection, and works when not editable. text_edit->set_editable(false); lines_edited_args.remove_at(0); text_edit->insert_line_at(0, "new"); @@ -424,7 +424,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("text_set"); text_edit->set_editable(true); - // can undo/redo as single action + // Can undo/redo as single action. ((Array)lines_edited_args[0])[0] = 1; ((Array)lines_edited_args[0])[1] = 0; text_edit->undo(); diff --git a/tests/scene/test_theme.h b/tests/scene/test_theme.h index 69d29e5ca5..ad1ce1fd50 100644 --- a/tests/scene/test_theme.h +++ b/tests/scene/test_theme.h @@ -31,6 +31,8 @@ #ifndef TEST_THEME_H #define TEST_THEME_H +#include "scene/resources/image_texture.h" +#include "scene/resources/style_box_flat.h" #include "scene/resources/theme.h" #include "tests/test_tools.h" diff --git a/tests/scene/test_viewport.h b/tests/scene/test_viewport.h index d76fc40125..ab17459a41 100644 --- a/tests/scene/test_viewport.h +++ b/tests/scene/test_viewport.h @@ -31,9 +31,13 @@ #ifndef TEST_VIEWPORT_H #define TEST_VIEWPORT_H -#include "scene/2d/node_2d.h" +#include "scene/2d/area_2d.h" +#include "scene/2d/collision_shape_2d.h" #include "scene/gui/control.h" +#include "scene/gui/subviewport_container.h" +#include "scene/main/canvas_layer.h" #include "scene/main/window.h" +#include "scene/resources/rectangle_shape_2d.h" #include "tests/test_macros.h" @@ -715,6 +719,463 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") { memdelete(node_a); } +TEST_CASE("[SceneTree][Viewport] Control mouse cursor shape") { + SUBCASE("[Viewport][CursorShape] Mouse cursor is not overridden by SubViewportContainer") { + SubViewportContainer *node_a = memnew(SubViewportContainer); + SubViewport *node_b = memnew(SubViewport); + Control *node_c = memnew(Control); + + node_a->set_name("SubViewportContainer"); + node_b->set_name("SubViewport"); + node_c->set_name("Control"); + node_a->set_position(Point2i(0, 0)); + node_c->set_position(Point2i(0, 0)); + node_a->set_size(Point2i(100, 100)); + node_b->set_size(Point2i(100, 100)); + node_c->set_size(Point2i(100, 100)); + node_a->set_default_cursor_shape(Control::CURSOR_ARROW); + node_c->set_default_cursor_shape(Control::CURSOR_FORBIDDEN); + Window *root = SceneTree::get_singleton()->get_root(); + DisplayServerMock *DS = (DisplayServerMock *)(DisplayServer::get_singleton()); + + // Scene tree: + // - root + // - node_a (SubViewportContainer) + // - node_b (SubViewport) + // - node_c (Control) + + root->add_child(node_a); + node_a->add_child(node_b); + node_b->add_child(node_c); + + Point2i on_c = Point2i(5, 5); + + SEND_GUI_MOUSE_MOTION_EVENT(on_c, MouseButtonMask::NONE, Key::NONE); + CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_FORBIDDEN); // GH-74805 + + memdelete(node_c); + memdelete(node_b); + memdelete(node_a); + } +} + +class TestArea2D : public Area2D { + GDCLASS(TestArea2D, Area2D); + + void _on_mouse_entered() { + enter_id = ++TestArea2D::counter; // > 0, if activated. + } + + void _on_mouse_exited() { + exit_id = ++TestArea2D::counter; // > 0, if activated. + } + + void _on_input_event(Node *p_vp, Ref<InputEvent> p_ev, int p_shape) { + last_input_event = p_ev; + } + +public: + static int counter; + int enter_id = 0; + int exit_id = 0; + Ref<InputEvent> last_input_event; + + void init_signals() { + connect(SNAME("mouse_entered"), callable_mp(this, &TestArea2D::_on_mouse_entered)); + connect(SNAME("mouse_exited"), callable_mp(this, &TestArea2D::_on_mouse_exited)); + connect(SNAME("input_event"), callable_mp(this, &TestArea2D::_on_input_event)); + } + + void test_reset() { + enter_id = 0; + exit_id = 0; + last_input_event.unref(); + } +}; + +int TestArea2D::counter = 0; + +TEST_CASE("[SceneTree][Viewport] Physics Picking 2D") { + // FIXME: MOUSE_MODE_CAPTURED if-conditions are not testable, because DisplayServerMock doesn't support it. + + struct PickingCollider { + TestArea2D *a; + CollisionShape2D *c; + Ref<RectangleShape2D> r; + }; + + SceneTree *tree = SceneTree::get_singleton(); + Window *root = tree->get_root(); + root->set_physics_object_picking(true); + + Point2i on_background = Point2i(800, 800); + Point2i on_outside = Point2i(-1, -1); + SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + + Vector<PickingCollider> v; + for (int i = 0; i < 4; i++) { + PickingCollider pc; + pc.a = memnew(TestArea2D); + pc.c = memnew(CollisionShape2D); + pc.r = Ref<RectangleShape2D>(memnew(RectangleShape2D)); + pc.r->set_size(Size2(150, 150)); + pc.c->set_shape(pc.r); + pc.a->add_child(pc.c); + pc.a->set_name("A" + itos(i)); + pc.c->set_name("C" + itos(i)); + v.push_back(pc); + SIGNAL_WATCH(pc.a, SNAME("mouse_entered")); + SIGNAL_WATCH(pc.a, SNAME("mouse_exited")); + } + + Node2D *node_a = memnew(Node2D); + node_a->set_position(Point2i(0, 0)); + v[0].a->set_position(Point2i(0, 0)); + v[1].a->set_position(Point2i(0, 100)); + node_a->add_child(v[0].a); + node_a->add_child(v[1].a); + Node2D *node_b = memnew(Node2D); + node_b->set_position(Point2i(100, 0)); + v[2].a->set_position(Point2i(0, 0)); + v[3].a->set_position(Point2i(0, 100)); + node_b->add_child(v[2].a); + node_b->add_child(v[3].a); + root->add_child(node_a); + root->add_child(node_b); + Point2i on_all = Point2i(50, 50); + Point2i on_0 = Point2i(10, 10); + Point2i on_01 = Point2i(10, 50); + Point2i on_02 = Point2i(50, 10); + + Array empty_signal_args_2; + empty_signal_args_2.push_back(Array()); + empty_signal_args_2.push_back(Array()); + + Array empty_signal_args_4; + empty_signal_args_4.push_back(Array()); + empty_signal_args_4.push_back(Array()); + empty_signal_args_4.push_back(Array()); + empty_signal_args_4.push_back(Array()); + + for (PickingCollider E : v) { + E.a->init_signals(); + } + + SUBCASE("[Viewport][Picking2D] Mouse Motion") { + SEND_GUI_MOUSE_MOTION_EVENT(on_all, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + SIGNAL_CHECK(SNAME("mouse_entered"), empty_signal_args_4); + SIGNAL_CHECK_FALSE(SNAME("mouse_exited")); + for (PickingCollider E : v) { + CHECK(E.a->enter_id); + CHECK_FALSE(E.a->exit_id); + E.a->test_reset(); + } + + SEND_GUI_MOUSE_MOTION_EVENT(on_01, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + SIGNAL_CHECK_FALSE(SNAME("mouse_entered")); + SIGNAL_CHECK(SNAME("mouse_exited"), empty_signal_args_2); + + for (int i = 0; i < v.size(); i++) { + CHECK_FALSE(v[i].a->enter_id); + if (i < 2) { + CHECK_FALSE(v[i].a->exit_id); + } else { + CHECK(v[i].a->exit_id); + } + v[i].a->test_reset(); + } + + SEND_GUI_MOUSE_MOTION_EVENT(on_outside, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + SIGNAL_CHECK_FALSE(SNAME("mouse_entered")); + SIGNAL_CHECK(SNAME("mouse_exited"), empty_signal_args_2); + for (int i = 0; i < v.size(); i++) { + CHECK_FALSE(v[i].a->enter_id); + if (i < 2) { + CHECK(v[i].a->exit_id); + } else { + CHECK_FALSE(v[i].a->exit_id); + } + v[i].a->test_reset(); + } + } + + SUBCASE("[Viewport][Picking2D] Object moved / passive hovering") { + SEND_GUI_MOUSE_MOTION_EVENT(on_all, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + for (int i = 0; i < v.size(); i++) { + CHECK(v[i].a->enter_id); + CHECK_FALSE(v[i].a->exit_id); + v[i].a->test_reset(); + } + + node_b->set_position(Point2i(200, 0)); + tree->physics_process(1); + for (int i = 0; i < v.size(); i++) { + CHECK_FALSE(v[i].a->enter_id); + if (i < 2) { + CHECK_FALSE(v[i].a->exit_id); + } else { + CHECK(v[i].a->exit_id); + } + v[i].a->test_reset(); + } + + node_b->set_position(Point2i(100, 0)); + tree->physics_process(1); + for (int i = 0; i < v.size(); i++) { + if (i < 2) { + CHECK_FALSE(v[i].a->enter_id); + } else { + CHECK(v[i].a->enter_id); + } + CHECK_FALSE(v[i].a->exit_id); + v[i].a->test_reset(); + } + } + + SUBCASE("[Viewport][Picking2D] No Processing") { + SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + for (PickingCollider E : v) { + E.a->test_reset(); + } + + v[0].a->set_process_mode(Node::PROCESS_MODE_DISABLED); + v[0].c->set_process_mode(Node::PROCESS_MODE_DISABLED); + SEND_GUI_MOUSE_MOTION_EVENT(on_02, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + CHECK_FALSE(v[0].a->enter_id); + CHECK_FALSE(v[0].a->exit_id); + CHECK(v[2].a->enter_id); + CHECK_FALSE(v[2].a->exit_id); + for (PickingCollider E : v) { + E.a->test_reset(); + } + + SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + CHECK_FALSE(v[0].a->enter_id); + CHECK_FALSE(v[0].a->exit_id); + CHECK_FALSE(v[2].a->enter_id); + CHECK(v[2].a->exit_id); + + for (PickingCollider E : v) { + E.a->test_reset(); + } + v[0].a->set_process_mode(Node::PROCESS_MODE_ALWAYS); + v[0].c->set_process_mode(Node::PROCESS_MODE_ALWAYS); + } + + SUBCASE("[Viewport][Picking2D] Multiple events in series") { + SEND_GUI_MOUSE_MOTION_EVENT(on_0, MouseButtonMask::NONE, Key::NONE); + SEND_GUI_MOUSE_MOTION_EVENT(on_0 + Point2i(10, 0), MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + + for (int i = 0; i < v.size(); i++) { + if (i < 1) { + CHECK(v[i].a->enter_id); + } else { + CHECK_FALSE(v[i].a->enter_id); + } + CHECK_FALSE(v[i].a->exit_id); + v[i].a->test_reset(); + } + + SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE); + SEND_GUI_MOUSE_MOTION_EVENT(on_background + Point2i(10, 10), MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + + for (int i = 0; i < v.size(); i++) { + CHECK_FALSE(v[i].a->enter_id); + if (i < 1) { + CHECK(v[i].a->exit_id); + } else { + CHECK_FALSE(v[i].a->exit_id); + } + v[i].a->test_reset(); + } + } + + SUBCASE("[Viewport][Picking2D] Disable Picking") { + SEND_GUI_MOUSE_MOTION_EVENT(on_02, MouseButtonMask::NONE, Key::NONE); + + root->set_physics_object_picking(false); + CHECK_FALSE(root->get_physics_object_picking()); + + tree->physics_process(1); + + for (int i = 0; i < v.size(); i++) { + CHECK_FALSE(v[i].a->enter_id); + v[i].a->test_reset(); + } + + root->set_physics_object_picking(true); + CHECK(root->get_physics_object_picking()); + } + + SUBCASE("[Viewport][Picking2D] CollisionObject in CanvasLayer") { + CanvasLayer *node_c = memnew(CanvasLayer); + node_c->set_rotation(Math_PI); + node_c->set_offset(Point2i(100, 100)); + root->add_child(node_c); + + v[2].a->reparent(node_c, false); + v[3].a->reparent(node_c, false); + + SEND_GUI_MOUSE_MOTION_EVENT(on_02, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + + for (int i = 0; i < v.size(); i++) { + if (i == 0 || i == 3) { + CHECK(v[i].a->enter_id); + } else { + CHECK_FALSE(v[i].a->enter_id); + } + v[i].a->test_reset(); + } + + v[2].a->reparent(node_b, false); + v[3].a->reparent(node_b, false); + root->remove_child(node_c); + memdelete(node_c); + } + + SUBCASE("[Viewport][Picking2D] Picking Sort") { + root->set_physics_object_picking_sort(true); + CHECK(root->get_physics_object_picking_sort()); + + SUBCASE("[Viewport][Picking2D] Picking Sort Z-Index") { + node_a->set_z_index(10); + v[0].a->set_z_index(0); + v[1].a->set_z_index(2); + node_b->set_z_index(5); + v[2].a->set_z_index(8); + v[3].a->set_z_index(11); + v[3].a->set_z_as_relative(false); + + TestArea2D::counter = 0; + SEND_GUI_MOUSE_MOTION_EVENT(on_all, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + + CHECK(v[0].a->enter_id == 4); + CHECK(v[1].a->enter_id == 2); + CHECK(v[2].a->enter_id == 1); + CHECK(v[3].a->enter_id == 3); + for (int i = 0; i < v.size(); i++) { + CHECK_FALSE(v[i].a->exit_id); + v[i].a->test_reset(); + } + + TestArea2D::counter = 0; + SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + + CHECK(v[0].a->exit_id == 4); + CHECK(v[1].a->exit_id == 2); + CHECK(v[2].a->exit_id == 1); + CHECK(v[3].a->exit_id == 3); + for (int i = 0; i < v.size(); i++) { + CHECK_FALSE(v[i].a->enter_id); + v[i].a->set_z_as_relative(true); + v[i].a->set_z_index(0); + v[i].a->test_reset(); + } + + node_a->set_z_index(0); + node_b->set_z_index(0); + } + + SUBCASE("[Viewport][Picking2D] Picking Sort Scene Tree Location") { + TestArea2D::counter = 0; + SEND_GUI_MOUSE_MOTION_EVENT(on_all, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + + for (int i = 0; i < v.size(); i++) { + CHECK(v[i].a->enter_id == 4 - i); + CHECK_FALSE(v[i].a->exit_id); + v[i].a->test_reset(); + } + + TestArea2D::counter = 0; + SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + + for (int i = 0; i < v.size(); i++) { + CHECK_FALSE(v[i].a->enter_id); + CHECK(v[i].a->exit_id == 4 - i); + v[i].a->test_reset(); + } + } + + root->set_physics_object_picking_sort(false); + CHECK_FALSE(root->get_physics_object_picking_sort()); + } + + SUBCASE("[Viewport][Picking2D] Mouse Button") { + SEND_GUI_MOUSE_BUTTON_EVENT(on_0, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + tree->physics_process(1); + + for (int i = 0; i < v.size(); i++) { + if (i == 0) { + CHECK(v[i].a->enter_id); + } else { + CHECK_FALSE(v[i].a->enter_id); + } + CHECK_FALSE(v[i].a->exit_id); + v[i].a->test_reset(); + } + + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_0, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + tree->physics_process(1); + + for (int i = 0; i < v.size(); i++) { + CHECK_FALSE(v[i].a->enter_id); + CHECK_FALSE(v[i].a->exit_id); + v[i].a->test_reset(); + } + } + + SUBCASE("[Viewport][Picking2D] Screen Touch") { + SEND_GUI_TOUCH_EVENT(on_01, true, false); + tree->physics_process(1); + for (int i = 0; i < v.size(); i++) { + if (i < 2) { + Ref<InputEventScreenTouch> st = v[i].a->last_input_event; + CHECK(st.is_valid()); + } else { + CHECK(v[i].a->last_input_event.is_null()); + } + v[i].a->test_reset(); + } + } + + for (PickingCollider E : v) { + SIGNAL_UNWATCH(E.a, SNAME("mouse_entered")); + SIGNAL_UNWATCH(E.a, SNAME("mouse_exited")); + memdelete(E.c); + memdelete(E.a); + } +} + +TEST_CASE("[SceneTree][Viewport] Embedded Windows") { + Window *root = SceneTree::get_singleton()->get_root(); + Window *w = memnew(Window); + + SUBCASE("[Viewport] Safe-rect of embedded Window") { + root->add_child(w); + root->subwindow_set_popup_safe_rect(w, Rect2i(10, 10, 10, 10)); + CHECK_EQ(root->subwindow_get_popup_safe_rect(w), Rect2i(10, 10, 10, 10)); + root->remove_child(w); + CHECK_EQ(root->subwindow_get_popup_safe_rect(w), Rect2i()); + } + + memdelete(w); +} + } // namespace TestViewport #endif // TEST_VIEWPORT_H diff --git a/tests/scene/test_window.h b/tests/scene/test_window.h new file mode 100644 index 0000000000..e0c55101de --- /dev/null +++ b/tests/scene/test_window.h @@ -0,0 +1,96 @@ +/**************************************************************************/ +/* test_window.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_WINDOW_H +#define TEST_WINDOW_H + +#include "scene/gui/control.h" +#include "scene/main/window.h" + +#include "tests/test_macros.h" + +namespace TestWindow { + +class NotificationControl : public Control { + GDCLASS(NotificationControl, Control); + +protected: + void _notification(int p_what) { + switch (p_what) { + case NOTIFICATION_MOUSE_ENTER: { + mouse_over = true; + } break; + + case NOTIFICATION_MOUSE_EXIT: { + mouse_over = false; + } break; + } + } + +public: + bool mouse_over = false; +}; + +TEST_CASE("[SceneTree][Window]") { + Window *root = SceneTree::get_singleton()->get_root(); + + SUBCASE("Control-mouse-over within Window-black bars should not happen") { + Window *w = memnew(Window); + root->add_child(w); + w->set_size(Size2i(400, 200)); + w->set_position(Size2i(0, 0)); + w->set_content_scale_size(Size2i(200, 200)); + w->set_content_scale_mode(Window::CONTENT_SCALE_MODE_CANVAS_ITEMS); + w->set_content_scale_aspect(Window::CONTENT_SCALE_ASPECT_KEEP); + NotificationControl *c = memnew(NotificationControl); + w->add_child(c); + c->set_size(Size2i(100, 100)); + c->set_position(Size2i(-50, -50)); + + CHECK_FALSE(c->mouse_over); + SEND_GUI_MOUSE_MOTION_EVENT(Point2i(110, 10), MouseButtonMask::NONE, Key::NONE); + CHECK(c->mouse_over); + SEND_GUI_MOUSE_MOTION_EVENT(Point2i(90, 10), MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(c->mouse_over); // GH-80011 + + /* TODO: + SEND_GUI_MOUSE_BUTTON_EVENT(Point2i(90, 10), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(Point2i(90, 10), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK(Control was not pressed); + */ + + memdelete(c); + memdelete(w); + } +} + +} // namespace TestWindow + +#endif // TEST_WINDOW_H diff --git a/tests/servers/rendering/test_shader_preprocessor.h b/tests/servers/rendering/test_shader_preprocessor.h new file mode 100644 index 0000000000..d65eb522e8 --- /dev/null +++ b/tests/servers/rendering/test_shader_preprocessor.h @@ -0,0 +1,333 @@ +/**************************************************************************/ +/* test_shader_preprocessor.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_SHADER_PREPROCESSOR_H +#define TEST_SHADER_PREPROCESSOR_H + +#include "servers/rendering/shader_preprocessor.h" + +#include "tests/test_macros.h" + +#include <cctype> + +namespace TestShaderPreprocessor { + +void erase_all_empty(Vector<String> &p_vec) { + int idx = p_vec.find(" "); + while (idx >= 0) { + p_vec.remove_at(idx); + idx = p_vec.find(" "); + } +} + +bool is_variable_char(unsigned char c) { + return std::isalnum(c) || c == '_'; +} + +bool is_operator_char(unsigned char c) { + return (c == '*') || (c == '+') || (c == '-') || (c == '/') || ((c >= '<') && (c <= '>')); +} + +// Remove unnecessary spaces from a line. +String remove_spaces(String &p_str) { + String res; + // Result is guaranteed to not be longer than the input. + res.resize(p_str.size()); + int wp = 0; + char32_t last = 0; + bool has_removed = false; + + for (int n = 0; n < p_str.size(); n++) { + // These test cases only use ASCII. + auto c = static_cast<unsigned char>(p_str[n]); + if (std::isblank(c)) { + has_removed = true; + } else { + if (has_removed) { + // Insert a space to avoid joining things that could potentially form a new token. + // E.g. "float x" or "- -". + if ((is_variable_char(c) && is_variable_char(last)) || + (is_operator_char(c) && is_operator_char(last))) { + res[wp++] = ' '; + } + has_removed = false; + } + res[wp++] = c; + last = c; + } + } + res.resize(wp); + return res; +} + +// The pre-processor changes indentation and inserts spaces when inserting macros. +// Re-format the code, without changing its meaning, to make it easier to compare. +String compact_spaces(String &p_str) { + Vector<String> lines = p_str.split("\n", false); + erase_all_empty(lines); + for (auto &line : lines) { + line = remove_spaces(line); + } + return String("\n").join(lines); +} + +#define CHECK_SHADER_EQ(a, b) CHECK_EQ(compact_spaces(a), compact_spaces(b)) +#define CHECK_SHADER_NE(a, b) CHECK_NE(compact_spaces(a), compact_spaces(b)) + +TEST_CASE("[ShaderPreprocessor] Simple defines") { + String code( + "#define X 1.0 // comment\n" + "#define Y mix\n" + "#define Z X\n" + "\n" + "#define func0 \\\n" + " vec3 my_fun(vec3 arg) {\\\n" + " return pow(arg, 2.2);\\\n" + " }\n" + "\n" + "func0\n" + "\n" + "fragment() {\n" + " ALBEDO = vec3(X);\n" + " float x = Y(0., Z, X);\n" + " #undef X\n" + " float X = x;\n" + " x = -Z;\n" + "}\n"); + String expected( + "vec3 my_fun(vec3 arg) { return pow(arg, 2.2); }\n" + "\n" + "fragment() {\n" + " ALBEDO = vec3( 1.0 );\n" + " float x = mix(0., 1.0 , 1.0 );\n" + " float X = x;\n" + " x = -X;\n" + "}\n"); + String result; + + ShaderPreprocessor preprocessor; + CHECK_EQ(preprocessor.preprocess(code, String("file.gdshader"), result), Error::OK); + + CHECK_SHADER_EQ(result, expected); +} + +TEST_CASE("[ShaderPreprocessor] Avoid merging adjacent tokens") { + String code( + "#define X -10\n" + "#define Y(s) s\n" + "\n" + "fragment() {\n" + " float v = 1.0-X-Y(-2);\n" + "}\n"); + String expected( + "fragment() {\n" + " float v = 1.0 - -10 - -2;\n" + "}\n"); + String result; + + ShaderPreprocessor preprocessor; + CHECK_EQ(preprocessor.preprocess(code, String("file.gdshader"), result), Error::OK); + + CHECK_SHADER_EQ(result, expected); +} + +TEST_CASE("[ShaderPreprocessor] Complex defines") { + String code( + "const float X = 2.0;\n" + "#define A(X) X*2.\n" + "#define X 1.0\n" + "#define Y Z(X, W)\n" + "#define Z max\n" + "#define C(X, Y) Z(A(Y), B(X))\n" + "#define W -X\n" + "#define B(X) X*3.\n" + "\n" + "fragment() {\n" + " float x = Y;\n" + " float y = C(5., 7.0);\n" + "}\n"); + String expected( + "const float X = 2.0;\n" + "fragment() {\n" + " float x = max(1.0, - 1.0);\n" + " float y = max(7.0*2. , 5.*3.);\n" + "}\n"); + String result; + + ShaderPreprocessor preprocessor; + CHECK_EQ(preprocessor.preprocess(code, String("file.gdshader"), result), Error::OK); + + CHECK_SHADER_EQ(result, expected); +} + +TEST_CASE("[ShaderPreprocessor] Concatenation") { + String code( + "fragment() {\n" + " #define X 1 // this is fine ##\n" + " #define y 2\n" + " #define z 3##.## 1## 4 ## 59\n" + " #define Z(y) X ## y\n" + " #define Z2(y) y##X\n" + " #define W(y) X, y\n" + " #define A(x) fl## oat a = 1##x ##.3 ## x\n" + " #define C(x, y) x##.##y\n" + " #define J(x) x##=\n" + " float Z(y) = 1.2;\n" + " float Z(z) = 2.3;\n" + " float Z2(y) = z;\n" + " float Z2(z) = 2.3;\n" + " int b = max(W(3));\n" + " Xy J(+) b J(=) 3 ? 0.1 : 0.2;\n" + " A(9);\n" + " Xy = C(X, y);\n" + "}\n"); + String expected( + "fragment() {\n" + " float Xy = 1.2;\n" + " float Xz = 2.3;\n" + " float yX = 3.1459;\n" + " float zX = 2.3;\n" + " int b = max(1, 3);\n" + " Xy += b == 3 ? 0.1 : 0.2;\n" + " float a = 19.39;\n" + " Xy = 1.2;\n" + "}\n"); + String result; + + ShaderPreprocessor preprocessor; + CHECK_EQ(preprocessor.preprocess(code, String("file.gdshader"), result), Error::OK); + + CHECK_SHADER_EQ(result, expected); +} + +TEST_CASE("[ShaderPreprocessor] Nested concatenation") { + // Concatenation ## should not expand adjacent tokens if they are macros, + // but this is currently not implemented in Godot's shader preprocessor. + // To force expanding, an extra macro should be required (B in this case). + + String code( + "fragment() {\n" + " vec2 X = vec2(0);\n" + " #define X 1\n" + " #define y 2\n" + " #define B(x, y) C(x, y)\n" + " #define C(x, y) x##.##y\n" + " C(X, y) = B(X, y);\n" + "}\n"); + String expected( + "fragment() {\n" + " vec2 X = vec2(0);\n" + " X.y = 1.2;\n" + "}\n"); + String result; + + ShaderPreprocessor preprocessor; + CHECK_EQ(preprocessor.preprocess(code, String("file.gdshader"), result), Error::OK); + + // TODO: Reverse the check when/if this is changed. + CHECK_SHADER_NE(result, expected); +} + +TEST_CASE("[ShaderPreprocessor] Concatenation sorting network") { + String code( + "fragment() {\n" + " #define ARR(X) test##X\n" + " #define ACMP(a, b) ARR(a) > ARR(b)\n" + " #define ASWAP(a, b) tmp = ARR(b); ARR(b) = ARR(a); ARR(a) = tmp;\n" + " #define ACSWAP(a, b) if(ACMP(a, b)) { ASWAP(a, b) }\n" + " float test0 = 1.2;\n" + " float test1 = 0.34;\n" + " float test3 = 0.8;\n" + " float test4 = 2.9;\n" + " float tmp;\n" + " ACSWAP(0,2)\n" + " ACSWAP(1,3)\n" + " ACSWAP(0,1)\n" + " ACSWAP(2,3)\n" + " ACSWAP(1,2)\n" + "}\n"); + String expected( + "fragment() {\n" + " float test0 = 1.2;\n" + " float test1 = 0.34;\n" + " float test3 = 0.8;\n" + " float test4 = 2.9;\n" + " float tmp;\n" + " if(test0 > test2) { tmp = test2; test2 = test0; test0 = tmp; }\n" + " if(test1 > test3) { tmp = test3; test3 = test1; test1 = tmp; }\n" + " if(test0 > test1) { tmp = test1; test1 = test0; test0 = tmp; }\n" + " if(test2 > test3) { tmp = test3; test3 = test2; test2 = tmp; }\n" + " if(test1 > test2) { tmp = test2; test2 = test1; test1 = tmp; }\n" + "}\n"); + String result; + + ShaderPreprocessor preprocessor; + CHECK_EQ(preprocessor.preprocess(code, String("file.gdshader"), result), Error::OK); + + CHECK_SHADER_EQ(result, expected); +} + +TEST_CASE("[ShaderPreprocessor] Undefined behaviour") { + // None of these are valid concatenation, nor valid shader code. + // Don't care about results, just make sure there's no crash. + const String filename("somefile.gdshader"); + String result; + ShaderPreprocessor preprocessor; + + preprocessor.preprocess("#define X ###\nX\n", filename, result); + preprocessor.preprocess("#define X ####\nX\n", filename, result); + preprocessor.preprocess("#define X #####\nX\n", filename, result); + preprocessor.preprocess("#define X 1 ### 2\nX\n", filename, result); + preprocessor.preprocess("#define X 1 #### 2\nX\n", filename, result); + preprocessor.preprocess("#define X 1 ##### 2\nX\n", filename, result); + preprocessor.preprocess("#define X ### 2\nX\n", filename, result); + preprocessor.preprocess("#define X #### 2\nX\n", filename, result); + preprocessor.preprocess("#define X ##### 2\nX\n", filename, result); + preprocessor.preprocess("#define X 1 ###\nX\n", filename, result); + preprocessor.preprocess("#define X 1 ####\nX\n", filename, result); + preprocessor.preprocess("#define X 1 #####\nX\n", filename, result); +} + +TEST_CASE("[ShaderPreprocessor] Invalid concatenations") { + const String filename("somefile.gdshader"); + String result; + ShaderPreprocessor preprocessor; + + CHECK_NE(preprocessor.preprocess("#define X ##", filename, result), Error::OK); + CHECK_NE(preprocessor.preprocess("#define X 1 ##", filename, result), Error::OK); + CHECK_NE(preprocessor.preprocess("#define X ## 1", filename, result), Error::OK); + CHECK_NE(preprocessor.preprocess("#define X(y) ## ", filename, result), Error::OK); + CHECK_NE(preprocessor.preprocess("#define X(y) y ## ", filename, result), Error::OK); + CHECK_NE(preprocessor.preprocess("#define X(y) ## y", filename, result), Error::OK); +} + +} // namespace TestShaderPreprocessor + +#endif // TEST_SHADER_PREPROCESSOR_H diff --git a/tests/servers/test_navigation_server_3d.h b/tests/servers/test_navigation_server_3d.h index ffd5b83231..a116559cb2 100644 --- a/tests/servers/test_navigation_server_3d.h +++ b/tests/servers/test_navigation_server_3d.h @@ -31,11 +31,38 @@ #ifndef TEST_NAVIGATION_SERVER_3D_H #define TEST_NAVIGATION_SERVER_3D_H +#include "scene/3d/mesh_instance_3d.h" +#include "scene/resources/primitive_meshes.h" #include "servers/navigation_server_3d.h" #include "tests/test_macros.h" namespace TestNavigationServer3D { + +// TODO: Find a more generic way to create `Callable` mocks. +class CallableMock : public Object { + GDCLASS(CallableMock, Object); + +public: + void function1(Variant arg0) { + function1_calls++; + function1_latest_arg0 = arg0; + } + + unsigned function1_calls{ 0 }; + Variant function1_latest_arg0{}; +}; + +static inline Array build_array() { + return Array(); +} +template <typename... Targs> +static inline Array build_array(Variant item, Targs... Fargs) { + Array a = build_array(Fargs...); + a.push_front(item); + return a; +} + TEST_SUITE("[Navigation]") { TEST_CASE("[NavigationServer3D] Server should be empty when initialized") { NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); @@ -56,7 +83,6 @@ TEST_SUITE("[Navigation]") { TEST_CASE("[NavigationServer3D] Server should manage agent properly") { NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); - CHECK_EQ(navigation_server->get_maps().size(), 0); RID agent = navigation_server->agent_create(); CHECK(agent.is_valid()); @@ -69,6 +95,7 @@ TEST_SUITE("[Navigation]") { bool initial_use_3d_avoidance = navigation_server->agent_get_use_3d_avoidance(agent); navigation_server->agent_set_use_3d_avoidance(agent, !initial_use_3d_avoidance); navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(navigation_server->agent_get_use_3d_avoidance(agent), !initial_use_3d_avoidance); // TODO: Add remaining setters/getters once the missing getters are added. } @@ -83,20 +110,40 @@ TEST_SUITE("[Navigation]") { navigation_server->agent_set_map(agent, RID()); navigation_server->free(map); navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(navigation_server->get_process_info(NavigationServer3D::INFO_AGENT_COUNT), 0); } navigation_server->free(agent); - - SUBCASE("'ProcessInfo' should not report removed agent") { - CHECK_EQ(navigation_server->get_process_info(NavigationServer3D::INFO_AGENT_COUNT), 0); - } } TEST_CASE("[NavigationServer3D] Server should manage map properly") { NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); - CHECK_EQ(navigation_server->get_maps().size(), 0); - RID map = navigation_server->map_create(); + RID map; + CHECK_FALSE(map.is_valid()); + + SUBCASE("Queries against invalid map should return empty or invalid values") { + CHECK_EQ(navigation_server->map_get_closest_point(map, Vector3(7, 7, 7)), Vector3()); + CHECK_EQ(navigation_server->map_get_closest_point_normal(map, Vector3(7, 7, 7)), Vector3()); + CHECK_FALSE(navigation_server->map_get_closest_point_owner(map, Vector3(7, 7, 7)).is_valid()); + CHECK_EQ(navigation_server->map_get_closest_point_to_segment(map, Vector3(7, 7, 7), Vector3(8, 8, 8), true), Vector3()); + CHECK_EQ(navigation_server->map_get_closest_point_to_segment(map, Vector3(7, 7, 7), Vector3(8, 8, 8), false), Vector3()); + CHECK_EQ(navigation_server->map_get_path(map, Vector3(7, 7, 7), Vector3(8, 8, 8), true).size(), 0); + CHECK_EQ(navigation_server->map_get_path(map, Vector3(7, 7, 7), Vector3(8, 8, 8), false).size(), 0); + + Ref<NavigationPathQueryParameters3D> query_parameters = memnew(NavigationPathQueryParameters3D); + query_parameters->set_map(map); + query_parameters->set_start_position(Vector3(7, 7, 7)); + query_parameters->set_target_position(Vector3(8, 8, 8)); + Ref<NavigationPathQueryResult3D> query_result = memnew(NavigationPathQueryResult3D); + navigation_server->query_path(query_parameters, query_result); + CHECK_EQ(query_result->get_path().size(), 0); + CHECK_EQ(query_result->get_path_types().size(), 0); + CHECK_EQ(query_result->get_path_rids().size(), 0); + CHECK_EQ(query_result->get_path_owner_ids().size(), 0); + } + + map = navigation_server->map_create(); CHECK(map.is_valid()); CHECK_EQ(navigation_server->get_maps().size(), 1); @@ -112,6 +159,7 @@ TEST_SUITE("[Navigation]") { bool initial_use_edge_connections = navigation_server->map_get_use_edge_connections(map); navigation_server->map_set_use_edge_connections(map, !initial_use_edge_connections); navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(navigation_server->map_get_cell_size(map), doctest::Approx(0.55)); CHECK_EQ(navigation_server->map_get_edge_connection_margin(map), doctest::Approx(0.66)); CHECK_EQ(navigation_server->map_get_link_connection_radius(map), doctest::Approx(0.77)); @@ -140,10 +188,431 @@ TEST_SUITE("[Navigation]") { CHECK_EQ(navigation_server->map_get_agents(map).size(), 0); } + SUBCASE("Number of links should be reported properly") { + RID link = navigation_server->link_create(); + CHECK(link.is_valid()); + navigation_server->link_set_map(link, map); + navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(navigation_server->map_get_links(map).size(), 1); + navigation_server->free(link); + navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(navigation_server->map_get_links(map).size(), 0); + } + + SUBCASE("Number of obstacles should be reported properly") { + RID obstacle = navigation_server->obstacle_create(); + CHECK(obstacle.is_valid()); + navigation_server->obstacle_set_map(obstacle, map); + navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(navigation_server->map_get_obstacles(map).size(), 1); + navigation_server->free(obstacle); + navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(navigation_server->map_get_obstacles(map).size(), 0); + } + + SUBCASE("Number of regions should be reported properly") { + RID region = navigation_server->region_create(); + CHECK(region.is_valid()); + navigation_server->region_set_map(region, map); + navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(navigation_server->map_get_regions(map).size(), 1); + navigation_server->free(region); + navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(navigation_server->map_get_regions(map).size(), 0); + } + + SUBCASE("Queries against empty map should return empty or invalid values") { + navigation_server->map_set_active(map, true); + navigation_server->process(0.0); // Give server some cycles to commit. + + CHECK_EQ(navigation_server->map_get_closest_point(map, Vector3(7, 7, 7)), Vector3()); + CHECK_EQ(navigation_server->map_get_closest_point_normal(map, Vector3(7, 7, 7)), Vector3()); + CHECK_FALSE(navigation_server->map_get_closest_point_owner(map, Vector3(7, 7, 7)).is_valid()); + CHECK_EQ(navigation_server->map_get_closest_point_to_segment(map, Vector3(7, 7, 7), Vector3(8, 8, 8), true), Vector3()); + CHECK_EQ(navigation_server->map_get_closest_point_to_segment(map, Vector3(7, 7, 7), Vector3(8, 8, 8), false), Vector3()); + CHECK_EQ(navigation_server->map_get_path(map, Vector3(7, 7, 7), Vector3(8, 8, 8), true).size(), 0); + CHECK_EQ(navigation_server->map_get_path(map, Vector3(7, 7, 7), Vector3(8, 8, 8), false).size(), 0); + + Ref<NavigationPathQueryParameters3D> query_parameters = memnew(NavigationPathQueryParameters3D); + query_parameters->set_map(map); + query_parameters->set_start_position(Vector3(7, 7, 7)); + query_parameters->set_target_position(Vector3(8, 8, 8)); + Ref<NavigationPathQueryResult3D> query_result = memnew(NavigationPathQueryResult3D); + navigation_server->query_path(query_parameters, query_result); + CHECK_EQ(query_result->get_path().size(), 0); + CHECK_EQ(query_result->get_path_types().size(), 0); + CHECK_EQ(query_result->get_path_rids().size(), 0); + CHECK_EQ(query_result->get_path_owner_ids().size(), 0); + + navigation_server->map_set_active(map, false); + navigation_server->process(0.0); // Give server some cycles to commit. + } + navigation_server->free(map); navigation_server->process(0.0); // Give server some cycles to actually remove map. CHECK_EQ(navigation_server->get_maps().size(), 0); } + + TEST_CASE("[NavigationServer3D] Server should manage link properly") { + NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); + + RID link = navigation_server->link_create(); + CHECK(link.is_valid()); + + SUBCASE("'ProcessInfo' should not report dangling link") { + CHECK_EQ(navigation_server->get_process_info(NavigationServer3D::INFO_LINK_COUNT), 0); + } + + SUBCASE("Setters/getters should work") { + bool initial_bidirectional = navigation_server->link_is_bidirectional(link); + navigation_server->link_set_bidirectional(link, !initial_bidirectional); + navigation_server->link_set_end_position(link, Vector3(7, 7, 7)); + navigation_server->link_set_enter_cost(link, 0.55); + navigation_server->link_set_navigation_layers(link, 6); + navigation_server->link_set_owner_id(link, ObjectID((int64_t)7)); + navigation_server->link_set_start_position(link, Vector3(8, 8, 8)); + navigation_server->link_set_travel_cost(link, 0.66); + navigation_server->process(0.0); // Give server some cycles to commit. + + CHECK_EQ(navigation_server->link_is_bidirectional(link), !initial_bidirectional); + CHECK_EQ(navigation_server->link_get_end_position(link), Vector3(7, 7, 7)); + CHECK_EQ(navigation_server->link_get_enter_cost(link), doctest::Approx(0.55)); + CHECK_EQ(navigation_server->link_get_navigation_layers(link), 6); + CHECK_EQ(navigation_server->link_get_owner_id(link), ObjectID((int64_t)7)); + CHECK_EQ(navigation_server->link_get_start_position(link), Vector3(8, 8, 8)); + CHECK_EQ(navigation_server->link_get_travel_cost(link), doctest::Approx(0.66)); + } + + SUBCASE("'ProcessInfo' should report link with active map") { + RID map = navigation_server->map_create(); + CHECK(map.is_valid()); + navigation_server->map_set_active(map, true); + navigation_server->link_set_map(link, map); + navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(navigation_server->get_process_info(NavigationServer3D::INFO_LINK_COUNT), 1); + navigation_server->link_set_map(link, RID()); + navigation_server->free(map); + navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(navigation_server->get_process_info(NavigationServer3D::INFO_LINK_COUNT), 0); + } + + navigation_server->free(link); + } + + TEST_CASE("[NavigationServer3D] Server should manage obstacles properly") { + NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); + + RID obstacle = navigation_server->obstacle_create(); + CHECK(obstacle.is_valid()); + + // TODO: Add tests for setters/getters once getters are added. + + navigation_server->free(obstacle); + } + + TEST_CASE("[NavigationServer3D] Server should manage regions properly") { + NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); + + RID region = navigation_server->region_create(); + CHECK(region.is_valid()); + + SUBCASE("'ProcessInfo' should not report dangling region") { + CHECK_EQ(navigation_server->get_process_info(NavigationServer3D::INFO_REGION_COUNT), 0); + } + + SUBCASE("Setters/getters should work") { + bool initial_use_edge_connections = navigation_server->region_get_use_edge_connections(region); + navigation_server->region_set_enter_cost(region, 0.55); + navigation_server->region_set_navigation_layers(region, 5); + navigation_server->region_set_owner_id(region, ObjectID((int64_t)7)); + navigation_server->region_set_travel_cost(region, 0.66); + navigation_server->region_set_use_edge_connections(region, !initial_use_edge_connections); + navigation_server->process(0.0); // Give server some cycles to commit. + + CHECK_EQ(navigation_server->region_get_enter_cost(region), doctest::Approx(0.55)); + CHECK_EQ(navigation_server->region_get_navigation_layers(region), 5); + CHECK_EQ(navigation_server->region_get_owner_id(region), ObjectID((int64_t)7)); + CHECK_EQ(navigation_server->region_get_travel_cost(region), doctest::Approx(0.66)); + CHECK_EQ(navigation_server->region_get_use_edge_connections(region), !initial_use_edge_connections); + } + + SUBCASE("'ProcessInfo' should report region with active map") { + RID map = navigation_server->map_create(); + CHECK(map.is_valid()); + navigation_server->map_set_active(map, true); + navigation_server->region_set_map(region, map); + navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(navigation_server->get_process_info(NavigationServer3D::INFO_REGION_COUNT), 1); + navigation_server->region_set_map(region, RID()); + navigation_server->free(map); + navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(navigation_server->get_process_info(NavigationServer3D::INFO_REGION_COUNT), 0); + } + + SUBCASE("Queries against empty region should return empty or invalid values") { + CHECK_EQ(navigation_server->region_get_connections_count(region), 0); + CHECK_EQ(navigation_server->region_get_connection_pathway_end(region, 55), Vector3()); + CHECK_EQ(navigation_server->region_get_connection_pathway_start(region, 55), Vector3()); + } + + navigation_server->free(region); + } + + // This test case does not check precise values on purpose - to not be too sensitivte. + TEST_CASE("[NavigationServer3D] Server should move agent properly") { + NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); + + RID map = navigation_server->map_create(); + RID agent = navigation_server->agent_create(); + + navigation_server->map_set_active(map, true); + navigation_server->agent_set_map(agent, map); + navigation_server->agent_set_avoidance_enabled(agent, true); + navigation_server->agent_set_velocity(agent, Vector3(1, 0, 1)); + CallableMock agent_avoidance_callback_mock; + navigation_server->agent_set_avoidance_callback(agent, callable_mp(&agent_avoidance_callback_mock, &CallableMock::function1)); + CHECK_EQ(agent_avoidance_callback_mock.function1_calls, 0); + navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(agent_avoidance_callback_mock.function1_calls, 1); + CHECK_NE(agent_avoidance_callback_mock.function1_latest_arg0, Vector3(0, 0, 0)); + + navigation_server->free(agent); + navigation_server->free(map); + } + + // This test case does not check precise values on purpose - to not be too sensitivte. + TEST_CASE("[NavigationServer3D] Server should make agents avoid each other when avoidance enabled") { + NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); + + RID map = navigation_server->map_create(); + RID agent_1 = navigation_server->agent_create(); + RID agent_2 = navigation_server->agent_create(); + + navigation_server->map_set_active(map, true); + + navigation_server->agent_set_map(agent_1, map); + navigation_server->agent_set_avoidance_enabled(agent_1, true); + navigation_server->agent_set_position(agent_1, Vector3(0, 0, 0)); + navigation_server->agent_set_radius(agent_1, 1); + navigation_server->agent_set_velocity(agent_1, Vector3(1, 0, 0)); + CallableMock agent_1_avoidance_callback_mock; + navigation_server->agent_set_avoidance_callback(agent_1, callable_mp(&agent_1_avoidance_callback_mock, &CallableMock::function1)); + + navigation_server->agent_set_map(agent_2, map); + navigation_server->agent_set_avoidance_enabled(agent_2, true); + navigation_server->agent_set_position(agent_2, Vector3(2.5, 0, 0.5)); + navigation_server->agent_set_radius(agent_2, 1); + navigation_server->agent_set_velocity(agent_2, Vector3(-1, 0, 0)); + CallableMock agent_2_avoidance_callback_mock; + navigation_server->agent_set_avoidance_callback(agent_2, callable_mp(&agent_2_avoidance_callback_mock, &CallableMock::function1)); + + CHECK_EQ(agent_1_avoidance_callback_mock.function1_calls, 0); + CHECK_EQ(agent_2_avoidance_callback_mock.function1_calls, 0); + navigation_server->process(0.0); // Give server some cycles to commit. + CHECK_EQ(agent_1_avoidance_callback_mock.function1_calls, 1); + CHECK_EQ(agent_2_avoidance_callback_mock.function1_calls, 1); + Vector3 agent_1_safe_velocity = agent_1_avoidance_callback_mock.function1_latest_arg0; + Vector3 agent_2_safe_velocity = agent_2_avoidance_callback_mock.function1_latest_arg0; + CHECK_MESSAGE(agent_1_safe_velocity.x > 0, "agent 1 should move a bit along desired velocity (+X)"); + CHECK_MESSAGE(agent_2_safe_velocity.x < 0, "agent 2 should move a bit along desired velocity (-X)"); + CHECK_MESSAGE(agent_1_safe_velocity.z < 0, "agent 1 should move a bit to the side so that it avoids agent 2"); + CHECK_MESSAGE(agent_2_safe_velocity.z > 0, "agent 2 should move a bit to the side so that it avoids agent 1"); + + navigation_server->free(agent_2); + navigation_server->free(agent_1); + navigation_server->free(map); + } + +#ifndef DISABLE_DEPRECATED + // This test case uses only public APIs on purpose - other test cases use simplified baking. + // FIXME: Remove once deprecated `region_bake_navigation_mesh()` is removed. + TEST_CASE("[NavigationServer3D][SceneTree][DEPRECATED] Server should be able to bake map correctly") { + NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); + + // Prepare scene tree with simple mesh to serve as an input geometry. + Node3D *node_3d = memnew(Node3D); + SceneTree::get_singleton()->get_root()->add_child(node_3d); + Ref<PlaneMesh> plane_mesh = memnew(PlaneMesh); + plane_mesh->set_size(Size2(10.0, 10.0)); + MeshInstance3D *mesh_instance = memnew(MeshInstance3D); + mesh_instance->set_mesh(plane_mesh); + node_3d->add_child(mesh_instance); + + // Prepare anything necessary to bake navigation mesh. + RID map = navigation_server->map_create(); + RID region = navigation_server->region_create(); + Ref<NavigationMesh> navigation_mesh = memnew(NavigationMesh); + navigation_server->map_set_active(map, true); + navigation_server->region_set_map(region, map); + navigation_server->region_set_navigation_mesh(region, navigation_mesh); + navigation_server->process(0.0); // Give server some cycles to commit. + + CHECK_EQ(navigation_mesh->get_polygon_count(), 0); + CHECK_EQ(navigation_mesh->get_vertices().size(), 0); + + navigation_server->region_bake_navigation_mesh(navigation_mesh, node_3d); + // FIXME: The above line should trigger the update (line below) under the hood. + navigation_server->region_set_navigation_mesh(region, navigation_mesh); // Force update. + CHECK_EQ(navigation_mesh->get_polygon_count(), 2); + CHECK_EQ(navigation_mesh->get_vertices().size(), 4); + + SUBCASE("Map should emit signal and take newly baked navigation mesh into account") { + SIGNAL_WATCH(navigation_server, "map_changed"); + SIGNAL_CHECK_FALSE("map_changed"); + navigation_server->process(0.0); // Give server some cycles to commit. + SIGNAL_CHECK("map_changed", build_array(build_array(map))); + SIGNAL_UNWATCH(navigation_server, "map_changed"); + CHECK_NE(navigation_server->map_get_closest_point(map, Vector3(0, 0, 0)), Vector3(0, 0, 0)); + } + + navigation_server->free(region); + navigation_server->free(map); + navigation_server->process(0.0); // Give server some cycles to commit. + memdelete(mesh_instance); + memdelete(node_3d); + } +#endif // DISABLE_DEPRECATED + + // This test case uses only public APIs on purpose - other test cases use simplified baking. + TEST_CASE("[NavigationServer3D][SceneTree] Server should be able to bake map correctly") { + NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); + + // Prepare scene tree with simple mesh to serve as an input geometry. + Node3D *node_3d = memnew(Node3D); + SceneTree::get_singleton()->get_root()->add_child(node_3d); + Ref<PlaneMesh> plane_mesh = memnew(PlaneMesh); + plane_mesh->set_size(Size2(10.0, 10.0)); + MeshInstance3D *mesh_instance = memnew(MeshInstance3D); + mesh_instance->set_mesh(plane_mesh); + node_3d->add_child(mesh_instance); + + // Prepare anything necessary to bake navigation mesh. + RID map = navigation_server->map_create(); + RID region = navigation_server->region_create(); + Ref<NavigationMesh> navigation_mesh = memnew(NavigationMesh); + navigation_server->map_set_active(map, true); + navigation_server->region_set_map(region, map); + navigation_server->region_set_navigation_mesh(region, navigation_mesh); + navigation_server->process(0.0); // Give server some cycles to commit. + + CHECK_EQ(navigation_mesh->get_polygon_count(), 0); + CHECK_EQ(navigation_mesh->get_vertices().size(), 0); + + Ref<NavigationMeshSourceGeometryData3D> source_geometry = memnew(NavigationMeshSourceGeometryData3D); + navigation_server->parse_source_geometry_data(navigation_mesh, source_geometry, node_3d); + navigation_server->bake_from_source_geometry_data(navigation_mesh, source_geometry, Callable()); + // FIXME: The above line should trigger the update (line below) under the hood. + navigation_server->region_set_navigation_mesh(region, navigation_mesh); // Force update. + CHECK_EQ(navigation_mesh->get_polygon_count(), 2); + CHECK_EQ(navigation_mesh->get_vertices().size(), 4); + + SUBCASE("Map should emit signal and take newly baked navigation mesh into account") { + SIGNAL_WATCH(navigation_server, "map_changed"); + SIGNAL_CHECK_FALSE("map_changed"); + navigation_server->process(0.0); // Give server some cycles to commit. + SIGNAL_CHECK("map_changed", build_array(build_array(map))); + SIGNAL_UNWATCH(navigation_server, "map_changed"); + CHECK_NE(navigation_server->map_get_closest_point(map, Vector3(0, 0, 0)), Vector3(0, 0, 0)); + } + + navigation_server->free(region); + navigation_server->free(map); + navigation_server->process(0.0); // Give server some cycles to commit. + memdelete(mesh_instance); + memdelete(node_3d); + } + + // This test case does not check precise values on purpose - to not be too sensitivte. + TEST_CASE("[NavigationServer3D] Server should respond to queries against valid map properly") { + NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); + Ref<NavigationMesh> navigation_mesh = memnew(NavigationMesh); + Ref<NavigationMeshSourceGeometryData3D> source_geometry = memnew(NavigationMeshSourceGeometryData3D); + + Array arr; + arr.resize(RS::ARRAY_MAX); + BoxMesh::create_mesh_array(arr, Vector3(10.0, 0.001, 10.0)); + source_geometry->add_mesh_array(arr, Transform3D()); + navigation_server->bake_from_source_geometry_data(navigation_mesh, source_geometry, Callable()); + CHECK_NE(navigation_mesh->get_polygon_count(), 0); + CHECK_NE(navigation_mesh->get_vertices().size(), 0); + + RID map = navigation_server->map_create(); + RID region = navigation_server->region_create(); + navigation_server->map_set_active(map, true); + navigation_server->region_set_map(region, map); + navigation_server->region_set_navigation_mesh(region, navigation_mesh); + navigation_server->process(0.0); // Give server some cycles to commit. + + SUBCASE("Simple queries should return non-default values") { + CHECK_NE(navigation_server->map_get_closest_point(map, Vector3(0, 0, 0)), Vector3(0, 0, 0)); + CHECK_NE(navigation_server->map_get_closest_point_normal(map, Vector3(0, 0, 0)), Vector3()); + CHECK(navigation_server->map_get_closest_point_owner(map, Vector3(0, 0, 0)).is_valid()); + // TODO: Test map_get_closest_point_to_segment() with p_use_collision=true as well. + CHECK_NE(navigation_server->map_get_closest_point_to_segment(map, Vector3(0, 0, 0), Vector3(1, 1, 1), false), Vector3()); + CHECK_NE(navigation_server->map_get_path(map, Vector3(0, 0, 0), Vector3(10, 0, 10), true).size(), 0); + CHECK_NE(navigation_server->map_get_path(map, Vector3(0, 0, 0), Vector3(10, 0, 10), false).size(), 0); + } + + SUBCASE("Elaborate query with 'CORRIDORFUNNEL' post-processing should yield non-empty result") { + Ref<NavigationPathQueryParameters3D> query_parameters = memnew(NavigationPathQueryParameters3D); + query_parameters->set_map(map); + query_parameters->set_start_position(Vector3(0, 0, 0)); + query_parameters->set_target_position(Vector3(10, 0, 10)); + query_parameters->set_path_postprocessing(NavigationPathQueryParameters3D::PATH_POSTPROCESSING_CORRIDORFUNNEL); + Ref<NavigationPathQueryResult3D> query_result = memnew(NavigationPathQueryResult3D); + navigation_server->query_path(query_parameters, query_result); + CHECK_NE(query_result->get_path().size(), 0); + CHECK_NE(query_result->get_path_types().size(), 0); + CHECK_NE(query_result->get_path_rids().size(), 0); + CHECK_NE(query_result->get_path_owner_ids().size(), 0); + } + + SUBCASE("Elaborate query with 'EDGECENTERED' post-processing should yield non-empty result") { + Ref<NavigationPathQueryParameters3D> query_parameters = memnew(NavigationPathQueryParameters3D); + query_parameters->set_map(map); + query_parameters->set_start_position(Vector3(10, 0, 10)); + query_parameters->set_target_position(Vector3(0, 0, 0)); + query_parameters->set_path_postprocessing(NavigationPathQueryParameters3D::PATH_POSTPROCESSING_EDGECENTERED); + Ref<NavigationPathQueryResult3D> query_result = memnew(NavigationPathQueryResult3D); + navigation_server->query_path(query_parameters, query_result); + CHECK_NE(query_result->get_path().size(), 0); + CHECK_NE(query_result->get_path_types().size(), 0); + CHECK_NE(query_result->get_path_rids().size(), 0); + CHECK_NE(query_result->get_path_owner_ids().size(), 0); + } + + SUBCASE("Elaborate query with non-matching navigation layer mask should yield empty result") { + Ref<NavigationPathQueryParameters3D> query_parameters = memnew(NavigationPathQueryParameters3D); + query_parameters->set_map(map); + query_parameters->set_start_position(Vector3(10, 0, 10)); + query_parameters->set_target_position(Vector3(0, 0, 0)); + query_parameters->set_navigation_layers(2); + Ref<NavigationPathQueryResult3D> query_result = memnew(NavigationPathQueryResult3D); + navigation_server->query_path(query_parameters, query_result); + CHECK_EQ(query_result->get_path().size(), 0); + CHECK_EQ(query_result->get_path_types().size(), 0); + CHECK_EQ(query_result->get_path_rids().size(), 0); + CHECK_EQ(query_result->get_path_owner_ids().size(), 0); + } + + SUBCASE("Elaborate query without metadata flags should yield path only") { + Ref<NavigationPathQueryParameters3D> query_parameters = memnew(NavigationPathQueryParameters3D); + query_parameters->set_map(map); + query_parameters->set_start_position(Vector3(10, 0, 10)); + query_parameters->set_target_position(Vector3(0, 0, 0)); + query_parameters->set_metadata_flags(0); + Ref<NavigationPathQueryResult3D> query_result = memnew(NavigationPathQueryResult3D); + navigation_server->query_path(query_parameters, query_result); + CHECK_NE(query_result->get_path().size(), 0); + CHECK_EQ(query_result->get_path_types().size(), 0); + CHECK_EQ(query_result->get_path_rids().size(), 0); + CHECK_EQ(query_result->get_path_owner_ids().size(), 0); + } + + navigation_server->free(region); + navigation_server->free(map); + navigation_server->process(0.0); // Give server some cycles to commit. + } } } //namespace TestNavigationServer3D diff --git a/tests/test_macros.h b/tests/test_macros.h index d39da7f8e8..bc85ec6ddc 100644 --- a/tests/test_macros.h +++ b/tests/test_macros.h @@ -177,6 +177,13 @@ int register_test_command(String p_command, TestFunc p_function); _UPDATE_EVENT_MODIFERS(event, m_modifers); \ event->set_pressed(true); +#define _CREATE_GUI_TOUCH_EVENT(m_screen_pos, m_pressed, m_double) \ + Ref<InputEventScreenTouch> event; \ + event.instantiate(); \ + event->set_position(m_screen_pos); \ + event->set_pressed(m_pressed); \ + event->set_double_tap(m_double); + #define SEND_GUI_MOUSE_BUTTON_EVENT(m_screen_pos, m_input, m_mask, m_modifers) \ { \ _CREATE_GUI_MOUSE_EVENT(m_screen_pos, m_input, m_mask, m_modifers); \ @@ -215,6 +222,13 @@ int register_test_command(String p_command, TestFunc p_function); CoreGlobals::print_error_enabled = errors_enabled; \ } +#define SEND_GUI_TOUCH_EVENT(m_screen_pos, m_pressed, m_double) \ + { \ + _CREATE_GUI_TOUCH_EVENT(m_screen_pos, m_pressed, m_double) \ + _SEND_DISPLAYSERVER_EVENT(event); \ + MessageQueue::get_singleton()->flush(); \ + } + // Utility class / macros for testing signals // // Use SIGNAL_WATCH(*object, "signal_name") to start watching diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 2e99e6fdb6..f1e348345b 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -31,6 +31,7 @@ #include "test_main.h" #include "tests/core/config/test_project_settings.h" +#include "tests/core/input/test_input_event.h" #include "tests/core/input/test_input_event_key.h" #include "tests/core/input/test_input_event_mouse.h" #include "tests/core/input/test_shortcut.h" @@ -71,6 +72,7 @@ #include "tests/core/string/test_node_path.h" #include "tests/core/string/test_string.h" #include "tests/core/string/test_translation.h" +#include "tests/core/string/test_translation_server.h" #include "tests/core/templates/test_command_queue.h" #include "tests/core/templates/test_hash_map.h" #include "tests/core/templates/test_hash_set.h" @@ -104,6 +106,7 @@ #include "tests/scene/test_navigation_region_2d.h" #include "tests/scene/test_navigation_region_3d.h" #include "tests/scene/test_node.h" +#include "tests/scene/test_packed_scene.h" #include "tests/scene/test_path_2d.h" #include "tests/scene/test_path_3d.h" #include "tests/scene/test_primitives.h" @@ -112,6 +115,8 @@ #include "tests/scene/test_theme.h" #include "tests/scene/test_viewport.h" #include "tests/scene/test_visual_shader.h" +#include "tests/scene/test_window.h" +#include "tests/servers/rendering/test_shader_preprocessor.h" #include "tests/servers/test_navigation_server_2d.h" #include "tests/servers/test_navigation_server_3d.h" #include "tests/servers/test_text_server.h" |