summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/core/config/test_project_settings.h60
-rw-r--r--tests/core/input/test_input_event.h115
-rw-r--r--tests/core/io/test_image.h90
-rw-r--r--tests/core/io/test_json.h86
-rw-r--r--tests/core/io/test_resource.h52
-rw-r--r--tests/core/object/test_class_db.h3
-rw-r--r--tests/core/object/test_object.h23
-rw-r--r--tests/core/string/test_string.h18
-rw-r--r--tests/core/string/test_translation_server.h136
-rw-r--r--tests/core/variant/test_array.h26
-rw-r--r--tests/data/images/icon.ddsbin0 -> 87536 bytes
-rw-r--r--tests/scene/test_packed_scene.h155
-rw-r--r--tests/scene/test_text_edit.h4
-rw-r--r--tests/scene/test_theme.h2
-rw-r--r--tests/scene/test_viewport.h463
-rw-r--r--tests/scene/test_window.h96
-rw-r--r--tests/servers/rendering/test_shader_preprocessor.h333
-rw-r--r--tests/servers/test_navigation_server_3d.h483
-rw-r--r--tests/test_macros.h14
-rw-r--r--tests/test_main.cpp5
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
new file mode 100644
index 0000000000..8a9de402cb
--- /dev/null
+++ b/tests/data/images/icon.dds
Binary files differ
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"