summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/SCsub33
-rw-r--r--tests/core/io/test_config_file.h164
-rw-r--r--tests/core/io/test_file_access.h85
-rw-r--r--tests/core/io/test_image.h303
-rw-r--r--tests/core/io/test_json.h151
-rw-r--r--tests/core/io/test_marshalls.h329
-rw-r--r--tests/core/io/test_pck_packer.h122
-rw-r--r--tests/core/io/test_resource.h114
-rw-r--r--tests/core/io/test_xml_parser.h71
-rw-r--r--tests/core/math/test_aabb.h395
-rw-r--r--tests/core/math/test_astar.h362
-rw-r--r--tests/core/math/test_basis.h286
-rw-r--r--tests/core/math/test_color.h207
-rw-r--r--tests/core/math/test_expression.h444
-rw-r--r--tests/core/math/test_geometry_2d.h567
-rw-r--r--tests/core/math/test_geometry_3d.h437
-rw-r--r--tests/core/math/test_math.cpp690
-rw-r--r--tests/core/math/test_math.h41
-rw-r--r--tests/core/math/test_random_number_generator.h275
-rw-r--r--tests/core/math/test_rect2.h575
-rw-r--r--tests/core/object/test_class_db.h865
-rw-r--r--tests/core/object/test_method_bind.h160
-rw-r--r--tests/core/object/test_object.h280
-rw-r--r--tests/core/string/test_node_path.h172
-rw-r--r--tests/core/string/test_string.h1481
-rw-r--r--tests/core/string/test_translation.h182
-rw-r--r--tests/core/templates/test_command_queue.h431
-rw-r--r--tests/core/templates/test_list.h548
-rw-r--r--tests/core/templates/test_local_vector.h240
-rw-r--r--tests/core/templates/test_lru.h99
-rw-r--r--tests/core/templates/test_oa_hash_map.cpp301
-rw-r--r--tests/core/templates/test_oa_hash_map.h41
-rw-r--r--tests/core/templates/test_ordered_hash_map.h135
-rw-r--r--tests/core/templates/test_paged_array.h153
-rw-r--r--tests/core/templates/test_vector.h520
-rw-r--r--tests/core/test_crypto.h73
-rw-r--r--tests/core/test_hashing_context.h165
-rw-r--r--tests/core/test_time.h145
-rw-r--r--tests/core/variant/test_array.h496
-rw-r--r--tests/core/variant/test_dictionary.h505
-rw-r--r--tests/core/variant/test_variant.h916
-rw-r--r--tests/data/images/icon.bmpbin0 -> 262282 bytes
-rw-r--r--tests/data/images/icon.jpgbin0 -> 14866 bytes
-rw-r--r--tests/data/images/icon.pngbin0 -> 29261 bytes
-rw-r--r--tests/data/images/icon.tgabin0 -> 262162 bytes
-rw-r--r--tests/data/images/icon.webpbin0 -> 4522 bytes
-rw-r--r--tests/data/translations.csv8
-rw-r--r--tests/scene/test_code_edit.h3253
-rw-r--r--tests/scene/test_curve.h254
-rw-r--r--tests/scene/test_gradient.h149
-rw-r--r--tests/scene/test_gui.cpp259
-rw-r--r--tests/scene/test_gui.h41
-rw-r--r--tests/scene/test_path_3d.h84
-rw-r--r--tests/scene/test_path_follow_2d.h240
-rw-r--r--tests/scene/test_path_follow_3d.h219
-rw-r--r--tests/servers/test_physics_2d.cpp398
-rw-r--r--tests/servers/test_physics_2d.h41
-rw-r--r--tests/servers/test_physics_3d.cpp410
-rw-r--r--tests/servers/test_physics_3d.h41
-rw-r--r--tests/servers/test_render.cpp232
-rw-r--r--tests/servers/test_render.h41
-rw-r--r--tests/servers/test_shader_lang.cpp367
-rw-r--r--tests/servers/test_shader_lang.h41
-rw-r--r--tests/servers/test_text_server.h296
-rw-r--r--tests/test_macros.cpp42
-rw-r--r--tests/test_macros.h350
-rw-r--r--tests/test_main.cpp293
-rw-r--r--tests/test_main.h36
-rw-r--r--tests/test_tools.h59
-rw-r--r--tests/test_utils.cpp42
-rw-r--r--tests/test_utils.h42
-rw-r--r--tests/test_validate_testing.h199
72 files changed, 20996 insertions, 0 deletions
diff --git a/tests/SCsub b/tests/SCsub
new file mode 100644
index 0000000000..31466fffc1
--- /dev/null
+++ b/tests/SCsub
@@ -0,0 +1,33 @@
+#!/usr/bin/python
+
+Import("env")
+
+env.tests_sources = []
+
+env_tests = env.Clone()
+
+# Include GDNative headers.
+if env["module_gdnative_enabled"]:
+ env_tests.Append(CPPPATH=["#modules/gdnative/include"])
+
+# We must disable the THREAD_LOCAL entirely in doctest to prevent crashes on debugging
+# Since we link with /MT thread_local is always expired when the header is used
+# So the debugger crashes the engine and it causes weird errors
+# Explained in https://github.com/onqtam/doctest/issues/401
+if env_tests["platform"] == "windows":
+ env_tests.Append(CPPDEFINES=[("DOCTEST_THREAD_LOCAL", "")])
+
+# Increase number of addressable sections in object files
+# due to doctest's heavy use of templates and macros.
+if env_tests.msvc:
+ env_tests.Append(CCFLAGS=["/bigobj"])
+
+env_tests.add_source_files(env.tests_sources, "core/*.cpp")
+env_tests.add_source_files(env.tests_sources, "core/math/*.cpp")
+env_tests.add_source_files(env.tests_sources, "core/templates/*.cpp")
+env_tests.add_source_files(env.tests_sources, "scene/*.cpp")
+env_tests.add_source_files(env.tests_sources, "servers/*.cpp")
+env_tests.add_source_files(env.tests_sources, "*.cpp")
+
+lib = env_tests.add_library("tests", env.tests_sources)
+env.Prepend(LIBS=[lib])
diff --git a/tests/core/io/test_config_file.h b/tests/core/io/test_config_file.h
new file mode 100644
index 0000000000..6e393c7a2d
--- /dev/null
+++ b/tests/core/io/test_config_file.h
@@ -0,0 +1,164 @@
+/*************************************************************************/
+/* test_config_file.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_CONFIG_FILE_H
+#define TEST_CONFIG_FILE_H
+
+#include "core/io/config_file.h"
+#include "core/os/os.h"
+
+#include "tests/test_macros.h"
+
+namespace TestConfigFile {
+
+TEST_CASE("[ConfigFile] Parsing well-formatted files") {
+ ConfigFile config_file;
+ // Formatting is intentionally hand-edited to see how human-friendly the parser is.
+ const Error error = config_file.parse(R"(
+[player]
+
+name = "Unnamed Player"
+tagline="Waiting
+for
+Godot"
+
+color =Color( 0, 0.5,1, 1) ; Inline comment
+position= Vector2(
+ 3,
+ 4
+)
+
+[graphics]
+
+antialiasing = true
+
+; Testing comments and case-sensitivity...
+antiAliasing = false
+)");
+
+ CHECK_MESSAGE(error == OK, "The configuration file should parse successfully.");
+ CHECK_MESSAGE(
+ String(config_file.get_value("player", "name")) == "Unnamed Player",
+ "Reading `player/name` should return the expected value.");
+ CHECK_MESSAGE(
+ String(config_file.get_value("player", "tagline")) == "Waiting\nfor\nGodot",
+ "Reading `player/tagline` should return the expected value.");
+ CHECK_MESSAGE(
+ Color(config_file.get_value("player", "color")).is_equal_approx(Color(0, 0.5, 1)),
+ "Reading `player/color` should return the expected value.");
+ CHECK_MESSAGE(
+ Vector2(config_file.get_value("player", "position")).is_equal_approx(Vector2(3, 4)),
+ "Reading `player/position` should return the expected value.");
+ CHECK_MESSAGE(
+ bool(config_file.get_value("graphics", "antialiasing")),
+ "Reading `graphics/antialiasing` should return `true`.");
+ CHECK_MESSAGE(
+ bool(config_file.get_value("graphics", "antiAliasing")) == false,
+ "Reading `graphics/antiAliasing` should return `false`.");
+
+ // An empty ConfigFile is valid.
+ const Error error_empty = config_file.parse("");
+ CHECK_MESSAGE(error_empty == OK,
+ "An empty configuration file should parse successfully.");
+}
+
+TEST_CASE("[ConfigFile] Parsing malformatted file") {
+ ConfigFile config_file;
+ ERR_PRINT_OFF;
+ const Error error = config_file.parse(R"(
+[player]
+
+name = "Unnamed Player"" ; Extraneous closing quote.
+tagline = "Waiting\nfor\nGodot"
+
+color = Color(0, 0.5, 1) ; Missing 4th parameter.
+position = Vector2(
+ 3,,
+ 4
+) ; Extraneous comma.
+
+[graphics]
+
+antialiasing = true
+antialiasing = false ; Duplicate key.
+)");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(error == ERR_PARSE_ERROR,
+ "The configuration file shouldn't parse successfully.");
+}
+
+TEST_CASE("[ConfigFile] Saving file") {
+ ConfigFile config_file;
+ config_file.set_value("player", "name", "Unnamed Player");
+ config_file.set_value("player", "tagline", "Waiting\nfor\nGodot");
+ config_file.set_value("player", "color", Color(0, 0.5, 1));
+ config_file.set_value("player", "position", Vector2(3, 4));
+ config_file.set_value("graphics", "antialiasing", true);
+ config_file.set_value("graphics", "antiAliasing", false);
+ config_file.set_value("quoted", String::utf8("静音"), 42);
+ config_file.set_value("quoted", "a=b", 7);
+
+#ifdef WINDOWS_ENABLED
+ const String config_path = OS::get_singleton()->get_environment("TEMP").plus_file("config.ini");
+#else
+ const String config_path = "/tmp/config.ini";
+#endif
+
+ config_file.save(config_path);
+
+ // Expected contents of the saved ConfigFile.
+ const String contents = String::utf8(R"([player]
+
+name="Unnamed Player"
+tagline="Waiting
+for
+Godot"
+color=Color(0, 0.5, 1, 1)
+position=Vector2(3, 4)
+
+[graphics]
+
+antialiasing=true
+antiAliasing=false
+
+[quoted]
+
+"静音"=42
+"a=b"=7
+)");
+
+ FileAccessRef file = FileAccess::open(config_path, FileAccess::READ);
+ CHECK_MESSAGE(file->get_as_utf8_string() == contents,
+ "The saved configuration file should match the expected format.");
+}
+} // namespace TestConfigFile
+
+#endif // TEST_CONFIG_FILE_H
diff --git a/tests/core/io/test_file_access.h b/tests/core/io/test_file_access.h
new file mode 100644
index 0000000000..eee57048cf
--- /dev/null
+++ b/tests/core/io/test_file_access.h
@@ -0,0 +1,85 @@
+/*************************************************************************/
+/* test_file_access.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_FILE_ACCESS_H
+#define TEST_FILE_ACCESS_H
+
+#include "core/io/file_access.h"
+#include "tests/test_macros.h"
+#include "tests/test_utils.h"
+
+namespace TestFileAccess {
+
+TEST_CASE("[FileAccess] CSV read") {
+ FileAccessRef f = FileAccess::open(TestUtils::get_data_path("translations.csv"), FileAccess::READ);
+
+ Vector<String> header = f->get_csv_line(); // Default delimiter: ",".
+ REQUIRE(header.size() == 3);
+
+ Vector<String> row1 = f->get_csv_line(","); // Explicit delimiter, should be the same.
+ REQUIRE(row1.size() == 3);
+ CHECK(row1[0] == "GOOD_MORNING");
+ CHECK(row1[1] == "Good Morning");
+ CHECK(row1[2] == "Guten Morgen");
+
+ Vector<String> row2 = f->get_csv_line();
+ REQUIRE(row2.size() == 3);
+ CHECK(row2[0] == "GOOD_EVENING");
+ CHECK(row2[1] == "Good Evening");
+ CHECK(row2[2].is_empty()); // Use case: not yet translated!
+ // https://github.com/godotengine/godot/issues/44269
+ CHECK_MESSAGE(row2[2] != "\"", "Should not parse empty string as a single double quote.");
+
+ Vector<String> row3 = f->get_csv_line();
+ REQUIRE(row3.size() == 6);
+ CHECK(row3[0] == "Without quotes");
+ CHECK(row3[1] == "With, comma");
+ CHECK(row3[2] == "With \"inner\" quotes");
+ CHECK(row3[3] == "With \"inner\", quotes\",\" and comma");
+ CHECK(row3[4] == "With \"inner\nsplit\" quotes and\nline breaks");
+ CHECK(row3[5] == "With \\nnewline chars"); // Escaped, not an actual newline.
+
+ Vector<String> row4 = f->get_csv_line("~"); // Custom delimiter, makes inline commas easier.
+ REQUIRE(row4.size() == 3);
+ CHECK(row4[0] == "Some other");
+ CHECK(row4[1] == "delimiter");
+ CHECK(row4[2] == "should still work, shouldn't it?");
+
+ Vector<String> row5 = f->get_csv_line("\t"); // Tab separated variables.
+ REQUIRE(row5.size() == 3);
+ CHECK(row5[0] == "What about");
+ CHECK(row5[1] == "tab separated");
+ CHECK(row5[2] == "lines, good?");
+
+ f->close();
+}
+} // namespace TestFileAccess
+
+#endif // TEST_FILE_ACCESS_H
diff --git a/tests/core/io/test_image.h b/tests/core/io/test_image.h
new file mode 100644
index 0000000000..dcf21dd7b0
--- /dev/null
+++ b/tests/core/io/test_image.h
@@ -0,0 +1,303 @@
+/*************************************************************************/
+/* test_image.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_IMAGE_H
+#define TEST_IMAGE_H
+
+#include "core/io/image.h"
+#include "core/os/os.h"
+
+#include "tests/test_utils.h"
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestImage {
+
+TEST_CASE("[Image] Instantiation") {
+ Ref<Image> image = memnew(Image(8, 4, false, Image::FORMAT_RGBA8));
+ CHECK_MESSAGE(
+ !image->is_empty(),
+ "An image created with specified size and format should not be empty at first.");
+ CHECK_MESSAGE(
+ image->is_invisible(),
+ "A newly created image should be invisible.");
+ CHECK_MESSAGE(
+ !image->is_compressed(),
+ "A newly created image should not be compressed.");
+ CHECK(!image->has_mipmaps());
+
+ PackedByteArray image_data = image->get_data();
+ for (int i = 0; i < image_data.size(); i++) {
+ CHECK_MESSAGE(
+ image_data[i] == 0,
+ "An image created without data specified should have its data zeroed out.");
+ }
+
+ Ref<Image> image_copy = memnew(Image());
+ CHECK_MESSAGE(
+ image_copy->is_empty(),
+ "An image created without any specified size and format be empty at first.");
+ image_copy->copy_internals_from(image);
+
+ CHECK_MESSAGE(
+ image->get_data() == image_copy->get_data(),
+ "Duplicated images should have the same data.");
+
+ image_data = image->get_data();
+ Ref<Image> image_from_data = memnew(Image(8, 4, false, Image::FORMAT_RGBA8, image_data));
+ CHECK_MESSAGE(
+ image->get_data() == image_from_data->get_data(),
+ "An image created from data of another image should have the same data of the original image.");
+}
+
+TEST_CASE("[Image] Saving and loading") {
+ Ref<Image> image = memnew(Image(4, 4, false, Image::FORMAT_RGBA8));
+ const String save_path_png = OS::get_singleton()->get_cache_path().plus_file("image.png");
+ const String save_path_exr = OS::get_singleton()->get_cache_path().plus_file("image.exr");
+
+ // Save PNG
+ Error err;
+ err = image->save_png(save_path_png);
+ CHECK_MESSAGE(
+ err == OK,
+ "The image should be saved successfully as a .png file.");
+
+ // Save EXR
+ err = image->save_exr(save_path_exr, false);
+ CHECK_MESSAGE(
+ err == OK,
+ "The image should be saved successfully as an .exr file.");
+
+ // Load using load()
+ Ref<Image> image_load = memnew(Image());
+ err = image_load->load(save_path_png);
+ CHECK_MESSAGE(
+ err == OK,
+ "The image should load successfully using load().");
+ CHECK_MESSAGE(
+ image->get_data() == image_load->get_data(),
+ "The loaded image should have the same data as the one that got saved.");
+
+ // Load BMP
+ Ref<Image> image_bmp = memnew(Image());
+ FileAccessRef f_bmp = FileAccess::open(TestUtils::get_data_path("images/icon.bmp"), FileAccess::READ, &err);
+ PackedByteArray data_bmp;
+ data_bmp.resize(f_bmp->get_length() + 1);
+ f_bmp->get_buffer(data_bmp.ptrw(), f_bmp->get_length());
+ CHECK_MESSAGE(
+ image_bmp->load_bmp_from_buffer(data_bmp) == OK,
+ "The BMP image should load successfully.");
+
+ // Load JPG
+ Ref<Image> image_jpg = memnew(Image());
+ FileAccessRef f_jpg = FileAccess::open(TestUtils::get_data_path("images/icon.jpg"), FileAccess::READ, &err);
+ PackedByteArray data_jpg;
+ data_jpg.resize(f_jpg->get_length() + 1);
+ f_jpg->get_buffer(data_jpg.ptrw(), f_jpg->get_length());
+ CHECK_MESSAGE(
+ image_jpg->load_jpg_from_buffer(data_jpg) == OK,
+ "The JPG image should load successfully.");
+
+ // Load WEBP
+ Ref<Image> image_webp = memnew(Image());
+ FileAccessRef f_webp = FileAccess::open(TestUtils::get_data_path("images/icon.webp"), FileAccess::READ, &err);
+ PackedByteArray data_webp;
+ data_webp.resize(f_webp->get_length() + 1);
+ f_webp->get_buffer(data_webp.ptrw(), f_webp->get_length());
+ CHECK_MESSAGE(
+ image_webp->load_webp_from_buffer(data_webp) == OK,
+ "The WEBP image should load successfully.");
+
+ // Load PNG
+ Ref<Image> image_png = memnew(Image());
+ FileAccessRef f_png = FileAccess::open(TestUtils::get_data_path("images/icon.png"), FileAccess::READ, &err);
+ PackedByteArray data_png;
+ data_png.resize(f_png->get_length() + 1);
+ f_png->get_buffer(data_png.ptrw(), f_png->get_length());
+ CHECK_MESSAGE(
+ image_png->load_png_from_buffer(data_png) == OK,
+ "The PNG image should load successfully.");
+
+ // Load TGA
+ Ref<Image> image_tga = memnew(Image());
+ FileAccessRef f_tga = FileAccess::open(TestUtils::get_data_path("images/icon.tga"), FileAccess::READ, &err);
+ PackedByteArray data_tga;
+ data_tga.resize(f_tga->get_length() + 1);
+ f_tga->get_buffer(data_tga.ptrw(), f_tga->get_length());
+ CHECK_MESSAGE(
+ image_tga->load_tga_from_buffer(data_tga) == OK,
+ "The TGA image should load successfully.");
+}
+
+TEST_CASE("[Image] Basic getters") {
+ Ref<Image> image = memnew(Image(8, 4, false, Image::FORMAT_LA8));
+ CHECK(image->get_width() == 8);
+ CHECK(image->get_height() == 4);
+ CHECK(image->get_size() == Vector2(8, 4));
+ CHECK(image->get_format() == Image::FORMAT_LA8);
+ CHECK(image->get_used_rect() == Rect2(0, 0, 0, 0));
+ Ref<Image> image_get_rect = image->get_rect(Rect2(0, 0, 2, 1));
+ CHECK(image_get_rect->get_size() == Vector2(2, 1));
+}
+
+TEST_CASE("[Image] Resizing") {
+ Ref<Image> image = memnew(Image(8, 8, false, Image::FORMAT_RGBA8));
+ // Crop
+ image->crop(4, 4);
+ CHECK_MESSAGE(
+ image->get_size() == Vector2(4, 4),
+ "get_size() should return the correct size after cropping.");
+ image->set_pixel(0, 0, Color(1, 1, 1, 1));
+
+ // Resize
+ for (int i = 0; i < 5; i++) {
+ Ref<Image> image_resized = memnew(Image());
+ image_resized->copy_internals_from(image);
+ Image::Interpolation interpolation = static_cast<Image::Interpolation>(i);
+ image_resized->resize(8, 8, interpolation);
+ CHECK_MESSAGE(
+ image_resized->get_size() == Vector2(8, 8),
+ "get_size() should return the correct size after resizing.");
+ CHECK_MESSAGE(
+ image_resized->get_pixel(1, 1).a > 0,
+ "Resizing an image should also affect its content.");
+ }
+
+ // shrink_x2()
+ image->shrink_x2();
+ CHECK_MESSAGE(
+ image->get_size() == Vector2(2, 2),
+ "get_size() should return the correct size after shrink_x2().");
+
+ // resize_to_po2()
+ Ref<Image> image_po_2 = memnew(Image(14, 28, false, Image::FORMAT_RGBA8));
+ image_po_2->resize_to_po2();
+ CHECK_MESSAGE(
+ image_po_2->get_size() == Vector2(16, 32),
+ "get_size() should return the correct size after resize_to_po2().");
+}
+
+TEST_CASE("[Image] Modifying pixels of an image") {
+ Ref<Image> image = memnew(Image(3, 3, false, Image::FORMAT_RGBA8));
+ image->set_pixel(0, 0, Color(1, 1, 1, 1));
+ CHECK_MESSAGE(
+ !image->is_invisible(),
+ "Image should not be invisible after drawing on it.");
+ CHECK_MESSAGE(
+ image->get_pixelv(Vector2(0, 0)).is_equal_approx(Color(1, 1, 1, 1)),
+ "Image's get_pixel() should return the same color value as the one being set with set_pixel() in the same position.");
+ CHECK_MESSAGE(
+ image->get_used_rect() == Rect2(0, 0, 1, 1),
+ "Image's get_used_rect should return the expected value, larger than Rect2(0, 0, 0, 0) if it's visible.");
+
+ image->set_pixelv(Vector2(0, 0), Color(0.5, 0.5, 0.5, 0.5));
+ Ref<Image> image2 = memnew(Image(3, 3, false, Image::FORMAT_RGBA8));
+
+ // Fill image with color
+ image2->fill(Color(0.5, 0.5, 0.5, 0.5));
+ for (int y = 0; y < image2->get_height(); y++) {
+ for (int x = 0; x < image2->get_width(); x++) {
+ CHECK_MESSAGE(
+ image2->get_pixel(x, y).r > 0.49,
+ "fill() should colorize all pixels of the image.");
+ }
+ }
+
+ // Fill rect with color
+ {
+ const int img_width = 3;
+ const int img_height = 3;
+ Vector<Rect2> rects;
+ rects.push_back(Rect2());
+ rects.push_back(Rect2(-5, -5, 3, 3));
+ rects.push_back(Rect2(img_width, 0, 12, 12));
+ rects.push_back(Rect2(0, img_height, 12, 12));
+ rects.push_back(Rect2(img_width + 1, img_height + 2, 12, 12));
+ rects.push_back(Rect2(1, 1, 1, 1));
+ rects.push_back(Rect2(0, 1, 2, 3));
+ rects.push_back(Rect2(-5, 0, img_width + 10, 2));
+ rects.push_back(Rect2(0, -5, 2, img_height + 10));
+ rects.push_back(Rect2(-1, -1, img_width + 1, img_height + 1));
+
+ for (const Rect2 &rect : rects) {
+ Ref<Image> img = memnew(Image(img_width, img_height, false, Image::FORMAT_RGBA8));
+ CHECK_NOTHROW_MESSAGE(
+ img->fill_rect(rect, Color(1, 1, 1, 1)),
+ "fill_rect() shouldn't throw for any rect.");
+ for (int y = 0; y < img->get_height(); y++) {
+ for (int x = 0; x < img->get_width(); x++) {
+ if (rect.abs().has_point(Point2(x, y))) {
+ CHECK_MESSAGE(
+ img->get_pixel(x, y).is_equal_approx(Color(1, 1, 1, 1)),
+ "fill_rect() should colorize all image pixels within rect bounds.");
+ } else {
+ CHECK_MESSAGE(
+ !img->get_pixel(x, y).is_equal_approx(Color(1, 1, 1, 1)),
+ "fill_rect() shouldn't colorize any image pixel out of rect bounds.");
+ }
+ }
+ }
+ }
+ }
+
+ // Blend two images together
+ image->blend_rect(image2, Rect2(Vector2(0, 0), image2->get_size()), Vector2(0, 0));
+ CHECK_MESSAGE(
+ image->get_pixel(0, 0).a > 0.7,
+ "blend_rect() should blend the alpha values of the two images.");
+ CHECK_MESSAGE(
+ image->get_used_rect().size == image->get_size(),
+ "get_used_rect() should return the expected value, its Rect size should be the same as get_size() if there are no transparent pixels.");
+
+ Ref<Image> image3 = memnew(Image(2, 2, false, Image::FORMAT_RGBA8));
+ image3->set_pixel(0, 0, Color(0, 1, 0, 1));
+
+ //blit_rect() two images together
+ image->blit_rect(image3, Rect2(Vector2(0, 0), image3->get_size()), Vector2(0, 0));
+ CHECK_MESSAGE(
+ image->get_pixel(0, 0).is_equal_approx(Color(0, 1, 0, 1)),
+ "blit_rect() should replace old colors and not blend them.");
+ CHECK_MESSAGE(
+ !image->get_pixel(2, 2).is_equal_approx(Color(0, 1, 0, 1)),
+ "blit_rect() should not affect the area of the image that is outside src_rect.");
+
+ // Flip image
+ image3->flip_x();
+ CHECK(image3->get_pixel(1, 0).is_equal_approx(Color(0, 1, 0, 1)));
+ CHECK_MESSAGE(
+ image3->get_pixel(0, 0).is_equal_approx(Color(0, 0, 0, 0)),
+ "flip_x() should not leave old pixels behind.");
+ image3->flip_y();
+ CHECK(image3->get_pixel(1, 1).is_equal_approx(Color(0, 1, 0, 1)));
+ CHECK_MESSAGE(
+ image3->get_pixel(1, 0).is_equal_approx(Color(0, 0, 0, 0)),
+ "flip_y() should not leave old pixels behind.");
+}
+} // namespace TestImage
+#endif // TEST_IMAGE_H
diff --git a/tests/core/io/test_json.h b/tests/core/io/test_json.h
new file mode 100644
index 0000000000..478cf1766e
--- /dev/null
+++ b/tests/core/io/test_json.h
@@ -0,0 +1,151 @@
+/*************************************************************************/
+/* test_json.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_JSON_H
+#define TEST_JSON_H
+
+#include "core/io/json.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestJSON {
+
+// NOTE: The current JSON parser accepts many non-conformant strings such as
+// single-quoted strings, duplicate commas and trailing commas.
+// This is intentionally not tested as users shouldn't rely on this behavior.
+
+TEST_CASE("[JSON] Parsing single data types") {
+ // Parsing a single data type as JSON is valid per the JSON specification.
+
+ JSON json;
+
+ json.parse("null");
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing `null` as JSON should parse successfully.");
+ CHECK_MESSAGE(
+ json.get_data() == Variant(),
+ "Parsing a double quoted string as JSON should return the expected value.");
+
+ json.parse("true");
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing boolean `true` as JSON should parse successfully.");
+ CHECK_MESSAGE(
+ json.get_data(),
+ "Parsing boolean `true` as JSON should return the expected value.");
+
+ json.parse("false");
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing boolean `false` as JSON should parse successfully.");
+ CHECK_MESSAGE(
+ !json.get_data(),
+ "Parsing boolean `false` as JSON should return the expected value.");
+
+ json.parse("123456");
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing an integer number as JSON should parse successfully.");
+ CHECK_MESSAGE(
+ (int)(json.get_data()) == 123456,
+ "Parsing an integer number as JSON should return the expected value.");
+
+ json.parse("0.123456");
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing a floating-point number as JSON should parse successfully.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(double(json.get_data()), 0.123456),
+ "Parsing a floating-point number as JSON should return the expected value.");
+
+ json.parse("\"hello\"");
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing a double quoted string as JSON should parse successfully.");
+ CHECK_MESSAGE(
+ json.get_data() == "hello",
+ "Parsing a double quoted string as JSON should return the expected value.");
+}
+
+TEST_CASE("[JSON] Parsing arrays") {
+ JSON json;
+
+ // JSON parsing fails if it's split over several lines (even if leading indentation is removed).
+ json.parse(R"(["Hello", "world.", "This is",["a","json","array.",[]], "Empty arrays ahoy:", [[["Gotcha!"]]]])");
+
+ const Array array = json.get_data();
+ CHECK_MESSAGE(
+ json.get_error_line() == 0,
+ "Parsing a JSON array should parse successfully.");
+ CHECK_MESSAGE(
+ array[0] == "Hello",
+ "The parsed JSON should contain the expected values.");
+ const Array sub_array = array[3];
+ CHECK_MESSAGE(
+ sub_array.size() == 4,
+ "The parsed JSON should contain the expected values.");
+ CHECK_MESSAGE(
+ sub_array[1] == "json",
+ "The parsed JSON should contain the expected values.");
+ CHECK_MESSAGE(
+ sub_array[3].hash() == Array().hash(),
+ "The parsed JSON should contain the expected values.");
+ const Array deep_array = Array(Array(array[5])[0])[0];
+ CHECK_MESSAGE(
+ deep_array[0] == "Gotcha!",
+ "The parsed JSON should contain the expected values.");
+}
+
+TEST_CASE("[JSON] Parsing objects (dictionaries)") {
+ JSON json;
+
+ json.parse(R"({"name": "Godot Engine", "is_free": true, "bugs": null, "apples": {"red": 500, "green": 0, "blue": -20}, "empty_object": {}})");
+
+ const Dictionary dictionary = json.get_data();
+ CHECK_MESSAGE(
+ dictionary["name"] == "Godot Engine",
+ "The parsed JSON should contain the expected values.");
+ CHECK_MESSAGE(
+ dictionary["is_free"],
+ "The parsed JSON should contain the expected values.");
+ CHECK_MESSAGE(
+ dictionary["bugs"] == Variant(),
+ "The parsed JSON should contain the expected values.");
+ CHECK_MESSAGE(
+ (int)Dictionary(dictionary["apples"])["blue"] == -20,
+ "The parsed JSON should contain the expected values.");
+ CHECK_MESSAGE(
+ dictionary["empty_object"].hash() == Dictionary().hash(),
+ "The parsed JSON should contain the expected values.");
+}
+} // namespace TestJSON
+
+#endif // TEST_JSON_H
diff --git a/tests/core/io/test_marshalls.h b/tests/core/io/test_marshalls.h
new file mode 100644
index 0000000000..546a2e9358
--- /dev/null
+++ b/tests/core/io/test_marshalls.h
@@ -0,0 +1,329 @@
+/*************************************************************************/
+/* test_marshalls.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_MARSHALLS_H
+#define TEST_MARSHALLS_H
+
+#include "core/io/marshalls.h"
+
+#include "tests/test_macros.h"
+
+namespace TestMarshalls {
+
+TEST_CASE("[Marshalls] Unsigned 16 bit integer encoding") {
+ uint8_t arr[2];
+
+ unsigned int actual_size = encode_uint16(0x1234, arr);
+ CHECK(actual_size == sizeof(uint16_t));
+ CHECK_MESSAGE(arr[0] == 0x34, "First encoded byte value should be equal to low order byte value.");
+ CHECK_MESSAGE(arr[1] == 0x12, "Last encoded byte value should be equal to high order byte value.");
+}
+
+TEST_CASE("[Marshalls] Unsigned 32 bit integer encoding") {
+ uint8_t arr[4];
+
+ unsigned int actual_size = encode_uint32(0x12345678, arr);
+ CHECK(actual_size == sizeof(uint32_t));
+ CHECK_MESSAGE(arr[0] == 0x78, "First encoded byte value should be equal to low order byte value.");
+ CHECK(arr[1] == 0x56);
+ CHECK(arr[2] == 0x34);
+ CHECK_MESSAGE(arr[3] == 0x12, "Last encoded byte value should be equal to high order byte value.");
+}
+
+TEST_CASE("[Marshalls] Unsigned 64 bit integer encoding") {
+ uint8_t arr[8];
+
+ unsigned int actual_size = encode_uint64(0x0f123456789abcdef, arr);
+ CHECK(actual_size == sizeof(uint64_t));
+ CHECK_MESSAGE(arr[0] == 0xef, "First encoded byte value should be equal to low order byte value.");
+ CHECK(arr[1] == 0xcd);
+ CHECK(arr[2] == 0xab);
+ CHECK(arr[3] == 0x89);
+ CHECK(arr[4] == 0x67);
+ CHECK(arr[5] == 0x45);
+ CHECK(arr[6] == 0x23);
+ CHECK_MESSAGE(arr[7] == 0xf1, "Last encoded byte value should be equal to high order byte value.");
+}
+
+TEST_CASE("[Marshalls] Unsigned 16 bit integer decoding") {
+ uint8_t arr[] = { 0x34, 0x12 };
+
+ CHECK(decode_uint16(arr) == 0x1234);
+}
+
+TEST_CASE("[Marshalls] Unsigned 32 bit integer decoding") {
+ uint8_t arr[] = { 0x78, 0x56, 0x34, 0x12 };
+
+ CHECK(decode_uint32(arr) == 0x12345678);
+}
+
+TEST_CASE("[Marshalls] Unsigned 64 bit integer decoding") {
+ uint8_t arr[] = { 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0xf1 };
+
+ CHECK(decode_uint64(arr) == 0x0f123456789abcdef);
+}
+
+TEST_CASE("[Marshalls] Floating point single precision encoding") {
+ uint8_t arr[4];
+
+ // Decimal: 0.15625
+ // IEEE 754 single-precision binary floating-point format:
+ // sign exponent (8 bits) fraction (23 bits)
+ // 0 01111100 01000000000000000000000
+ // Hexadecimal: 0x3E200000
+ unsigned int actual_size = encode_float(0.15625f, arr);
+ CHECK(actual_size == sizeof(uint32_t));
+ CHECK(arr[0] == 0x00);
+ CHECK(arr[1] == 0x00);
+ CHECK(arr[2] == 0x20);
+ CHECK(arr[3] == 0x3e);
+}
+
+TEST_CASE("[Marshalls] Floating point double precision encoding") {
+ uint8_t arr[8];
+
+ // Decimal: 0.333333333333333314829616256247390992939472198486328125
+ // IEEE 754 double-precision binary floating-point format:
+ // sign exponent (11 bits) fraction (52 bits)
+ // 0 01111111101 0101010101010101010101010101010101010101010101010101
+ // Hexadecimal: 0x3FD5555555555555
+ unsigned int actual_size = encode_double(0.33333333333333333, arr);
+ CHECK(actual_size == sizeof(uint64_t));
+ CHECK(arr[0] == 0x55);
+ CHECK(arr[1] == 0x55);
+ CHECK(arr[2] == 0x55);
+ CHECK(arr[3] == 0x55);
+ CHECK(arr[4] == 0x55);
+ CHECK(arr[5] == 0x55);
+ CHECK(arr[6] == 0xd5);
+ CHECK(arr[7] == 0x3f);
+}
+
+TEST_CASE("[Marshalls] Floating point single precision decoding") {
+ uint8_t arr[] = { 0x00, 0x00, 0x20, 0x3e };
+
+ // See floating point encoding test case for details behind expected values
+ CHECK(decode_float(arr) == 0.15625f);
+}
+
+TEST_CASE("[Marshalls] Floating point double precision decoding") {
+ uint8_t arr[] = { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0xd5, 0x3f };
+
+ // See floating point encoding test case for details behind expected values
+ CHECK(decode_double(arr) == 0.33333333333333333);
+}
+
+TEST_CASE("[Marshalls] C string encoding") {
+ char cstring[] = "Godot"; // 5 characters
+ uint8_t data[6];
+
+ int actual_size = encode_cstring(cstring, data);
+ CHECK(actual_size == 6);
+ CHECK(data[0] == 'G');
+ CHECK(data[1] == 'o');
+ CHECK(data[2] == 'd');
+ CHECK(data[3] == 'o');
+ CHECK(data[4] == 't');
+ CHECK(data[5] == '\0');
+}
+
+TEST_CASE("[Marshalls] NIL Variant encoding") {
+ int r_len;
+ Variant variant;
+ uint8_t buffer[4];
+
+ CHECK(encode_variant(variant, buffer, r_len) == OK);
+ CHECK_MESSAGE(r_len == 4, "Length == 4 bytes for Variant::Type");
+ CHECK_MESSAGE(buffer[0] == 0x00, "Variant::NIL");
+ CHECK(buffer[1] == 0x00);
+ CHECK(buffer[2] == 0x00);
+ CHECK(buffer[3] == 0x00);
+ // No value
+}
+
+TEST_CASE("[Marshalls] INT 32 bit Variant encoding") {
+ int r_len;
+ Variant variant(0x12345678);
+ uint8_t buffer[8];
+
+ CHECK(encode_variant(variant, buffer, r_len) == OK);
+ CHECK_MESSAGE(r_len == 8, "Length == 4 bytes for Variant::Type + 4 bytes for int32_t");
+ CHECK_MESSAGE(buffer[0] == 0x02, "Variant::INT");
+ CHECK(buffer[1] == 0x00);
+ CHECK(buffer[2] == 0x00);
+ CHECK(buffer[3] == 0x00);
+ // Check value
+ CHECK(buffer[4] == 0x78);
+ CHECK(buffer[5] == 0x56);
+ CHECK(buffer[6] == 0x34);
+ CHECK(buffer[7] == 0x12);
+}
+
+TEST_CASE("[Marshalls] INT 64 bit Variant encoding") {
+ int r_len;
+ Variant variant(uint64_t(0x0f123456789abcdef));
+ uint8_t buffer[12];
+
+ CHECK(encode_variant(variant, buffer, r_len) == OK);
+ CHECK_MESSAGE(r_len == 12, "Length == 4 bytes for Variant::Type + 8 bytes for int64_t");
+ CHECK_MESSAGE(buffer[0] == 0x02, "Variant::INT");
+ CHECK(buffer[1] == 0x00);
+ CHECK_MESSAGE(buffer[2] == 0x01, "ENCODE_FLAG_64");
+ CHECK(buffer[3] == 0x00);
+ // Check value
+ CHECK(buffer[4] == 0xef);
+ CHECK(buffer[5] == 0xcd);
+ CHECK(buffer[6] == 0xab);
+ CHECK(buffer[7] == 0x89);
+ CHECK(buffer[8] == 0x67);
+ CHECK(buffer[9] == 0x45);
+ CHECK(buffer[10] == 0x23);
+ CHECK(buffer[11] == 0xf1);
+}
+
+TEST_CASE("[Marshalls] FLOAT single precision Variant encoding") {
+ int r_len;
+ Variant variant(0.15625f);
+ uint8_t buffer[8];
+
+ CHECK(encode_variant(variant, buffer, r_len) == OK);
+ CHECK_MESSAGE(r_len == 8, "Length == 4 bytes for Variant::Type + 4 bytes for float");
+ CHECK_MESSAGE(buffer[0] == 0x03, "Variant::FLOAT");
+ CHECK(buffer[1] == 0x00);
+ CHECK(buffer[2] == 0x00);
+ CHECK(buffer[3] == 0x00);
+ // Check value
+ CHECK(buffer[4] == 0x00);
+ CHECK(buffer[5] == 0x00);
+ CHECK(buffer[6] == 0x20);
+ CHECK(buffer[7] == 0x3e);
+}
+
+TEST_CASE("[Marshalls] FLOAT double precision Variant encoding") {
+ int r_len;
+ Variant variant(0.33333333333333333);
+ uint8_t buffer[12];
+
+ CHECK(encode_variant(variant, buffer, r_len) == OK);
+ CHECK_MESSAGE(r_len == 12, "Length == 4 bytes for Variant::Type + 8 bytes for double");
+ CHECK_MESSAGE(buffer[0] == 0x03, "Variant::FLOAT");
+ CHECK(buffer[1] == 0x00);
+ CHECK_MESSAGE(buffer[2] == 0x01, "ENCODE_FLAG_64");
+ CHECK(buffer[3] == 0x00);
+ // Check value
+ CHECK(buffer[4] == 0x55);
+ CHECK(buffer[5] == 0x55);
+ CHECK(buffer[6] == 0x55);
+ CHECK(buffer[7] == 0x55);
+ CHECK(buffer[8] == 0x55);
+ CHECK(buffer[9] == 0x55);
+ CHECK(buffer[10] == 0xd5);
+ CHECK(buffer[11] == 0x3f);
+}
+
+TEST_CASE("[Marshalls] Invalid data Variant decoding") {
+ Variant variant;
+ int r_len = 0;
+ uint8_t some_buffer[1] = { 0x00 };
+ uint8_t out_of_range_type_buffer[4] = { 0xff }; // Greater than Variant::VARIANT_MAX
+
+ CHECK(decode_variant(variant, some_buffer, /* less than 4 */ 1, &r_len) == ERR_INVALID_DATA);
+ CHECK(r_len == 0);
+
+ CHECK(decode_variant(variant, out_of_range_type_buffer, 4, &r_len) == ERR_INVALID_DATA);
+ CHECK(r_len == 0);
+}
+
+TEST_CASE("[Marshalls] NIL Variant decoding") {
+ Variant variant;
+ int r_len;
+ uint8_t buffer[] = {
+ 0x00, 0x00, 0x00, 0x00 // Variant::NIL
+ };
+
+ CHECK(decode_variant(variant, buffer, 4, &r_len) == OK);
+ CHECK(r_len == 4);
+ CHECK(variant == Variant());
+}
+
+TEST_CASE("[Marshalls] INT 32 bit Variant decoding") {
+ Variant variant;
+ int r_len;
+ uint8_t buffer[] = {
+ 0x02, 0x00, 0x00, 0x00, // Variant::INT
+ 0x78, 0x56, 0x34, 0x12 // value
+ };
+
+ CHECK(decode_variant(variant, buffer, 8, &r_len) == OK);
+ CHECK(r_len == 8);
+ CHECK(variant == Variant(0x12345678));
+}
+
+TEST_CASE("[Marshalls] INT 64 bit Variant decoding") {
+ Variant variant;
+ int r_len;
+ uint8_t buffer[] = {
+ 0x02, 0x00, 0x01, 0x00, // Variant::INT & ENCODE_FLAG_64
+ 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0xf1 // value
+ };
+
+ CHECK(decode_variant(variant, buffer, 12, &r_len) == OK);
+ CHECK(r_len == 12);
+ CHECK(variant == Variant(uint64_t(0x0f123456789abcdef)));
+}
+
+TEST_CASE("[Marshalls] FLOAT single precision Variant decoding") {
+ Variant variant;
+ int r_len;
+ uint8_t buffer[] = {
+ 0x03, 0x00, 0x00, 0x00, // Variant::FLOAT
+ 0x00, 0x00, 0x20, 0x3e // value
+ };
+
+ CHECK(decode_variant(variant, buffer, 8, &r_len) == OK);
+ CHECK(r_len == 8);
+ CHECK(variant == Variant(0.15625f));
+}
+
+TEST_CASE("[Marshalls] FLOAT double precision Variant decoding") {
+ Variant variant;
+ int r_len;
+ uint8_t buffer[] = {
+ 0x03, 0x00, 0x01, 0x00, // Variant::FLOAT & ENCODE_FLAG_64
+ 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0xd5, 0x3f // value
+ };
+
+ CHECK(decode_variant(variant, buffer, 12, &r_len) == OK);
+ CHECK(r_len == 12);
+ CHECK(variant == Variant(0.33333333333333333));
+}
+} // namespace TestMarshalls
+
+#endif // TEST_MARSHALLS_H
diff --git a/tests/core/io/test_pck_packer.h b/tests/core/io/test_pck_packer.h
new file mode 100644
index 0000000000..95adca6d68
--- /dev/null
+++ b/tests/core/io/test_pck_packer.h
@@ -0,0 +1,122 @@
+/*************************************************************************/
+/* test_pck_packer.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_PCK_PACKER_H
+#define TEST_PCK_PACKER_H
+
+#include "core/io/file_access_pack.h"
+#include "core/io/pck_packer.h"
+#include "core/os/os.h"
+
+#include "tests/test_utils.h"
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestPCKPacker {
+
+TEST_CASE("[PCKPacker] Pack an empty PCK file") {
+ PCKPacker pck_packer;
+ const String output_pck_path = OS::get_singleton()->get_cache_path().plus_file("output_empty.pck");
+ CHECK_MESSAGE(
+ pck_packer.pck_start(output_pck_path) == OK,
+ "Starting a PCK file should return an OK error code.");
+
+ CHECK_MESSAGE(
+ pck_packer.flush() == OK,
+ "Flushing the PCK should return an OK error code.");
+
+ Error err;
+ FileAccessRef f = FileAccess::open(output_pck_path, FileAccess::READ, &err);
+ CHECK_MESSAGE(
+ err == OK,
+ "The generated empty PCK file should be opened successfully.");
+ CHECK_MESSAGE(
+ f->get_length() >= 100,
+ "The generated empty PCK file shouldn't be too small (it should have the PCK header).");
+ CHECK_MESSAGE(
+ f->get_length() <= 500,
+ "The generated empty PCK file shouldn't be too large.");
+}
+
+TEST_CASE("[PCKPacker] Pack empty with zero alignment invalid") {
+ PCKPacker pck_packer;
+ const String output_pck_path = OS::get_singleton()->get_cache_path().plus_file("output_empty.pck");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(pck_packer.pck_start(output_pck_path, 0) != OK, "PCK with zero alignment should fail.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[PCKPacker] Pack empty with invalid key") {
+ PCKPacker pck_packer;
+ const String output_pck_path = OS::get_singleton()->get_cache_path().plus_file("output_empty.pck");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(pck_packer.pck_start(output_pck_path, 32, "") != OK, "PCK with invalid key should fail.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[PCKPacker] Pack a PCK file with some files and directories") {
+ PCKPacker pck_packer;
+ const String output_pck_path = OS::get_singleton()->get_cache_path().plus_file("output_with_files.pck");
+ CHECK_MESSAGE(
+ pck_packer.pck_start(output_pck_path) == OK,
+ "Starting a PCK file should return an OK error code.");
+
+ const String base_dir = OS::get_singleton()->get_executable_path().get_base_dir();
+
+ CHECK_MESSAGE(
+ pck_packer.add_file("version.py", base_dir.plus_file("../version.py"), "version.py") == OK,
+ "Adding a file to the PCK should return an OK error code.");
+ CHECK_MESSAGE(
+ pck_packer.add_file("some/directories with spaces/to/create/icon.png", base_dir.plus_file("../icon.png")) == OK,
+ "Adding a file to a new subdirectory in the PCK should return an OK error code.");
+ CHECK_MESSAGE(
+ pck_packer.add_file("some/directories with spaces/to/create/icon.svg", base_dir.plus_file("../icon.svg")) == OK,
+ "Adding a file to an existing subdirectory in the PCK should return an OK error code.");
+ CHECK_MESSAGE(
+ pck_packer.add_file("some/directories with spaces/to/create/icon.png", base_dir.plus_file("../logo.png")) == OK,
+ "Overriding a non-flushed file to an existing subdirectory in the PCK should return an OK error code.");
+ CHECK_MESSAGE(
+ pck_packer.flush() == OK,
+ "Flushing the PCK should return an OK error code.");
+
+ Error err;
+ FileAccessRef f = FileAccess::open(output_pck_path, FileAccess::READ, &err);
+ CHECK_MESSAGE(
+ err == OK,
+ "The generated non-empty PCK file should be opened successfully.");
+ CHECK_MESSAGE(
+ f->get_length() >= 25000,
+ "The generated non-empty PCK file should be large enough to actually hold the contents specified above.");
+ CHECK_MESSAGE(
+ f->get_length() <= 35000,
+ "The generated non-empty PCK file shouldn't be too large.");
+}
+} // namespace TestPCKPacker
+
+#endif // TEST_PCK_PACKER_H
diff --git a/tests/core/io/test_resource.h b/tests/core/io/test_resource.h
new file mode 100644
index 0000000000..b3983bb06d
--- /dev/null
+++ b/tests/core/io/test_resource.h
@@ -0,0 +1,114 @@
+/*************************************************************************/
+/* test_resource.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_RESOURCE
+#define TEST_RESOURCE
+
+#include "core/io/resource.h"
+#include "core/io/resource_loader.h"
+#include "core/io/resource_saver.h"
+#include "core/os/os.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestResource {
+
+TEST_CASE("[Resource] Duplication") {
+ Ref<Resource> resource = memnew(Resource);
+ resource->set_name("Hello world");
+ Ref<Resource> child_resource = memnew(Resource);
+ child_resource->set_name("I'm a child resource");
+ resource->set_meta("other_resource", child_resource);
+
+ Ref<Resource> resource_dupe = resource->duplicate();
+ const Ref<Resource> &resource_dupe_reference = resource_dupe;
+ resource_dupe->set_name("Changed name");
+ child_resource->set_name("My name was changed too");
+
+ CHECK_MESSAGE(
+ resource_dupe->get_name() == "Changed name",
+ "Duplicated resource should have the new name.");
+ CHECK_MESSAGE(
+ resource_dupe_reference->get_name() == "Changed name",
+ "Reference to the duplicated resource should have the new name.");
+ CHECK_MESSAGE(
+ resource->get_name() == "Hello world",
+ "Original resource name should not be affected after editing the duplicate's name.");
+ CHECK_MESSAGE(
+ Ref<Resource>(resource_dupe->get_meta("other_resource"))->get_name() == "My name was changed too",
+ "Duplicated resource should share its child resource with the original.");
+}
+
+TEST_CASE("[Resource] Saving and loading") {
+ Ref<Resource> resource = memnew(Resource);
+ resource->set_name("Hello world");
+ resource->set_meta(" ExampleMetadata ", Vector2i(40, 80));
+ resource->set_meta("string", "The\nstring\nwith\nunnecessary\nline\n\t\\\nbreaks");
+ Ref<Resource> child_resource = memnew(Resource);
+ child_resource->set_name("I'm a child resource");
+ resource->set_meta("other_resource", child_resource);
+ const String save_path_binary = OS::get_singleton()->get_cache_path().plus_file("resource.res");
+ const String save_path_text = OS::get_singleton()->get_cache_path().plus_file("resource.tres");
+ ResourceSaver::save(save_path_binary, resource);
+ ResourceSaver::save(save_path_text, resource);
+
+ const Ref<Resource> &loaded_resource_binary = ResourceLoader::load(save_path_binary);
+ CHECK_MESSAGE(
+ loaded_resource_binary->get_name() == "Hello world",
+ "The loaded resource name should be equal to the expected value.");
+ CHECK_MESSAGE(
+ loaded_resource_binary->get_meta(" ExampleMetadata ") == Vector2i(40, 80),
+ "The loaded resource metadata should be equal to the expected value.");
+ CHECK_MESSAGE(
+ loaded_resource_binary->get_meta("string") == "The\nstring\nwith\nunnecessary\nline\n\t\\\nbreaks",
+ "The loaded resource metadata should be equal to the expected value.");
+ const Ref<Resource> &loaded_child_resource_binary = loaded_resource_binary->get_meta("other_resource");
+ CHECK_MESSAGE(
+ loaded_child_resource_binary->get_name() == "I'm a child resource",
+ "The loaded child resource name should be equal to the expected value.");
+
+ const Ref<Resource> &loaded_resource_text = ResourceLoader::load(save_path_text);
+ CHECK_MESSAGE(
+ loaded_resource_text->get_name() == "Hello world",
+ "The loaded resource name should be equal to the expected value.");
+ CHECK_MESSAGE(
+ loaded_resource_text->get_meta(" ExampleMetadata ") == Vector2i(40, 80),
+ "The loaded resource metadata should be equal to the expected value.");
+ CHECK_MESSAGE(
+ loaded_resource_text->get_meta("string") == "The\nstring\nwith\nunnecessary\nline\n\t\\\nbreaks",
+ "The loaded resource metadata should be equal to the expected value.");
+ const Ref<Resource> &loaded_child_resource_text = loaded_resource_text->get_meta("other_resource");
+ CHECK_MESSAGE(
+ loaded_child_resource_text->get_name() == "I'm a child resource",
+ "The loaded child resource name should be equal to the expected value.");
+}
+} // namespace TestResource
+
+#endif // TEST_RESOURCE
diff --git a/tests/core/io/test_xml_parser.h b/tests/core/io/test_xml_parser.h
new file mode 100644
index 0000000000..87592b56ce
--- /dev/null
+++ b/tests/core/io/test_xml_parser.h
@@ -0,0 +1,71 @@
+/*************************************************************************/
+/* test_xml_parser.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_XML_PARSER_H
+#define TEST_XML_PARSER_H
+
+#include "core/io/xml_parser.h"
+
+#include "tests/test_macros.h"
+
+namespace TestXMLParser {
+TEST_CASE("[XMLParser] End-to-end") {
+ String source = "<?xml version = \"1.0\" encoding=\"UTF-8\" ?>\
+<top attr=\"attr value\">\
+ Text&lt;&#65;&#x42;&gt;\
+</top>";
+ Vector<uint8_t> buff = source.to_utf8_buffer();
+
+ XMLParser parser;
+ parser.open_buffer(buff);
+
+ // <?xml ...?> gets parsed as NODE_UNKNOWN
+ CHECK(parser.read() == OK);
+ CHECK(parser.get_node_type() == XMLParser::NodeType::NODE_UNKNOWN);
+
+ CHECK(parser.read() == OK);
+ CHECK(parser.get_node_type() == XMLParser::NodeType::NODE_ELEMENT);
+ CHECK(parser.get_node_name() == "top");
+ CHECK(parser.has_attribute("attr"));
+ CHECK(parser.get_attribute_value("attr") == "attr value");
+
+ CHECK(parser.read() == OK);
+ CHECK(parser.get_node_type() == XMLParser::NodeType::NODE_TEXT);
+ CHECK(parser.get_node_data().lstrip(" \t") == "Text<AB>");
+
+ CHECK(parser.read() == OK);
+ CHECK(parser.get_node_type() == XMLParser::NodeType::NODE_ELEMENT_END);
+ CHECK(parser.get_node_name() == "top");
+
+ parser.close();
+}
+} // namespace TestXMLParser
+
+#endif // TEST_XML_PARSER_H
diff --git a/tests/core/math/test_aabb.h b/tests/core/math/test_aabb.h
new file mode 100644
index 0000000000..526972a82f
--- /dev/null
+++ b/tests/core/math/test_aabb.h
@@ -0,0 +1,395 @@
+/*************************************************************************/
+/* test_aabb.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_AABB_H
+#define TEST_AABB_H
+
+#include "core/math/aabb.h"
+
+#include "tests/test_macros.h"
+
+namespace TestAABB {
+
+TEST_CASE("[AABB] Constructor methods") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ const AABB aabb_copy = AABB(aabb);
+
+ CHECK_MESSAGE(
+ aabb == aabb_copy,
+ "AABBs created with the same dimensions but by different methods should be equal.");
+}
+
+TEST_CASE("[AABB] String conversion") {
+ CHECK_MESSAGE(
+ String(AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6))) == "[P: (-1.5, 2, -2.5), S: (4, 5, 6)]",
+ "The string representation should match the expected value.");
+}
+
+TEST_CASE("[AABB] Basic getters") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.get_position().is_equal_approx(Vector3(-1.5, 2, -2.5)),
+ "get_position() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_size().is_equal_approx(Vector3(4, 5, 6)),
+ "get_size() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_end().is_equal_approx(Vector3(2.5, 7, 3.5)),
+ "get_end() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_center().is_equal_approx(Vector3(0.5, 4.5, 0.5)),
+ "get_center() should return the expected value.");
+}
+
+TEST_CASE("[AABB] Basic setters") {
+ AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ aabb.set_end(Vector3(100, 0, 100));
+ CHECK_MESSAGE(
+ aabb.is_equal_approx(AABB(Vector3(-1.5, 2, -2.5), Vector3(101.5, -2, 102.5))),
+ "set_end() should result in the expected AABB.");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ aabb.set_position(Vector3(-1000, -2000, -3000));
+ CHECK_MESSAGE(
+ aabb.is_equal_approx(AABB(Vector3(-1000, -2000, -3000), Vector3(4, 5, 6))),
+ "set_position() should result in the expected AABB.");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ aabb.set_size(Vector3(0, 0, -50));
+ CHECK_MESSAGE(
+ aabb.is_equal_approx(AABB(Vector3(-1.5, 2, -2.5), Vector3(0, 0, -50))),
+ "set_size() should result in the expected AABB.");
+}
+
+TEST_CASE("[AABB] Volume getters") {
+ AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ Math::is_equal_approx(aabb.get_volume(), 120),
+ "get_volume() should return the expected value with positive size.");
+ CHECK_MESSAGE(
+ !aabb.has_no_volume(),
+ "Non-empty volumetric AABB should have a volume.");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(-4, 5, 6));
+ CHECK_MESSAGE(
+ Math::is_equal_approx(aabb.get_volume(), -120),
+ "get_volume() should return the expected value with negative size (1 component).");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(-4, -5, 6));
+ CHECK_MESSAGE(
+ Math::is_equal_approx(aabb.get_volume(), 120),
+ "get_volume() should return the expected value with negative size (2 components).");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(-4, -5, -6));
+ CHECK_MESSAGE(
+ Math::is_equal_approx(aabb.get_volume(), -120),
+ "get_volume() should return the expected value with negative size (3 components).");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 0, 6));
+ CHECK_MESSAGE(
+ aabb.has_no_volume(),
+ "Non-empty flat AABB should not have a volume.");
+
+ CHECK_MESSAGE(
+ AABB().has_no_volume(),
+ "Empty AABB should not have a volume.");
+}
+
+TEST_CASE("[AABB] Surface getters") {
+ AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ !aabb.has_no_surface(),
+ "Non-empty volumetric AABB should have an surface.");
+
+ aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 0, 6));
+ CHECK_MESSAGE(
+ !aabb.has_no_surface(),
+ "Non-empty flat AABB should have a surface.");
+
+ CHECK_MESSAGE(
+ AABB().has_no_surface(),
+ "Empty AABB should not have an surface.");
+}
+
+TEST_CASE("[AABB] Intersection") {
+ const AABB aabb_big = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+
+ AABB aabb_small = AABB(Vector3(-1.5, 2, -2.5), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.intersects(aabb_small),
+ "intersects() with fully contained AABB (touching the edge) should return the expected result.");
+
+ aabb_small = AABB(Vector3(0.5, 1.5, -2), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.intersects(aabb_small),
+ "intersects() with partially contained AABB (overflowing on Y axis) should return the expected result.");
+
+ aabb_small = AABB(Vector3(10, -10, -10), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ !aabb_big.intersects(aabb_small),
+ "intersects() with non-contained AABB should return the expected result.");
+
+ aabb_small = AABB(Vector3(-1.5, 2, -2.5), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.intersection(aabb_small).is_equal_approx(aabb_small),
+ "intersection() with fully contained AABB (touching the edge) should return the expected result.");
+
+ aabb_small = AABB(Vector3(0.5, 1.5, -2), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.intersection(aabb_small).is_equal_approx(AABB(Vector3(0.5, 2, -2), Vector3(1, 0.5, 1))),
+ "intersection() with partially contained AABB (overflowing on Y axis) should return the expected result.");
+
+ aabb_small = AABB(Vector3(10, -10, -10), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.intersection(aabb_small).is_equal_approx(AABB()),
+ "intersection() with non-contained AABB should return the expected result.");
+
+ CHECK_MESSAGE(
+ aabb_big.intersects_plane(Plane(Vector3(0, 1, 0), 4)),
+ "intersects_plane() should return the expected result.");
+ CHECK_MESSAGE(
+ aabb_big.intersects_plane(Plane(Vector3(0, -1, 0), -4)),
+ "intersects_plane() should return the expected result.");
+ CHECK_MESSAGE(
+ !aabb_big.intersects_plane(Plane(Vector3(0, 1, 0), 200)),
+ "intersects_plane() should return the expected result.");
+
+ CHECK_MESSAGE(
+ aabb_big.intersects_segment(Vector3(1, 3, 0), Vector3(0, 3, 0)),
+ "intersects_segment() should return the expected result.");
+ CHECK_MESSAGE(
+ aabb_big.intersects_segment(Vector3(0, 3, 0), Vector3(0, -300, 0)),
+ "intersects_segment() should return the expected result.");
+ CHECK_MESSAGE(
+ aabb_big.intersects_segment(Vector3(-50, 3, -50), Vector3(50, 3, 50)),
+ "intersects_segment() should return the expected result.");
+ CHECK_MESSAGE(
+ !aabb_big.intersects_segment(Vector3(-50, 25, -50), Vector3(50, 25, 50)),
+ "intersects_segment() should return the expected result.");
+ CHECK_MESSAGE(
+ aabb_big.intersects_segment(Vector3(0, 3, 0), Vector3(0, 3, 0)),
+ "intersects_segment() should return the expected result with segment of length 0.");
+ CHECK_MESSAGE(
+ !aabb_big.intersects_segment(Vector3(0, 300, 0), Vector3(0, 300, 0)),
+ "intersects_segment() should return the expected result with segment of length 0.");
+}
+
+TEST_CASE("[AABB] Merging") {
+ const AABB aabb_big = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+
+ AABB aabb_small = AABB(Vector3(-1.5, 2, -2.5), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.merge(aabb_small).is_equal_approx(aabb_big),
+ "merge() with fully contained AABB (touching the edge) should return the expected result.");
+
+ aabb_small = AABB(Vector3(0.5, 1.5, -2), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.merge(aabb_small).is_equal_approx(AABB(Vector3(-1.5, 1.5, -2.5), Vector3(4, 5.5, 6))),
+ "merge() with partially contained AABB (overflowing on Y axis) should return the expected result.");
+
+ aabb_small = AABB(Vector3(10, -10, -10), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.merge(aabb_small).is_equal_approx(AABB(Vector3(-1.5, -10, -10), Vector3(12.5, 17, 13.5))),
+ "merge() with non-contained AABB should return the expected result.");
+}
+
+TEST_CASE("[AABB] Encloses") {
+ const AABB aabb_big = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+
+ AABB aabb_small = AABB(Vector3(-1.5, 2, -2.5), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ aabb_big.encloses(aabb_small),
+ "encloses() with fully contained AABB (touching the edge) should return the expected result.");
+
+ aabb_small = AABB(Vector3(0.5, 1.5, -2), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ !aabb_big.encloses(aabb_small),
+ "encloses() with partially contained AABB (overflowing on Y axis) should return the expected result.");
+
+ aabb_small = AABB(Vector3(10, -10, -10), Vector3(1, 1, 1));
+ CHECK_MESSAGE(
+ !aabb_big.encloses(aabb_small),
+ "encloses() with non-contained AABB should return the expected result.");
+}
+
+TEST_CASE("[AABB] Get endpoints") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.get_endpoint(0).is_equal_approx(Vector3(-1.5, 2, -2.5)),
+ "The endpoint at index 0 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(1).is_equal_approx(Vector3(-1.5, 2, 3.5)),
+ "The endpoint at index 1 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(2).is_equal_approx(Vector3(-1.5, 7, -2.5)),
+ "The endpoint at index 2 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(3).is_equal_approx(Vector3(-1.5, 7, 3.5)),
+ "The endpoint at index 3 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(4).is_equal_approx(Vector3(2.5, 2, -2.5)),
+ "The endpoint at index 4 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(5).is_equal_approx(Vector3(2.5, 2, 3.5)),
+ "The endpoint at index 5 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(6).is_equal_approx(Vector3(2.5, 7, -2.5)),
+ "The endpoint at index 6 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(7).is_equal_approx(Vector3(2.5, 7, 3.5)),
+ "The endpoint at index 7 should match the expected value.");
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ aabb.get_endpoint(8).is_equal_approx(Vector3()),
+ "The endpoint at invalid index 8 should match the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_endpoint(-1).is_equal_approx(Vector3()),
+ "The endpoint at invalid index -1 should match the expected value.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[AABB] Get longest/shortest axis") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.get_longest_axis() == Vector3(0, 0, 1),
+ "get_longest_axis() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_longest_axis_index() == Vector3::AXIS_Z,
+ "get_longest_axis_index() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_longest_axis_size() == 6,
+ "get_longest_axis_size() should return the expected value.");
+
+ CHECK_MESSAGE(
+ aabb.get_shortest_axis() == Vector3(1, 0, 0),
+ "get_shortest_axis() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_shortest_axis_index() == Vector3::AXIS_X,
+ "get_shortest_axis_index() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_shortest_axis_size() == 4,
+ "get_shortest_axis_size() should return the expected value.");
+}
+
+#ifndef _MSC_VER
+#warning Support tests need to be re-done
+#endif
+
+/* Support function was actually broken. As it was fixed, the tests now fail. Tests need to be re-done.
+
+TEST_CASE("[AABB] Get support") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.get_support(Vector3(1, 0, 0)).is_equal_approx(Vector3(-1.5, 7, 3.5)),
+ "get_support() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_support(Vector3(0.5, 1, 0)).is_equal_approx(Vector3(-1.5, 2, 3.5)),
+ "get_support() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_support(Vector3(0.5, 1, -400)).is_equal_approx(Vector3(-1.5, 2, 3.5)),
+ "get_support() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_support(Vector3(0, -1, 0)).is_equal_approx(Vector3(2.5, 7, 3.5)),
+ "get_support() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_support(Vector3(0, -0.1, 0)).is_equal_approx(Vector3(2.5, 7, 3.5)),
+ "get_support() should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.get_support(Vector3()).is_equal_approx(Vector3(2.5, 7, 3.5)),
+ "get_support() should return the expected value with a null vector.");
+}
+*/
+TEST_CASE("[AABB] Grow") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.grow(0.25).is_equal_approx(AABB(Vector3(-1.75, 1.75, -2.75), Vector3(4.5, 5.5, 6.5))),
+ "grow() with positive value should return the expected AABB.");
+ CHECK_MESSAGE(
+ aabb.grow(-0.25).is_equal_approx(AABB(Vector3(-1.25, 2.25, -2.25), Vector3(3.5, 4.5, 5.5))),
+ "grow() with negative value should return the expected AABB.");
+ CHECK_MESSAGE(
+ aabb.grow(-10).is_equal_approx(AABB(Vector3(8.5, 12, 7.5), Vector3(-16, -15, -14))),
+ "grow() with large negative value should return the expected AABB.");
+}
+
+TEST_CASE("[AABB] Has point") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(-1, 3, 0)),
+ "has_point() with contained point should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(2, 3, 0)),
+ "has_point() with contained point should return the expected value.");
+ CHECK_MESSAGE(
+ !aabb.has_point(Vector3(-20, 0, 0)),
+ "has_point() with non-contained point should return the expected value.");
+
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(-1.5, 3, 0)),
+ "has_point() with positive size should include point on near face (X axis).");
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(2.5, 3, 0)),
+ "has_point() with positive size should include point on far face (X axis).");
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(0, 2, 0)),
+ "has_point() with positive size should include point on near face (Y axis).");
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(0, 7, 0)),
+ "has_point() with positive size should include point on far face (Y axis).");
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(0, 3, -2.5)),
+ "has_point() with positive size should include point on near face (Z axis).");
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(0, 3, 3.5)),
+ "has_point() with positive size should include point on far face (Z axis).");
+}
+
+TEST_CASE("[AABB] Expanding") {
+ const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
+ CHECK_MESSAGE(
+ aabb.expand(Vector3(-1, 3, 0)).is_equal_approx(aabb),
+ "expand() with contained point should return the expected AABB.");
+ CHECK_MESSAGE(
+ aabb.expand(Vector3(2, 3, 0)).is_equal_approx(aabb),
+ "expand() with contained point should return the expected AABB.");
+ CHECK_MESSAGE(
+ aabb.expand(Vector3(-1.5, 3, 0)).is_equal_approx(aabb),
+ "expand() with contained point on negative edge should return the expected AABB.");
+ CHECK_MESSAGE(
+ aabb.expand(Vector3(2.5, 3, 0)).is_equal_approx(aabb),
+ "expand() with contained point on positive edge should return the expected AABB.");
+ CHECK_MESSAGE(
+ aabb.expand(Vector3(-20, 0, 0)).is_equal_approx(AABB(Vector3(-20, 0, -2.5), Vector3(22.5, 7, 6))),
+ "expand() with non-contained point should return the expected AABB.");
+}
+} // namespace TestAABB
+
+#endif // TEST_AABB_H
diff --git a/tests/core/math/test_astar.h b/tests/core/math/test_astar.h
new file mode 100644
index 0000000000..859172dca3
--- /dev/null
+++ b/tests/core/math/test_astar.h
@@ -0,0 +1,362 @@
+/*************************************************************************/
+/* test_astar.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_ASTAR_H
+#define TEST_ASTAR_H
+
+#include "core/math/a_star.h"
+
+#include "tests/test_macros.h"
+
+namespace TestAStar {
+
+class ABCX : public AStar {
+public:
+ enum {
+ A,
+ B,
+ C,
+ X,
+ };
+
+ ABCX() {
+ add_point(A, Vector3(0, 0, 0));
+ add_point(B, Vector3(1, 0, 0));
+ add_point(C, Vector3(0, 1, 0));
+ add_point(X, Vector3(0, 0, 1));
+ connect_points(A, B);
+ connect_points(A, C);
+ connect_points(B, C);
+ connect_points(X, A);
+ }
+
+ // Disable heuristic completely.
+ real_t _compute_cost(int p_from, int p_to) {
+ if (p_from == A && p_to == C) {
+ return 1000;
+ }
+ return 100;
+ }
+};
+
+TEST_CASE("[AStar] ABC path") {
+ ABCX abcx;
+ Vector<int> path = abcx.get_id_path(ABCX::A, ABCX::C);
+ REQUIRE(path.size() == 3);
+ CHECK(path[0] == ABCX::A);
+ CHECK(path[1] == ABCX::B);
+ CHECK(path[2] == ABCX::C);
+}
+
+TEST_CASE("[AStar] ABCX path") {
+ ABCX abcx;
+ Vector<int> path = abcx.get_id_path(ABCX::X, ABCX::C);
+ REQUIRE(path.size() == 4);
+ CHECK(path[0] == ABCX::X);
+ CHECK(path[1] == ABCX::A);
+ CHECK(path[2] == ABCX::B);
+ CHECK(path[3] == ABCX::C);
+}
+
+TEST_CASE("[AStar] Add/Remove") {
+ AStar a;
+
+ // Manual tests.
+ a.add_point(1, Vector3(0, 0, 0));
+ a.add_point(2, Vector3(0, 1, 0));
+ a.add_point(3, Vector3(1, 1, 0));
+ a.add_point(4, Vector3(2, 0, 0));
+ a.connect_points(1, 2, true);
+ a.connect_points(1, 3, true);
+ a.connect_points(1, 4, false);
+
+ CHECK(a.are_points_connected(2, 1));
+ CHECK(a.are_points_connected(4, 1));
+ CHECK(a.are_points_connected(2, 1, false));
+ CHECK_FALSE(a.are_points_connected(4, 1, false));
+
+ a.disconnect_points(1, 2, true);
+ CHECK(a.get_point_connections(1).size() == 2); // 3, 4
+ CHECK(a.get_point_connections(2).size() == 0);
+
+ a.disconnect_points(4, 1, false);
+ CHECK(a.get_point_connections(1).size() == 2); // 3, 4
+ CHECK(a.get_point_connections(4).size() == 0);
+
+ a.disconnect_points(4, 1, true);
+ CHECK(a.get_point_connections(1).size() == 1); // 3
+ CHECK(a.get_point_connections(4).size() == 0);
+
+ a.connect_points(2, 3, false);
+ CHECK(a.get_point_connections(2).size() == 1); // 3
+ CHECK(a.get_point_connections(3).size() == 1); // 1
+
+ a.connect_points(2, 3, true);
+ CHECK(a.get_point_connections(2).size() == 1); // 3
+ CHECK(a.get_point_connections(3).size() == 2); // 1, 2
+
+ a.disconnect_points(2, 3, false);
+ CHECK(a.get_point_connections(2).size() == 0);
+ CHECK(a.get_point_connections(3).size() == 2); // 1, 2
+
+ a.connect_points(4, 3, true);
+ CHECK(a.get_point_connections(3).size() == 3); // 1, 2, 4
+ CHECK(a.get_point_connections(4).size() == 1); // 3
+
+ a.disconnect_points(3, 4, false);
+ CHECK(a.get_point_connections(3).size() == 2); // 1, 2
+ CHECK(a.get_point_connections(4).size() == 1); // 3
+
+ a.remove_point(3);
+ CHECK(a.get_point_connections(1).size() == 0);
+ CHECK(a.get_point_connections(2).size() == 0);
+ CHECK(a.get_point_connections(4).size() == 0);
+
+ a.add_point(0, Vector3(0, -1, 0));
+ a.add_point(3, Vector3(2, 1, 0));
+ // 0: (0, -1)
+ // 1: (0, 0)
+ // 2: (0, 1)
+ // 3: (2, 1)
+ // 4: (2, 0)
+
+ // Tests for get_closest_position_in_segment.
+ a.connect_points(2, 3);
+ CHECK(a.get_closest_position_in_segment(Vector3(0.5, 0.5, 0)) == Vector3(0.5, 1, 0));
+
+ a.connect_points(3, 4);
+ a.connect_points(0, 3);
+ a.connect_points(1, 4);
+ a.disconnect_points(1, 4, false);
+ a.disconnect_points(4, 3, false);
+ a.disconnect_points(3, 4, false);
+ // Remaining edges: <2, 3>, <0, 3>, <1, 4> (directed).
+ CHECK(a.get_closest_position_in_segment(Vector3(2, 0.5, 0)) == Vector3(1.75, 0.75, 0));
+ CHECK(a.get_closest_position_in_segment(Vector3(-1, 0.2, 0)) == Vector3(0, 0, 0));
+ CHECK(a.get_closest_position_in_segment(Vector3(3, 2, 0)) == Vector3(2, 1, 0));
+
+ Math::seed(0);
+
+ // Random tests for connectivity checks
+ for (int i = 0; i < 20000; i++) {
+ int u = Math::rand() % 5;
+ int v = Math::rand() % 4;
+ if (u == v) {
+ v = 4;
+ }
+ if (Math::rand() % 2 == 1) {
+ // Add a (possibly existing) directed edge and confirm connectivity.
+ a.connect_points(u, v, false);
+ CHECK(a.are_points_connected(u, v, false));
+ } else {
+ // Remove a (possibly nonexistent) directed edge and confirm disconnectivity.
+ a.disconnect_points(u, v, false);
+ CHECK_FALSE(a.are_points_connected(u, v, false));
+ }
+ }
+
+ // Random tests for point removal.
+ for (int i = 0; i < 20000; i++) {
+ a.clear();
+ for (int j = 0; j < 5; j++) {
+ a.add_point(j, Vector3(0, 0, 0));
+ }
+
+ // Add or remove random edges.
+ for (int j = 0; j < 10; j++) {
+ int u = Math::rand() % 5;
+ int v = Math::rand() % 4;
+ if (u == v) {
+ v = 4;
+ }
+ if (Math::rand() % 2 == 1) {
+ a.connect_points(u, v, false);
+ } else {
+ a.disconnect_points(u, v, false);
+ }
+ }
+
+ // Remove point 0.
+ a.remove_point(0);
+ // White box: this will check all edges remaining in the segments set.
+ for (int j = 1; j < 5; j++) {
+ CHECK_FALSE(a.are_points_connected(0, j, true));
+ }
+ }
+ // It's been great work, cheers. \(^ ^)/
+}
+
+TEST_CASE("[Stress][AStar] Find paths") {
+ // Random stress tests with Floyd-Warshall.
+ const int N = 30;
+ Math::seed(0);
+
+ for (int test = 0; test < 1000; test++) {
+ AStar a;
+ Vector3 p[N];
+ bool adj[N][N] = { { false } };
+
+ // Assign initial coordinates.
+ for (int u = 0; u < N; u++) {
+ p[u].x = Math::rand() % 100;
+ p[u].y = Math::rand() % 100;
+ p[u].z = Math::rand() % 100;
+ a.add_point(u, p[u]);
+ }
+ // Generate a random sequence of operations.
+ for (int i = 0; i < 1000; i++) {
+ // Pick two different vertices.
+ int u, v;
+ u = Math::rand() % N;
+ v = Math::rand() % (N - 1);
+ if (u == v) {
+ v = N - 1;
+ }
+ // Pick a random operation.
+ int op = Math::rand();
+ switch (op % 9) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ // Add edge (u, v); possibly bidirectional.
+ a.connect_points(u, v, op % 2);
+ adj[u][v] = true;
+ if (op % 2) {
+ adj[v][u] = true;
+ }
+ break;
+ case 6:
+ case 7:
+ // Remove edge (u, v); possibly bidirectional.
+ a.disconnect_points(u, v, op % 2);
+ adj[u][v] = false;
+ if (op % 2) {
+ adj[v][u] = false;
+ }
+ break;
+ case 8:
+ // Remove point u and add it back; clears adjacent edges and changes coordinates.
+ a.remove_point(u);
+ p[u].x = Math::rand() % 100;
+ p[u].y = Math::rand() % 100;
+ p[u].z = Math::rand() % 100;
+ a.add_point(u, p[u]);
+ for (v = 0; v < N; v++) {
+ adj[u][v] = adj[v][u] = false;
+ }
+ break;
+ }
+ }
+ // Floyd-Warshall.
+ float d[N][N];
+ for (int u = 0; u < N; u++) {
+ for (int v = 0; v < N; v++) {
+ d[u][v] = (u == v || adj[u][v]) ? p[u].distance_to(p[v]) : INFINITY;
+ }
+ }
+ for (int w = 0; w < N; w++) {
+ for (int u = 0; u < N; u++) {
+ for (int v = 0; v < N; v++) {
+ if (d[u][v] > d[u][w] + d[w][v]) {
+ d[u][v] = d[u][w] + d[w][v];
+ }
+ }
+ }
+ }
+ // Display statistics.
+ int count = 0;
+ for (int u = 0; u < N; u++) {
+ for (int v = 0; v < N; v++) {
+ if (adj[u][v]) {
+ count++;
+ }
+ }
+ }
+ print_verbose(vformat("Test #%4d: %3d edges, ", test + 1, count));
+ count = 0;
+ for (int u = 0; u < N; u++) {
+ for (int v = 0; v < N; v++) {
+ if (!Math::is_inf(d[u][v])) {
+ count++;
+ }
+ }
+ }
+ print_verbose(vformat("%3d/%d pairs of reachable points\n", count - N, N * (N - 1)));
+
+ // Check A*'s output.
+ bool match = true;
+ for (int u = 0; u < N; u++) {
+ for (int v = 0; v < N; v++) {
+ if (u != v) {
+ Vector<int> route = a.get_id_path(u, v);
+ if (!Math::is_inf(d[u][v])) {
+ // Reachable.
+ if (route.size() == 0) {
+ print_verbose(vformat("From %d to %d: A* did not find a path\n", u, v));
+ match = false;
+ goto exit;
+ }
+ float astar_dist = 0;
+ for (int i = 1; i < route.size(); i++) {
+ if (!adj[route[i - 1]][route[i]]) {
+ print_verbose(vformat("From %d to %d: edge (%d, %d) does not exist\n",
+ u, v, route[i - 1], route[i]));
+ match = false;
+ goto exit;
+ }
+ astar_dist += p[route[i - 1]].distance_to(p[route[i]]);
+ }
+ if (!Math::is_equal_approx(astar_dist, d[u][v])) {
+ print_verbose(vformat("From %d to %d: Floyd-Warshall gives %.6f, A* gives %.6f\n",
+ u, v, d[u][v], astar_dist));
+ match = false;
+ goto exit;
+ }
+ } else {
+ // Unreachable.
+ if (route.size() > 0) {
+ print_verbose(vformat("From %d to %d: A* somehow found a nonexistent path\n", u, v));
+ match = false;
+ goto exit;
+ }
+ }
+ }
+ }
+ }
+ exit:
+ CHECK_MESSAGE(match, "Found all paths.");
+ }
+}
+} // namespace TestAStar
+
+#endif // TEST_ASTAR_H
diff --git a/tests/core/math/test_basis.h b/tests/core/math/test_basis.h
new file mode 100644
index 0000000000..257e41e82c
--- /dev/null
+++ b/tests/core/math/test_basis.h
@@ -0,0 +1,286 @@
+/*************************************************************************/
+/* test_basis.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_BASIS_H
+#define TEST_BASIS_H
+
+#include "core/math/basis.h"
+#include "core/math/random_number_generator.h"
+
+#include "tests/test_macros.h"
+
+namespace TestBasis {
+
+enum RotOrder {
+ EulerXYZ,
+ EulerXZY,
+ EulerYZX,
+ EulerYXZ,
+ EulerZXY,
+ EulerZYX
+};
+
+Vector3 deg2rad(const Vector3 &p_rotation) {
+ return p_rotation / 180.0 * Math_PI;
+}
+
+Vector3 rad2deg(const Vector3 &p_rotation) {
+ return p_rotation / Math_PI * 180.0;
+}
+
+Basis EulerToBasis(RotOrder mode, const Vector3 &p_rotation) {
+ Basis ret;
+ switch (mode) {
+ case EulerXYZ:
+ ret.set_euler(p_rotation, Basis::EULER_ORDER_XYZ);
+ break;
+
+ case EulerXZY:
+ ret.set_euler(p_rotation, Basis::EULER_ORDER_XZY);
+ break;
+
+ case EulerYZX:
+ ret.set_euler(p_rotation, Basis::EULER_ORDER_YZX);
+ break;
+
+ case EulerYXZ:
+ ret.set_euler(p_rotation, Basis::EULER_ORDER_YXZ);
+ break;
+
+ case EulerZXY:
+ ret.set_euler(p_rotation, Basis::EULER_ORDER_ZXY);
+ break;
+
+ case EulerZYX:
+ ret.set_euler(p_rotation, Basis::EULER_ORDER_ZYX);
+ break;
+
+ default:
+ // If you land here, Please integrate all rotation orders.
+ FAIL("This is not unreachable.");
+ }
+
+ return ret;
+}
+
+Vector3 BasisToEuler(RotOrder mode, const Basis &p_rotation) {
+ switch (mode) {
+ case EulerXYZ:
+ return p_rotation.get_euler(Basis::EULER_ORDER_XYZ);
+
+ case EulerXZY:
+ return p_rotation.get_euler(Basis::EULER_ORDER_XZY);
+
+ case EulerYZX:
+ return p_rotation.get_euler(Basis::EULER_ORDER_YZX);
+
+ case EulerYXZ:
+ return p_rotation.get_euler(Basis::EULER_ORDER_YXZ);
+
+ case EulerZXY:
+ return p_rotation.get_euler(Basis::EULER_ORDER_ZXY);
+
+ case EulerZYX:
+ return p_rotation.get_euler(Basis::EULER_ORDER_ZYX);
+
+ default:
+ // If you land here, Please integrate all rotation orders.
+ FAIL("This is not unreachable.");
+ return Vector3();
+ }
+}
+
+String get_rot_order_name(RotOrder ro) {
+ switch (ro) {
+ case EulerXYZ:
+ return "XYZ";
+ case EulerXZY:
+ return "XZY";
+ case EulerYZX:
+ return "YZX";
+ case EulerYXZ:
+ return "YXZ";
+ case EulerZXY:
+ return "ZXY";
+ case EulerZYX:
+ return "ZYX";
+ default:
+ return "[Not supported]";
+ }
+}
+
+void test_rotation(Vector3 deg_original_euler, RotOrder rot_order) {
+ // This test:
+ // 1. Converts the rotation vector from deg to rad.
+ // 2. Converts euler to basis.
+ // 3. Converts the above basis back into euler.
+ // 4. Converts the above euler into basis again.
+ // 5. Compares the basis obtained in step 2 with the basis of step 4
+ //
+ // The conversion "basis to euler", done in the step 3, may be different from
+ // the original euler, even if the final rotation are the same.
+ // This happens because there are more ways to represents the same rotation,
+ // both valid, using eulers.
+ // For this reason is necessary to convert that euler back to basis and finally
+ // compares it.
+ //
+ // In this way we can assert that both functions: basis to euler / euler to basis
+ // are correct.
+
+ // Euler to rotation
+ const Vector3 original_euler = deg2rad(deg_original_euler);
+ const Basis to_rotation = EulerToBasis(rot_order, original_euler);
+
+ // Euler from rotation
+ const Vector3 euler_from_rotation = BasisToEuler(rot_order, to_rotation);
+ const Basis rotation_from_computed_euler = EulerToBasis(rot_order, euler_from_rotation);
+
+ Basis res = to_rotation.inverse() * rotation_from_computed_euler;
+
+ CHECK_MESSAGE((res.get_axis(0) - Vector3(1.0, 0.0, 0.0)).length() <= 0.1, vformat("Fail due to X %s\n", String(res.get_axis(0))).utf8().ptr());
+ CHECK_MESSAGE((res.get_axis(1) - Vector3(0.0, 1.0, 0.0)).length() <= 0.1, vformat("Fail due to Y %s\n", String(res.get_axis(1))).utf8().ptr());
+ CHECK_MESSAGE((res.get_axis(2) - Vector3(0.0, 0.0, 1.0)).length() <= 0.1, vformat("Fail due to Z %s\n", String(res.get_axis(2))).utf8().ptr());
+
+ // Double check `to_rotation` decomposing with XYZ rotation order.
+ const Vector3 euler_xyz_from_rotation = to_rotation.get_euler(Basis::EULER_ORDER_XYZ);
+ Basis rotation_from_xyz_computed_euler;
+ rotation_from_xyz_computed_euler.set_euler(euler_xyz_from_rotation, Basis::EULER_ORDER_XYZ);
+
+ res = to_rotation.inverse() * rotation_from_xyz_computed_euler;
+
+ CHECK_MESSAGE((res.get_axis(0) - Vector3(1.0, 0.0, 0.0)).length() <= 0.1, vformat("Double check with XYZ rot order failed, due to X %s\n", String(res.get_axis(0))).utf8().ptr());
+ CHECK_MESSAGE((res.get_axis(1) - Vector3(0.0, 1.0, 0.0)).length() <= 0.1, vformat("Double check with XYZ rot order failed, due to Y %s\n", String(res.get_axis(1))).utf8().ptr());
+ CHECK_MESSAGE((res.get_axis(2) - Vector3(0.0, 0.0, 1.0)).length() <= 0.1, vformat("Double check with XYZ rot order failed, due to Z %s\n", String(res.get_axis(2))).utf8().ptr());
+
+ INFO(vformat("Rotation order: %s\n.", get_rot_order_name(rot_order)).utf8().ptr());
+ INFO(vformat("Original Rotation: %s\n", String(deg_original_euler)).utf8().ptr());
+ INFO(vformat("Quaternion to rotation order: %s\n", String(rad2deg(euler_from_rotation))).utf8().ptr());
+}
+
+TEST_CASE("[Basis] Euler conversions") {
+ Vector<RotOrder> rotorder_to_test;
+ rotorder_to_test.push_back(EulerXYZ);
+ rotorder_to_test.push_back(EulerXZY);
+ rotorder_to_test.push_back(EulerYZX);
+ rotorder_to_test.push_back(EulerYXZ);
+ rotorder_to_test.push_back(EulerZXY);
+ rotorder_to_test.push_back(EulerZYX);
+
+ Vector<Vector3> vectors_to_test;
+
+ // Test the special cases.
+ vectors_to_test.push_back(Vector3(0.0, 0.0, 0.0));
+ vectors_to_test.push_back(Vector3(0.5, 0.5, 0.5));
+ vectors_to_test.push_back(Vector3(-0.5, -0.5, -0.5));
+ vectors_to_test.push_back(Vector3(40.0, 40.0, 40.0));
+ vectors_to_test.push_back(Vector3(-40.0, -40.0, -40.0));
+ vectors_to_test.push_back(Vector3(0.0, 0.0, -90.0));
+ vectors_to_test.push_back(Vector3(0.0, -90.0, 0.0));
+ vectors_to_test.push_back(Vector3(-90.0, 0.0, 0.0));
+ vectors_to_test.push_back(Vector3(0.0, 0.0, 90.0));
+ vectors_to_test.push_back(Vector3(0.0, 90.0, 0.0));
+ vectors_to_test.push_back(Vector3(90.0, 0.0, 0.0));
+ vectors_to_test.push_back(Vector3(0.0, 0.0, -30.0));
+ vectors_to_test.push_back(Vector3(0.0, -30.0, 0.0));
+ vectors_to_test.push_back(Vector3(-30.0, 0.0, 0.0));
+ vectors_to_test.push_back(Vector3(0.0, 0.0, 30.0));
+ vectors_to_test.push_back(Vector3(0.0, 30.0, 0.0));
+ vectors_to_test.push_back(Vector3(30.0, 0.0, 0.0));
+ vectors_to_test.push_back(Vector3(0.5, 50.0, 20.0));
+ vectors_to_test.push_back(Vector3(-0.5, -50.0, -20.0));
+ vectors_to_test.push_back(Vector3(0.5, 0.0, 90.0));
+ vectors_to_test.push_back(Vector3(0.5, 0.0, -90.0));
+ vectors_to_test.push_back(Vector3(360.0, 360.0, 360.0));
+ vectors_to_test.push_back(Vector3(-360.0, -360.0, -360.0));
+ vectors_to_test.push_back(Vector3(-90.0, 60.0, -90.0));
+ vectors_to_test.push_back(Vector3(90.0, 60.0, -90.0));
+ vectors_to_test.push_back(Vector3(90.0, -60.0, -90.0));
+ vectors_to_test.push_back(Vector3(-90.0, -60.0, -90.0));
+ vectors_to_test.push_back(Vector3(-90.0, 60.0, 90.0));
+ vectors_to_test.push_back(Vector3(90.0, 60.0, 90.0));
+ vectors_to_test.push_back(Vector3(90.0, -60.0, 90.0));
+ vectors_to_test.push_back(Vector3(-90.0, -60.0, 90.0));
+ vectors_to_test.push_back(Vector3(60.0, 90.0, -40.0));
+ vectors_to_test.push_back(Vector3(60.0, -90.0, -40.0));
+ vectors_to_test.push_back(Vector3(-60.0, -90.0, -40.0));
+ vectors_to_test.push_back(Vector3(-60.0, 90.0, 40.0));
+ vectors_to_test.push_back(Vector3(60.0, 90.0, 40.0));
+ vectors_to_test.push_back(Vector3(60.0, -90.0, 40.0));
+ vectors_to_test.push_back(Vector3(-60.0, -90.0, 40.0));
+ vectors_to_test.push_back(Vector3(-90.0, 90.0, -90.0));
+ vectors_to_test.push_back(Vector3(90.0, 90.0, -90.0));
+ vectors_to_test.push_back(Vector3(90.0, -90.0, -90.0));
+ vectors_to_test.push_back(Vector3(-90.0, -90.0, -90.0));
+ vectors_to_test.push_back(Vector3(-90.0, 90.0, 90.0));
+ vectors_to_test.push_back(Vector3(90.0, 90.0, 90.0));
+ vectors_to_test.push_back(Vector3(90.0, -90.0, 90.0));
+ vectors_to_test.push_back(Vector3(20.0, 150.0, 30.0));
+ vectors_to_test.push_back(Vector3(20.0, -150.0, 30.0));
+ vectors_to_test.push_back(Vector3(-120.0, -150.0, 30.0));
+ vectors_to_test.push_back(Vector3(-120.0, -150.0, -130.0));
+ vectors_to_test.push_back(Vector3(120.0, -150.0, -130.0));
+ vectors_to_test.push_back(Vector3(120.0, 150.0, -130.0));
+ vectors_to_test.push_back(Vector3(120.0, 150.0, 130.0));
+
+ for (int h = 0; h < rotorder_to_test.size(); h += 1) {
+ for (int i = 0; i < vectors_to_test.size(); i += 1) {
+ test_rotation(vectors_to_test[i], rotorder_to_test[h]);
+ }
+ }
+}
+
+TEST_CASE("[Stress][Basis] Euler conversions") {
+ Vector<RotOrder> rotorder_to_test;
+ rotorder_to_test.push_back(EulerXYZ);
+ rotorder_to_test.push_back(EulerXZY);
+ rotorder_to_test.push_back(EulerYZX);
+ rotorder_to_test.push_back(EulerYXZ);
+ rotorder_to_test.push_back(EulerZXY);
+ rotorder_to_test.push_back(EulerZYX);
+
+ Vector<Vector3> vectors_to_test;
+ // Add 1000 random vectors with weirds numbers.
+ RandomNumberGenerator rng;
+ for (int _ = 0; _ < 1000; _ += 1) {
+ vectors_to_test.push_back(Vector3(
+ rng.randf_range(-1800, 1800),
+ rng.randf_range(-1800, 1800),
+ rng.randf_range(-1800, 1800)));
+ }
+
+ for (int h = 0; h < rotorder_to_test.size(); h += 1) {
+ for (int i = 0; i < vectors_to_test.size(); i += 1) {
+ test_rotation(vectors_to_test[i], rotorder_to_test[h]);
+ }
+ }
+}
+} // namespace TestBasis
+
+#endif
diff --git a/tests/core/math/test_color.h b/tests/core/math/test_color.h
new file mode 100644
index 0000000000..e62ce6ec60
--- /dev/null
+++ b/tests/core/math/test_color.h
@@ -0,0 +1,207 @@
+/*************************************************************************/
+/* test_color.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_COLOR_H
+#define TEST_COLOR_H
+
+#include "core/math/color.h"
+
+#include "tests/test_macros.h"
+
+namespace TestColor {
+
+TEST_CASE("[Color] Constructor methods") {
+ const Color blue_rgba = Color(0.25098, 0.376471, 1, 0.501961);
+ const Color blue_html = Color::html("#4060ff80");
+ const Color blue_hex = Color::hex(0x4060ff80);
+ const Color blue_hex64 = Color::hex64(0x4040'6060'ffff'8080);
+
+ CHECK_MESSAGE(
+ blue_rgba.is_equal_approx(blue_html),
+ "Creation with HTML notation should result in components approximately equal to the default constructor.");
+ CHECK_MESSAGE(
+ blue_rgba.is_equal_approx(blue_hex),
+ "Creation with a 32-bit hexadecimal number should result in components approximately equal to the default constructor.");
+ CHECK_MESSAGE(
+ blue_rgba.is_equal_approx(blue_hex64),
+ "Creation with a 64-bit hexadecimal number should result in components approximately equal to the default constructor.");
+
+ ERR_PRINT_OFF;
+ const Color html_invalid = Color::html("invalid");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ html_invalid.is_equal_approx(Color()),
+ "Creation with invalid HTML notation should result in a Color with the default values.");
+
+ const Color green_rgba = Color(0, 1, 0, 0.25);
+ const Color green_hsva = Color(0, 0, 0).from_hsv(120 / 360.0, 1, 1, 0.25);
+
+ CHECK_MESSAGE(
+ green_rgba.is_equal_approx(green_hsva),
+ "Creation with HSV notation should result in components approximately equal to the default constructor.");
+}
+
+TEST_CASE("[Color] Operators") {
+ const Color blue = Color(0.2, 0.2, 1);
+ const Color dark_red = Color(0.3, 0.1, 0.1);
+
+ // Color components may be negative. Also, the alpha component may be greater than 1.0.
+ CHECK_MESSAGE(
+ (blue + dark_red).is_equal_approx(Color(0.5, 0.3, 1.1, 2)),
+ "Color addition should behave as expected.");
+ CHECK_MESSAGE(
+ (blue - dark_red).is_equal_approx(Color(-0.1, 0.1, 0.9, 0)),
+ "Color subtraction should behave as expected.");
+ CHECK_MESSAGE(
+ (blue * 2).is_equal_approx(Color(0.4, 0.4, 2, 2)),
+ "Color multiplication with a scalar should behave as expected.");
+ CHECK_MESSAGE(
+ (blue / 2).is_equal_approx(Color(0.1, 0.1, 0.5, 0.5)),
+ "Color division with a scalar should behave as expected.");
+ CHECK_MESSAGE(
+ (blue * dark_red).is_equal_approx(Color(0.06, 0.02, 0.1)),
+ "Color multiplication with another Color should behave as expected.");
+ CHECK_MESSAGE(
+ (blue / dark_red).is_equal_approx(Color(0.666667, 2, 10)),
+ "Color division with another Color should behave as expected.");
+ CHECK_MESSAGE(
+ (-blue).is_equal_approx(Color(0.8, 0.8, 0, 0)),
+ "Color negation should behave as expected (affecting the alpha channel, unlike `invert()`).");
+}
+
+TEST_CASE("[Color] Reading methods") {
+ const Color dark_blue = Color(0, 0, 0.5, 0.4);
+
+ CHECK_MESSAGE(
+ Math::is_equal_approx(dark_blue.get_h(), 240.0f / 360.0f),
+ "The returned HSV hue should match the expected value.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(dark_blue.get_s(), 1.0f),
+ "The returned HSV saturation should match the expected value.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(dark_blue.get_v(), 0.5f),
+ "The returned HSV value should match the expected value.");
+}
+
+TEST_CASE("[Color] Conversion methods") {
+ const Color cyan = Color(0, 1, 1);
+ const Color cyan_transparent = Color(0, 1, 1, 0);
+
+ CHECK_MESSAGE(
+ cyan.to_html() == "00ffffff",
+ "The returned RGB HTML color code should match the expected value.");
+ CHECK_MESSAGE(
+ cyan_transparent.to_html() == "00ffff00",
+ "The returned RGBA HTML color code should match the expected value.");
+ CHECK_MESSAGE(
+ cyan.to_argb32() == 0xff00ffff,
+ "The returned 32-bit RGB number should match the expected value.");
+ CHECK_MESSAGE(
+ cyan.to_abgr32() == 0xffffff00,
+ "The returned 32-bit BGR number should match the expected value.");
+ CHECK_MESSAGE(
+ cyan.to_rgba32() == 0x00ffffff,
+ "The returned 32-bit BGR number should match the expected value.");
+ CHECK_MESSAGE(
+ cyan.to_argb64() == 0xffff'0000'ffff'ffff,
+ "The returned 64-bit RGB number should match the expected value.");
+ CHECK_MESSAGE(
+ cyan.to_abgr64() == 0xffff'ffff'ffff'0000,
+ "The returned 64-bit BGR number should match the expected value.");
+ CHECK_MESSAGE(
+ cyan.to_rgba64() == 0x0000'ffff'ffff'ffff,
+ "The returned 64-bit BGR number should match the expected value.");
+ CHECK_MESSAGE(
+ String(cyan) == "(0, 1, 1, 1)",
+ "The string representation should match the expected value.");
+}
+
+TEST_CASE("[Color] Named colors") {
+ CHECK_MESSAGE(
+ Color::named("red").is_equal_approx(Color(1, 0, 0)),
+ "The named color \"red\" should match the expected value.");
+
+ // Named colors have their names automatically normalized.
+ CHECK_MESSAGE(
+ Color::named("white_smoke").is_equal_approx(Color(0.96, 0.96, 0.96)),
+ "The named color \"white_smoke\" should match the expected value.");
+ CHECK_MESSAGE(
+ Color::named("Slate Blue").is_equal_approx(Color(0.42, 0.35, 0.80)),
+ "The named color \"Slate Blue\" should match the expected value.");
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ Color::named("doesn't exist").is_equal_approx(Color()),
+ "The invalid named color \"doesn't exist\" should result in a Color with the default values.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[Color] Validation methods") {
+ CHECK_MESSAGE(
+ Color::html_is_valid("#4080ff"),
+ "Valid HTML color (with leading #) should be considered valid.");
+ CHECK_MESSAGE(
+ Color::html_is_valid("4080ff"),
+ "Valid HTML color (without leading #) should be considered valid.");
+ CHECK_MESSAGE(
+ !Color::html_is_valid("12345"),
+ "Invalid HTML color should be considered invalid.");
+ CHECK_MESSAGE(
+ !Color::html_is_valid("#fuf"),
+ "Invalid HTML color should be considered invalid.");
+}
+
+TEST_CASE("[Color] Manipulation methods") {
+ const Color blue = Color(0, 0, 1, 0.4);
+
+ CHECK_MESSAGE(
+ blue.inverted().is_equal_approx(Color(1, 1, 0, 0.4)),
+ "Inverted color should have its red, green and blue components inverted.");
+
+ const Color purple = Color(0.5, 0.2, 0.5, 0.25);
+
+ CHECK_MESSAGE(
+ purple.lightened(0.2).is_equal_approx(Color(0.6, 0.36, 0.6, 0.25)),
+ "Color should be lightened by the expected amount.");
+ CHECK_MESSAGE(
+ purple.darkened(0.2).is_equal_approx(Color(0.4, 0.16, 0.4, 0.25)),
+ "Color should be darkened by the expected amount.");
+
+ const Color red = Color(1, 0, 0, 0.2);
+ const Color yellow = Color(1, 1, 0, 0.8);
+
+ CHECK_MESSAGE(
+ red.lerp(yellow, 0.5).is_equal_approx(Color(1, 0.5, 0, 0.5)),
+ "Red interpolated with yellow should be orange (with interpolated alpha).");
+}
+} // namespace TestColor
+
+#endif // TEST_COLOR_H
diff --git a/tests/core/math/test_expression.h b/tests/core/math/test_expression.h
new file mode 100644
index 0000000000..5a894b20f3
--- /dev/null
+++ b/tests/core/math/test_expression.h
@@ -0,0 +1,444 @@
+/*************************************************************************/
+/* test_expression.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_EXPRESSION_H
+#define TEST_EXPRESSION_H
+
+#include "core/math/expression.h"
+
+#include "tests/test_macros.h"
+
+namespace TestExpression {
+
+TEST_CASE("[Expression] Integer arithmetic") {
+ Expression expression;
+
+ CHECK_MESSAGE(
+ expression.parse("-123456") == OK,
+ "Integer identity should parse successfully.");
+ CHECK_MESSAGE(
+ int(expression.execute()) == -123456,
+ "Integer identity should return the expected result.");
+
+ CHECK_MESSAGE(
+ expression.parse("2 + 3") == OK,
+ "Integer addition should parse successfully.");
+ CHECK_MESSAGE(
+ int(expression.execute()) == 5,
+ "Integer addition should return the expected result.");
+
+ CHECK_MESSAGE(
+ expression.parse("999999999999 + 999999999999") == OK,
+ "Large integer addition should parse successfully.");
+ CHECK_MESSAGE(
+ int64_t(expression.execute()) == 1'999'999'999'998,
+ "Large integer addition should return the expected result.");
+
+ CHECK_MESSAGE(
+ expression.parse("25 / 10") == OK,
+ "Integer / integer division should parse successfully.");
+ CHECK_MESSAGE(
+ int(expression.execute()) == 2,
+ "Integer / integer division should return the expected result.");
+
+ CHECK_MESSAGE(
+ expression.parse("2 * (6 + 14) / 2 - 5") == OK,
+ "Integer multiplication-addition-subtraction-division should parse successfully.");
+ CHECK_MESSAGE(
+ int(expression.execute()) == 15,
+ "Integer multiplication-addition-subtraction-division should return the expected result.");
+}
+
+TEST_CASE("[Expression] Floating-point arithmetic") {
+ Expression expression;
+
+ CHECK_MESSAGE(
+ expression.parse("-123.456") == OK,
+ "Float identity should parse successfully.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(double(expression.execute()), -123.456),
+ "Float identity should return the expected result.");
+
+ CHECK_MESSAGE(
+ expression.parse("2.0 + 3.0") == OK,
+ "Float addition should parse successfully.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(double(expression.execute()), 5),
+ "Float addition should return the expected result.");
+
+ CHECK_MESSAGE(
+ expression.parse("3.0 / 10") == OK,
+ "Float / integer division should parse successfully.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(double(expression.execute()), 0.3),
+ "Float / integer division should return the expected result.");
+
+ CHECK_MESSAGE(
+ expression.parse("3 / 10.0") == OK,
+ "Basic integer / float division should parse successfully.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(double(expression.execute()), 0.3),
+ "Basic integer / float division should return the expected result.");
+
+ CHECK_MESSAGE(
+ expression.parse("3.0 / 10.0") == OK,
+ "Float / float division should parse successfully.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(double(expression.execute()), 0.3),
+ "Float / float division should return the expected result.");
+
+ CHECK_MESSAGE(
+ expression.parse("2.5 * (6.0 + 14.25) / 2.0 - 5.12345") == OK,
+ "Float multiplication-addition-subtraction-division should parse successfully.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(double(expression.execute()), 20.18905),
+ "Float multiplication-addition-subtraction-division should return the expected result.");
+}
+
+TEST_CASE("[Expression] Scientific notation") {
+ Expression expression;
+
+ CHECK_MESSAGE(
+ expression.parse("2.e5") == OK,
+ "The expression should parse successfully.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(double(expression.execute()), 200'000),
+ "The expression should return the expected result.");
+
+ // The middle "e" is ignored here.
+ CHECK_MESSAGE(
+ expression.parse("2e5") == OK,
+ "The expression should parse successfully.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(double(expression.execute()), 25),
+ "The expression should return the expected result.");
+
+ CHECK_MESSAGE(
+ expression.parse("2e.5") == OK,
+ "The expression should parse successfully.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(double(expression.execute()), 2),
+ "The expression should return the expected result.");
+}
+
+TEST_CASE("[Expression] Underscored numeric literals") {
+ Expression expression;
+
+ CHECK_MESSAGE(
+ expression.parse("1_000_000") == OK,
+ "The expression should parse successfully.");
+ CHECK_MESSAGE(
+ expression.parse("1_000.000") == OK,
+ "The expression should parse successfully.");
+ CHECK_MESSAGE(
+ expression.parse("0xff_99_00") == OK,
+ "The expression should parse successfully.");
+}
+
+TEST_CASE("[Expression] Built-in functions") {
+ Expression expression;
+
+ CHECK_MESSAGE(
+ expression.parse("sqrt(pow(3, 2) + pow(4, 2))") == OK,
+ "The expression should parse successfully.");
+ CHECK_MESSAGE(
+ int(expression.execute()) == 5,
+ "`sqrt(pow(3, 2) + pow(4, 2))` should return the expected result.");
+
+ CHECK_MESSAGE(
+ expression.parse("snapped(sin(0.5), 0.01)") == OK,
+ "The expression should parse successfully.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(double(expression.execute()), 0.48),
+ "`snapped(sin(0.5), 0.01)` should return the expected result.");
+
+ CHECK_MESSAGE(
+ expression.parse("pow(2.0, -2500)") == OK,
+ "The expression should parse successfully.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(double(expression.execute())),
+ "`pow(2.0, -2500)` should return the expected result (asymptotically zero).");
+}
+
+TEST_CASE("[Expression] Boolean expressions") {
+ Expression expression;
+
+ CHECK_MESSAGE(
+ expression.parse("24 >= 12") == OK,
+ "The boolean expression should parse successfully.");
+ CHECK_MESSAGE(
+ bool(expression.execute()),
+ "The boolean expression should evaluate to `true`.");
+
+ CHECK_MESSAGE(
+ expression.parse("1.0 < 1.25 && 1.25 < 2.0") == OK,
+ "The boolean expression should parse successfully.");
+ CHECK_MESSAGE(
+ bool(expression.execute()),
+ "The boolean expression should evaluate to `true`.");
+
+ CHECK_MESSAGE(
+ expression.parse("!2") == OK,
+ "The boolean expression should parse successfully.");
+ CHECK_MESSAGE(
+ !bool(expression.execute()),
+ "The boolean expression should evaluate to `false`.");
+
+ CHECK_MESSAGE(
+ expression.parse("!!2") == OK,
+ "The boolean expression should parse successfully.");
+ CHECK_MESSAGE(
+ bool(expression.execute()),
+ "The boolean expression should evaluate to `true`.");
+
+ CHECK_MESSAGE(
+ expression.parse("!0") == OK,
+ "The boolean expression should parse successfully.");
+ CHECK_MESSAGE(
+ bool(expression.execute()),
+ "The boolean expression should evaluate to `true`.");
+
+ CHECK_MESSAGE(
+ expression.parse("!!0") == OK,
+ "The boolean expression should parse successfully.");
+ CHECK_MESSAGE(
+ !bool(expression.execute()),
+ "The boolean expression should evaluate to `false`.");
+
+ CHECK_MESSAGE(
+ expression.parse("2 && 5") == OK,
+ "The boolean expression should parse successfully.");
+ CHECK_MESSAGE(
+ bool(expression.execute()),
+ "The boolean expression should evaluate to `true`.");
+
+ CHECK_MESSAGE(
+ expression.parse("0 || 0") == OK,
+ "The boolean expression should parse successfully.");
+ CHECK_MESSAGE(
+ !bool(expression.execute()),
+ "The boolean expression should evaluate to `false`.");
+
+ CHECK_MESSAGE(
+ expression.parse("(2 <= 4) && (2 > 5)") == OK,
+ "The boolean expression should parse successfully.");
+ CHECK_MESSAGE(
+ !bool(expression.execute()),
+ "The boolean expression should evaluate to `false`.");
+}
+
+TEST_CASE("[Expression] Expressions with variables") {
+ Expression expression;
+
+ PackedStringArray parameter_names;
+ parameter_names.push_back("foo");
+ parameter_names.push_back("bar");
+ CHECK_MESSAGE(
+ expression.parse("foo + bar + 50", parameter_names) == OK,
+ "The expression should parse successfully.");
+ Array values;
+ values.push_back(60);
+ values.push_back(20);
+ CHECK_MESSAGE(
+ int(expression.execute(values)) == 130,
+ "The expression should return the expected value.");
+
+ PackedStringArray parameter_names_invalid;
+ parameter_names_invalid.push_back("foo");
+ parameter_names_invalid.push_back("baz"); // Invalid parameter name.
+ CHECK_MESSAGE(
+ expression.parse("foo + bar + 50", parameter_names_invalid) == OK,
+ "The expression should parse successfully.");
+ Array values_invalid;
+ values_invalid.push_back(60);
+ values_invalid.push_back(20);
+ // Invalid parameters will parse successfully but print an error message when executing.
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ int(expression.execute(values_invalid)) == 0,
+ "The expression should return the expected value.");
+ ERR_PRINT_ON;
+
+ // Mismatched argument count (more values than parameters).
+ PackedStringArray parameter_names_mismatch;
+ parameter_names_mismatch.push_back("foo");
+ parameter_names_mismatch.push_back("bar");
+ CHECK_MESSAGE(
+ expression.parse("foo + bar + 50", parameter_names_mismatch) == OK,
+ "The expression should parse successfully.");
+ Array values_mismatch;
+ values_mismatch.push_back(60);
+ values_mismatch.push_back(20);
+ values_mismatch.push_back(110);
+ CHECK_MESSAGE(
+ int(expression.execute(values_mismatch)) == 130,
+ "The expression should return the expected value.");
+
+ // Mismatched argument count (more parameters than values).
+ PackedStringArray parameter_names_mismatch2;
+ parameter_names_mismatch2.push_back("foo");
+ parameter_names_mismatch2.push_back("bar");
+ parameter_names_mismatch2.push_back("baz");
+ CHECK_MESSAGE(
+ expression.parse("foo + bar + baz + 50", parameter_names_mismatch2) == OK,
+ "The expression should parse successfully.");
+ Array values_mismatch2;
+ values_mismatch2.push_back(60);
+ values_mismatch2.push_back(20);
+ // Having more parameters than values will parse successfully but print an
+ // error message when executing.
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ int(expression.execute(values_mismatch2)) == 0,
+ "The expression should return the expected value.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[Expression] Invalid expressions") {
+ Expression expression;
+
+ CHECK_MESSAGE(
+ expression.parse("\\") == ERR_INVALID_PARAMETER,
+ "The expression shouldn't parse successfully.");
+
+ CHECK_MESSAGE(
+ expression.parse("0++") == ERR_INVALID_PARAMETER,
+ "The expression shouldn't parse successfully.");
+
+ CHECK_MESSAGE(
+ expression.parse("()") == ERR_INVALID_PARAMETER,
+ "The expression shouldn't parse successfully.");
+
+ CHECK_MESSAGE(
+ expression.parse("()()") == ERR_INVALID_PARAMETER,
+ "The expression shouldn't parse successfully.");
+
+ CHECK_MESSAGE(
+ expression.parse("() - ()") == ERR_INVALID_PARAMETER,
+ "The expression shouldn't parse successfully.");
+
+ CHECK_MESSAGE(
+ expression.parse("() * 12345") == ERR_INVALID_PARAMETER,
+ "The expression shouldn't parse successfully.");
+
+ CHECK_MESSAGE(
+ expression.parse("() * 12345") == ERR_INVALID_PARAMETER,
+ "The expression shouldn't parse successfully.");
+
+ CHECK_MESSAGE(
+ expression.parse("123'456") == ERR_INVALID_PARAMETER,
+ "The expression shouldn't parse successfully.");
+
+ CHECK_MESSAGE(
+ expression.parse("123\"456") == ERR_INVALID_PARAMETER,
+ "The expression shouldn't parse successfully.");
+}
+
+TEST_CASE("[Expression] Unusual expressions") {
+ Expression expression;
+
+ // Redundant parentheses don't cause a parse error as long as they're matched.
+ CHECK_MESSAGE(
+ expression.parse("(((((((((((((((666)))))))))))))))") == OK,
+ "The expression should parse successfully.");
+
+ // Using invalid identifiers doesn't cause a parse error.
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ expression.parse("hello + hello") == OK,
+ "The expression should parse successfully.");
+ CHECK_MESSAGE(
+ int(expression.execute()) == 0,
+ "The expression should return the expected result.");
+ ERR_PRINT_ON;
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ expression.parse("$1.00 + ???5") == OK,
+ "The expression should parse successfully.");
+ CHECK_MESSAGE(
+ int(expression.execute()) == 0,
+ "The expression should return the expected result.");
+ ERR_PRINT_ON;
+
+ // Commas can't be used as a decimal parameter.
+ CHECK_MESSAGE(
+ expression.parse("123,456") == OK,
+ "The expression should parse successfully.");
+ CHECK_MESSAGE(
+ int(expression.execute()) == 123,
+ "The expression should return the expected result.");
+
+ // Spaces can't be used as a separator for large numbers.
+ CHECK_MESSAGE(
+ expression.parse("123 456") == OK,
+ "The expression should parse successfully.");
+ CHECK_MESSAGE(
+ int(expression.execute()) == 123,
+ "The expression should return the expected result.");
+
+ // Division by zero is accepted, even though it prints an error message normally.
+ CHECK_MESSAGE(
+ expression.parse("-25.4 / 0") == OK,
+ "The expression should parse successfully.");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ Math::is_inf(double(expression.execute())),
+ "`-25.4 / 0` should return inf.");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ expression.parse("0 / 0") == OK,
+ "The expression should parse successfully.");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ int(expression.execute()) == 0,
+ "`0 / 0` should return 0.");
+ ERR_PRINT_ON;
+
+ // The tests below currently crash the engine.
+ //
+ //CHECK_MESSAGE(
+ // expression.parse("(-9223372036854775807 - 1) % -1") == OK,
+ // "The expression should parse successfully.");
+ //CHECK_MESSAGE(
+ // int64_t(expression.execute()) == 0,
+ // "`(-9223372036854775807 - 1) % -1` should return the expected result.");
+ //
+ //CHECK_MESSAGE(
+ // expression.parse("(-9223372036854775807 - 1) / -1") == OK,
+ // "The expression should parse successfully.");
+ //CHECK_MESSAGE(
+ // int64_t(expression.execute()) == 0,
+ // "`(-9223372036854775807 - 1) / -1` should return the expected result.");
+}
+} // namespace TestExpression
+
+#endif // TEST_EXPRESSION_H
diff --git a/tests/core/math/test_geometry_2d.h b/tests/core/math/test_geometry_2d.h
new file mode 100644
index 0000000000..3487e4d7e8
--- /dev/null
+++ b/tests/core/math/test_geometry_2d.h
@@ -0,0 +1,567 @@
+/*************************************************************************/
+/* test_geometry_2d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_GEOMETRY_2D_H
+#define TEST_GEOMETRY_2D_H
+
+#include "core/math/geometry_2d.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestGeometry2D {
+
+TEST_CASE("[Geometry2D] Point in circle") {
+ CHECK(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(0, 0), 1.0));
+
+ CHECK(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(11.99, 0), 12));
+ CHECK(Geometry2D::is_point_in_circle(Vector2(-11.99, 0), Vector2(0, 0), 12));
+
+ CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(12.01, 0), 12));
+ CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(-12.01, 0), Vector2(0, 0), 12));
+
+ CHECK(Geometry2D::is_point_in_circle(Vector2(7, -42), Vector2(4, -40), 3.7));
+ CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(7, -42), Vector2(4, -40), 3.5));
+
+ // This tests points on the edge of the circle. They are treated as being inside the circle.
+ CHECK(Geometry2D::is_point_in_circle(Vector2(1.0, 0.0), Vector2(0, 0), 1.0));
+ CHECK(Geometry2D::is_point_in_circle(Vector2(0.0, -1.0), Vector2(0, 0), 1.0));
+}
+
+TEST_CASE("[Geometry2D] Point in triangle") {
+ CHECK(Geometry2D::is_point_in_triangle(Vector2(0, 0), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));
+ CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(-1.01, 1.0), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));
+
+ CHECK(Geometry2D::is_point_in_triangle(Vector2(3, 2.5), Vector2(1, 4), Vector2(3, 2), Vector2(5, 4)));
+ CHECK(Geometry2D::is_point_in_triangle(Vector2(-3, -2.5), Vector2(-1, -4), Vector2(-3, -2), Vector2(-5, -4)));
+ CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(0, 0), Vector2(1, 4), Vector2(3, 2), Vector2(5, 4)));
+
+ // This tests points on the edge of the triangle. They are treated as being outside the triangle.
+ // In `is_point_in_circle` and `is_point_in_polygon` they are treated as being inside, so in order the make
+ // the behaviour consistent this may change in the future (see issue #44717 and PR #44274).
+ CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(1, 1), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));
+ CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(0, 1), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));
+}
+
+TEST_CASE("[Geometry2D] Point in polygon") {
+ Vector<Vector2> p;
+ CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(0, 0), p));
+
+ p.push_back(Vector2(-88, 120));
+ p.push_back(Vector2(-74, -38));
+ p.push_back(Vector2(135, -145));
+ p.push_back(Vector2(425, 70));
+ p.push_back(Vector2(68, 112));
+ p.push_back(Vector2(-120, 370));
+ p.push_back(Vector2(-323, -145));
+ CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-350, 0), p));
+ CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-110, 60), p));
+ CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(412, 96), p));
+ CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(83, 130), p));
+ CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-320, -153), p));
+
+ CHECK(Geometry2D::is_point_in_polygon(Vector2(0, 0), p));
+ CHECK(Geometry2D::is_point_in_polygon(Vector2(-230, 0), p));
+ CHECK(Geometry2D::is_point_in_polygon(Vector2(130, -110), p));
+ CHECK(Geometry2D::is_point_in_polygon(Vector2(370, 55), p));
+ CHECK(Geometry2D::is_point_in_polygon(Vector2(-160, 190), p));
+
+ // This tests points on the edge of the polygon. They are treated as being inside the polygon.
+ int c = p.size();
+ for (int i = 0; i < c; i++) {
+ const Vector2 &p1 = p[i];
+ CHECK(Geometry2D::is_point_in_polygon(p1, p));
+
+ const Vector2 &p2 = p[(i + 1) % c];
+ Vector2 midpoint((p1 + p2) * 0.5);
+ CHECK(Geometry2D::is_point_in_polygon(midpoint, p));
+ }
+}
+
+TEST_CASE("[Geometry2D] Polygon clockwise") {
+ Vector<Vector2> p;
+ CHECK_FALSE(Geometry2D::is_polygon_clockwise(p));
+
+ p.push_back(Vector2(5, -5));
+ p.push_back(Vector2(-1, -5));
+ p.push_back(Vector2(-5, -1));
+ p.push_back(Vector2(-1, 3));
+ p.push_back(Vector2(1, 5));
+ CHECK(Geometry2D::is_polygon_clockwise(p));
+
+ p.reverse();
+ CHECK_FALSE(Geometry2D::is_polygon_clockwise(p));
+}
+
+TEST_CASE("[Geometry2D] Line intersection") {
+ Vector2 r;
+ CHECK(Geometry2D::line_intersects_line(Vector2(2, 0), Vector2(0, 1), Vector2(0, 2), Vector2(1, 0), r));
+ CHECK(r.is_equal_approx(Vector2(2, 2)));
+
+ CHECK(Geometry2D::line_intersects_line(Vector2(-1, 1), Vector2(1, -1), Vector2(4, 1), Vector2(-1, -1), r));
+ CHECK(r.is_equal_approx(Vector2(1.5, -1.5)));
+
+ CHECK(Geometry2D::line_intersects_line(Vector2(-1, 0), Vector2(-1, -1), Vector2(1, 0), Vector2(1, -1), r));
+ CHECK(r.is_equal_approx(Vector2(0, 1)));
+
+ CHECK_FALSE_MESSAGE(
+ Geometry2D::line_intersects_line(Vector2(-1, 1), Vector2(1, -1), Vector2(0, 1), Vector2(1, -1), r),
+ "Parallel lines should not intersect.");
+}
+
+TEST_CASE("[Geometry2D] Segment intersection.") {
+ Vector2 r;
+
+ CHECK(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(-1, -1), &r));
+ CHECK(r.is_equal_approx(Vector2(0, 0)));
+
+ CHECK_FALSE(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(0.1, 0.1), &r));
+ CHECK_FALSE(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(0.1, 0.1), Vector2(1, 1), &r));
+
+ CHECK_FALSE_MESSAGE(
+ Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(0, 1), Vector2(2, -1), &r),
+ "Parallel segments should not intersect.");
+
+ CHECK_MESSAGE(
+ Geometry2D::segment_intersects_segment(Vector2(0, 0), Vector2(0, 1), Vector2(0, 0), Vector2(1, 0), &r),
+ "Touching segments should intersect.");
+ CHECK(r.is_equal_approx(Vector2(0, 0)));
+
+ CHECK_MESSAGE(
+ Geometry2D::segment_intersects_segment(Vector2(0, 1), Vector2(0, 0), Vector2(0, 0), Vector2(1, 0), &r),
+ "Touching segments should intersect.");
+ CHECK(r.is_equal_approx(Vector2(0, 0)));
+}
+
+TEST_CASE("[Geometry2D] Closest point to segment") {
+ Vector2 s[] = { Vector2(-4, -4), Vector2(4, 4) };
+ CHECK(Geometry2D::get_closest_point_to_segment(Vector2(4.1, 4.1), s).is_equal_approx(Vector2(4, 4)));
+ CHECK(Geometry2D::get_closest_point_to_segment(Vector2(-4.1, -4.1), s).is_equal_approx(Vector2(-4, -4)));
+ CHECK(Geometry2D::get_closest_point_to_segment(Vector2(-1, 1), s).is_equal_approx(Vector2(0, 0)));
+}
+
+TEST_CASE("[Geometry2D] Closest point to uncapped segment") {
+ Vector2 s[] = { Vector2(-4, -4), Vector2(4, 4) };
+ CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(-1, 1), s).is_equal_approx(Vector2(0, 0)));
+ CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(-4, -6), s).is_equal_approx(Vector2(-5, -5)));
+ CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(4, 6), s).is_equal_approx(Vector2(5, 5)));
+}
+
+TEST_CASE("[Geometry2D] Closest points between segments") {
+ Vector2 c1, c2;
+ Geometry2D::get_closest_points_between_segments(Vector2(2, 2), Vector2(3, 3), Vector2(4, 4), Vector2(4, 5), c1, c2);
+ CHECK(c1.is_equal_approx(Vector2(3, 3)));
+ CHECK(c2.is_equal_approx(Vector2(4, 4)));
+
+ Geometry2D::get_closest_points_between_segments(Vector2(0, 1), Vector2(-2, -1), Vector2(0, 0), Vector2(2, -2), c1, c2);
+ CHECK(c1.is_equal_approx(Vector2(-0.5, 0.5)));
+ CHECK(c2.is_equal_approx(Vector2(0, 0)));
+
+ Geometry2D::get_closest_points_between_segments(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(-1, -1), c1, c2);
+ CHECK(c1.is_equal_approx(Vector2(0, 0)));
+ CHECK(c2.is_equal_approx(Vector2(0, 0)));
+}
+
+TEST_CASE("[Geometry2D] Make atlas") {
+ Vector<Point2i> result;
+ Size2i size;
+
+ Vector<Size2i> r;
+ r.push_back(Size2i(2, 2));
+ Geometry2D::make_atlas(r, result, size);
+ CHECK(size == Size2i(2, 2));
+ CHECK(result.size() == r.size());
+
+ r.clear();
+ result.clear();
+ r.push_back(Size2i(1, 2));
+ r.push_back(Size2i(3, 4));
+ r.push_back(Size2i(5, 6));
+ r.push_back(Size2i(7, 8));
+ Geometry2D::make_atlas(r, result, size);
+ CHECK(result.size() == r.size());
+}
+
+TEST_CASE("[Geometry2D] Polygon intersection") {
+ Vector<Point2> a;
+ Vector<Point2> b;
+ Vector<Vector<Point2>> r;
+
+ a.push_back(Point2(30, 60));
+ a.push_back(Point2(70, 5));
+ a.push_back(Point2(200, 40));
+ a.push_back(Point2(80, 200));
+
+ SUBCASE("[Geometry2D] Both polygons are empty") {
+ r = Geometry2D::intersect_polygons(Vector<Point2>(), Vector<Point2>());
+ CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The intersection should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] One polygon is empty") {
+ r = Geometry2D::intersect_polygons(a, b);
+ REQUIRE_MESSAGE(r.is_empty(), "One polygon is empty. The intersection should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] Basic intersection") {
+ b.push_back(Point2(200, 300));
+ b.push_back(Point2(90, 200));
+ b.push_back(Point2(50, 100));
+ b.push_back(Point2(200, 90));
+ r = Geometry2D::intersect_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 1, "The polygons should intersect each other with 1 resulting intersection polygon.");
+ REQUIRE_MESSAGE(r[0].size() == 3, "The resulting intersection polygon should have 3 vertices.");
+ CHECK(r[0][0].is_equal_approx(Point2(86.52174, 191.30436)));
+ CHECK(r[0][1].is_equal_approx(Point2(50, 100)));
+ CHECK(r[0][2].is_equal_approx(Point2(160.52632, 92.63157)));
+ }
+
+ SUBCASE("[Geometry2D] Intersection with one polygon being completely inside the other polygon") {
+ b.push_back(Point2(80, 100));
+ b.push_back(Point2(50, 50));
+ b.push_back(Point2(150, 50));
+ r = Geometry2D::intersect_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 1, "The polygons should intersect each other with 1 resulting intersection polygon.");
+ REQUIRE_MESSAGE(r[0].size() == 3, "The resulting intersection polygon should have 3 vertices.");
+ CHECK(r[0][0].is_equal_approx(b[0]));
+ CHECK(r[0][1].is_equal_approx(b[1]));
+ CHECK(r[0][2].is_equal_approx(b[2]));
+ }
+
+ SUBCASE("[Geometry2D] No intersection with 2 non-empty polygons") {
+ b.push_back(Point2(150, 150));
+ b.push_back(Point2(250, 100));
+ b.push_back(Point2(300, 200));
+ r = Geometry2D::intersect_polygons(a, b);
+ REQUIRE_MESSAGE(r.is_empty(), "The polygons should not intersect each other.");
+ }
+
+ SUBCASE("[Geometry2D] Intersection with 2 resulting polygons") {
+ a.clear();
+ a.push_back(Point2(70, 5));
+ a.push_back(Point2(140, 7));
+ a.push_back(Point2(100, 52));
+ a.push_back(Point2(170, 50));
+ a.push_back(Point2(60, 125));
+ b.push_back(Point2(70, 105));
+ b.push_back(Point2(115, 55));
+ b.push_back(Point2(90, 15));
+ b.push_back(Point2(160, 50));
+ r = Geometry2D::intersect_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 2, "The polygons should intersect each other with 2 resulting intersection polygons.");
+ REQUIRE_MESSAGE(r[0].size() == 4, "The resulting intersection polygon should have 4 vertices.");
+ CHECK(r[0][0].is_equal_approx(Point2(70, 105)));
+ CHECK(r[0][1].is_equal_approx(Point2(115, 55)));
+ CHECK(r[0][2].is_equal_approx(Point2(112.894737, 51.63158)));
+ CHECK(r[0][3].is_equal_approx(Point2(159.509537, 50.299728)));
+
+ REQUIRE_MESSAGE(r[1].size() == 3, "The intersection polygon should have 3 vertices.");
+ CHECK(r[1][0].is_equal_approx(Point2(119.692307, 29.846149)));
+ CHECK(r[1][1].is_equal_approx(Point2(107.706421, 43.33028)));
+ CHECK(r[1][2].is_equal_approx(Point2(90, 15)));
+ }
+}
+
+TEST_CASE("[Geometry2D] Merge polygons") {
+ Vector<Point2> a;
+ Vector<Point2> b;
+ Vector<Vector<Point2>> r;
+
+ a.push_back(Point2(225, 180));
+ a.push_back(Point2(160, 230));
+ a.push_back(Point2(20, 212));
+ a.push_back(Point2(50, 115));
+
+ SUBCASE("[Geometry2D] Both polygons are empty") {
+ r = Geometry2D::merge_polygons(Vector<Point2>(), Vector<Point2>());
+ REQUIRE_MESSAGE(r.is_empty(), "Both polygons are empty. The union should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] One polygon is empty") {
+ r = Geometry2D::merge_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 1, "One polygon is non-empty. There should be 1 resulting merged polygon.");
+ REQUIRE_MESSAGE(r[0].size() == 4, "The resulting merged polygon should have 4 vertices.");
+ CHECK(r[0][0].is_equal_approx(a[0]));
+ CHECK(r[0][1].is_equal_approx(a[1]));
+ CHECK(r[0][2].is_equal_approx(a[2]));
+ CHECK(r[0][3].is_equal_approx(a[3]));
+ }
+
+ SUBCASE("[Geometry2D] Basic merge with 2 polygons") {
+ b.push_back(Point2(180, 190));
+ b.push_back(Point2(60, 140));
+ b.push_back(Point2(160, 80));
+ r = Geometry2D::merge_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 1, "The merged polygons should result in 1 polygon.");
+ REQUIRE_MESSAGE(r[0].size() == 7, "The resulting merged polygon should have 7 vertices.");
+ CHECK(r[0][0].is_equal_approx(Point2(174.791077, 161.350967)));
+ CHECK(r[0][1].is_equal_approx(Point2(225, 180)));
+ CHECK(r[0][2].is_equal_approx(Point2(160, 230)));
+ CHECK(r[0][3].is_equal_approx(Point2(20, 212)));
+ CHECK(r[0][4].is_equal_approx(Point2(50, 115)));
+ CHECK(r[0][5].is_equal_approx(Point2(81.911758, 126.852943)));
+ CHECK(r[0][6].is_equal_approx(Point2(160, 80)));
+ }
+
+ SUBCASE("[Geometry2D] Merge with 2 resulting merged polygons (outline and hole)") {
+ b.push_back(Point2(180, 190));
+ b.push_back(Point2(140, 125));
+ b.push_back(Point2(60, 140));
+ b.push_back(Point2(160, 80));
+ r = Geometry2D::merge_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 2, "The merged polygons should result in 2 polygons.");
+
+ REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The merged polygon (outline) should be counter-clockwise.");
+ REQUIRE_MESSAGE(r[0].size() == 7, "The resulting merged polygon (outline) should have 7 vertices.");
+ CHECK(r[0][0].is_equal_approx(Point2(174.791077, 161.350967)));
+ CHECK(r[0][1].is_equal_approx(Point2(225, 180)));
+ CHECK(r[0][2].is_equal_approx(Point2(160, 230)));
+ CHECK(r[0][3].is_equal_approx(Point2(20, 212)));
+ CHECK(r[0][4].is_equal_approx(Point2(50, 115)));
+ CHECK(r[0][5].is_equal_approx(Point2(81.911758, 126.852943)));
+ CHECK(r[0][6].is_equal_approx(Point2(160, 80)));
+
+ REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting merged polygon (hole) should be clockwise.");
+ REQUIRE_MESSAGE(r[1].size() == 3, "The resulting merged polygon (hole) should have 3 vertices.");
+ CHECK(r[1][0].is_equal_approx(Point2(98.083069, 132.859421)));
+ CHECK(r[1][1].is_equal_approx(Point2(158.689453, 155.370377)));
+ CHECK(r[1][2].is_equal_approx(Point2(140, 125)));
+ }
+}
+
+TEST_CASE("[Geometry2D] Clip polygons") {
+ Vector<Point2> a;
+ Vector<Point2> b;
+ Vector<Vector<Point2>> r;
+
+ a.push_back(Point2(225, 180));
+ a.push_back(Point2(160, 230));
+ a.push_back(Point2(20, 212));
+ a.push_back(Point2(50, 115));
+
+ SUBCASE("[Geometry2D] Both polygons are empty") {
+ r = Geometry2D::clip_polygons(Vector<Point2>(), Vector<Point2>());
+ CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The clip should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] Basic clip with one result polygon") {
+ b.push_back(Point2(250, 170));
+ b.push_back(Point2(175, 270));
+ b.push_back(Point2(120, 260));
+ b.push_back(Point2(25, 80));
+ r = Geometry2D::clip_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 1, "The clipped polygons should result in 1 polygon.");
+ REQUIRE_MESSAGE(r[0].size() == 3, "The resulting clipped polygon should have 3 vertices.");
+ CHECK(r[0][0].is_equal_approx(Point2(100.102173, 222.298843)));
+ CHECK(r[0][1].is_equal_approx(Point2(20, 212)));
+ CHECK(r[0][2].is_equal_approx(Point2(47.588089, 122.798492)));
+ }
+
+ SUBCASE("[Geometry2D] Polygon b completely overlaps polygon a") {
+ b.push_back(Point2(250, 170));
+ b.push_back(Point2(175, 270));
+ b.push_back(Point2(10, 210));
+ b.push_back(Point2(55, 80));
+ r = Geometry2D::clip_polygons(a, b);
+ CHECK_MESSAGE(r.is_empty(), "Polygon 'b' completely overlaps polygon 'a'. This should result in no clipped polygons.");
+ }
+
+ SUBCASE("[Geometry2D] Polygon a completely overlaps polygon b") {
+ b.push_back(Point2(150, 200));
+ b.push_back(Point2(65, 190));
+ b.push_back(Point2(80, 140));
+ r = Geometry2D::clip_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 2, "Polygon 'a' completely overlaps polygon 'b'. This should result in 2 clipped polygons.");
+ REQUIRE_MESSAGE(r[0].size() == 4, "The resulting clipped polygon should have 4 vertices.");
+ REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The resulting clipped polygon (outline) should be counter-clockwise.");
+ CHECK(r[0][0].is_equal_approx(a[0]));
+ CHECK(r[0][1].is_equal_approx(a[1]));
+ CHECK(r[0][2].is_equal_approx(a[2]));
+ CHECK(r[0][3].is_equal_approx(a[3]));
+ REQUIRE_MESSAGE(r[1].size() == 3, "The resulting clipped polygon should have 3 vertices.");
+ REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting clipped polygon (hole) should be clockwise.");
+ CHECK(r[1][0].is_equal_approx(b[1]));
+ CHECK(r[1][1].is_equal_approx(b[0]));
+ CHECK(r[1][2].is_equal_approx(b[2]));
+ }
+}
+
+TEST_CASE("[Geometry2D] Exclude polygons") {
+ Vector<Point2> a;
+ Vector<Point2> b;
+ Vector<Vector<Point2>> r;
+
+ a.push_back(Point2(225, 180));
+ a.push_back(Point2(160, 230));
+ a.push_back(Point2(20, 212));
+ a.push_back(Point2(50, 115));
+
+ SUBCASE("[Geometry2D] Both polygons are empty") {
+ r = Geometry2D::exclude_polygons(Vector<Point2>(), Vector<Point2>());
+ CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The excluded polygon should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] One polygon is empty") {
+ r = Geometry2D::exclude_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 1, "One polygon is non-empty. There should be 1 resulting excluded polygon.");
+ REQUIRE_MESSAGE(r[0].size() == 4, "The resulting excluded polygon should have 4 vertices.");
+ CHECK(r[0][0].is_equal_approx(a[0]));
+ CHECK(r[0][1].is_equal_approx(a[1]));
+ CHECK(r[0][2].is_equal_approx(a[2]));
+ CHECK(r[0][3].is_equal_approx(a[3]));
+ }
+
+ SUBCASE("[Geometry2D] Exclude with 2 resulting polygons (outline and hole)") {
+ b.push_back(Point2(140, 160));
+ b.push_back(Point2(150, 220));
+ b.push_back(Point2(40, 200));
+ b.push_back(Point2(60, 140));
+ r = Geometry2D::exclude_polygons(a, b);
+ REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting excluded polygons (outline and hole).");
+ REQUIRE_MESSAGE(r[0].size() == 4, "The resulting excluded polygon should have 4 vertices.");
+ REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The resulting excluded polygon (outline) should be counter-clockwise.");
+ CHECK(r[0][0].is_equal_approx(a[0]));
+ CHECK(r[0][1].is_equal_approx(a[1]));
+ CHECK(r[0][2].is_equal_approx(a[2]));
+ CHECK(r[0][3].is_equal_approx(a[3]));
+ REQUIRE_MESSAGE(r[1].size() == 4, "The resulting excluded polygon should have 4 vertices.");
+ REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting excluded polygon (hole) should be clockwise.");
+ CHECK(r[1][0].is_equal_approx(Point2(40, 200)));
+ CHECK(r[1][1].is_equal_approx(Point2(150, 220)));
+ CHECK(r[1][2].is_equal_approx(Point2(140, 160)));
+ CHECK(r[1][3].is_equal_approx(Point2(60, 140)));
+ }
+}
+
+TEST_CASE("[Geometry2D] Intersect polyline with polygon") {
+ Vector<Vector2> l;
+ Vector<Vector2> p;
+ Vector<Vector<Point2>> r;
+
+ l.push_back(Vector2(100, 90));
+ l.push_back(Vector2(120, 250));
+
+ p.push_back(Vector2(225, 180));
+ p.push_back(Vector2(160, 230));
+ p.push_back(Vector2(20, 212));
+ p.push_back(Vector2(50, 115));
+
+ SUBCASE("[Geometry2D] Both line and polygon are empty") {
+ r = Geometry2D::intersect_polyline_with_polygon(Vector<Vector2>(), Vector<Vector2>());
+ CHECK_MESSAGE(r.is_empty(), "Both line and polygon are empty. The intersection line should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] Line is non-empty and polygon is empty") {
+ r = Geometry2D::intersect_polyline_with_polygon(l, Vector<Vector2>());
+ CHECK_MESSAGE(r.is_empty(), "The polygon is empty while the line is non-empty. The intersection line should be empty.");
+ }
+
+ SUBCASE("[Geometry2D] Basic intersection with 1 resulting intersection line") {
+ r = Geometry2D::intersect_polyline_with_polygon(l, p);
+ REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting intersection line.");
+ REQUIRE_MESSAGE(r[0].size() == 2, "The resulting intersection line should have 2 vertices.");
+ CHECK(r[0][0].is_equal_approx(Vector2(105.711609, 135.692886)));
+ CHECK(r[0][1].is_equal_approx(Vector2(116.805809, 224.446457)));
+ }
+
+ SUBCASE("[Geometry2D] Complex intersection with 2 resulting intersection lines") {
+ l.clear();
+ l.push_back(Vector2(100, 90));
+ l.push_back(Vector2(190, 255));
+ l.push_back(Vector2(135, 260));
+ l.push_back(Vector2(57, 200));
+ l.push_back(Vector2(50, 170));
+ l.push_back(Vector2(15, 155));
+ r = Geometry2D::intersect_polyline_with_polygon(l, p);
+ REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting intersection lines.");
+ REQUIRE_MESSAGE(r[0].size() == 2, "The resulting intersection line should have 2 vertices.");
+ CHECK(r[0][0].is_equal_approx(Vector2(129.804565, 144.641693)));
+ CHECK(r[0][1].is_equal_approx(Vector2(171.527084, 221.132996)));
+ REQUIRE_MESSAGE(r[1].size() == 4, "The resulting intersection line should have 4 vertices.");
+ CHECK(r[1][0].is_equal_approx(Vector2(83.15609, 220.120087)));
+ CHECK(r[1][1].is_equal_approx(Vector2(57, 200)));
+ CHECK(r[1][2].is_equal_approx(Vector2(50, 170)));
+ CHECK(r[1][3].is_equal_approx(Vector2(34.980492, 163.563065)));
+ }
+}
+
+TEST_CASE("[Geometry2D] Clip polyline with polygon") {
+ Vector<Vector2> l;
+ Vector<Vector2> p;
+ Vector<Vector<Point2>> r;
+
+ l.push_back(Vector2(70, 140));
+ l.push_back(Vector2(160, 320));
+
+ p.push_back(Vector2(225, 180));
+ p.push_back(Vector2(160, 230));
+ p.push_back(Vector2(20, 212));
+ p.push_back(Vector2(50, 115));
+
+ SUBCASE("[Geometry2D] Both line and polygon are empty") {
+ r = Geometry2D::clip_polyline_with_polygon(Vector<Vector2>(), Vector<Vector2>());
+ CHECK_MESSAGE(r.is_empty(), "Both line and polygon are empty. The clipped line should also be empty.");
+ }
+
+ SUBCASE("[Geometry2D] Polygon is empty and line is non-empty") {
+ r = Geometry2D::clip_polyline_with_polygon(l, Vector<Vector2>());
+ REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting clipped line.");
+ REQUIRE_MESSAGE(r[0].size() == 2, "The resulting clipped line should have 2 vertices.");
+ CHECK(r[0][0].is_equal_approx(l[0]));
+ CHECK(r[0][1].is_equal_approx(l[1]));
+ }
+
+ SUBCASE("[Geometry2D] Basic clip with 1 resulting clipped line") {
+ r = Geometry2D::clip_polyline_with_polygon(l, p);
+ REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting clipped line.");
+ REQUIRE_MESSAGE(r[0].size() == 2, "The resulting clipped line should have 2 vertices.");
+ CHECK(r[0][0].is_equal_approx(Vector2(111.908401, 223.816803)));
+ CHECK(r[0][1].is_equal_approx(Vector2(160, 320)));
+ }
+
+ SUBCASE("[Geometry2D] Complex clip with 2 resulting clipped lines") {
+ l.clear();
+ l.push_back(Vector2(55, 70));
+ l.push_back(Vector2(50, 190));
+ l.push_back(Vector2(120, 165));
+ l.push_back(Vector2(122, 250));
+ l.push_back(Vector2(160, 320));
+ r = Geometry2D::clip_polyline_with_polygon(l, p);
+ REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting clipped lines.");
+ REQUIRE_MESSAGE(r[0].size() == 3, "The resulting clipped line should have 3 vertices.");
+ CHECK(r[0][0].is_equal_approx(Vector2(160, 320)));
+ CHECK(r[0][1].is_equal_approx(Vector2(122, 250)));
+ CHECK(r[0][2].is_equal_approx(Vector2(121.412682, 225.038757)));
+ REQUIRE_MESSAGE(r[1].size() == 2, "The resulting clipped line should have 2 vertices.");
+ CHECK(r[1][0].is_equal_approx(Vector2(53.07737, 116.143021)));
+ CHECK(r[1][1].is_equal_approx(Vector2(55, 70)));
+ }
+}
+} // namespace TestGeometry2D
+
+#endif // TEST_GEOMETRY_2D_H
diff --git a/tests/core/math/test_geometry_3d.h b/tests/core/math/test_geometry_3d.h
new file mode 100644
index 0000000000..1b8d2eee34
--- /dev/null
+++ b/tests/core/math/test_geometry_3d.h
@@ -0,0 +1,437 @@
+/*************************************************************************/
+/* test_geometry_3d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_GEOMETRY_3D_H
+#define TEST_GEOMETRY_3D_H
+
+#include "core/math/geometry_3d.h"
+#include "tests/test_macros.h"
+
+namespace TestGeometry3D {
+TEST_CASE("[Geometry3D] Closest Points Between Segments") {
+ struct Case {
+ Vector3 p_1, p_2, p_3, p_4;
+ Vector3 got_1, got_2;
+ Vector3 want_1, want_2;
+ Case(){};
+ Case(Vector3 p_p_1, Vector3 p_p_2, Vector3 p_p_3, Vector3 p_p_4, Vector3 p_want_1, Vector3 p_want_2) :
+ p_1(p_p_1), p_2(p_p_2), p_3(p_p_3), p_4(p_p_4), want_1(p_want_1), want_2(p_want_2){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(1, -1, 1), Vector3(1, 1, -1), Vector3(-1, -2, -1), Vector3(-1, 1, 1), Vector3(1, -0.2, 0.2), Vector3(-1, -0.2, 0.2)));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Geometry3D::get_closest_points_between_segments(current_case.p_1, current_case.p_2, current_case.p_3, current_case.p_4, current_case.got_1, current_case.got_2);
+ CHECK(current_case.got_1.is_equal_approx(current_case.want_1));
+ CHECK(current_case.got_2.is_equal_approx(current_case.want_2));
+ }
+}
+
+TEST_CASE("[Geometry3D] Closest Distance Between Segments") {
+ struct Case {
+ Vector3 p_1, p_2, p_3, p_4;
+ float want;
+ Case(){};
+ Case(Vector3 p_p_1, Vector3 p_p_2, Vector3 p_p_3, Vector3 p_p_4, float p_want) :
+ p_1(p_p_1), p_2(p_p_2), p_3(p_p_3), p_4(p_p_4), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(1, -2, 0), Vector3(1, 2, 0), Vector3(-1, 2, 0), Vector3(-1, -2, 0), 0.0f));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ float out = Geometry3D::get_closest_distance_between_segments(current_case.p_1, current_case.p_2, current_case.p_3, current_case.p_4);
+ CHECK(out == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Build Box Planes") {
+ const Vector3 extents = Vector3(5, 5, 20);
+ Vector<Plane> box = Geometry3D::build_box_planes(extents);
+ CHECK(box.size() == 6);
+ CHECK(extents.x == box[0].d);
+ CHECK(box[0].normal == Vector3(1, 0, 0));
+ CHECK(extents.x == box[1].d);
+ CHECK(box[1].normal == Vector3(-1, 0, 0));
+ CHECK(extents.y == box[2].d);
+ CHECK(box[2].normal == Vector3(0, 1, 0));
+ CHECK(extents.y == box[3].d);
+ CHECK(box[3].normal == Vector3(0, -1, 0));
+ CHECK(extents.z == box[4].d);
+ CHECK(box[4].normal == Vector3(0, 0, 1));
+ CHECK(extents.z == box[5].d);
+ CHECK(box[5].normal == Vector3(0, 0, -1));
+}
+
+TEST_CASE("[Geometry3D] Build Capsule Planes") {
+ struct Case {
+ real_t radius, height;
+ int sides, lats;
+ Vector3::Axis axis;
+ int want_size;
+ Case(){};
+ Case(real_t p_radius, real_t p_height, int p_sides, int p_lats, Vector3::Axis p_axis, int p_want) :
+ radius(p_radius), height(p_height), sides(p_sides), lats(p_lats), axis(p_axis), want_size(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(10, 20, 6, 10, Vector3::Axis(), 126));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Vector<Plane> capsule = Geometry3D::build_capsule_planes(current_case.radius, current_case.height, current_case.sides, current_case.lats, current_case.axis);
+ // Should equal (p_sides * p_lats) * 2 + p_sides
+ CHECK(capsule.size() == current_case.want_size);
+ }
+}
+
+TEST_CASE("[Geometry3D] Build Cylinder Planes") {
+ struct Case {
+ real_t radius, height;
+ int sides;
+ Vector3::Axis axis;
+ int want_size;
+ Case(){};
+ Case(real_t p_radius, real_t p_height, int p_sides, Vector3::Axis p_axis, int p_want) :
+ radius(p_radius), height(p_height), sides(p_sides), axis(p_axis), want_size(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(3.0f, 10.0f, 10, Vector3::Axis(), 12));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Vector<Plane> planes = Geometry3D::build_cylinder_planes(current_case.radius, current_case.height, current_case.sides, current_case.axis);
+ CHECK(planes.size() == current_case.want_size);
+ }
+}
+
+TEST_CASE("[Geometry3D] Build Sphere Planes") {
+ struct Case {
+ real_t radius;
+ int lats, lons;
+ Vector3::Axis axis;
+ int want_size;
+ Case(){};
+ Case(real_t p_radius, int p_lat, int p_lons, Vector3::Axis p_axis, int p_want) :
+ radius(p_radius), lats(p_lat), lons(p_lons), axis(p_axis), want_size(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(10.0f, 10, 3, Vector3::Axis(), 63));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Vector<Plane> planes = Geometry3D::build_sphere_planes(current_case.radius, current_case.lats, current_case.lons, current_case.axis);
+ CHECK(planes.size() == 63);
+ }
+}
+
+#if false
+// This test has been temporarily disabled because it's really fragile and
+// breaks if calculations change very slightly. For example, it breaks when
+// using doubles, and it breaks when making Plane calculations more accurate.
+TEST_CASE("[Geometry3D] Build Convex Mesh") {
+ struct Case {
+ Vector<Plane> object;
+ int want_faces, want_edges, want_vertices;
+ Case(){};
+ Case(Vector<Plane> p_object, int p_want_faces, int p_want_edges, int p_want_vertices) :
+ object(p_object), want_faces(p_want_faces), want_edges(p_want_edges), want_vertices(p_want_vertices){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Geometry3D::build_box_planes(Vector3(5, 10, 5)), 6, 12, 8));
+ tt.push_back(Case(Geometry3D::build_capsule_planes(5, 5, 20, 20, Vector3::Axis()), 820, 7603, 6243));
+ tt.push_back(Case(Geometry3D::build_cylinder_planes(5, 5, 20, Vector3::Axis()), 22, 100, 80));
+ tt.push_back(Case(Geometry3D::build_sphere_planes(5, 5, 20), 220, 1011, 522));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Geometry3D::MeshData mesh = Geometry3D::build_convex_mesh(current_case.object);
+ CHECK(mesh.faces.size() == current_case.want_faces);
+ CHECK(mesh.edges.size() == current_case.want_edges);
+ CHECK(mesh.vertices.size() == current_case.want_vertices);
+ }
+}
+#endif
+
+TEST_CASE("[Geometry3D] Clip Polygon") {
+ struct Case {
+ Plane clipping_plane;
+ Vector<Vector3> polygon;
+ bool want;
+ Case(){};
+ Case(Plane p_clipping_plane, Vector<Vector3> p_polygon, bool p_want) :
+ clipping_plane(p_clipping_plane), polygon(p_polygon), want(p_want){};
+ };
+ Vector<Case> tt;
+ Vector<Plane> box_planes = Geometry3D::build_box_planes(Vector3(5, 10, 5));
+ Vector<Vector3> box = Geometry3D::compute_convex_mesh_points(&box_planes[0], box_planes.size());
+ tt.push_back(Case(Plane(), box, true));
+ tt.push_back(Case(Plane(Vector3(0, 1, 0), Vector3(0, 3, 0)), box, false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Vector<Vector3> output = Geometry3D::clip_polygon(current_case.polygon, current_case.clipping_plane);
+ if (current_case.want) {
+ CHECK(output == current_case.polygon);
+ } else {
+ CHECK(output != current_case.polygon);
+ }
+ }
+}
+
+TEST_CASE("[Geometry3D] Compute Convex Mesh Points") {
+ struct Case {
+ Vector<Plane> mesh;
+ Vector<Vector3> want;
+ Case(){};
+ Case(Vector<Plane> p_mesh, Vector<Vector3> p_want) :
+ mesh(p_mesh), want(p_want){};
+ };
+ Vector<Case> tt;
+ Vector<Vector3> cube;
+ cube.push_back(Vector3(-5, -5, -5));
+ cube.push_back(Vector3(5, -5, -5));
+ cube.push_back(Vector3(-5, 5, -5));
+ cube.push_back(Vector3(5, 5, -5));
+ cube.push_back(Vector3(-5, -5, 5));
+ cube.push_back(Vector3(5, -5, 5));
+ cube.push_back(Vector3(-5, 5, 5));
+ cube.push_back(Vector3(5, 5, 5));
+ tt.push_back(Case(Geometry3D::build_box_planes(Vector3(5, 5, 5)), cube));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Vector<Vector3> vectors = Geometry3D::compute_convex_mesh_points(&current_case.mesh[0], current_case.mesh.size());
+ CHECK(vectors == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Get Closest Point To Segment") {
+ struct Case {
+ Vector3 point;
+ Vector<Vector3> segment;
+ Vector3 want;
+ Case(){};
+ Case(Vector3 p_point, Vector<Vector3> p_segment, Vector3 p_want) :
+ point(p_point), segment(p_segment), want(p_want){};
+ };
+ Vector<Case> tt;
+ Vector<Vector3> test_segment;
+ test_segment.push_back(Vector3(1, 1, 1));
+ test_segment.push_back(Vector3(5, 5, 5));
+ tt.push_back(Case(Vector3(2, 1, 4), test_segment, Vector3(2.33333, 2.33333, 2.33333)));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ Vector3 output = Geometry3D::get_closest_point_to_segment(current_case.point, &current_case.segment[0]);
+ CHECK(output.is_equal_approx(current_case.want));
+ }
+}
+
+TEST_CASE("[Geometry3D] Plane and Box Overlap") {
+ struct Case {
+ Vector3 normal, max_box;
+ float d;
+ bool want;
+ Case(){};
+ Case(Vector3 p_normal, float p_d, Vector3 p_max_box, bool p_want) :
+ normal(p_normal), max_box(p_max_box), d(p_d), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(3, 4, 2), 5, Vector3(5, 5, 5), true));
+ tt.push_back(Case(Vector3(0, 1, 0), -10, Vector3(5, 5, 5), false));
+ tt.push_back(Case(Vector3(1, 0, 0), -6, Vector3(5, 5, 5), false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool overlap = Geometry3D::planeBoxOverlap(current_case.normal, current_case.d, current_case.max_box);
+ CHECK(overlap == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Is Point in Projected Triangle") {
+ struct Case {
+ Vector3 point, v_1, v_2, v_3;
+ bool want;
+ Case(){};
+ Case(Vector3 p_point, Vector3 p_v_1, Vector3 p_v_2, Vector3 p_v_3, bool p_want) :
+ point(p_point), v_1(p_v_1), v_2(p_v_2), v_3(p_v_3), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(1, 1, 0), Vector3(3, 0, 0), Vector3(0, 3, 0), Vector3(-3, 0, 0), true));
+ tt.push_back(Case(Vector3(5, 1, 0), Vector3(3, 0, 0), Vector3(0, 3, 0), Vector3(-3, 0, 0), false));
+ tt.push_back(Case(Vector3(3, 0, 0), Vector3(3, 0, 0), Vector3(0, 3, 0), Vector3(-3, 0, 0), true));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::point_in_projected_triangle(current_case.point, current_case.v_1, current_case.v_2, current_case.v_3);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Does Ray Intersect Triangle") {
+ struct Case {
+ Vector3 from, direction, v_1, v_2, v_3;
+ Vector3 *result;
+ bool want;
+ Case(){};
+ Case(Vector3 p_from, Vector3 p_direction, Vector3 p_v_1, Vector3 p_v_2, Vector3 p_v_3, bool p_want) :
+ from(p_from), direction(p_direction), v_1(p_v_1), v_2(p_v_2), v_3(p_v_3), result(nullptr), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(0, 1, 1), Vector3(0, 0, -10), Vector3(0, 3, 0), Vector3(-3, 0, 0), Vector3(3, 0, 0), true));
+ tt.push_back(Case(Vector3(5, 10, 1), Vector3(0, 0, -10), Vector3(0, 3, 0), Vector3(-3, 0, 0), Vector3(3, 0, 0), false));
+ tt.push_back(Case(Vector3(0, 1, 1), Vector3(0, 0, 10), Vector3(0, 3, 0), Vector3(-3, 0, 0), Vector3(3, 0, 0), false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::ray_intersects_triangle(current_case.from, current_case.direction, current_case.v_1, current_case.v_2, current_case.v_3, current_case.result);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Does Segment Intersect Convex") {
+ struct Case {
+ Vector3 from, to;
+ Vector<Plane> planes;
+ Vector3 *result, *normal;
+ bool want;
+ Case(){};
+ Case(Vector3 p_from, Vector3 p_to, Vector<Plane> p_planes, bool p_want) :
+ from(p_from), to(p_to), planes(p_planes), result(nullptr), normal(nullptr), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(0, 0, 0), Geometry3D::build_box_planes(Vector3(5, 5, 5)), true));
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(5, 5, 5), Geometry3D::build_box_planes(Vector3(5, 5, 5)), true));
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(6, 5, 5), Geometry3D::build_box_planes(Vector3(5, 5, 5)), false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::segment_intersects_convex(current_case.from, current_case.to, &current_case.planes[0], current_case.planes.size(), current_case.result, current_case.normal);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Segment Intersects Cylinder") {
+ struct Case {
+ Vector3 from, to;
+ real_t height, radius;
+ Vector3 *result, *normal;
+ bool want;
+ Case(){};
+ Case(Vector3 p_from, Vector3 p_to, real_t p_height, real_t p_radius, bool p_want) :
+ from(p_from), to(p_to), height(p_height), radius(p_radius), result(nullptr), normal(nullptr), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(0, 0, 0), 5, 5, true));
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(6, 6, 6), 5, 5, false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::segment_intersects_cylinder(current_case.from, current_case.to, current_case.height, current_case.radius, current_case.result, current_case.normal);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Segment Intersects Cylinder") {
+ struct Case {
+ Vector3 from, to, sphere_pos;
+ real_t radius;
+ Vector3 *result, *normal;
+ bool want;
+ Case(){};
+ Case(Vector3 p_from, Vector3 p_to, Vector3 p_sphere_pos, real_t p_radius, bool p_want) :
+ from(p_from), to(p_to), sphere_pos(p_sphere_pos), radius(p_radius), result(nullptr), normal(nullptr), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(0, 0, 0), Vector3(0, 0, 0), 5, true));
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(0, 0, 2.5), Vector3(0, 0, 0), 5, true));
+ tt.push_back(Case(Vector3(10, 10, 10), Vector3(5, 5, 5), Vector3(0, 0, 0), 5, false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::segment_intersects_sphere(current_case.from, current_case.to, current_case.sphere_pos, current_case.radius, current_case.result, current_case.normal);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Segment Intersects Triangle") {
+ struct Case {
+ Vector3 from, to, v_1, v_2, v_3, *result;
+ bool want;
+ Case(){};
+ Case(Vector3 p_from, Vector3 p_to, Vector3 p_v_1, Vector3 p_v_2, Vector3 p_v_3, bool p_want) :
+ from(p_from), to(p_to), v_1(p_v_1), v_2(p_v_2), v_3(p_v_3), result(nullptr), want(p_want){};
+ };
+ Vector<Case> tt;
+ tt.push_back(Case(Vector3(1, 1, 1), Vector3(-1, -1, -1), Vector3(-3, 0, 0), Vector3(0, 3, 0), Vector3(3, 0, 0), true));
+ tt.push_back(Case(Vector3(1, 1, 1), Vector3(3, 0, 0), Vector3(-3, 0, 0), Vector3(0, 3, 0), Vector3(3, 0, 0), true));
+ tt.push_back(Case(Vector3(1, 1, 1), Vector3(10, -1, -1), Vector3(-3, 0, 0), Vector3(0, 3, 0), Vector3(3, 0, 0), false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::segment_intersects_triangle(current_case.from, current_case.to, current_case.v_1, current_case.v_2, current_case.v_3, current_case.result);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Triangle and Box Overlap") {
+ struct Case {
+ Vector3 box_centre;
+ Vector3 box_half_size;
+ Vector3 *tri_verts;
+ bool want;
+ Case(){};
+ Case(Vector3 p_centre, Vector3 p_half_size, Vector3 *p_verts, bool p_want) :
+ box_centre(p_centre), box_half_size(p_half_size), tri_verts(p_verts), want(p_want){};
+ };
+ Vector<Case> tt;
+ Vector3 GoodTriangle[3] = { Vector3(3, 2, 3), Vector3(2, 2, 1), Vector3(2, 1, 1) };
+ tt.push_back(Case(Vector3(0, 0, 0), Vector3(5, 5, 5), GoodTriangle, true));
+ Vector3 BadTriangle[3] = { Vector3(100, 100, 100), Vector3(-100, -100, -100), Vector3(10, 10, 10) };
+ tt.push_back(Case(Vector3(1000, 1000, 1000), Vector3(1, 1, 1), BadTriangle, false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::triangle_box_overlap(current_case.box_centre, current_case.box_half_size, current_case.tri_verts);
+ CHECK(output == current_case.want);
+ }
+}
+
+TEST_CASE("[Geometry3D] Triangle and Sphere Intersect") {
+ struct Case {
+ Vector<Vector3> triangle;
+ Vector3 normal, sphere_pos, triangle_contact, sphere_contact;
+ real_t sphere_radius;
+ bool want;
+ Case(){};
+ Case(Vector<Vector3> p_triangle, Vector3 p_normal, Vector3 p_sphere_pos, real_t p_sphere_radius, bool p_want) :
+ triangle(p_triangle), normal(p_normal), sphere_pos(p_sphere_pos), triangle_contact(Vector3()), sphere_contact(Vector3()), sphere_radius(p_sphere_radius), want(p_want){};
+ };
+ Vector<Case> tt;
+ Vector<Vector3> triangle;
+ triangle.push_back(Vector3(3, 0, 0));
+ triangle.push_back(Vector3(-3, 0, 0));
+ triangle.push_back(Vector3(0, 3, 0));
+ tt.push_back(Case(triangle, Vector3(0, -1, 0), Vector3(0, 0, 0), 5, true));
+ tt.push_back(Case(triangle, Vector3(0, 1, 0), Vector3(0, 0, 0), 5, true));
+ tt.push_back(Case(triangle, Vector3(0, 1, 0), Vector3(20, 0, 0), 5, false));
+ for (int i = 0; i < tt.size(); ++i) {
+ Case current_case = tt[i];
+ bool output = Geometry3D::triangle_sphere_intersection_test(&current_case.triangle[0], current_case.normal, current_case.sphere_pos, current_case.sphere_radius, current_case.triangle_contact, current_case.sphere_contact);
+ CHECK(output == current_case.want);
+ }
+}
+} // namespace TestGeometry3D
+
+#endif // TEST_GEOMETRY_3D_H
diff --git a/tests/core/math/test_math.cpp b/tests/core/math/test_math.cpp
new file mode 100644
index 0000000000..7b1f3af2b9
--- /dev/null
+++ b/tests/core/math/test_math.cpp
@@ -0,0 +1,690 @@
+/*************************************************************************/
+/* test_math.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "test_math.h"
+
+#include "core/math/camera_matrix.h"
+#include "core/math/delaunay_3d.h"
+#include "core/math/geometry_2d.h"
+#include "core/os/main_loop.h"
+#include "core/os/os.h"
+
+namespace TestMath {
+
+class GetClassAndNamespace {
+ String code;
+ int idx;
+ int line;
+ String error_str;
+ bool error;
+ Variant value;
+
+ String class_name;
+
+ enum Token {
+ TK_BRACKET_OPEN,
+ TK_BRACKET_CLOSE,
+ TK_CURLY_BRACKET_OPEN,
+ TK_CURLY_BRACKET_CLOSE,
+ TK_PERIOD,
+ TK_COLON,
+ TK_COMMA,
+ TK_SYMBOL,
+ TK_IDENTIFIER,
+ TK_STRING,
+ TK_NUMBER,
+ TK_EOF,
+ TK_ERROR
+ };
+
+ Token get_token() {
+ while (true) {
+ switch (code[idx]) {
+ case '\n': {
+ line++;
+ idx++;
+ break;
+ };
+ case 0: {
+ return TK_EOF;
+
+ } break;
+ case '{': {
+ idx++;
+ return TK_CURLY_BRACKET_OPEN;
+ };
+ case '}': {
+ idx++;
+ return TK_CURLY_BRACKET_CLOSE;
+ };
+ case '[': {
+ idx++;
+ return TK_BRACKET_OPEN;
+ };
+ case ']': {
+ idx++;
+ return TK_BRACKET_CLOSE;
+ };
+ case ':': {
+ idx++;
+ return TK_COLON;
+ };
+ case ',': {
+ idx++;
+ return TK_COMMA;
+ };
+ case '.': {
+ idx++;
+ return TK_PERIOD;
+ };
+ case '#': {
+ //compiler directive
+ while (code[idx] != '\n' && code[idx] != 0) {
+ idx++;
+ }
+ continue;
+ } break;
+ case '/': {
+ switch (code[idx + 1]) {
+ case '*': { // block comment
+
+ idx += 2;
+ while (true) {
+ if (code[idx] == 0) {
+ error_str = "Unterminated comment";
+ error = true;
+ return TK_ERROR;
+ } else if (code[idx] == '*' && code[idx + 1] == '/') {
+ idx += 2;
+ break;
+ } else if (code[idx] == '\n') {
+ line++;
+ }
+
+ idx++;
+ }
+
+ } break;
+ case '/': { // line comment skip
+
+ while (code[idx] != '\n' && code[idx] != 0) {
+ idx++;
+ }
+
+ } break;
+ default: {
+ value = "/";
+ idx++;
+ return TK_SYMBOL;
+ }
+ }
+
+ continue; // a comment
+ } break;
+ case '\'':
+ case '"': {
+ char32_t begin_str = code[idx];
+ idx++;
+ String tk_string = String();
+ while (true) {
+ if (code[idx] == 0) {
+ error_str = "Unterminated String";
+ error = true;
+ return TK_ERROR;
+ } else if (code[idx] == begin_str) {
+ idx++;
+ break;
+ } else if (code[idx] == '\\') {
+ //escaped characters...
+ idx++;
+ char32_t next = code[idx];
+ if (next == 0) {
+ error_str = "Unterminated String";
+ error = true;
+ return TK_ERROR;
+ }
+ char32_t res = 0;
+
+ switch (next) {
+ case 'b':
+ res = 8;
+ break;
+ case 't':
+ res = 9;
+ break;
+ case 'n':
+ res = 10;
+ break;
+ case 'f':
+ res = 12;
+ break;
+ case 'r':
+ res = 13;
+ break;
+ case '\"':
+ res = '\"';
+ break;
+ case '\\':
+ res = '\\';
+ break;
+ default: {
+ res = next;
+ } break;
+ }
+
+ tk_string += res;
+
+ } else {
+ if (code[idx] == '\n') {
+ line++;
+ }
+ tk_string += code[idx];
+ }
+ idx++;
+ }
+
+ value = tk_string;
+
+ return TK_STRING;
+
+ } break;
+ default: {
+ if (code[idx] <= 32) {
+ idx++;
+ break;
+ }
+
+ if ((code[idx] >= 33 && code[idx] <= 47) || (code[idx] >= 58 && code[idx] <= 64) || (code[idx] >= 91 && code[idx] <= 96) || (code[idx] >= 123 && code[idx] <= 127)) {
+ value = String::chr(code[idx]);
+ idx++;
+ return TK_SYMBOL;
+ }
+
+ if (code[idx] == '-' || (code[idx] >= '0' && code[idx] <= '9')) {
+ //a number
+ const char32_t *rptr;
+ double number = String::to_float(&code[idx], &rptr);
+ idx += (rptr - &code[idx]);
+ value = number;
+ return TK_NUMBER;
+
+ } else if ((code[idx] >= 'A' && code[idx] <= 'Z') || (code[idx] >= 'a' && code[idx] <= 'z') || code[idx] > 127) {
+ String id;
+
+ while ((code[idx] >= 'A' && code[idx] <= 'Z') || (code[idx] >= 'a' && code[idx] <= 'z') || code[idx] > 127) {
+ id += code[idx];
+ idx++;
+ }
+
+ value = id;
+ return TK_IDENTIFIER;
+ } else {
+ error_str = "Unexpected character.";
+ error = true;
+ return TK_ERROR;
+ }
+ }
+ }
+ }
+ }
+
+public:
+ Error parse(const String &p_code, const String &p_known_class_name = String()) {
+ code = p_code;
+ idx = 0;
+ line = 0;
+ error_str = String();
+ error = false;
+ value = Variant();
+ class_name = String();
+
+ bool use_next_class = false;
+ Token tk = get_token();
+
+ Map<int, String> namespace_stack;
+ int curly_stack = 0;
+
+ while (!error || tk != TK_EOF) {
+ if (tk == TK_BRACKET_OPEN) {
+ tk = get_token();
+ if (tk == TK_IDENTIFIER && String(value) == "ScriptClass") {
+ if (get_token() == TK_BRACKET_CLOSE) {
+ use_next_class = true;
+ }
+ }
+ } else if (tk == TK_IDENTIFIER && String(value) == "class") {
+ tk = get_token();
+ if (tk == TK_IDENTIFIER) {
+ String name = value;
+ if (use_next_class || p_known_class_name == name) {
+ for (const KeyValue<int, String> &E : namespace_stack) {
+ class_name += E.value + ".";
+ }
+ class_name += String(value);
+ break;
+ }
+ }
+
+ } else if (tk == TK_IDENTIFIER && String(value) == "namespace") {
+ String name;
+ int at_level = curly_stack;
+ while (true) {
+ tk = get_token();
+ if (tk == TK_IDENTIFIER) {
+ name += String(value);
+ }
+
+ tk = get_token();
+ if (tk == TK_PERIOD) {
+ name += ".";
+ } else if (tk == TK_CURLY_BRACKET_OPEN) {
+ curly_stack++;
+ break;
+ } else {
+ break; //whathever else
+ }
+ }
+
+ if (!name.is_empty()) {
+ namespace_stack[at_level] = name;
+ }
+
+ } else if (tk == TK_CURLY_BRACKET_OPEN) {
+ curly_stack++;
+ } else if (tk == TK_CURLY_BRACKET_CLOSE) {
+ curly_stack--;
+ if (namespace_stack.has(curly_stack)) {
+ namespace_stack.erase(curly_stack);
+ }
+ }
+
+ tk = get_token();
+ }
+
+ if (error) {
+ return ERR_PARSE_ERROR;
+ }
+
+ return OK;
+ }
+
+ String get_error() {
+ return error_str;
+ }
+
+ String get_class() {
+ return class_name;
+ }
+};
+
+void test_vec(Plane p_vec) {
+ CameraMatrix cm;
+ cm.set_perspective(45, 1, 0, 100);
+ Plane v0 = cm.xform4(p_vec);
+
+ print_line("out: " + v0);
+ v0.normal.z = (v0.d / 100.0 * 2.0 - 1.0) * v0.d;
+ print_line("out_F: " + v0);
+}
+
+uint32_t ihash(uint32_t a) {
+ a = (a + 0x7ed55d16) + (a << 12);
+ a = (a ^ 0xc761c23c) ^ (a >> 19);
+ a = (a + 0x165667b1) + (a << 5);
+ a = (a + 0xd3a2646c) ^ (a << 9);
+ a = (a + 0xfd7046c5) + (a << 3);
+ a = (a ^ 0xb55a4f09) ^ (a >> 16);
+ return a;
+}
+
+uint32_t ihash2(uint32_t a) {
+ a = (a ^ 61) ^ (a >> 16);
+ a = a + (a << 3);
+ a = a ^ (a >> 4);
+ a = a * 0x27d4eb2d;
+ a = a ^ (a >> 15);
+ return a;
+}
+
+uint32_t ihash3(uint32_t a) {
+ a = (a + 0x479ab41d) + (a << 8);
+ a = (a ^ 0xe4aa10ce) ^ (a >> 5);
+ a = (a + 0x9942f0a6) - (a << 14);
+ a = (a ^ 0x5aedd67d) ^ (a >> 3);
+ a = (a + 0x17bea992) + (a << 7);
+ return a;
+}
+
+MainLoop *test() {
+ {
+ Vector<Vector3> points;
+ points.push_back(Vector3(0, 0, 0));
+ points.push_back(Vector3(0, 0, 1));
+ points.push_back(Vector3(0, 1, 0));
+ points.push_back(Vector3(0, 1, 1));
+ points.push_back(Vector3(1, 1, 0));
+ points.push_back(Vector3(1, 0, 0));
+ points.push_back(Vector3(1, 0, 1));
+ points.push_back(Vector3(1, 1, 1));
+
+ for (int i = 0; i < 800; i++) {
+ points.push_back(Vector3(Math::randf() * 2.0 - 1.0, Math::randf() * 2.0 - 1.0, Math::randf() * 2.0 - 1.0) * Vector3(25, 30, 33));
+ }
+
+ Vector<Delaunay3D::OutputSimplex> os = Delaunay3D::tetrahedralize(points);
+ print_line("simplices in the end: " + itos(os.size()));
+ for (int i = 0; i < os.size(); i++) {
+ print_line("Simplex " + itos(i) + ": ");
+ print_line(points[os[i].points[0]]);
+ print_line(points[os[i].points[1]]);
+ print_line(points[os[i].points[2]]);
+ print_line(points[os[i].points[3]]);
+ }
+
+ {
+ FileAccessRef f = FileAccess::open("res://bsp.obj", FileAccess::WRITE);
+ for (int i = 0; i < os.size(); i++) {
+ f->store_line("o Simplex" + itos(i));
+ for (int j = 0; j < 4; j++) {
+ f->store_line(vformat("v %f %f %f", points[os[i].points[j]].x, points[os[i].points[j]].y, points[os[i].points[j]].z));
+ }
+ static const int face_order[4][3] = {
+ { 1, 2, 3 },
+ { 1, 3, 4 },
+ { 1, 2, 4 },
+ { 2, 3, 4 }
+ };
+
+ for (int j = 0; j < 4; j++) {
+ f->store_line(vformat("f %d %d %d", 4 * i + face_order[j][0], 4 * i + face_order[j][1], 4 * i + face_order[j][2]));
+ }
+ }
+ f->close();
+ }
+
+ return nullptr;
+ }
+
+ {
+ float r = 1;
+ float g = 0.5;
+ float b = 0.1;
+
+ const float pow2to9 = 512.0f;
+ const float B = 15.0f;
+ const float N = 9.0f;
+
+ float sharedexp = 65408.000f;
+
+ float cRed = MAX(0.0f, MIN(sharedexp, r));
+ float cGreen = MAX(0.0f, MIN(sharedexp, g));
+ float cBlue = MAX(0.0f, MIN(sharedexp, b));
+
+ float cMax = MAX(cRed, MAX(cGreen, cBlue));
+
+ float expp = MAX(-B - 1.0f, floor(Math::log(cMax) / Math_LN2)) + 1.0f + B;
+
+ float sMax = (float)floor((cMax / Math::pow(2.0f, expp - B - N)) + 0.5f);
+
+ float exps = expp + 1.0f;
+
+ if (0.0 <= sMax && sMax < pow2to9) {
+ exps = expp;
+ }
+
+ float sRed = Math::floor((cRed / pow(2.0f, exps - B - N)) + 0.5f);
+ float sGreen = Math::floor((cGreen / pow(2.0f, exps - B - N)) + 0.5f);
+ float sBlue = Math::floor((cBlue / pow(2.0f, exps - B - N)) + 0.5f);
+
+ print_line("R: " + rtos(sRed) + " G: " + rtos(sGreen) + " B: " + rtos(sBlue) + " EXP: " + rtos(exps));
+
+ uint32_t rgbe = (Math::fast_ftoi(sRed) & 0x1FF) | ((Math::fast_ftoi(sGreen) & 0x1FF) << 9) | ((Math::fast_ftoi(sBlue) & 0x1FF) << 18) | ((Math::fast_ftoi(exps) & 0x1F) << 27);
+
+ float rb = rgbe & 0x1ff;
+ float gb = (rgbe >> 9) & 0x1ff;
+ float bb = (rgbe >> 18) & 0x1ff;
+ float eb = (rgbe >> 27);
+ float mb = Math::pow(2.0, eb - 15.0 - 9.0);
+ float rd = rb * mb;
+ float gd = gb * mb;
+ float bd = bb * mb;
+
+ print_line("RGBE: " + Color(rd, gd, bd));
+ }
+
+ Vector<int> ints;
+ ints.resize(20);
+
+ {
+ int *w;
+ w = ints.ptrw();
+ for (int i = 0; i < ints.size(); i++) {
+ w[i] = i;
+ }
+ }
+
+ Vector<int> posho = ints;
+
+ {
+ const int *r = posho.ptr();
+ for (int i = 0; i < posho.size(); i++) {
+ print_line(itos(i) + " : " + itos(r[i]));
+ }
+ }
+
+ List<String> cmdlargs = OS::get_singleton()->get_cmdline_args();
+
+ if (cmdlargs.is_empty()) {
+ //try editor!
+ return nullptr;
+ }
+
+ String test = cmdlargs.back()->get();
+ if (test == "math") {
+ // Not a file name but the test name, abort.
+ // FIXME: This test is ugly as heck, needs fixing :)
+ return nullptr;
+ }
+
+ FileAccess *fa = FileAccess::open(test, FileAccess::READ);
+ ERR_FAIL_COND_V_MSG(!fa, nullptr, "Could not open file: " + test);
+
+ Vector<uint8_t> buf;
+ uint64_t flen = fa->get_length();
+ buf.resize(fa->get_length() + 1);
+ fa->get_buffer(buf.ptrw(), flen);
+ buf.write[flen] = 0;
+
+ String code;
+ code.parse_utf8((const char *)&buf[0]);
+
+ GetClassAndNamespace getclass;
+ if (getclass.parse(code)) {
+ print_line("Parse error: " + getclass.get_error());
+ } else {
+ print_line("Found class: " + getclass.get_class());
+ }
+
+ {
+ Vector<int> hashes;
+ List<StringName> tl;
+ ClassDB::get_class_list(&tl);
+
+ for (const StringName &E : tl) {
+ Vector<uint8_t> m5b = E.operator String().md5_buffer();
+ hashes.push_back(hashes.size());
+ }
+
+ for (int i = nearest_shift(hashes.size()); i < 20; i++) {
+ bool success = true;
+ for (int s = 0; s < 10000; s++) {
+ Set<uint32_t> existing;
+ success = true;
+
+ for (int j = 0; j < hashes.size(); j++) {
+ uint32_t eh = ihash2(ihash3(hashes[j] + ihash(s) + s)) & ((1 << i) - 1);
+ if (existing.has(eh)) {
+ success = false;
+ break;
+ }
+ existing.insert(eh);
+ }
+
+ if (success) {
+ print_line("success at " + itos(i) + "/" + itos(nearest_shift(hashes.size())) + " shift " + itos(s));
+ break;
+ }
+ }
+ if (success) {
+ break;
+ }
+ }
+
+ print_line("DONE");
+ }
+
+ {
+ print_line("NUM: " + itos(-128));
+ }
+
+ {
+ Vector3 v(1, 2, 3);
+ v.normalize();
+ real_t a = 0.3;
+
+ Basis m(v, a);
+
+ Vector3 v2(7, 3, 1);
+ v2.normalize();
+ real_t a2 = 0.8;
+
+ Basis m2(v2, a2);
+
+ Quaternion q = m;
+ Quaternion q2 = m2;
+
+ Basis m3 = m.inverse() * m2;
+ Quaternion q3 = (q.inverse() * q2); //.normalized();
+
+ print_line(Quaternion(m3));
+ print_line(q3);
+
+ print_line("before v: " + v + " a: " + rtos(a));
+ q.get_axis_angle(v, a);
+ print_line("after v: " + v + " a: " + rtos(a));
+ }
+
+ String ret;
+
+ List<String> args;
+ args.push_back("-l");
+ Error err = OS::get_singleton()->execute("/bin/ls", args, &ret);
+ print_line("error: " + itos(err));
+ print_line(ret);
+
+ Basis m3;
+ m3.rotate(Vector3(1, 0, 0), 0.2);
+ m3.rotate(Vector3(0, 1, 0), 1.77);
+ m3.rotate(Vector3(0, 0, 1), 212);
+ Basis m32;
+ m32.set_euler(m3.get_euler());
+ print_line("ELEULEEEEEEEEEEEEEEEEEER: " + m3.get_euler() + " vs " + m32.get_euler());
+
+ {
+ Dictionary d;
+ d["momo"] = 1;
+ Dictionary b = d;
+ b["44"] = 4;
+ }
+
+ print_line("inters: " + rtos(Geometry2D::segment_intersects_circle(Vector2(-5, 0), Vector2(-2, 0), Vector2(), 1.0)));
+
+ print_line("cross: " + Vector3(1, 2, 3).cross(Vector3(4, 5, 7)));
+ print_line("dot: " + rtos(Vector3(1, 2, 3).dot(Vector3(4, 5, 7))));
+ print_line("abs: " + Vector3(-1, 2, -3).abs());
+ print_line("distance_to: " + rtos(Vector3(1, 2, 3).distance_to(Vector3(4, 5, 7))));
+ print_line("distance_squared_to: " + rtos(Vector3(1, 2, 3).distance_squared_to(Vector3(4, 5, 7))));
+ print_line("plus: " + (Vector3(1, 2, 3) + Vector3(Vector3(4, 5, 7))));
+ print_line("minus: " + (Vector3(1, 2, 3) - Vector3(Vector3(4, 5, 7))));
+ print_line("mul: " + (Vector3(1, 2, 3) * Vector3(Vector3(4, 5, 7))));
+ print_line("div: " + (Vector3(1, 2, 3) / Vector3(Vector3(4, 5, 7))));
+ print_line("mul scalar: " + (Vector3(1, 2, 3) * 2.0));
+ print_line("premul scalar: " + (2.0 * Vector3(1, 2, 3)));
+ print_line("div scalar: " + (Vector3(1, 2, 3) / 3.0));
+ print_line("length: " + rtos(Vector3(1, 2, 3).length()));
+ print_line("length squared: " + rtos(Vector3(1, 2, 3).length_squared()));
+ print_line("normalized: " + Vector3(1, 2, 3).normalized());
+ print_line("inverse: " + Vector3(1, 2, 3).inverse());
+
+ {
+ Vector3 v(4, 5, 7);
+ v.normalize();
+ print_line("normalize: " + v);
+ }
+
+ {
+ Vector3 v(4, 5, 7);
+ v += Vector3(1, 2, 3);
+ print_line("+=: " + v);
+ }
+
+ {
+ Vector3 v(4, 5, 7);
+ v -= Vector3(1, 2, 3);
+ print_line("-=: " + v);
+ }
+
+ {
+ Vector3 v(4, 5, 7);
+ v *= Vector3(1, 2, 3);
+ print_line("*=: " + v);
+ }
+
+ {
+ Vector3 v(4, 5, 7);
+ v /= Vector3(1, 2, 3);
+ print_line("/=: " + v);
+ }
+
+ {
+ Vector3 v(4, 5, 7);
+ v *= 2.0;
+ print_line("scalar *=: " + v);
+ }
+
+ {
+ Vector3 v(4, 5, 7);
+ v /= 2.0;
+ print_line("scalar /=: " + v);
+ }
+
+ return nullptr;
+}
+} // namespace TestMath
diff --git a/tests/core/math/test_math.h b/tests/core/math/test_math.h
new file mode 100644
index 0000000000..a8aa8f6847
--- /dev/null
+++ b/tests/core/math/test_math.h
@@ -0,0 +1,41 @@
+/*************************************************************************/
+/* test_math.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_MATH_H
+#define TEST_MATH_H
+
+class MainLoop;
+
+namespace TestMath {
+
+MainLoop *test();
+}
+
+#endif
diff --git a/tests/core/math/test_random_number_generator.h b/tests/core/math/test_random_number_generator.h
new file mode 100644
index 0000000000..e8cd47b9d7
--- /dev/null
+++ b/tests/core/math/test_random_number_generator.h
@@ -0,0 +1,275 @@
+/*************************************************************************/
+/* test_random_number_generator.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_RANDOM_NUMBER_GENERATOR_H
+#define TEST_RANDOM_NUMBER_GENERATOR_H
+
+#include "core/math/random_number_generator.h"
+#include "tests/test_macros.h"
+
+namespace TestRandomNumberGenerator {
+
+TEST_CASE("[RandomNumberGenerator] Float") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(0);
+
+ INFO("Should give float between 0.0 and 1.0.");
+ for (int i = 0; i < 1000; i++) {
+ real_t n = rng->randf();
+ CHECK(n >= 0.0);
+ CHECK(n <= 1.0);
+ }
+}
+
+TEST_CASE("[RandomNumberGenerator] Integer range via modulo") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(0);
+
+ INFO("Should give integer between 0 and 100.");
+ for (int i = 0; i < 1000; i++) {
+ uint32_t n = rng->randi() % 100;
+ CHECK(n >= 0);
+ CHECK(n <= 100);
+ }
+}
+
+TEST_CASE_MAY_FAIL("[RandomNumberGenerator] Integer 32 bit") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(0); // Change the seed if this fails.
+
+ bool higher = false;
+ int i;
+ for (i = 0; i < 1000; i++) {
+ uint32_t n = rng->randi();
+ if (n > 0x0fff'ffff) {
+ higher = true;
+ break;
+ }
+ }
+ INFO("Current seed: ", rng->get_seed());
+ INFO("Current iteration: ", i);
+ CHECK_MESSAGE(higher, "Given current seed, this should give an integer higher than 0x0fff'ffff at least once.");
+}
+
+TEST_CASE("[RandomNumberGenerator] Float and integer range") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(0);
+ uint64_t initial_state = rng->get_state();
+ uint32_t initial_seed = rng->get_seed();
+
+ INFO("Should give float between -100.0 and 100.0, base test.");
+ for (int i = 0; i < 1000; i++) {
+ real_t n0 = rng->randf_range(-100.0, 100.0);
+ CHECK(n0 >= -100);
+ CHECK(n0 <= 100);
+ }
+
+ rng->randomize();
+ INFO("Should give float between -75.0 and 75.0.");
+ INFO("Shouldn't be affected by randomize.");
+ for (int i = 0; i < 1000; i++) {
+ real_t n1 = rng->randf_range(-75.0, 75.0);
+ CHECK(n1 >= -75);
+ CHECK(n1 <= 75);
+ }
+
+ rng->set_state(initial_state);
+ INFO("Should give integer between -50 and 50.");
+ INFO("Shouldn't be affected by set_state.");
+ for (int i = 0; i < 1000; i++) {
+ real_t n2 = rng->randi_range(-50, 50);
+ CHECK(n2 >= -50);
+ CHECK(n2 <= 50);
+ }
+
+ rng->set_seed(initial_seed);
+ INFO("Should give integer between -25 and 25.");
+ INFO("Shouldn't be affected by set_seed.");
+ for (int i = 0; i < 1000; i++) {
+ int32_t n3 = rng->randi_range(-25, 25);
+ CHECK(n3 >= -25);
+ CHECK(n3 <= 25);
+ }
+
+ rng->randf();
+ rng->randf();
+
+ INFO("Should give float between -10.0 and 10.0.");
+ INFO("Shouldn't be affected after generating new numbers.");
+ for (int i = 0; i < 1000; i++) {
+ real_t n4 = rng->randf_range(-10.0, 10.0);
+ CHECK(n4 >= -10);
+ CHECK(n4 <= 10);
+ }
+
+ rng->randi();
+ rng->randi();
+
+ INFO("Should give integer between -5 and 5.");
+ INFO("Shouldn't be affected after generating new numbers.");
+ for (int i = 0; i < 1000; i++) {
+ real_t n5 = rng->randf_range(-5, 5);
+ CHECK(n5 >= -5);
+ CHECK(n5 <= 5);
+ }
+}
+
+TEST_CASE_MAY_FAIL("[RandomNumberGenerator] Normal distribution") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(1); // Change the seed if this fails.
+ INFO("Should give a number between -5 to 5 (5 std deviations away; above 99.7% chance it will be in this range).");
+ INFO("Standard randfn function call.");
+ for (int i = 0; i < 100; i++) {
+ real_t n = rng->randfn();
+ CHECK(n >= -5);
+ CHECK(n <= 5);
+ }
+
+ INFO("Should give number between -5 to 5 after multiple randi/randf calls.");
+ INFO("5 std deviations away; above 99.7% chance it will be in this range.");
+ rng->randf();
+ rng->randi();
+ for (int i = 0; i < 100; i++) {
+ real_t n = rng->randfn();
+ CHECK(n >= -5);
+ CHECK(n <= 5);
+ }
+
+ INFO("Checks if user defined mean and deviation work properly.");
+ INFO("5 std deviations away; above 99.7% chance it will be in this range.");
+ for (int i = 0; i < 100; i++) {
+ real_t n = rng->randfn(5, 10);
+ CHECK(n >= -45);
+ CHECK(n <= 55);
+ }
+
+ INFO("Checks if randfn works with changed seeds.");
+ INFO("5 std deviations away; above 99.7% chance it will be in this range.");
+ rng->randomize();
+ for (int i = 0; i < 100; i++) {
+ real_t n = rng->randfn(3, 3);
+ CHECK(n >= -12);
+ CHECK(n <= 18);
+ }
+}
+
+TEST_CASE("[RandomNumberGenerator] Zero for first number immediately after seeding") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(0);
+ uint32_t n1 = rng->randi();
+ uint32_t n2 = rng->randi();
+ INFO("Initial random values: ", n1, " ", n2);
+ CHECK(n1 != 0);
+
+ rng->set_seed(1);
+ uint32_t n3 = rng->randi();
+ uint32_t n4 = rng->randi();
+ INFO("Values after changing the seed: ", n3, " ", n4);
+ CHECK(n3 != 0);
+}
+
+TEST_CASE("[RandomNumberGenerator] Restore state") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->randomize();
+ uint64_t last_seed = rng->get_seed();
+ INFO("Current seed: ", last_seed);
+
+ rng->randi();
+ rng->randi();
+
+ CHECK_MESSAGE(rng->get_seed() == last_seed,
+ "The seed should remain the same after generating some numbers");
+
+ uint64_t saved_state = rng->get_state();
+ INFO("Current state: ", saved_state);
+
+ real_t f1_before = rng->randf();
+ real_t f2_before = rng->randf();
+ INFO("This seed produces: ", f1_before, " ", f2_before);
+
+ // Restore now.
+ rng->set_state(saved_state);
+
+ real_t f1_after = rng->randf();
+ real_t f2_after = rng->randf();
+ INFO("Resetting the state produces: ", f1_after, " ", f2_after);
+
+ String msg = "Should restore the sequence of numbers after resetting the state";
+ CHECK_MESSAGE(f1_before == f1_after, msg);
+ CHECK_MESSAGE(f2_before == f2_after, msg);
+}
+
+TEST_CASE("[RandomNumberGenerator] Restore from seed") {
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ rng->set_seed(0);
+ INFO("Current seed: ", rng->get_seed());
+ uint32_t s0_1_before = rng->randi();
+ uint32_t s0_2_before = rng->randi();
+ INFO("This seed produces: ", s0_1_before, " ", s0_2_before);
+
+ rng->set_seed(9000);
+ INFO("Current seed: ", rng->get_seed());
+ uint32_t s9000_1 = rng->randi();
+ uint32_t s9000_2 = rng->randi();
+ INFO("This seed produces: ", s9000_1, " ", s9000_2);
+
+ rng->set_seed(0);
+ INFO("Current seed: ", rng->get_seed());
+ uint32_t s0_1_after = rng->randi();
+ uint32_t s0_2_after = rng->randi();
+ INFO("This seed produces: ", s0_1_after, " ", s0_2_after);
+
+ String msg = "Should restore the sequence of numbers after resetting the seed";
+ CHECK_MESSAGE(s0_1_before == s0_1_after, msg);
+ CHECK_MESSAGE(s0_2_before == s0_2_after, msg);
+}
+
+TEST_CASE_MAY_FAIL("[RandomNumberGenerator] randi_range bias check") {
+ int zeros = 0;
+ int ones = 0;
+ Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
+ for (int i = 0; i < 10000; i++) {
+ int val = rng->randi_range(0, 1);
+ val == 0 ? zeros++ : ones++;
+ }
+ CHECK_MESSAGE(abs(zeros * 1.0 / ones - 1.0) < 0.1, "The ratio of zeros to ones should be nearly 1");
+
+ int vals[10] = { 0 };
+ for (int i = 0; i < 1000000; i++) {
+ vals[rng->randi_range(0, 9)]++;
+ }
+
+ for (int i = 0; i < 10; i++) {
+ CHECK_MESSAGE(abs(vals[i] / 1000000.0 - 0.1) < 0.01, "Each element should appear roughly 10% of the time");
+ }
+}
+} // namespace TestRandomNumberGenerator
+
+#endif // TEST_RANDOM_NUMBER_GENERATOR_H
diff --git a/tests/core/math/test_rect2.h b/tests/core/math/test_rect2.h
new file mode 100644
index 0000000000..e07250a8a2
--- /dev/null
+++ b/tests/core/math/test_rect2.h
@@ -0,0 +1,575 @@
+/*************************************************************************/
+/* test_rect2.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_RECT2_H
+#define TEST_RECT2_H
+
+#include "core/math/rect2.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestRect2 {
+// We also test Rect2i here, for consistency with the source code where Rect2
+// and Rect2i are defined in the same file.
+
+// Rect2
+
+TEST_CASE("[Rect2] Constructor methods") {
+ const Rect2 rect = Rect2(0, 100, 1280, 720);
+ const Rect2 rect_vector = Rect2(Vector2(0, 100), Vector2(1280, 720));
+ const Rect2 rect_copy_rect = Rect2(rect);
+ const Rect2 rect_copy_recti = Rect2(Rect2i(0, 100, 1280, 720));
+
+ CHECK_MESSAGE(
+ rect == rect_vector,
+ "Rect2s created with the same dimensions but by different methods should be equal.");
+ CHECK_MESSAGE(
+ rect == rect_copy_rect,
+ "Rect2s created with the same dimensions but by different methods should be equal.");
+ CHECK_MESSAGE(
+ rect == rect_copy_recti,
+ "Rect2s created with the same dimensions but by different methods should be equal.");
+}
+
+TEST_CASE("[Rect2] String conversion") {
+ // Note: This also depends on the Vector2 string representation.
+ CHECK_MESSAGE(
+ String(Rect2(0, 100, 1280, 720)) == "[P: (0, 100), S: (1280, 720)]",
+ "The string representation should match the expected value.");
+}
+
+TEST_CASE("[Rect2] Basic getters") {
+ const Rect2 rect = Rect2(0, 100, 1280, 720);
+ CHECK_MESSAGE(
+ rect.get_position().is_equal_approx(Vector2(0, 100)),
+ "get_position() should return the expected value.");
+ CHECK_MESSAGE(
+ rect.get_size().is_equal_approx(Vector2(1280, 720)),
+ "get_size() should return the expected value.");
+ CHECK_MESSAGE(
+ rect.get_end().is_equal_approx(Vector2(1280, 820)),
+ "get_end() should return the expected value.");
+ CHECK_MESSAGE(
+ rect.get_center().is_equal_approx(Vector2(640, 460)),
+ "get_center() should return the expected value.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1281, 721).get_center().is_equal_approx(Vector2(640.5, 460.5)),
+ "get_center() should return the expected value.");
+}
+
+TEST_CASE("[Rect2] Basic setters") {
+ Rect2 rect = Rect2(0, 100, 1280, 720);
+ rect.set_end(Vector2(4000, 4000));
+ CHECK_MESSAGE(
+ rect.is_equal_approx(Rect2(0, 100, 4000, 3900)),
+ "set_end() should result in the expected Rect2.");
+
+ rect = Rect2(0, 100, 1280, 720);
+ rect.set_position(Vector2(4000, 4000));
+ CHECK_MESSAGE(
+ rect.is_equal_approx(Rect2(4000, 4000, 1280, 720)),
+ "set_position() should result in the expected Rect2.");
+
+ rect = Rect2(0, 100, 1280, 720);
+ rect.set_size(Vector2(4000, 4000));
+ CHECK_MESSAGE(
+ rect.is_equal_approx(Rect2(0, 100, 4000, 4000)),
+ "set_size() should result in the expected Rect2.");
+}
+
+TEST_CASE("[Rect2] Area getters") {
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Rect2(0, 100, 1280, 720).get_area(), 921'600),
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Rect2(0, 100, -1280, -720).get_area(), 921'600),
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Rect2(0, 100, 1280, -720).get_area(), -921'600),
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(Rect2(0, 100, -1280, 720).get_area(), -921'600),
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(Rect2(0, 100, 0, 720).get_area()),
+ "get_area() should return the expected value.");
+
+ CHECK_MESSAGE(
+ !Rect2(0, 100, 1280, 720).has_no_area(),
+ "has_no_area() should return the expected value on Rect2 with an area.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 0, 500).has_no_area(),
+ "has_no_area() should return the expected value on Rect2 with no area.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 500, 0).has_no_area(),
+ "has_no_area() should return the expected value on Rect2 with no area.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 0, 0).has_no_area(),
+ "has_no_area() should return the expected value on Rect2 with no area.");
+}
+
+TEST_CASE("[Rect2] Absolute coordinates") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).abs().is_equal_approx(Rect2(0, 100, 1280, 720)),
+ "abs() should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, -100, 1280, 720).abs().is_equal_approx(Rect2(0, -100, 1280, 720)),
+ "abs() should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, -100, -1280, -720).abs().is_equal_approx(Rect2(-1280, -820, 1280, 720)),
+ "abs() should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, -1280, 720).abs().is_equal_approx(Rect2(-1280, 100, 1280, 720)),
+ "abs() should return the expected Rect2.");
+}
+
+TEST_CASE("[Rect2] Intersection") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).intersection(Rect2(0, 300, 100, 100)).is_equal_approx(Rect2(0, 300, 100, 100)),
+ "intersection() with fully enclosed Rect2 should return the expected result.");
+ // The resulting Rect2 is 100 pixels high because the first Rect2 is vertically offset by 100 pixels.
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).intersection(Rect2(1200, 700, 100, 100)).is_equal_approx(Rect2(1200, 700, 80, 100)),
+ "intersection() with partially enclosed Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).intersection(Rect2(-4000, -4000, 100, 100)).is_equal_approx(Rect2()),
+ "intersection() with non-enclosed Rect2 should return the expected result.");
+}
+
+TEST_CASE("[Rect2] Enclosing") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).encloses(Rect2(0, 300, 100, 100)),
+ "encloses() with fully contained Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2(0, 100, 1280, 720).encloses(Rect2(1200, 700, 100, 100)),
+ "encloses() with partially contained Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2(0, 100, 1280, 720).encloses(Rect2(-4000, -4000, 100, 100)),
+ "encloses() with non-contained Rect2 should return the expected result.");
+}
+
+TEST_CASE("[Rect2] Expanding") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).expand(Vector2(500, 600)).is_equal_approx(Rect2(0, 100, 1280, 720)),
+ "expand() with contained Vector2 should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).expand(Vector2(0, 0)).is_equal_approx(Rect2(0, 0, 1280, 820)),
+ "expand() with non-contained Vector2 should return the expected result.");
+}
+
+TEST_CASE("[Rect2] Growing") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow(100).is_equal_approx(Rect2(-100, 0, 1480, 920)),
+ "grow() with positive value should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow(-100).is_equal_approx(Rect2(100, 200, 1080, 520)),
+ "grow() with negative value should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow(-4000).is_equal_approx(Rect2(4000, 4100, -6720, -7280)),
+ "grow() with large negative value should return the expected Rect2.");
+
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow_individual(100, 200, 300, 400).is_equal_approx(Rect2(-100, -100, 1680, 1320)),
+ "grow_individual() with positive values should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow_individual(-100, 200, 300, -400).is_equal_approx(Rect2(100, -100, 1480, 520)),
+ "grow_individual() with positive and negative values should return the expected Rect2.");
+
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow_side(SIDE_TOP, 500).is_equal_approx(Rect2(0, -400, 1280, 1220)),
+ "grow_side() with positive value should return the expected Rect2.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).grow_side(SIDE_TOP, -500).is_equal_approx(Rect2(0, 600, 1280, 220)),
+ "grow_side() with negative value should return the expected Rect2.");
+}
+
+TEST_CASE("[Rect2] Has point") {
+ Rect2 rect = Rect2(0, 100, 1280, 720);
+ CHECK_MESSAGE(
+ rect.has_point(Vector2(500, 600)),
+ "has_point() with contained Vector2 should return the expected result.");
+ CHECK_MESSAGE(
+ !rect.has_point(Vector2(0, 0)),
+ "has_point() with non-contained Vector2 should return the expected result.");
+
+ CHECK_MESSAGE(
+ rect.has_point(rect.position),
+ "has_point() with positive size should include `position`.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2(1, 1)),
+ "has_point() with positive size should include `position + (1, 1)`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2(1, -1)),
+ "has_point() with positive size should not include `position + (1, -1)`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size),
+ "has_point() with positive size should not include `position + size`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size + Vector2(1, 1)),
+ "has_point() with positive size should not include `position + size + (1, 1)`.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + rect.size + Vector2(-1, -1)),
+ "has_point() with positive size should include `position + size + (-1, -1)`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size + Vector2(-1, 1)),
+ "has_point() with positive size should not include `position + size + (-1, 1)`.");
+
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2(0, 10)),
+ "has_point() with point located on left edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2(rect.size.x, 10)),
+ "has_point() with point located on right edge should return false.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2(10, 0)),
+ "has_point() with point located on top edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2(10, rect.size.y)),
+ "has_point() with point located on bottom edge should return false.");
+
+ /*
+ // FIXME: Disabled for now until GH-37617 is fixed one way or another.
+ // More tests should then be written like for the positive size case.
+ rect = Rect2(0, 100, -1280, -720);
+ CHECK_MESSAGE(
+ rect.has_point(rect.position),
+ "has_point() with negative size should include `position`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size),
+ "has_point() with negative size should not include `position + size`.");
+ */
+
+ rect = Rect2(-4000, -200, 1280, 720);
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2(0, 10)),
+ "has_point() with negative position and point located on left edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2(rect.size.x, 10)),
+ "has_point() with negative position and point located on right edge should return false.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2(10, 0)),
+ "has_point() with negative position and point located on top edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2(10, rect.size.y)),
+ "has_point() with negative position and point located on bottom edge should return false.");
+}
+
+TEST_CASE("[Rect2] Intersection") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).intersects(Rect2(0, 300, 100, 100)),
+ "intersects() with fully enclosed Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).intersects(Rect2(1200, 700, 100, 100)),
+ "intersects() with partially enclosed Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2(0, 100, 1280, 720).intersects(Rect2(-4000, -4000, 100, 100)),
+ "intersects() with non-enclosed Rect2 should return the expected result.");
+}
+
+TEST_CASE("[Rect2] Merging") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).merge(Rect2(0, 300, 100, 100)).is_equal_approx(Rect2(0, 100, 1280, 720)),
+ "merge() with fully enclosed Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).merge(Rect2(1200, 700, 100, 100)).is_equal_approx(Rect2(0, 100, 1300, 720)),
+ "merge() with partially enclosed Rect2 should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).merge(Rect2(-4000, -4000, 100, 100)).is_equal_approx(Rect2(-4000, -4000, 5280, 4820)),
+ "merge() with non-enclosed Rect2 should return the expected result.");
+}
+
+// Rect2i
+
+TEST_CASE("[Rect2i] Constructor methods") {
+ Rect2i recti = Rect2i(0, 100, 1280, 720);
+ Rect2i recti_vector = Rect2i(Vector2i(0, 100), Vector2i(1280, 720));
+ Rect2i recti_copy_recti = Rect2i(recti);
+ Rect2i recti_copy_rect = Rect2i(Rect2(0, 100, 1280, 720));
+
+ CHECK_MESSAGE(
+ recti == recti_vector,
+ "Rect2is created with the same dimensions but by different methods should be equal.");
+ CHECK_MESSAGE(
+ recti == recti_copy_recti,
+ "Rect2is created with the same dimensions but by different methods should be equal.");
+ CHECK_MESSAGE(
+ recti == recti_copy_rect,
+ "Rect2is created with the same dimensions but by different methods should be equal.");
+}
+
+TEST_CASE("[Rect2i] String conversion") {
+ // Note: This also depends on the Vector2 string representation.
+ CHECK_MESSAGE(
+ String(Rect2i(0, 100, 1280, 720)) == "[P: (0, 100), S: (1280, 720)]",
+ "The string representation should match the expected value.");
+}
+
+TEST_CASE("[Rect2i] Basic getters") {
+ const Rect2i rect = Rect2i(0, 100, 1280, 720);
+ CHECK_MESSAGE(
+ rect.get_position() == Vector2i(0, 100),
+ "get_position() should return the expected value.");
+ CHECK_MESSAGE(
+ rect.get_size() == Vector2i(1280, 720),
+ "get_size() should return the expected value.");
+ CHECK_MESSAGE(
+ rect.get_end() == Vector2i(1280, 820),
+ "get_end() should return the expected value.");
+ CHECK_MESSAGE(
+ rect.get_center() == Vector2i(640, 460),
+ "get_center() should return the expected value.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1281, 721).get_center() == Vector2i(640, 460),
+ "get_center() should return the expected value.");
+}
+
+TEST_CASE("[Rect2i] Basic setters") {
+ Rect2i rect = Rect2i(0, 100, 1280, 720);
+ rect.set_end(Vector2i(4000, 4000));
+ CHECK_MESSAGE(
+ rect == Rect2i(0, 100, 4000, 3900),
+ "set_end() should result in the expected Rect2i.");
+
+ rect = Rect2i(0, 100, 1280, 720);
+ rect.set_position(Vector2i(4000, 4000));
+ CHECK_MESSAGE(
+ rect == Rect2i(4000, 4000, 1280, 720),
+ "set_position() should result in the expected Rect2i.");
+
+ rect = Rect2i(0, 100, 1280, 720);
+ rect.set_size(Vector2i(4000, 4000));
+ CHECK_MESSAGE(
+ rect == Rect2i(0, 100, 4000, 4000),
+ "set_size() should result in the expected Rect2i.");
+}
+
+TEST_CASE("[Rect2i] Area getters") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).get_area() == 921'600,
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, -1280, -720).get_area() == 921'600,
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, -720).get_area() == -921'600,
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, -1280, 720).get_area() == -921'600,
+ "get_area() should return the expected value.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 0, 720).get_area() == 0,
+ "get_area() should return the expected value.");
+
+ CHECK_MESSAGE(
+ !Rect2i(0, 100, 1280, 720).has_no_area(),
+ "has_no_area() should return the expected value on Rect2i with an area.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 0, 500).has_no_area(),
+ "has_no_area() should return the expected value on Rect2i with no area.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 500, 0).has_no_area(),
+ "has_no_area() should return the expected value on Rect2i with no area.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 0, 0).has_no_area(),
+ "has_no_area() should return the expected value on Rect2i with no area.");
+}
+
+TEST_CASE("[Rect2i] Absolute coordinates") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).abs() == Rect2i(0, 100, 1280, 720),
+ "abs() should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, -100, 1280, 720).abs() == Rect2i(0, -100, 1280, 720),
+ "abs() should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, -100, -1280, -720).abs() == Rect2i(-1280, -820, 1280, 720),
+ "abs() should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, -1280, 720).abs() == Rect2i(-1280, 100, 1280, 720),
+ "abs() should return the expected Rect2i.");
+}
+
+TEST_CASE("[Rect2i] Intersection") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).intersection(Rect2i(0, 300, 100, 100)) == Rect2i(0, 300, 100, 100),
+ "intersection() with fully enclosed Rect2i should return the expected result.");
+ // The resulting Rect2i is 100 pixels high because the first Rect2i is vertically offset by 100 pixels.
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).intersection(Rect2i(1200, 700, 100, 100)) == Rect2i(1200, 700, 80, 100),
+ "intersection() with partially enclosed Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).intersection(Rect2i(-4000, -4000, 100, 100)) == Rect2i(),
+ "intersection() with non-enclosed Rect2i should return the expected result.");
+}
+
+TEST_CASE("[Rect2i] Enclosing") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).encloses(Rect2i(0, 300, 100, 100)),
+ "encloses() with fully contained Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2i(0, 100, 1280, 720).encloses(Rect2i(1200, 700, 100, 100)),
+ "encloses() with partially contained Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2i(0, 100, 1280, 720).encloses(Rect2i(-4000, -4000, 100, 100)),
+ "encloses() with non-contained Rect2i should return the expected result.");
+}
+
+TEST_CASE("[Rect2i] Expanding") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).expand(Vector2i(500, 600)) == Rect2i(0, 100, 1280, 720),
+ "expand() with contained Vector2i should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).expand(Vector2i(0, 0)) == Rect2i(0, 0, 1280, 820),
+ "expand() with non-contained Vector2i should return the expected result.");
+}
+
+TEST_CASE("[Rect2i] Growing") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow(100) == Rect2i(-100, 0, 1480, 920),
+ "grow() with positive value should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow(-100) == Rect2i(100, 200, 1080, 520),
+ "grow() with negative value should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow(-4000) == Rect2i(4000, 4100, -6720, -7280),
+ "grow() with large negative value should return the expected Rect2i.");
+
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow_individual(100, 200, 300, 400) == Rect2i(-100, -100, 1680, 1320),
+ "grow_individual() with positive values should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow_individual(-100, 200, 300, -400) == Rect2i(100, -100, 1480, 520),
+ "grow_individual() with positive and negative values should return the expected Rect2i.");
+
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow_side(SIDE_TOP, 500) == Rect2i(0, -400, 1280, 1220),
+ "grow_side() with positive value should return the expected Rect2i.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).grow_side(SIDE_TOP, -500) == Rect2i(0, 600, 1280, 220),
+ "grow_side() with negative value should return the expected Rect2i.");
+}
+
+TEST_CASE("[Rect2i] Has point") {
+ Rect2i rect = Rect2i(0, 100, 1280, 720);
+ CHECK_MESSAGE(
+ rect.has_point(Vector2i(500, 600)),
+ "has_point() with contained Vector2i should return the expected result.");
+ CHECK_MESSAGE(
+ !rect.has_point(Vector2i(0, 0)),
+ "has_point() with non-contained Vector2i should return the expected result.");
+
+ CHECK_MESSAGE(
+ rect.has_point(rect.position),
+ "has_point() with positive size should include `position`.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2i(1, 1)),
+ "has_point() with positive size should include `position + (1, 1)`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2i(1, -1)),
+ "has_point() with positive size should not include `position + (1, -1)`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size),
+ "has_point() with positive size should not include `position + size`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size + Vector2i(1, 1)),
+ "has_point() with positive size should not include `position + size + (1, 1)`.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + rect.size + Vector2i(-1, -1)),
+ "has_point() with positive size should include `position + size + (-1, -1)`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size + Vector2i(-1, 1)),
+ "has_point() with positive size should not include `position + size + (-1, 1)`.");
+
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2i(0, 10)),
+ "has_point() with point located on left edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2i(rect.size.x, 10)),
+ "has_point() with point located on right edge should return false.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2i(10, 0)),
+ "has_point() with point located on top edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2i(10, rect.size.y)),
+ "has_point() with point located on bottom edge should return false.");
+
+ /*
+ // FIXME: Disabled for now until GH-37617 is fixed one way or another.
+ // More tests should then be written like for the positive size case.
+ rect = Rect2i(0, 100, -1280, -720);
+ CHECK_MESSAGE(
+ rect.has_point(rect.position),
+ "has_point() with negative size should include `position`.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + rect.size),
+ "has_point() with negative size should not include `position + size`.");
+ */
+
+ rect = Rect2i(-4000, -200, 1280, 720);
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2i(0, 10)),
+ "has_point() with negative position and point located on left edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2i(rect.size.x, 10)),
+ "has_point() with negative position and point located on right edge should return false.");
+ CHECK_MESSAGE(
+ rect.has_point(rect.position + Vector2i(10, 0)),
+ "has_point() with negative position and point located on top edge should return true.");
+ CHECK_MESSAGE(
+ !rect.has_point(rect.position + Vector2i(10, rect.size.y)),
+ "has_point() with negative position and point located on bottom edge should return false.");
+}
+
+TEST_CASE("[Rect2i] Intersection") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).intersects(Rect2i(0, 300, 100, 100)),
+ "intersects() with fully enclosed Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).intersects(Rect2i(1200, 700, 100, 100)),
+ "intersects() with partially enclosed Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2i(0, 100, 1280, 720).intersects(Rect2i(-4000, -4000, 100, 100)),
+ "intersects() with non-enclosed Rect2i should return the expected result.");
+}
+
+TEST_CASE("[Rect2i] Merging") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).merge(Rect2i(0, 300, 100, 100)) == Rect2i(0, 100, 1280, 720),
+ "merge() with fully enclosed Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).merge(Rect2i(1200, 700, 100, 100)) == Rect2i(0, 100, 1300, 720),
+ "merge() with partially enclosed Rect2i should return the expected result.");
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).merge(Rect2i(-4000, -4000, 100, 100)) == Rect2i(-4000, -4000, 5280, 4820),
+ "merge() with non-enclosed Rect2i should return the expected result.");
+}
+} // namespace TestRect2
+
+#endif // TEST_RECT2_H
diff --git a/tests/core/object/test_class_db.h b/tests/core/object/test_class_db.h
new file mode 100644
index 0000000000..e4145c8408
--- /dev/null
+++ b/tests/core/object/test_class_db.h
@@ -0,0 +1,865 @@
+/*************************************************************************/
+/* test_class_db.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_CLASS_DB_H
+#define TEST_CLASS_DB_H
+
+#include "core/core_bind.h"
+#include "core/core_constants.h"
+#include "core/object/class_db.h"
+
+#include "tests/test_macros.h"
+
+namespace TestClassDB {
+
+struct TypeReference {
+ StringName name;
+ bool is_enum = false;
+};
+
+struct ConstantData {
+ String name;
+ int value = 0;
+};
+
+struct EnumData {
+ StringName name;
+ List<ConstantData> constants;
+
+ _FORCE_INLINE_ bool operator==(const EnumData &p_enum) const {
+ return p_enum.name == name;
+ }
+};
+
+struct PropertyData {
+ StringName name;
+ int index = 0;
+
+ StringName getter;
+ StringName setter;
+};
+
+struct ArgumentData {
+ TypeReference type;
+ String name;
+ bool has_defval = false;
+ Variant defval;
+};
+
+struct MethodData {
+ StringName name;
+ TypeReference return_type;
+ List<ArgumentData> arguments;
+ bool is_virtual = false;
+ bool is_vararg = false;
+};
+
+struct SignalData {
+ StringName name;
+ List<ArgumentData> arguments;
+};
+
+struct ExposedClass {
+ StringName name;
+ StringName base;
+
+ bool is_singleton = false;
+ bool is_instantiable = false;
+ bool is_ref_counted = false;
+
+ ClassDB::APIType api_type;
+
+ List<ConstantData> constants;
+ List<EnumData> enums;
+ List<PropertyData> properties;
+ List<MethodData> methods;
+ List<SignalData> signals_;
+
+ const PropertyData *find_property_by_name(const StringName &p_name) const {
+ for (const PropertyData &E : properties) {
+ if (E.name == p_name) {
+ return &E;
+ }
+ }
+
+ return nullptr;
+ }
+
+ const MethodData *find_method_by_name(const StringName &p_name) const {
+ for (const MethodData &E : methods) {
+ if (E.name == p_name) {
+ return &E;
+ }
+ }
+
+ return nullptr;
+ }
+};
+
+struct NamesCache {
+ StringName variant_type = StaticCString::create("Variant");
+ StringName object_class = StaticCString::create("Object");
+ StringName ref_counted_class = StaticCString::create("RefCounted");
+ StringName string_type = StaticCString::create("String");
+ StringName string_name_type = StaticCString::create("StringName");
+ StringName node_path_type = StaticCString::create("NodePath");
+ StringName bool_type = StaticCString::create("bool");
+ StringName int_type = StaticCString::create("int");
+ StringName float_type = StaticCString::create("float");
+ StringName void_type = StaticCString::create("void");
+ StringName vararg_stub_type = StaticCString::create("@VarArg@");
+ StringName vector2_type = StaticCString::create("Vector2");
+ StringName rect2_type = StaticCString::create("Rect2");
+ StringName vector3_type = StaticCString::create("Vector3");
+
+ // Object not included as it must be checked for all derived classes
+ static constexpr int nullable_types_count = 17;
+ StringName nullable_types[nullable_types_count] = {
+ string_type,
+ string_name_type,
+ node_path_type,
+
+ StaticCString::create(_STR(Array)),
+ StaticCString::create(_STR(Dictionary)),
+ StaticCString::create(_STR(Callable)),
+ StaticCString::create(_STR(Signal)),
+
+ StaticCString::create(_STR(PackedByteArray)),
+ StaticCString::create(_STR(PackedInt32Array)),
+ StaticCString::create(_STR(PackedInt64rray)),
+ StaticCString::create(_STR(PackedFloat32Array)),
+ StaticCString::create(_STR(PackedFloat64Array)),
+ StaticCString::create(_STR(PackedStringArray)),
+ StaticCString::create(_STR(PackedVector2Array)),
+ StaticCString::create(_STR(PackedVector3Array)),
+ StaticCString::create(_STR(PackedColorArray)),
+ };
+
+ bool is_nullable_type(const StringName &p_type) const {
+ for (int i = 0; i < nullable_types_count; i++) {
+ if (p_type == nullable_types[i]) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+};
+
+typedef OrderedHashMap<StringName, ExposedClass> ExposedClasses;
+
+struct Context {
+ Vector<StringName> enum_types;
+ Vector<StringName> builtin_types;
+ ExposedClasses exposed_classes;
+ List<EnumData> global_enums;
+ NamesCache names_cache;
+
+ const ExposedClass *find_exposed_class(const StringName &p_name) const {
+ ExposedClasses::ConstElement elem = exposed_classes.find(p_name);
+ return elem ? &elem.value() : nullptr;
+ }
+
+ const ExposedClass *find_exposed_class(const TypeReference &p_type_ref) const {
+ ExposedClasses::ConstElement elem = exposed_classes.find(p_type_ref.name);
+ return elem ? &elem.value() : nullptr;
+ }
+
+ bool has_type(const TypeReference &p_type_ref) const {
+ if (builtin_types.find(p_type_ref.name) >= 0) {
+ return true;
+ }
+
+ if (p_type_ref.is_enum) {
+ if (enum_types.find(p_type_ref.name) >= 0) {
+ return true;
+ }
+
+ // Enum not found. Most likely because none of its constants were bound, so it's empty. That's fine. Use int instead.
+ return builtin_types.find(names_cache.int_type);
+ }
+
+ return false;
+ }
+};
+
+bool arg_default_value_is_assignable_to_type(const Context &p_context, const Variant &p_val, const TypeReference &p_arg_type, String *r_err_msg = nullptr) {
+ if (p_arg_type.name == p_context.names_cache.variant_type) {
+ // Variant can take anything
+ return true;
+ }
+
+ switch (p_val.get_type()) {
+ case Variant::NIL:
+ return p_context.find_exposed_class(p_arg_type) ||
+ p_context.names_cache.is_nullable_type(p_arg_type.name);
+ case Variant::BOOL:
+ return p_arg_type.name == p_context.names_cache.bool_type;
+ case Variant::INT:
+ return p_arg_type.name == p_context.names_cache.int_type ||
+ p_arg_type.name == p_context.names_cache.float_type ||
+ p_arg_type.is_enum;
+ case Variant::FLOAT:
+ return p_arg_type.name == p_context.names_cache.float_type;
+ case Variant::STRING:
+ case Variant::STRING_NAME:
+ return p_arg_type.name == p_context.names_cache.string_type ||
+ p_arg_type.name == p_context.names_cache.string_name_type ||
+ p_arg_type.name == p_context.names_cache.node_path_type;
+ case Variant::NODE_PATH:
+ return p_arg_type.name == p_context.names_cache.node_path_type;
+ case Variant::TRANSFORM3D:
+ case Variant::TRANSFORM2D:
+ case Variant::BASIS:
+ case Variant::QUATERNION:
+ case Variant::PLANE:
+ case Variant::AABB:
+ case Variant::COLOR:
+ case Variant::VECTOR2:
+ case Variant::RECT2:
+ case Variant::VECTOR3:
+ case Variant::RID:
+ case Variant::ARRAY:
+ case Variant::DICTIONARY:
+ case Variant::PACKED_BYTE_ARRAY:
+ case Variant::PACKED_INT32_ARRAY:
+ case Variant::PACKED_INT64_ARRAY:
+ case Variant::PACKED_FLOAT32_ARRAY:
+ case Variant::PACKED_FLOAT64_ARRAY:
+ case Variant::PACKED_STRING_ARRAY:
+ case Variant::PACKED_VECTOR2_ARRAY:
+ case Variant::PACKED_VECTOR3_ARRAY:
+ case Variant::PACKED_COLOR_ARRAY:
+ case Variant::CALLABLE:
+ case Variant::SIGNAL:
+ return p_arg_type.name == Variant::get_type_name(p_val.get_type());
+ case Variant::OBJECT:
+ return p_context.find_exposed_class(p_arg_type);
+ case Variant::VECTOR2I:
+ return p_arg_type.name == p_context.names_cache.vector2_type ||
+ p_arg_type.name == Variant::get_type_name(p_val.get_type());
+ case Variant::RECT2I:
+ return p_arg_type.name == p_context.names_cache.rect2_type ||
+ p_arg_type.name == Variant::get_type_name(p_val.get_type());
+ case Variant::VECTOR3I:
+ return p_arg_type.name == p_context.names_cache.vector3_type ||
+ p_arg_type.name == Variant::get_type_name(p_val.get_type());
+ default:
+ if (r_err_msg) {
+ *r_err_msg = "Unexpected Variant type: " + itos(p_val.get_type());
+ }
+ break;
+ }
+
+ return false;
+}
+
+void validate_property(const Context &p_context, const ExposedClass &p_class, const PropertyData &p_prop) {
+ const MethodData *setter = p_class.find_method_by_name(p_prop.setter);
+
+ // Search it in base classes too
+ const ExposedClass *top = &p_class;
+ while (!setter && top->base != StringName()) {
+ top = p_context.find_exposed_class(top->base);
+ TEST_FAIL_COND(!top, "Class not found '", top->base, "'. Inherited by '", top->name, "'.");
+ setter = top->find_method_by_name(p_prop.setter);
+ }
+
+ const MethodData *getter = p_class.find_method_by_name(p_prop.getter);
+
+ // Search it in base classes too
+ top = &p_class;
+ while (!getter && top->base != StringName()) {
+ top = p_context.find_exposed_class(top->base);
+ TEST_FAIL_COND(!top, "Class not found '", top->base, "'. Inherited by '", top->name, "'.");
+ getter = top->find_method_by_name(p_prop.getter);
+ }
+
+ TEST_FAIL_COND((!setter && !getter),
+ "Couldn't find neither the setter nor the getter for property: '", p_class.name, ".", String(p_prop.name), "'.");
+
+ if (setter) {
+ int setter_argc = p_prop.index != -1 ? 2 : 1;
+ TEST_FAIL_COND(setter->arguments.size() != setter_argc,
+ "Invalid property setter argument count: '", p_class.name, ".", String(p_prop.name), "'.");
+ }
+
+ if (getter) {
+ int getter_argc = p_prop.index != -1 ? 1 : 0;
+ TEST_FAIL_COND(getter->arguments.size() != getter_argc,
+ "Invalid property setter argument count: '", p_class.name, ".", String(p_prop.name), "'.");
+ }
+
+ if (getter && setter) {
+ const ArgumentData &setter_first_arg = setter->arguments.back()->get();
+ if (getter->return_type.name != setter_first_arg.type.name) {
+ // Special case for Node::set_name
+ bool whitelisted = getter->return_type.name == p_context.names_cache.string_name_type &&
+ setter_first_arg.type.name == p_context.names_cache.string_type;
+
+ TEST_FAIL_COND(!whitelisted,
+ "Return type from getter doesn't match first argument of setter, for property: '", p_class.name, ".", String(p_prop.name), "'.");
+ }
+ }
+
+ const TypeReference &prop_type_ref = getter ? getter->return_type : setter->arguments.back()->get().type;
+
+ const ExposedClass *prop_class = p_context.find_exposed_class(prop_type_ref);
+ if (prop_class) {
+ TEST_COND(prop_class->is_singleton,
+ "Property type is a singleton: '", p_class.name, ".", String(p_prop.name), "'.");
+
+ if (p_class.api_type == ClassDB::API_CORE) {
+ TEST_COND(prop_class->api_type == ClassDB::API_EDITOR,
+ "Property '", p_class.name, ".", p_prop.name, "' has type '", prop_class->name,
+ "' from the editor API. Core API cannot have dependencies on the editor API.");
+ }
+ } else {
+ // Look for types that don't inherit Object
+ TEST_FAIL_COND(!p_context.has_type(prop_type_ref),
+ "Property type '", prop_type_ref.name, "' not found: '", p_class.name, ".", String(p_prop.name), "'.");
+ }
+
+ if (getter) {
+ if (p_prop.index != -1) {
+ const ArgumentData &idx_arg = getter->arguments.front()->get();
+ if (idx_arg.type.name != p_context.names_cache.int_type) {
+ // If not an int, it can be an enum
+ TEST_COND(p_context.enum_types.find(idx_arg.type.name) < 0,
+ "Invalid type '", idx_arg.type.name, "' for index argument of property getter: '", p_class.name, ".", String(p_prop.name), "'.");
+ }
+ }
+ }
+
+ if (setter) {
+ if (p_prop.index != -1) {
+ const ArgumentData &idx_arg = setter->arguments.front()->get();
+ if (idx_arg.type.name != p_context.names_cache.int_type) {
+ // Assume the index parameter is an enum
+ // If not an int, it can be an enum
+ TEST_COND(p_context.enum_types.find(idx_arg.type.name) < 0,
+ "Invalid type '", idx_arg.type.name, "' for index argument of property setter: '", p_class.name, ".", String(p_prop.name), "'.");
+ }
+ }
+ }
+}
+
+void validate_method(const Context &p_context, const ExposedClass &p_class, const MethodData &p_method) {
+ if (p_method.return_type.name != StringName()) {
+ const ExposedClass *return_class = p_context.find_exposed_class(p_method.return_type);
+ if (return_class) {
+ TEST_COND(return_class->is_singleton,
+ "Method return type is a singleton: '", p_class.name, ".", p_method.name, "'.");
+
+ if (p_class.api_type == ClassDB::API_CORE) {
+ TEST_COND(return_class->api_type == ClassDB::API_EDITOR,
+ "Method '", p_class.name, ".", p_method.name, "' has return type '", return_class->name,
+ "' from the editor API. Core API cannot have dependencies on the editor API.");
+ }
+ } else {
+ // Look for types that don't inherit Object
+ TEST_FAIL_COND(!p_context.has_type(p_method.return_type),
+ "Method return type '", p_method.return_type.name, "' not found: '", p_class.name, ".", p_method.name, "'.");
+ }
+ }
+
+ for (const ArgumentData &F : p_method.arguments) {
+ const ArgumentData &arg = F;
+
+ const ExposedClass *arg_class = p_context.find_exposed_class(arg.type);
+ if (arg_class) {
+ TEST_COND(arg_class->is_singleton,
+ "Argument type is a singleton: '", arg.name, "' of method '", p_class.name, ".", p_method.name, "'.");
+
+ if (p_class.api_type == ClassDB::API_CORE) {
+ TEST_COND(arg_class->api_type == ClassDB::API_EDITOR,
+ "Argument '", arg.name, "' of method '", p_class.name, ".", p_method.name, "' has type '",
+ arg_class->name, "' from the editor API. Core API cannot have dependencies on the editor API.");
+ }
+ } else {
+ // Look for types that don't inherit Object
+ TEST_FAIL_COND(!p_context.has_type(arg.type),
+ "Argument type '", arg.type.name, "' not found: '", arg.name, "' of method", p_class.name, ".", p_method.name, "'.");
+ }
+
+ if (arg.has_defval) {
+ String type_error_msg;
+ bool arg_defval_assignable_to_type = arg_default_value_is_assignable_to_type(p_context, arg.defval, arg.type, &type_error_msg);
+ String err_msg = vformat("Invalid default value for parameter '%s' of method '%s.%s'.", arg.name, p_class.name, p_method.name);
+ if (!type_error_msg.is_empty()) {
+ err_msg += " " + type_error_msg;
+ }
+ TEST_COND(!arg_defval_assignable_to_type, err_msg.utf8().get_data());
+ }
+ }
+}
+
+void validate_signal(const Context &p_context, const ExposedClass &p_class, const SignalData &p_signal) {
+ for (const ArgumentData &F : p_signal.arguments) {
+ const ArgumentData &arg = F;
+
+ const ExposedClass *arg_class = p_context.find_exposed_class(arg.type);
+ if (arg_class) {
+ TEST_COND(arg_class->is_singleton,
+ "Argument class is a singleton: '", arg.name, "' of signal '", p_class.name, ".", p_signal.name, "'.");
+
+ if (p_class.api_type == ClassDB::API_CORE) {
+ TEST_COND(arg_class->api_type == ClassDB::API_EDITOR,
+ "Argument '", arg.name, "' of signal '", p_class.name, ".", p_signal.name, "' has type '",
+ arg_class->name, "' from the editor API. Core API cannot have dependencies on the editor API.");
+ }
+ } else {
+ // Look for types that don't inherit Object
+ TEST_FAIL_COND(!p_context.has_type(arg.type),
+ "Argument type '", arg.type.name, "' not found: '", arg.name, "' of signal", p_class.name, ".", p_signal.name, "'.");
+ }
+ }
+}
+
+void validate_class(const Context &p_context, const ExposedClass &p_exposed_class) {
+ bool is_derived_type = p_exposed_class.base != StringName();
+
+ if (!is_derived_type) {
+ // Asserts about the base Object class
+ TEST_FAIL_COND(p_exposed_class.name != p_context.names_cache.object_class,
+ "Class '", p_exposed_class.name, "' has no base class.");
+ TEST_FAIL_COND(!p_exposed_class.is_instantiable,
+ "Object class is not instantiable.");
+ TEST_FAIL_COND(p_exposed_class.api_type != ClassDB::API_CORE,
+ "Object class is API is not API_CORE.");
+ TEST_FAIL_COND(p_exposed_class.is_singleton,
+ "Object class is registered as a singleton.");
+ }
+
+ TEST_FAIL_COND((p_exposed_class.is_singleton && p_exposed_class.base != p_context.names_cache.object_class),
+ "Singleton base class '", String(p_exposed_class.base), "' is not Object, for class '", p_exposed_class.name, "'.");
+
+ TEST_FAIL_COND((is_derived_type && !p_context.exposed_classes.has(p_exposed_class.base)),
+ "Base type '", p_exposed_class.base.operator String(), "' does not exist, for class '", p_exposed_class.name, "'.");
+
+ for (const PropertyData &F : p_exposed_class.properties) {
+ validate_property(p_context, p_exposed_class, F);
+ }
+
+ for (const MethodData &F : p_exposed_class.methods) {
+ validate_method(p_context, p_exposed_class, F);
+ }
+
+ for (const SignalData &F : p_exposed_class.signals_) {
+ validate_signal(p_context, p_exposed_class, F);
+ }
+}
+
+void add_exposed_classes(Context &r_context) {
+ List<StringName> class_list;
+ ClassDB::get_class_list(&class_list);
+ class_list.sort_custom<StringName::AlphCompare>();
+
+ while (class_list.size()) {
+ StringName class_name = class_list.front()->get();
+
+ ClassDB::APIType api_type = ClassDB::get_api_type(class_name);
+
+ if (api_type == ClassDB::API_NONE) {
+ class_list.pop_front();
+ continue;
+ }
+
+ if (!ClassDB::is_class_exposed(class_name)) {
+ MESSAGE(vformat("Ignoring class '%s' because it's not exposed.", class_name).utf8().get_data());
+ class_list.pop_front();
+ continue;
+ }
+
+ if (!ClassDB::is_class_enabled(class_name)) {
+ MESSAGE(vformat("Ignoring class '%s' because it's not enabled.", class_name).utf8().get_data());
+ class_list.pop_front();
+ continue;
+ }
+
+ ClassDB::ClassInfo *class_info = ClassDB::classes.getptr(class_name);
+
+ ExposedClass exposed_class;
+ exposed_class.name = class_name;
+ exposed_class.api_type = api_type;
+ exposed_class.is_singleton = Engine::get_singleton()->has_singleton(class_name);
+ exposed_class.is_instantiable = class_info->creation_func && !exposed_class.is_singleton;
+ exposed_class.is_ref_counted = ClassDB::is_parent_class(class_name, "RefCounted");
+ exposed_class.base = ClassDB::get_parent_class(class_name);
+
+ // Add properties
+
+ List<PropertyInfo> property_list;
+ ClassDB::get_property_list(class_name, &property_list, true);
+
+ Map<StringName, StringName> accessor_methods;
+
+ for (const PropertyInfo &property : property_list) {
+ if (property.usage & PROPERTY_USAGE_GROUP || property.usage & PROPERTY_USAGE_SUBGROUP || property.usage & PROPERTY_USAGE_CATEGORY || (property.type == Variant::NIL && property.usage & PROPERTY_USAGE_ARRAY)) {
+ continue;
+ }
+
+ PropertyData prop;
+ prop.name = property.name;
+ prop.setter = ClassDB::get_property_setter(class_name, prop.name);
+ prop.getter = ClassDB::get_property_getter(class_name, prop.name);
+
+ if (prop.setter != StringName()) {
+ accessor_methods[prop.setter] = prop.name;
+ }
+ if (prop.getter != StringName()) {
+ accessor_methods[prop.getter] = prop.name;
+ }
+
+ bool valid = false;
+ prop.index = ClassDB::get_property_index(class_name, prop.name, &valid);
+ TEST_FAIL_COND(!valid, "Invalid property: '", exposed_class.name, ".", String(prop.name), "'.");
+
+ exposed_class.properties.push_back(prop);
+ }
+
+ // Add methods
+
+ List<MethodInfo> virtual_method_list;
+ ClassDB::get_virtual_methods(class_name, &virtual_method_list, true);
+
+ List<MethodInfo> method_list;
+ ClassDB::get_method_list(class_name, &method_list, true);
+ method_list.sort();
+
+ for (const MethodInfo &E : method_list) {
+ const MethodInfo &method_info = E;
+
+ int argc = method_info.arguments.size();
+
+ if (method_info.name.is_empty()) {
+ continue;
+ }
+
+ MethodData method;
+ method.name = method_info.name;
+
+ if (method_info.flags & METHOD_FLAG_VIRTUAL) {
+ method.is_virtual = true;
+ }
+
+ PropertyInfo return_info = method_info.return_val;
+
+ MethodBind *m = method.is_virtual ? nullptr : ClassDB::get_method(class_name, method_info.name);
+
+ method.is_vararg = m && m->is_vararg();
+
+ if (!m && !method.is_virtual) {
+ TEST_FAIL_COND(!virtual_method_list.find(method_info),
+ "Missing MethodBind for non-virtual method: '", exposed_class.name, ".", method.name, "'.");
+
+ // A virtual method without the virtual flag. This is a special case.
+
+ // The method Object.free is registered as a virtual method, but without the virtual flag.
+ // This is because this method is not supposed to be overridden, but called.
+ // We assume the return type is void.
+ method.return_type.name = r_context.names_cache.void_type;
+
+ // Actually, more methods like this may be added in the future, which could return
+ // something different. Let's put this check to notify us if that ever happens.
+ String warn_msg = vformat(
+ "Notification: New unexpected virtual non-overridable method found. "
+ "We only expected Object.free, but found '%s.%s'.",
+ exposed_class.name, method.name);
+ TEST_FAIL_COND_WARN(
+ (exposed_class.name != r_context.names_cache.object_class || String(method.name) != "free"),
+ warn_msg.utf8().get_data());
+
+ } else if (return_info.type == Variant::INT && return_info.usage & PROPERTY_USAGE_CLASS_IS_ENUM) {
+ method.return_type.name = return_info.class_name;
+ method.return_type.is_enum = true;
+ } else if (return_info.class_name != StringName()) {
+ method.return_type.name = return_info.class_name;
+
+ bool bad_reference_hint = !method.is_virtual && return_info.hint != PROPERTY_HINT_RESOURCE_TYPE &&
+ ClassDB::is_parent_class(return_info.class_name, r_context.names_cache.ref_counted_class);
+ TEST_COND(bad_reference_hint, "Return type is reference but hint is not '" _STR(PROPERTY_HINT_RESOURCE_TYPE) "'.", " Are you returning a reference type by pointer? Method: '",
+ exposed_class.name, ".", method.name, "'.");
+ } else if (return_info.hint == PROPERTY_HINT_RESOURCE_TYPE) {
+ method.return_type.name = return_info.hint_string;
+ } else if (return_info.type == Variant::NIL && return_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT) {
+ method.return_type.name = r_context.names_cache.variant_type;
+ } else if (return_info.type == Variant::NIL) {
+ method.return_type.name = r_context.names_cache.void_type;
+ } else {
+ // NOTE: We don't care about the size and sign of int and float in these tests
+ method.return_type.name = Variant::get_type_name(return_info.type);
+ }
+
+ for (int i = 0; i < argc; i++) {
+ PropertyInfo arg_info = method_info.arguments[i];
+
+ String orig_arg_name = arg_info.name;
+
+ ArgumentData arg;
+ arg.name = orig_arg_name;
+
+ if (arg_info.type == Variant::INT && arg_info.usage & PROPERTY_USAGE_CLASS_IS_ENUM) {
+ arg.type.name = arg_info.class_name;
+ arg.type.is_enum = true;
+ } else if (arg_info.class_name != StringName()) {
+ arg.type.name = arg_info.class_name;
+ } else if (arg_info.hint == PROPERTY_HINT_RESOURCE_TYPE) {
+ arg.type.name = arg_info.hint_string;
+ } else if (arg_info.type == Variant::NIL) {
+ arg.type.name = r_context.names_cache.variant_type;
+ } else {
+ // NOTE: We don't care about the size and sign of int and float in these tests
+ arg.type.name = Variant::get_type_name(arg_info.type);
+ }
+
+ if (m && m->has_default_argument(i)) {
+ arg.has_defval = true;
+ arg.defval = m->get_default_argument(i);
+ }
+
+ method.arguments.push_back(arg);
+ }
+
+ if (method.is_vararg) {
+ ArgumentData vararg;
+ vararg.type.name = r_context.names_cache.vararg_stub_type;
+ vararg.name = "@varargs@";
+ method.arguments.push_back(vararg);
+ }
+
+ TEST_COND(exposed_class.find_property_by_name(method.name),
+ "Method name conflicts with property: '", String(class_name), ".", String(method.name), "'.");
+
+ // Methods starting with an underscore are ignored unless they're virtual or used as a property setter or getter.
+ if (!method.is_virtual && String(method.name)[0] == '_') {
+ for (const PropertyData &F : exposed_class.properties) {
+ const PropertyData &prop = F;
+
+ if (prop.setter == method.name || prop.getter == method.name) {
+ exposed_class.methods.push_back(method);
+ break;
+ }
+ }
+ } else {
+ exposed_class.methods.push_back(method);
+ }
+
+ if (method.is_virtual) {
+ TEST_COND(String(method.name)[0] != '_', "Virtual method ", String(method.name), " does not start with underscore.");
+ }
+ }
+
+ // Add signals
+
+ const HashMap<StringName, MethodInfo> &signal_map = class_info->signal_map;
+ const StringName *k = nullptr;
+
+ while ((k = signal_map.next(k))) {
+ SignalData signal;
+
+ const MethodInfo &method_info = signal_map.get(*k);
+
+ signal.name = method_info.name;
+
+ int argc = method_info.arguments.size();
+
+ for (int i = 0; i < argc; i++) {
+ PropertyInfo arg_info = method_info.arguments[i];
+
+ String orig_arg_name = arg_info.name;
+
+ ArgumentData arg;
+ arg.name = orig_arg_name;
+
+ if (arg_info.type == Variant::INT && arg_info.usage & PROPERTY_USAGE_CLASS_IS_ENUM) {
+ arg.type.name = arg_info.class_name;
+ arg.type.is_enum = true;
+ } else if (arg_info.class_name != StringName()) {
+ arg.type.name = arg_info.class_name;
+ } else if (arg_info.hint == PROPERTY_HINT_RESOURCE_TYPE) {
+ arg.type.name = arg_info.hint_string;
+ } else if (arg_info.type == Variant::NIL) {
+ arg.type.name = r_context.names_cache.variant_type;
+ } else {
+ // NOTE: We don't care about the size and sign of int and float in these tests
+ arg.type.name = Variant::get_type_name(arg_info.type);
+ }
+
+ signal.arguments.push_back(arg);
+ }
+
+ bool method_conflict = exposed_class.find_property_by_name(signal.name);
+
+ // TODO:
+ // ClassDB allows signal names that conflict with method or property names.
+ // However registering a signal with a conflicting name is still considered wrong.
+ // Unfortunately there are some existing cases that are yet to be fixed.
+ // Until those are fixed we will print a warning instead of failing the test.
+ String warn_msg = vformat(
+ "Signal name conflicts with %s: '%s.%s.",
+ method_conflict ? "method" : "property", class_name, signal.name);
+ TEST_FAIL_COND_WARN((method_conflict || exposed_class.find_method_by_name(signal.name)),
+ warn_msg.utf8().get_data());
+
+ exposed_class.signals_.push_back(signal);
+ }
+
+ // Add enums and constants
+
+ List<String> constants;
+ ClassDB::get_integer_constant_list(class_name, &constants, true);
+
+ const HashMap<StringName, List<StringName>> &enum_map = class_info->enum_map;
+ k = nullptr;
+
+ while ((k = enum_map.next(k))) {
+ EnumData enum_;
+ enum_.name = *k;
+
+ const List<StringName> &enum_constants = enum_map.get(*k);
+ for (const StringName &E : enum_constants) {
+ const StringName &constant_name = E;
+ TEST_FAIL_COND(String(constant_name).find("::") != -1,
+ "Enum constant contains '::', check bindings to remove the scope: '",
+ String(class_name), ".", String(enum_.name), ".", String(constant_name), "'.");
+ int *value = class_info->constant_map.getptr(constant_name);
+ TEST_FAIL_COND(!value, "Missing enum constant value: '",
+ String(class_name), ".", String(enum_.name), ".", String(constant_name), "'.");
+ constants.erase(constant_name);
+
+ ConstantData constant;
+ constant.name = constant_name;
+ constant.value = *value;
+
+ enum_.constants.push_back(constant);
+ }
+
+ exposed_class.enums.push_back(enum_);
+
+ r_context.enum_types.push_back(String(class_name) + "." + String(*k));
+ }
+
+ for (const String &E : constants) {
+ const String &constant_name = E;
+ TEST_FAIL_COND(constant_name.find("::") != -1,
+ "Constant contains '::', check bindings to remove the scope: '",
+ String(class_name), ".", constant_name, "'.");
+ int *value = class_info->constant_map.getptr(StringName(E));
+ TEST_FAIL_COND(!value, "Missing constant value: '", String(class_name), ".", String(constant_name), "'.");
+
+ ConstantData constant;
+ constant.name = constant_name;
+ constant.value = *value;
+
+ exposed_class.constants.push_back(constant);
+ }
+
+ r_context.exposed_classes.insert(class_name, exposed_class);
+ class_list.pop_front();
+ }
+}
+
+void add_builtin_types(Context &r_context) {
+ // NOTE: We don't care about the size and sign of int and float in these tests
+ for (int i = 0; i < Variant::VARIANT_MAX; i++) {
+ r_context.builtin_types.push_back(Variant::get_type_name(Variant::Type(i)));
+ }
+
+ r_context.builtin_types.push_back(_STR(Variant));
+ r_context.builtin_types.push_back(r_context.names_cache.vararg_stub_type);
+ r_context.builtin_types.push_back("void");
+}
+
+void add_global_enums(Context &r_context) {
+ int global_constants_count = CoreConstants::get_global_constant_count();
+
+ if (global_constants_count > 0) {
+ for (int i = 0; i < global_constants_count; i++) {
+ StringName enum_name = CoreConstants::get_global_constant_enum(i);
+
+ if (enum_name != StringName()) {
+ ConstantData constant;
+ constant.name = CoreConstants::get_global_constant_name(i);
+ constant.value = CoreConstants::get_global_constant_value(i);
+
+ EnumData enum_;
+ enum_.name = enum_name;
+ List<EnumData>::Element *enum_match = r_context.global_enums.find(enum_);
+ if (enum_match) {
+ enum_match->get().constants.push_back(constant);
+ } else {
+ enum_.constants.push_back(constant);
+ r_context.global_enums.push_back(enum_);
+ }
+ }
+ }
+
+ for (const EnumData &E : r_context.global_enums) {
+ r_context.enum_types.push_back(E.name);
+ }
+ }
+
+ // HARDCODED
+ List<StringName> hardcoded_enums;
+ hardcoded_enums.push_back("Vector2.Axis");
+ hardcoded_enums.push_back("Vector2i.Axis");
+ hardcoded_enums.push_back("Vector3.Axis");
+ hardcoded_enums.push_back("Vector3i.Axis");
+ for (const StringName &E : hardcoded_enums) {
+ // These enums are not generated and must be written manually (e.g.: Vector3.Axis)
+ // Here, we assume core types do not begin with underscore
+ r_context.enum_types.push_back(E);
+ }
+}
+
+TEST_SUITE("[ClassDB]") {
+ TEST_CASE("[ClassDB] Add exposed classes, builtin types, and global enums") {
+ Context context;
+
+ add_exposed_classes(context);
+ add_builtin_types(context);
+ add_global_enums(context);
+
+ SUBCASE("[ClassDB] Find exposed class") {
+ const ExposedClass *object_class = context.find_exposed_class(context.names_cache.object_class);
+ TEST_FAIL_COND(!object_class, "Object class not found.");
+ TEST_FAIL_COND(object_class->base != StringName(),
+ "Object class derives from another class: '", object_class->base, "'.");
+
+ for (ExposedClasses::Element E = context.exposed_classes.front(); E; E = E.next()) {
+ validate_class(context, E.value());
+ }
+ }
+ }
+}
+} // namespace TestClassDB
+
+#endif // TEST_CLASS_DB_H
diff --git a/tests/core/object/test_method_bind.h b/tests/core/object/test_method_bind.h
new file mode 100644
index 0000000000..0c7e47fc89
--- /dev/null
+++ b/tests/core/object/test_method_bind.h
@@ -0,0 +1,160 @@
+/*************************************************************************/
+/* test_method_bind.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_METHOD_BIND_H
+#define TEST_METHOD_BIND_H
+
+#include "core/object/class_db.h"
+
+#include "tests/test_macros.h"
+
+namespace TestMethodBind {
+
+class MethodBindTester : public Object {
+ GDCLASS(MethodBindTester, Object);
+
+public:
+ enum Test {
+ TEST_METHOD,
+ TEST_METHOD_ARGS,
+ TEST_METHODC,
+ TEST_METHODC_ARGS,
+ TEST_METHODR,
+ TEST_METHODR_ARGS,
+ TEST_METHODRC,
+ TEST_METHODRC_ARGS,
+ TEST_METHOD_DEFARGS,
+ TEST_MAX
+ };
+
+ int test_num = 0;
+
+ bool test_valid[TEST_MAX];
+
+ void test_method() {
+ test_valid[TEST_METHOD] = true;
+ }
+
+ void test_method_args(int p_arg) {
+ test_valid[TEST_METHOD_ARGS] = p_arg == test_num;
+ }
+
+ void test_methodc() {
+ test_valid[TEST_METHODC] = true;
+ }
+
+ void test_methodc_args(int p_arg) {
+ test_valid[TEST_METHODC_ARGS] = p_arg == test_num;
+ }
+
+ int test_methodr() {
+ test_valid[TEST_METHODR] = true; //temporary
+ return test_num;
+ }
+
+ int test_methodr_args(int p_arg) {
+ test_valid[TEST_METHODR_ARGS] = true; //temporary
+ return p_arg;
+ }
+
+ int test_methodrc() {
+ test_valid[TEST_METHODRC] = true; //temporary
+ return test_num;
+ }
+
+ int test_methodrc_args(int p_arg) {
+ test_valid[TEST_METHODRC_ARGS] = true; //temporary
+ return p_arg;
+ }
+
+ void test_method_default_args(int p_arg1, int p_arg2, int p_arg3, int p_arg4, int p_arg5) {
+ test_valid[TEST_METHOD_DEFARGS] = p_arg1 == 1 && p_arg2 == 2 && p_arg3 == 3 && p_arg4 == 4 && p_arg5 == 5; //temporary
+ }
+
+ static void _bind_methods() {
+ ClassDB::bind_method(D_METHOD("test_method"), &MethodBindTester::test_method);
+ ClassDB::bind_method(D_METHOD("test_method_args"), &MethodBindTester::test_method_args);
+ ClassDB::bind_method(D_METHOD("test_methodc"), &MethodBindTester::test_methodc);
+ ClassDB::bind_method(D_METHOD("test_methodc_args"), &MethodBindTester::test_methodc_args);
+ ClassDB::bind_method(D_METHOD("test_methodr"), &MethodBindTester::test_methodr);
+ ClassDB::bind_method(D_METHOD("test_methodr_args"), &MethodBindTester::test_methodr_args);
+ ClassDB::bind_method(D_METHOD("test_methodrc"), &MethodBindTester::test_methodrc);
+ ClassDB::bind_method(D_METHOD("test_methodrc_args"), &MethodBindTester::test_methodrc_args);
+ ClassDB::bind_method(D_METHOD("test_method_default_args"), &MethodBindTester::test_method_default_args, DEFVAL(9) /* wrong on purpose */, DEFVAL(4), DEFVAL(5));
+ }
+
+ virtual void run_tests() {
+ for (int i = 0; i < TEST_MAX; i++) {
+ test_valid[i] = false;
+ }
+ //regular
+ test_num = Math::rand();
+ call("test_method");
+ test_num = Math::rand();
+ call("test_method_args", test_num);
+ test_num = Math::rand();
+ call("test_methodc");
+ test_num = Math::rand();
+ call("test_methodc_args", test_num);
+ //return
+ test_num = Math::rand();
+ test_valid[TEST_METHODR] = int(call("test_methodr")) == test_num && test_valid[TEST_METHODR];
+ test_num = Math::rand();
+ test_valid[TEST_METHODR_ARGS] = int(call("test_methodr_args", test_num)) == test_num && test_valid[TEST_METHODR_ARGS];
+ test_num = Math::rand();
+ test_valid[TEST_METHODRC] = int(call("test_methodrc")) == test_num && test_valid[TEST_METHODRC];
+ test_num = Math::rand();
+ test_valid[TEST_METHODRC_ARGS] = int(call("test_methodrc_args", test_num)) == test_num && test_valid[TEST_METHODRC_ARGS];
+
+ call("test_method_default_args", 1, 2, 3, 4);
+ }
+};
+
+TEST_CASE("[MethodBind] check all method binds") {
+ MethodBindTester *mbt = memnew(MethodBindTester);
+
+ print_line("testing method bind");
+ mbt->run_tests();
+
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHOD]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHOD_ARGS]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHODC]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHODC_ARGS]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHODR]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHODR_ARGS]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHODRC]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHODRC_ARGS]);
+ CHECK(mbt->test_valid[MethodBindTester::TEST_METHOD_DEFARGS]);
+
+ memdelete(mbt);
+}
+} // namespace TestMethodBind
+
+#endif // TEST_METHOD_BIND_H
diff --git a/tests/core/object/test_object.h b/tests/core/object/test_object.h
new file mode 100644
index 0000000000..f9158eccec
--- /dev/null
+++ b/tests/core/object/test_object.h
@@ -0,0 +1,280 @@
+/*************************************************************************/
+/* test_object.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_OBJECT_H
+#define TEST_OBJECT_H
+
+#include "core/core_string_names.h"
+#include "core/object/class_db.h"
+#include "core/object/object.h"
+#include "core/object/script_language.h"
+
+#include "tests/test_macros.h"
+
+// Declared in global namespace because of GDCLASS macro warning (Windows):
+// "Unqualified friend declaration referring to type outside of the nearest enclosing namespace
+// is a Microsoft extension; add a nested name specifier".
+class _TestDerivedObject : public Object {
+ GDCLASS(_TestDerivedObject, Object);
+
+ int property_value;
+
+protected:
+ static void _bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_property", "property"), &_TestDerivedObject::set_property);
+ ClassDB::bind_method(D_METHOD("get_property"), &_TestDerivedObject::get_property);
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "property"), "set_property", "get_property");
+ }
+
+public:
+ void set_property(int value) { property_value = value; }
+ int get_property() const { return property_value; }
+};
+
+namespace TestObject {
+
+class _MockScriptInstance : public ScriptInstance {
+ StringName property_name = "NO_NAME";
+ Variant property_value;
+
+public:
+ bool set(const StringName &p_name, const Variant &p_value) override {
+ property_name = p_name;
+ property_value = p_value;
+ return true;
+ }
+ bool get(const StringName &p_name, Variant &r_ret) const override {
+ if (property_name == p_name) {
+ r_ret = property_value;
+ return true;
+ }
+ return false;
+ }
+ void get_property_list(List<PropertyInfo> *p_properties) const override {
+ }
+ Variant::Type get_property_type(const StringName &p_name, bool *r_is_valid) const override {
+ return Variant::PACKED_FLOAT32_ARRAY;
+ }
+ void get_method_list(List<MethodInfo> *p_list) const override {
+ }
+ bool has_method(const StringName &p_method) const override {
+ return false;
+ }
+ Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override {
+ return Variant();
+ }
+ void notification(int p_notification) override {
+ }
+ Ref<Script> get_script() const override {
+ return Ref<Script>();
+ }
+ const Vector<Multiplayer::RPCConfig> get_rpc_methods() const override {
+ return Vector<Multiplayer::RPCConfig>();
+ }
+ ScriptLanguage *get_language() override {
+ return nullptr;
+ }
+};
+
+TEST_CASE("[Object] Core getters") {
+ Object object;
+
+ CHECK_MESSAGE(
+ object.is_class("Object"),
+ "is_class() should return the expected value.");
+ CHECK_MESSAGE(
+ object.get_class() == "Object",
+ "The returned class should match the expected value.");
+ CHECK_MESSAGE(
+ object.get_class_name() == "Object",
+ "The returned class name should match the expected value.");
+ CHECK_MESSAGE(
+ object.get_class_static() == "Object",
+ "The returned static class should match the expected value.");
+ CHECK_MESSAGE(
+ object.get_save_class() == "Object",
+ "The returned save class should match the expected value.");
+
+ List<String> inheritance_list;
+ object.get_inheritance_list_static(&inheritance_list);
+ CHECK_MESSAGE(
+ inheritance_list.size() == 1,
+ "The inheritance list should consist of Object only");
+ CHECK_MESSAGE(
+ inheritance_list[0] == "Object",
+ "The inheritance list should consist of Object only");
+}
+
+TEST_CASE("[Object] Metadata") {
+ const String meta_path = "hello/world complex métadata\n\n\t\tpath";
+ Object object;
+
+ object.set_meta(meta_path, Color(0, 1, 0));
+ CHECK_MESSAGE(
+ Color(object.get_meta(meta_path)).is_equal_approx(Color(0, 1, 0)),
+ "The returned object metadata after setting should match the expected value.");
+
+ List<StringName> meta_list;
+ object.get_meta_list(&meta_list);
+ CHECK_MESSAGE(
+ meta_list.size() == 1,
+ "The metadata list should only contain 1 item after adding one metadata item.");
+
+ object.remove_meta(meta_path);
+ // Also try removing nonexistent metadata (it should do nothing, without printing an error message).
+ object.remove_meta("I don't exist");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ object.get_meta(meta_path) == Variant(),
+ "The returned object metadata after removing should match the expected value.");
+ ERR_PRINT_ON;
+
+ List<StringName> meta_list2;
+ object.get_meta_list(&meta_list2);
+ CHECK_MESSAGE(
+ meta_list2.size() == 0,
+ "The metadata list should contain 0 items after removing all metadata items.");
+}
+
+TEST_CASE("[Object] Construction") {
+ Object object;
+
+ CHECK_MESSAGE(
+ !object.is_ref_counted(),
+ "Object is not a RefCounted.");
+
+ Object *p_db = ObjectDB::get_instance(object.get_instance_id());
+ CHECK_MESSAGE(
+ p_db == &object,
+ "The database pointer returned by the object id should reference same object.");
+}
+
+TEST_CASE("[Object] Script instance property setter") {
+ Object object;
+ _MockScriptInstance *script_instance = memnew(_MockScriptInstance);
+ object.set_script_instance(script_instance);
+
+ bool valid = false;
+ object.set("some_name", 100, &valid);
+ CHECK(valid);
+ Variant actual_value;
+ CHECK_MESSAGE(
+ script_instance->get("some_name", actual_value),
+ "The assigned script instance should successfully retrieve value by name.");
+ CHECK_MESSAGE(
+ actual_value == Variant(100),
+ "The returned value should equal the one which was set by the object.");
+}
+
+TEST_CASE("[Object] Script instance property getter") {
+ Object object;
+ _MockScriptInstance *script_instance = memnew(_MockScriptInstance);
+ script_instance->set("some_name", 100); // Make sure script instance has the property
+ object.set_script_instance(script_instance);
+
+ bool valid = false;
+ const Variant &actual_value = object.get("some_name", &valid);
+ CHECK(valid);
+ CHECK_MESSAGE(
+ actual_value == Variant(100),
+ "The returned value should equal the one which was set by the script instance.");
+}
+
+TEST_CASE("[Object] Built-in property setter") {
+ GDREGISTER_CLASS(_TestDerivedObject);
+ _TestDerivedObject derived_object;
+
+ bool valid = false;
+ derived_object.set("property", 100, &valid);
+ CHECK(valid);
+ CHECK_MESSAGE(
+ derived_object.get_property() == 100,
+ "The property value should equal the one which was set with built-in setter.");
+}
+
+TEST_CASE("[Object] Built-in property getter") {
+ GDREGISTER_CLASS(_TestDerivedObject);
+ _TestDerivedObject derived_object;
+ derived_object.set_property(100);
+
+ bool valid = false;
+ const Variant &actual_value = derived_object.get("property", &valid);
+ CHECK(valid);
+ CHECK_MESSAGE(
+ actual_value == Variant(100),
+ "The returned value should equal the one which was set with built-in setter.");
+}
+
+TEST_CASE("[Object] Script property setter") {
+ Object object;
+ Variant script;
+
+ bool valid = false;
+ object.set(CoreStringNames::get_singleton()->_script, script, &valid);
+ CHECK(valid);
+ CHECK_MESSAGE(
+ object.get_script() == script,
+ "The object script should be equal to the assigned one.");
+}
+
+TEST_CASE("[Object] Script property getter") {
+ Object object;
+ Variant script;
+ object.set_script(script);
+
+ bool valid = false;
+ const Variant &actual_value = object.get(CoreStringNames::get_singleton()->_script, &valid);
+ CHECK(valid);
+ CHECK_MESSAGE(
+ actual_value == script,
+ "The returned value should be equal to the assigned script.");
+}
+
+TEST_CASE("[Object] Absent name setter") {
+ Object object;
+
+ bool valid = true;
+ object.set("absent_name", 100, &valid);
+ CHECK(!valid);
+}
+
+TEST_CASE("[Object] Absent name getter") {
+ Object object;
+
+ bool valid = true;
+ const Variant &actual_value = object.get("absent_name", &valid);
+ CHECK(!valid);
+ CHECK_MESSAGE(
+ actual_value == Variant(),
+ "The returned value should equal nil variant.");
+}
+} // namespace TestObject
+
+#endif // TEST_OBJECT_H
diff --git a/tests/core/string/test_node_path.h b/tests/core/string/test_node_path.h
new file mode 100644
index 0000000000..d2de766889
--- /dev/null
+++ b/tests/core/string/test_node_path.h
@@ -0,0 +1,172 @@
+/*************************************************************************/
+/* test_node_path.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_NODE_PATH_H
+#define TEST_NODE_PATH_H
+
+#include "core/string/node_path.h"
+
+#include "tests/test_macros.h"
+
+namespace TestNodePath {
+
+TEST_CASE("[NodePath] Relative path") {
+ const NodePath node_path_relative = NodePath("Path2D/PathFollow2D/Sprite2D:position:x");
+
+ CHECK_MESSAGE(
+ node_path_relative.get_as_property_path() == NodePath(":Path2D/PathFollow2D/Sprite2D:position:x"),
+ "The returned property path should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_relative.get_concatenated_subnames() == "position:x",
+ "The returned concatenated subnames should match the expected value.");
+
+ CHECK_MESSAGE(
+ node_path_relative.get_name(0) == "Path2D",
+ "The returned name at index 0 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_relative.get_name(1) == "PathFollow2D",
+ "The returned name at index 1 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_relative.get_name(2) == "Sprite2D",
+ "The returned name at index 2 should match the expected value.");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ node_path_relative.get_name(3) == "",
+ "The returned name at invalid index 3 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_relative.get_name(-1) == "",
+ "The returned name at invalid index -1 should match the expected value.");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ node_path_relative.get_name_count() == 3,
+ "The returned number of names should match the expected value.");
+
+ CHECK_MESSAGE(
+ node_path_relative.get_subname(0) == "position",
+ "The returned subname at index 0 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_relative.get_subname(1) == "x",
+ "The returned subname at index 1 should match the expected value.");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ node_path_relative.get_subname(2) == "",
+ "The returned subname at invalid index 2 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_relative.get_subname(-1) == "",
+ "The returned subname at invalid index -1 should match the expected value.");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ node_path_relative.get_subname_count() == 2,
+ "The returned number of subnames should match the expected value.");
+
+ CHECK_MESSAGE(
+ !node_path_relative.is_absolute(),
+ "The node path should be considered relative.");
+
+ CHECK_MESSAGE(
+ !node_path_relative.is_empty(),
+ "The node path shouldn't be considered empty.");
+}
+
+TEST_CASE("[NodePath] Absolute path") {
+ const NodePath node_path_aboslute = NodePath("/root/Sprite2D");
+
+ CHECK_MESSAGE(
+ node_path_aboslute.get_as_property_path() == NodePath(":root/Sprite2D"),
+ "The returned property path should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_aboslute.get_concatenated_subnames() == "",
+ "The returned concatenated subnames should match the expected value.");
+
+ CHECK_MESSAGE(
+ node_path_aboslute.get_name(0) == "root",
+ "The returned name at index 0 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_aboslute.get_name(1) == "Sprite2D",
+ "The returned name at index 1 should match the expected value.");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ node_path_aboslute.get_name(2) == "",
+ "The returned name at invalid index 2 should match the expected value.");
+ CHECK_MESSAGE(
+ node_path_aboslute.get_name(-1) == "",
+ "The returned name at invalid index -1 should match the expected value.");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ node_path_aboslute.get_name_count() == 2,
+ "The returned number of names should match the expected value.");
+
+ CHECK_MESSAGE(
+ node_path_aboslute.get_subname_count() == 0,
+ "The returned number of subnames should match the expected value.");
+
+ CHECK_MESSAGE(
+ node_path_aboslute.is_absolute(),
+ "The node path should be considered absolute.");
+
+ CHECK_MESSAGE(
+ !node_path_aboslute.is_empty(),
+ "The node path shouldn't be considered empty.");
+}
+
+TEST_CASE("[NodePath] Empty path") {
+ const NodePath node_path_empty = NodePath();
+
+ CHECK_MESSAGE(
+ node_path_empty.get_as_property_path() == NodePath(),
+ "The returned property path should match the expected value.");
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ node_path_empty.get_concatenated_subnames() == "",
+ "The returned concatenated subnames should match the expected value.");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ node_path_empty.get_name_count() == 0,
+ "The returned number of names should match the expected value.");
+
+ CHECK_MESSAGE(
+ node_path_empty.get_subname_count() == 0,
+ "The returned number of subnames should match the expected value.");
+
+ CHECK_MESSAGE(
+ !node_path_empty.is_absolute(),
+ "The node path shouldn't be considered absolute.");
+
+ CHECK_MESSAGE(
+ node_path_empty.is_empty(),
+ "The node path should be considered empty.");
+}
+} // namespace TestNodePath
+
+#endif // TEST_NODE_PATH_H
diff --git a/tests/core/string/test_string.h b/tests/core/string/test_string.h
new file mode 100644
index 0000000000..2f611c26a9
--- /dev/null
+++ b/tests/core/string/test_string.h
@@ -0,0 +1,1481 @@
+/*************************************************************************/
+/* test_string.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_STRING_H
+#define TEST_STRING_H
+
+#include "core/string/ustring.h"
+
+#include "tests/test_macros.h"
+
+namespace TestString {
+
+int u32scmp(const char32_t *l, const char32_t *r) {
+ for (; *l == *r && *l && *r; l++, r++)
+ ;
+ return *l - *r;
+}
+
+TEST_CASE("[String] Assign Latin-1 char string") {
+ String s = "Hello";
+ CHECK(u32scmp(s.get_data(), U"Hello") == 0);
+}
+
+TEST_CASE("[String] Assign from Latin-1 char string (operator=)") {
+ String s = "Dolly";
+ const String &t = s;
+ CHECK(u32scmp(t.get_data(), U"Dolly") == 0);
+}
+
+TEST_CASE("[String] Assign from Latin-1 char string (copycon)") {
+ String s("Sheep");
+ const String &t1(s);
+ CHECK(u32scmp(t1.get_data(), U"Sheep") == 0);
+
+ String t2 = String("Sheep", 3);
+ CHECK(u32scmp(t2.get_data(), U"She") == 0);
+}
+
+TEST_CASE("[String] Assign from wchar_t string (operator=)") {
+ String s = L"Give me";
+ CHECK(u32scmp(s.get_data(), U"Give me") == 0);
+}
+
+TEST_CASE("[String] Assign from wchar_t string (copycon)") {
+ String s(L"Wool");
+ CHECK(u32scmp(s.get_data(), U"Wool") == 0);
+}
+
+TEST_CASE("[String] Assign from char32_t string (operator=)") {
+ String s = U"Give me";
+ CHECK(u32scmp(s.get_data(), U"Give me") == 0);
+}
+
+TEST_CASE("[String] Assign from char32_t string (copycon)") {
+ String s(U"Wool");
+ CHECK(u32scmp(s.get_data(), U"Wool") == 0);
+}
+
+TEST_CASE("[String] UTF8") {
+ /* how can i embed UTF in here? */
+ static const char32_t u32str[] = { 0x0045, 0x0020, 0x304A, 0x360F, 0x3088, 0x3046, 0x1F3A4, 0 };
+ static const uint8_t u8str[] = { 0x45, 0x20, 0xE3, 0x81, 0x8A, 0xE3, 0x98, 0x8F, 0xE3, 0x82, 0x88, 0xE3, 0x81, 0x86, 0xF0, 0x9F, 0x8E, 0xA4, 0 };
+ String s = u32str;
+ bool err = s.parse_utf8(s.utf8().get_data());
+ CHECK(!err);
+ CHECK(s == u32str);
+
+ err = s.parse_utf8((const char *)u8str);
+ CHECK(!err);
+ CHECK(s == u32str);
+
+ CharString cs = (const char *)u8str;
+ CHECK(String::utf8(cs) == s);
+}
+
+TEST_CASE("[String] UTF16") {
+ /* how can i embed UTF in here? */
+ static const char32_t u32str[] = { 0x0045, 0x0020, 0x304A, 0x360F, 0x3088, 0x3046, 0x1F3A4, 0 };
+ static const char16_t u16str[] = { 0x0045, 0x0020, 0x304A, 0x360F, 0x3088, 0x3046, 0xD83C, 0xDFA4, 0 };
+ String s = u32str;
+ bool err = s.parse_utf16(s.utf16().get_data());
+ CHECK(!err);
+ CHECK(s == u32str);
+
+ err = s.parse_utf16(u16str);
+ CHECK(!err);
+ CHECK(s == u32str);
+
+ Char16String cs = u16str;
+ CHECK(String::utf16(cs) == s);
+}
+
+TEST_CASE("[String] UTF8 with BOM") {
+ /* how can i embed UTF in here? */
+ static const char32_t u32str[] = { 0x0045, 0x0020, 0x304A, 0x360F, 0x3088, 0x3046, 0x1F3A4, 0 };
+ static const uint8_t u8str[] = { 0xEF, 0xBB, 0xBF, 0x45, 0x20, 0xE3, 0x81, 0x8A, 0xE3, 0x98, 0x8F, 0xE3, 0x82, 0x88, 0xE3, 0x81, 0x86, 0xF0, 0x9F, 0x8E, 0xA4, 0 };
+ String s;
+ bool err = s.parse_utf8((const char *)u8str);
+ CHECK(!err);
+ CHECK(s == u32str);
+
+ CharString cs = (const char *)u8str;
+ CHECK(String::utf8(cs) == s);
+}
+
+TEST_CASE("[String] UTF16 with BOM") {
+ /* how can i embed UTF in here? */
+ static const char32_t u32str[] = { 0x0020, 0x0045, 0x304A, 0x360F, 0x3088, 0x3046, 0x1F3A4, 0 };
+ static const char16_t u16str[] = { 0xFEFF, 0x0020, 0x0045, 0x304A, 0x360F, 0x3088, 0x3046, 0xD83C, 0xDFA4, 0 };
+ static const char16_t u16str_swap[] = { 0xFFFE, 0x2000, 0x4500, 0x4A30, 0x0F36, 0x8830, 0x4630, 0x3CD8, 0xA4DF, 0 };
+ String s;
+ bool err = s.parse_utf16(u16str);
+ CHECK(!err);
+ CHECK(s == u32str);
+
+ err = s.parse_utf16(u16str_swap);
+ CHECK(!err);
+ CHECK(s == u32str);
+
+ Char16String cs = u16str;
+ CHECK(String::utf16(cs) == s);
+
+ cs = u16str_swap;
+ CHECK(String::utf16(cs) == s);
+}
+
+TEST_CASE("[String] Invalid UTF8") {
+ ERR_PRINT_OFF
+ static const uint8_t u8str[] = { 0x45, 0xE3, 0x81, 0x8A, 0x8F, 0xE3, 0xE3, 0x98, 0x8F, 0xE3, 0x82, 0x88, 0xE3, 0x81, 0x86, 0xF0, 0x9F, 0x8E, 0xA4, 0 };
+ String s;
+ bool err = s.parse_utf8((const char *)u8str);
+ CHECK(err);
+ CHECK(s.is_empty());
+
+ CharString cs = (const char *)u8str;
+ CHECK(String::utf8(cs).is_empty());
+ ERR_PRINT_ON
+}
+
+TEST_CASE("[String] Invalid UTF16") {
+ ERR_PRINT_OFF
+ static const char16_t u16str[] = { 0x0045, 0x304A, 0x3088, 0x3046, 0xDFA4, 0 };
+ String s;
+ bool err = s.parse_utf16(u16str);
+ CHECK(err);
+ CHECK(s.is_empty());
+
+ Char16String cs = u16str;
+ CHECK(String::utf16(cs).is_empty());
+ ERR_PRINT_ON
+}
+
+TEST_CASE("[String] ASCII") {
+ String s = U"Primero Leche";
+ String t = s.ascii(false).get_data();
+ CHECK(s == t);
+
+ t = s.ascii(true).get_data();
+ CHECK(s == t);
+}
+
+TEST_CASE("[String] Comparisons (equal)") {
+ String s = "Test Compare";
+ CHECK(s == "Test Compare");
+ CHECK(s == U"Test Compare");
+ CHECK(s == L"Test Compare");
+ CHECK(s == String("Test Compare"));
+}
+
+TEST_CASE("[String] Comparisons (not equal)") {
+ String s = "Test Compare";
+ CHECK(s != "Peanut");
+ CHECK(s != U"Coconut");
+ CHECK(s != L"Coconut");
+ CHECK(s != String("Butter"));
+}
+
+TEST_CASE("[String] Comparisons (operator <)") {
+ String s = "Bees";
+ CHECK(s < "Elephant");
+ CHECK(!(s < U"Amber"));
+ CHECK(!(s < L"Amber"));
+ CHECK(!(s < String("Beatrix")));
+}
+
+TEST_CASE("[String] Concatenation") {
+ String s;
+
+ s += "Have";
+ s += ' ';
+ s += 'a';
+ s += String(" ");
+ s = s + U"Nice";
+ s = s + " ";
+ s = s + String("Day");
+
+ CHECK(s == "Have a Nice Day");
+}
+
+TEST_CASE("[String] Testing size and length of string") {
+ // todo: expand this test to do more tests on size() as it is complicated under the hood.
+ CHECK(String("Mellon").size() == 7);
+ CHECK(String("Mellon1").size() == 8);
+
+ // length works fine and is easier to test
+ CHECK(String("Mellon").length() == 6);
+ CHECK(String("Mellon1").length() == 7);
+ CHECK(String("Mellon2").length() == 7);
+ CHECK(String("Mellon3").length() == 7);
+}
+
+TEST_CASE("[String] Testing for empty string") {
+ CHECK(!String("Mellon").is_empty());
+ // do this more than once, to check for string corruption
+ CHECK(String("").is_empty());
+ CHECK(String("").is_empty());
+ CHECK(String("").is_empty());
+}
+
+TEST_CASE("[String] Test chr") {
+ CHECK(String::chr('H') == "H");
+ CHECK(String::chr(0x3012)[0] == 0x3012);
+ ERR_PRINT_OFF
+ CHECK(String::chr(0xd812)[0] == 0xfffd); // Unpaired UTF-16 surrogate
+ CHECK(String::chr(0x20d812)[0] == 0xfffd); // Outside UTF-32 range
+ ERR_PRINT_ON
+}
+
+TEST_CASE("[String] Operator []") {
+ String a = "Kugar Sane";
+ a[0] = 'S';
+ a[6] = 'C';
+ CHECK(a == "Sugar Cane");
+ CHECK(a[1] == 'u');
+ CHECK(a.unicode_at(1) == 'u');
+}
+
+TEST_CASE("[String] Case function test") {
+ String a = "MoMoNgA";
+
+ CHECK(a.to_upper() == "MOMONGA");
+ CHECK(a.to_lower() == "momonga");
+}
+
+TEST_CASE("[String] Case compare function test") {
+ String a = "MoMoNgA";
+
+ CHECK(a.casecmp_to("momonga") != 0);
+ CHECK(a.nocasecmp_to("momonga") == 0);
+}
+
+TEST_CASE("[String] Natural compare function test") {
+ String a = "img2.png";
+
+ CHECK(a.nocasecmp_to("img10.png") > 0);
+ CHECK(a.naturalnocasecmp_to("img10.png") < 0);
+}
+
+TEST_CASE("[String] hex_encode_buffer") {
+ static const uint8_t u8str[] = { 0x45, 0xE3, 0x81, 0x8A, 0x8F, 0xE3 };
+ String s = String::hex_encode_buffer(u8str, 6);
+ CHECK(s == U"45e3818a8fe3");
+}
+
+TEST_CASE("[String] Substr") {
+ String s = "Killer Baby";
+ CHECK(s.substr(3, 4) == "ler ");
+ CHECK(s.substr(3) == "ler Baby");
+}
+
+TEST_CASE("[String] Find") {
+ String s = "Pretty Woman Woman";
+ CHECK(s.find("tty") == 3);
+ CHECK(s.find("Wo", 9) == 13);
+ CHECK(s.find("Revenge of the Monster Truck") == -1);
+ CHECK(s.rfind("man") == 15);
+}
+
+TEST_CASE("[String] Find no case") {
+ String s = "Pretty Whale Whale";
+ CHECK(s.findn("WHA") == 7);
+ CHECK(s.findn("WHA", 9) == 13);
+ CHECK(s.findn("Revenge of the Monster SawFish") == -1);
+ CHECK(s.rfindn("WHA") == 13);
+}
+
+TEST_CASE("[String] Find MK") {
+ Vector<String> keys;
+ keys.push_back("sty");
+ keys.push_back("tty");
+ keys.push_back("man");
+
+ String s = "Pretty Woman";
+ int key = 0;
+
+ CHECK(s.findmk(keys, 0, &key) == 3);
+ CHECK(key == 1);
+
+ CHECK(s.findmk(keys, 5, &key) == 9);
+ CHECK(key == 2);
+}
+
+TEST_CASE("[String] Find and replace") {
+ String s = "Happy Birthday, Anna!";
+ s = s.replace("Birthday", "Halloween");
+ CHECK(s == "Happy Halloween, Anna!");
+
+ s = s.replace_first("H", "W");
+ CHECK(s == "Wappy Halloween, Anna!");
+}
+
+TEST_CASE("[String] Insertion") {
+ String s = "Who is Frederic?";
+ s = s.insert(s.find("?"), " Chopin");
+ CHECK(s == "Who is Frederic Chopin?");
+}
+
+TEST_CASE("[String] Number to string") {
+ CHECK(String::num(0) == "0");
+ CHECK(String::num(0.0) == "0"); // No trailing zeros.
+ CHECK(String::num(-0.0) == "-0"); // Includes sign even for zero.
+ CHECK(String::num(3.141593) == "3.141593");
+ CHECK(String::num(3.141593, 3) == "3.142");
+ CHECK(String::num_scientific(30000000) == "3e+07");
+ CHECK(String::num_int64(3141593) == "3141593");
+ CHECK(String::num_int64(0xA141593, 16) == "a141593");
+ CHECK(String::num_int64(0xA141593, 16, true) == "A141593");
+ CHECK(String::num(42.100023, 4) == "42.1"); // No trailing zeros.
+
+ // String::num_real tests.
+ CHECK(String::num_real(3.141593) == "3.141593");
+ CHECK(String::num_real(3.141) == "3.141"); // No trailing zeros.
+#ifdef REAL_T_IS_DOUBLE
+ CHECK_MESSAGE(String::num_real(Math_PI) == "3.14159265358979", "Prints the appropriate amount of digits for real_t = double.");
+ CHECK_MESSAGE(String::num_real(3.1415f) == "3.14149999618530", "Prints more digits of 32-bit float when real_t = double (ones that would be reliable for double).");
+#else
+ CHECK_MESSAGE(String::num_real(Math_PI) == "3.141593", "Prints the appropriate amount of digits for real_t = float.");
+ CHECK_MESSAGE(String::num_real(3.1415f) == "3.1415", "Prints only reliable digits of 32-bit float when real_t = float.");
+#endif // REAL_T_IS_DOUBLE
+
+ // Checks doubles with many decimal places.
+ CHECK(String::num(0.0000012345432123454321, -1) == "0.00000123454321"); // -1 uses 14 as sane default.
+ CHECK(String::num(0.0000012345432123454321) == "0.00000123454321"); // -1 is the default value.
+ CHECK(String::num(-0.0000012345432123454321) == "-0.00000123454321");
+ CHECK(String::num(-10000.0000012345432123454321) == "-10000.0000012345");
+ CHECK(String::num(0.0000000000012345432123454321) == "0.00000000000123");
+ CHECK(String::num(0.0000000000012345432123454321, 3) == "0");
+
+ // Note: When relevant (remainder > 0.5), the last digit gets rounded up,
+ // which can also lead to not include a trailing zero, e.g. "...89" -> "...9".
+ CHECK(String::num(0.0000056789876567898765) == "0.00000567898766"); // Should round last digit.
+ CHECK(String::num(10000.000005678999999999) == "10000.000005679"); // We cut at ...789|99 which is rounded to ...79, so only 13 decimals.
+ CHECK(String::num(42.12999999, 6) == "42.13"); // Also happens with lower decimals count.
+
+ // 32 is MAX_DECIMALS. We can't reliably store that many so we can't compare against a string,
+ // but we can check that the string length is 34 (32 + 2 for "0.").
+ CHECK(String::num(0.00000123456789987654321123456789987654321, 32).length() == 34);
+ CHECK(String::num(0.00000123456789987654321123456789987654321, 42).length() == 34); // Should enforce MAX_DECIMALS.
+ CHECK(String::num(10000.00000123456789987654321123456789987654321, 42).length() == 38); // 32 decimals + "10000.".
+}
+
+TEST_CASE("[String] String to integer") {
+ static const char *nums[4] = { "1237461283", "- 22", "0", " - 1123412" };
+ static const int num[4] = { 1237461283, -22, 0, -1123412 };
+
+ for (int i = 0; i < 4; i++) {
+ CHECK(String(nums[i]).to_int() == num[i]);
+ }
+}
+
+TEST_CASE("[String] Hex to integer") {
+ static const char *nums[4] = { "0xFFAE", "22", "0", "AADDAD" };
+ static const int64_t num[4] = { 0xFFAE, 0x22, 0, 0xAADDAD };
+
+ for (int i = 0; i < 4; i++) {
+ CHECK(String(nums[i]).hex_to_int() == num[i]);
+ }
+}
+
+TEST_CASE("[String] String to float") {
+ static const char *nums[4] = { "-12348298412.2", "0.05", "2.0002", " -0.0001" };
+ static const double num[4] = { -12348298412.2, 0.05, 2.0002, -0.0001 };
+
+ for (int i = 0; i < 4; i++) {
+ CHECK(!(ABS(String(nums[i]).to_float() - num[i]) > 0.00001));
+ }
+}
+
+TEST_CASE("[String] CamelCase to underscore") {
+ CHECK(String("TestTestStringGD").camelcase_to_underscore(false) == String("Test_Test_String_GD"));
+ CHECK(String("TestTestStringGD").camelcase_to_underscore(true) == String("test_test_string_gd"));
+}
+
+TEST_CASE("[String] Slicing") {
+ String s = "Mars,Jupiter,Saturn,Uranus";
+
+ const char *slices[4] = { "Mars", "Jupiter", "Saturn", "Uranus" };
+ for (int i = 0; i < s.get_slice_count(","); i++) {
+ CHECK(s.get_slice(",", i) == slices[i]);
+ }
+}
+
+TEST_CASE("[String] Splitting") {
+ String s = "Mars,Jupiter,Saturn,Uranus";
+ Vector<String> l;
+
+ const char *slices_l[3] = { "Mars", "Jupiter", "Saturn,Uranus" };
+ const char *slices_r[3] = { "Mars,Jupiter", "Saturn", "Uranus" };
+
+ l = s.split(",", true, 2);
+ CHECK(l.size() == 3);
+ for (int i = 0; i < l.size(); i++) {
+ CHECK(l[i] == slices_l[i]);
+ }
+
+ l = s.rsplit(",", true, 2);
+ CHECK(l.size() == 3);
+ for (int i = 0; i < l.size(); i++) {
+ CHECK(l[i] == slices_r[i]);
+ }
+
+ s = "Mars Jupiter Saturn Uranus";
+ const char *slices_s[4] = { "Mars", "Jupiter", "Saturn", "Uranus" };
+ l = s.split_spaces();
+ for (int i = 0; i < l.size(); i++) {
+ CHECK(l[i] == slices_s[i]);
+ }
+
+ s = "1.2;2.3 4.5";
+ const double slices_d[3] = { 1.2, 2.3, 4.5 };
+
+ Vector<float> f;
+ f = s.split_floats(";");
+ CHECK(f.size() == 2);
+ for (int i = 0; i < f.size(); i++) {
+ CHECK(ABS(f[i] - slices_d[i]) <= 0.00001);
+ }
+
+ Vector<String> keys;
+ keys.push_back(";");
+ keys.push_back(" ");
+
+ f = s.split_floats_mk(keys);
+ CHECK(f.size() == 3);
+ for (int i = 0; i < f.size(); i++) {
+ CHECK(ABS(f[i] - slices_d[i]) <= 0.00001);
+ }
+
+ s = "1;2 4";
+ const int slices_i[3] = { 1, 2, 4 };
+
+ Vector<int> ii;
+ ii = s.split_ints(";");
+ CHECK(ii.size() == 2);
+ for (int i = 0; i < ii.size(); i++) {
+ CHECK(ii[i] == slices_i[i]);
+ }
+
+ ii = s.split_ints_mk(keys);
+ CHECK(ii.size() == 3);
+ for (int i = 0; i < ii.size(); i++) {
+ CHECK(ii[i] == slices_i[i]);
+ }
+}
+
+struct test_27_data {
+ char const *data;
+ char const *part;
+ bool expected;
+};
+
+TEST_CASE("[String] Begins with") {
+ test_27_data tc[] = {
+ { "res://foobar", "res://", true },
+ { "res", "res://", false },
+ { "abc", "abc", true }
+ };
+ size_t count = sizeof(tc) / sizeof(tc[0]);
+ bool state = true;
+ for (size_t i = 0; state && i < count; ++i) {
+ String s = tc[i].data;
+ state = s.begins_with(tc[i].part) == tc[i].expected;
+ if (state) {
+ String sb = tc[i].part;
+ state = s.begins_with(sb) == tc[i].expected;
+ }
+ CHECK(state);
+ if (!state) {
+ break;
+ }
+ };
+ CHECK(state);
+}
+
+TEST_CASE("[String] Ends with") {
+ test_27_data tc[] = {
+ { "res://foobar", "foobar", true },
+ { "res", "res://", false },
+ { "abc", "abc", true }
+ };
+ size_t count = sizeof(tc) / sizeof(tc[0]);
+ bool state = true;
+ for (size_t i = 0; state && i < count; ++i) {
+ String s = tc[i].data;
+ state = s.ends_with(tc[i].part) == tc[i].expected;
+ if (state) {
+ String sb = tc[i].part;
+ state = s.ends_with(sb) == tc[i].expected;
+ }
+ CHECK(state);
+ if (!state) {
+ break;
+ }
+ };
+ CHECK(state);
+}
+
+TEST_CASE("[String] format") {
+ const String value_format = "red=\"$red\" green=\"$green\" blue=\"$blue\" alpha=\"$alpha\"";
+
+ Dictionary value_dictionary;
+ value_dictionary["red"] = 10;
+ value_dictionary["green"] = 20;
+ value_dictionary["blue"] = "bla";
+ value_dictionary["alpha"] = 0.4;
+ String value = value_format.format(value_dictionary, "$_");
+
+ CHECK(value == "red=\"10\" green=\"20\" blue=\"bla\" alpha=\"0.4\"");
+}
+
+TEST_CASE("[String] sprintf") {
+ String format, output;
+ Array args;
+ bool error;
+
+ // %%
+ format = "fish %% frog";
+ args.clear();
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish % frog"));
+ //////// INTS
+
+ // Int
+ format = "fish %d frog";
+ args.clear();
+ args.push_back(5);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 5 frog"));
+
+ // Int left padded with zeroes.
+ format = "fish %05d frog";
+ args.clear();
+ args.push_back(5);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 00005 frog"));
+
+ // Int left padded with spaces.
+ format = "fish %5d frog";
+ args.clear();
+ args.push_back(5);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 5 frog"));
+
+ // Int right padded with spaces.
+ format = "fish %-5d frog";
+ args.clear();
+ args.push_back(5);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 5 frog"));
+
+ // Int with sign (positive).
+ format = "fish %+d frog";
+ args.clear();
+ args.push_back(5);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish +5 frog"));
+
+ // Negative int.
+ format = "fish %d frog";
+ args.clear();
+ args.push_back(-5);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish -5 frog"));
+
+ // Hex (lower)
+ format = "fish %x frog";
+ args.clear();
+ args.push_back(45);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 2d frog"));
+
+ // Hex (upper)
+ format = "fish %X frog";
+ args.clear();
+ args.push_back(45);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 2D frog"));
+
+ // Octal
+ format = "fish %o frog";
+ args.clear();
+ args.push_back(99);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 143 frog"));
+
+ ////// REALS
+
+ // Real
+ format = "fish %f frog";
+ args.clear();
+ args.push_back(99.99);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 99.990000 frog"));
+
+ // Real left-padded
+ format = "fish %11f frog";
+ args.clear();
+ args.push_back(99.99);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 99.990000 frog"));
+
+ // Real right-padded
+ format = "fish %-11f frog";
+ args.clear();
+ args.push_back(99.99);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 99.990000 frog"));
+
+ // Real given int.
+ format = "fish %f frog";
+ args.clear();
+ args.push_back(99);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 99.000000 frog"));
+
+ // Real with sign (positive).
+ format = "fish %+f frog";
+ args.clear();
+ args.push_back(99.99);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish +99.990000 frog"));
+
+ // Real with 1 decimals.
+ format = "fish %.1f frog";
+ args.clear();
+ args.push_back(99.99);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 100.0 frog"));
+
+ // Real with 12 decimals.
+ format = "fish %.12f frog";
+ args.clear();
+ args.push_back(99.99);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 99.990000000000 frog"));
+
+ // Real with no decimals.
+ format = "fish %.f frog";
+ args.clear();
+ args.push_back(99.99);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 100 frog"));
+
+ /////// Strings.
+
+ // String
+ format = "fish %s frog";
+ args.clear();
+ args.push_back("cheese");
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish cheese frog"));
+
+ // String left-padded
+ format = "fish %10s frog";
+ args.clear();
+ args.push_back("cheese");
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish cheese frog"));
+
+ // String right-padded
+ format = "fish %-10s frog";
+ args.clear();
+ args.push_back("cheese");
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish cheese frog"));
+
+ ///// Characters
+
+ // Character as string.
+ format = "fish %c frog";
+ args.clear();
+ args.push_back("A");
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish A frog"));
+
+ // Character as int.
+ format = "fish %c frog";
+ args.clear();
+ args.push_back(65);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish A frog"));
+
+ ///// Dynamic width
+
+ // String dynamic width
+ format = "fish %*s frog";
+ args.clear();
+ args.push_back(10);
+ args.push_back("cheese");
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ REQUIRE(output == String("fish cheese frog"));
+
+ // Int dynamic width
+ format = "fish %*d frog";
+ args.clear();
+ args.push_back(10);
+ args.push_back(99);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ REQUIRE(output == String("fish 99 frog"));
+
+ // Float dynamic width
+ format = "fish %*.*f frog";
+ args.clear();
+ args.push_back(10);
+ args.push_back(3);
+ args.push_back(99.99);
+ output = format.sprintf(args, &error);
+ REQUIRE(error == false);
+ CHECK(output == String("fish 99.990 frog"));
+
+ ///// Errors
+
+ // More formats than arguments.
+ format = "fish %s %s frog";
+ args.clear();
+ args.push_back("cheese");
+ output = format.sprintf(args, &error);
+ REQUIRE(error);
+ CHECK(output == "not enough arguments for format string");
+
+ // More arguments than formats.
+ format = "fish %s frog";
+ args.clear();
+ args.push_back("hello");
+ args.push_back("cheese");
+ output = format.sprintf(args, &error);
+ REQUIRE(error);
+ CHECK(output == "not all arguments converted during string formatting");
+
+ // Incomplete format.
+ format = "fish %10";
+ args.clear();
+ args.push_back("cheese");
+ output = format.sprintf(args, &error);
+ REQUIRE(error);
+ CHECK(output == "incomplete format");
+
+ // Bad character in format string
+ format = "fish %&f frog";
+ args.clear();
+ args.push_back("cheese");
+ output = format.sprintf(args, &error);
+ REQUIRE(error);
+ CHECK(output == "unsupported format character");
+
+ // Too many decimals.
+ format = "fish %2.2.2f frog";
+ args.clear();
+ args.push_back(99.99);
+ output = format.sprintf(args, &error);
+ REQUIRE(error);
+ CHECK(output == "too many decimal points in format");
+
+ // * not a number
+ format = "fish %*f frog";
+ args.clear();
+ args.push_back("cheese");
+ args.push_back(99.99);
+ output = format.sprintf(args, &error);
+ REQUIRE(error);
+ CHECK(output == "* wants number");
+
+ // Character too long.
+ format = "fish %c frog";
+ args.clear();
+ args.push_back("sc");
+ output = format.sprintf(args, &error);
+ REQUIRE(error);
+ CHECK(output == "%c requires number or single-character string");
+
+ // Character bad type.
+ format = "fish %c frog";
+ args.clear();
+ args.push_back(Array());
+ output = format.sprintf(args, &error);
+ REQUIRE(error);
+ CHECK(output == "%c requires number or single-character string");
+}
+
+TEST_CASE("[String] is_numeric") {
+ CHECK(String("12").is_numeric());
+ CHECK(String("1.2").is_numeric());
+ CHECK(!String("AF").is_numeric());
+ CHECK(String("-12").is_numeric());
+ CHECK(String("-1.2").is_numeric());
+}
+
+TEST_CASE("[String] pad") {
+ String s = String("test");
+ CHECK(s.lpad(10, "x") == U"xxxxxxtest");
+ CHECK(s.rpad(10, "x") == U"testxxxxxx");
+
+ s = String("10.10");
+ CHECK(s.pad_decimals(4) == U"10.1000");
+ CHECK(s.pad_zeros(4) == U"0010.10");
+}
+
+TEST_CASE("[String] is_subsequence_of") {
+ String a = "is subsequence of";
+ CHECK(String("sub").is_subsequence_of(a));
+ CHECK(!String("Sub").is_subsequence_of(a));
+ CHECK(String("Sub").is_subsequence_ofi(a));
+}
+
+TEST_CASE("[String] match") {
+ CHECK(String("img1.png").match("*.png"));
+ CHECK(!String("img1.jpeg").match("*.png"));
+ CHECK(!String("img1.Png").match("*.png"));
+ CHECK(String("img1.Png").matchn("*.png"));
+}
+
+TEST_CASE("[String] IPVX address to string") {
+ IPAddress ip0("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
+ IPAddress ip(0x0123, 0x4567, 0x89ab, 0xcdef, true);
+ IPAddress ip2("fe80::52e5:49ff:fe93:1baf");
+ IPAddress ip3("::ffff:192.168.0.1");
+ String ip4 = "192.168.0.1";
+ CHECK(ip4.is_valid_ip_address());
+
+ ip4 = "192.368.0.1";
+ CHECK(!ip4.is_valid_ip_address());
+
+ String ip6 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334";
+ CHECK(ip6.is_valid_ip_address());
+
+ ip6 = "2001:0db8:85j3:0000:0000:8a2e:0370:7334";
+ CHECK(!ip6.is_valid_ip_address());
+
+ ip6 = "2001:0db8:85f345:0000:0000:8a2e:0370:7334";
+ CHECK(!ip6.is_valid_ip_address());
+
+ ip6 = "2001:0db8::0:8a2e:370:7334";
+ CHECK(ip6.is_valid_ip_address());
+
+ ip6 = "::ffff:192.168.0.1";
+ CHECK(ip6.is_valid_ip_address());
+}
+
+TEST_CASE("[String] Capitalize against many strings") {
+ String input = "bytes2var";
+ String output = "Bytes 2 Var";
+ CHECK(input.capitalize() == output);
+
+ input = "linear2db";
+ output = "Linear 2 Db";
+ CHECK(input.capitalize() == output);
+
+ input = "vector3";
+ output = "Vector 3";
+ CHECK(input.capitalize() == output);
+
+ input = "sha256";
+ output = "Sha 256";
+ CHECK(input.capitalize() == output);
+
+ input = "2db";
+ output = "2 Db";
+ CHECK(input.capitalize() == output);
+
+ input = "PascalCase";
+ output = "Pascal Case";
+ CHECK(input.capitalize() == output);
+
+ input = "PascalPascalCase";
+ output = "Pascal Pascal Case";
+ CHECK(input.capitalize() == output);
+
+ input = "snake_case";
+ output = "Snake Case";
+ CHECK(input.capitalize() == output);
+
+ input = "snake_snake_case";
+ output = "Snake Snake Case";
+ CHECK(input.capitalize() == output);
+
+ input = "sha256sum";
+ output = "Sha 256 Sum";
+ CHECK(input.capitalize() == output);
+
+ input = "cat2dog";
+ output = "Cat 2 Dog";
+ CHECK(input.capitalize() == output);
+
+ input = "function(name)";
+ output = "Function(name)";
+ CHECK(input.capitalize() == output);
+
+ input = "snake_case_function(snake_case_arg)";
+ output = "Snake Case Function(snake Case Arg)";
+ CHECK(input.capitalize() == output);
+
+ input = "snake_case_function( snake_case_arg )";
+ output = "Snake Case Function( Snake Case Arg )";
+ CHECK(input.capitalize() == output);
+}
+
+TEST_CASE("[String] Checking string is empty when it should be") {
+ bool state = true;
+ bool success;
+
+ String a = "";
+ success = a[0] == 0;
+ if (!success) {
+ state = false;
+ }
+ String b = "Godot";
+ success = b[b.size()] == 0;
+ if (!success) {
+ state = false;
+ }
+ const String c = "";
+ success = c[0] == 0;
+ if (!success) {
+ state = false;
+ }
+
+ const String d = "Godot";
+ success = d[d.size()] == 0;
+ if (!success) {
+ state = false;
+ }
+
+ CHECK(state);
+}
+
+TEST_CASE("[String] lstrip and rstrip") {
+#define STRIP_TEST(x) \
+ { \
+ bool success = x; \
+ state = state && success; \
+ }
+
+ bool state = true;
+
+ // strip none
+ STRIP_TEST(String("abc").lstrip("") == "abc");
+ STRIP_TEST(String("abc").rstrip("") == "abc");
+ // strip one
+ STRIP_TEST(String("abc").lstrip("a") == "bc");
+ STRIP_TEST(String("abc").rstrip("c") == "ab");
+ // strip lots
+ STRIP_TEST(String("bababbababccc").lstrip("ab") == "ccc");
+ STRIP_TEST(String("aaabcbcbcbbcbbc").rstrip("cb") == "aaa");
+ // strip empty string
+ STRIP_TEST(String("").lstrip("") == "");
+ STRIP_TEST(String("").rstrip("") == "");
+ // strip to empty string
+ STRIP_TEST(String("abcabcabc").lstrip("bca") == "");
+ STRIP_TEST(String("abcabcabc").rstrip("bca") == "");
+ // don't strip wrong end
+ STRIP_TEST(String("abc").lstrip("c") == "abc");
+ STRIP_TEST(String("abca").lstrip("a") == "bca");
+ STRIP_TEST(String("abc").rstrip("a") == "abc");
+ STRIP_TEST(String("abca").rstrip("a") == "abc");
+ // in utf-8 "¿" (\u00bf) has the same first byte as "µ" (\u00b5)
+ // and the same second as "ÿ" (\u00ff)
+ STRIP_TEST(String::utf8("¿").lstrip(String::utf8("µÿ")) == String::utf8("¿"));
+ STRIP_TEST(String::utf8("¿").rstrip(String::utf8("µÿ")) == String::utf8("¿"));
+ STRIP_TEST(String::utf8("µ¿ÿ").lstrip(String::utf8("µÿ")) == String::utf8("¿ÿ"));
+ STRIP_TEST(String::utf8("µ¿ÿ").rstrip(String::utf8("µÿ")) == String::utf8("µ¿"));
+
+ // the above tests repeated with additional superfluous strip chars
+
+ // strip none
+ STRIP_TEST(String("abc").lstrip("qwjkl") == "abc");
+ STRIP_TEST(String("abc").rstrip("qwjkl") == "abc");
+ // strip one
+ STRIP_TEST(String("abc").lstrip("qwajkl") == "bc");
+ STRIP_TEST(String("abc").rstrip("qwcjkl") == "ab");
+ // strip lots
+ STRIP_TEST(String("bababbababccc").lstrip("qwabjkl") == "ccc");
+ STRIP_TEST(String("aaabcbcbcbbcbbc").rstrip("qwcbjkl") == "aaa");
+ // strip empty string
+ STRIP_TEST(String("").lstrip("qwjkl") == "");
+ STRIP_TEST(String("").rstrip("qwjkl") == "");
+ // strip to empty string
+ STRIP_TEST(String("abcabcabc").lstrip("qwbcajkl") == "");
+ STRIP_TEST(String("abcabcabc").rstrip("qwbcajkl") == "");
+ // don't strip wrong end
+ STRIP_TEST(String("abc").lstrip("qwcjkl") == "abc");
+ STRIP_TEST(String("abca").lstrip("qwajkl") == "bca");
+ STRIP_TEST(String("abc").rstrip("qwajkl") == "abc");
+ STRIP_TEST(String("abca").rstrip("qwajkl") == "abc");
+ // in utf-8 "¿" (\u00bf) has the same first byte as "µ" (\u00b5)
+ // and the same second as "ÿ" (\u00ff)
+ STRIP_TEST(String::utf8("¿").lstrip(String::utf8("qwaµÿjkl")) == String::utf8("¿"));
+ STRIP_TEST(String::utf8("¿").rstrip(String::utf8("qwaµÿjkl")) == String::utf8("¿"));
+ STRIP_TEST(String::utf8("µ¿ÿ").lstrip(String::utf8("qwaµÿjkl")) == String::utf8("¿ÿ"));
+ STRIP_TEST(String::utf8("µ¿ÿ").rstrip(String::utf8("qwaµÿjkl")) == String::utf8("µ¿"));
+
+ CHECK(state);
+
+#undef STRIP_TEST
+}
+
+TEST_CASE("[String] ensuring empty string into parse_utf8 passes empty string") {
+ String empty;
+ CHECK(empty.parse_utf8(nullptr, -1));
+}
+
+TEST_CASE("[String] Cyrillic to_lower()") {
+ String upper = String::utf8("АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ");
+ String lower = String::utf8("абвгдеёжзийклмнопрстуфхцчшщъыьэюя");
+
+ String test = upper.to_lower();
+
+ bool state = test == lower;
+
+ CHECK(state);
+}
+
+TEST_CASE("[String] Count and countn functionality") {
+#define COUNT_TEST(x) \
+ { \
+ bool success = x; \
+ state = state && success; \
+ }
+
+ bool state = true;
+
+ COUNT_TEST(String("").count("Test") == 0);
+ COUNT_TEST(String("Test").count("") == 0);
+ COUNT_TEST(String("Test").count("test") == 0);
+ COUNT_TEST(String("Test").count("TEST") == 0);
+ COUNT_TEST(String("TEST").count("TEST") == 1);
+ COUNT_TEST(String("Test").count("Test") == 1);
+ COUNT_TEST(String("aTest").count("Test") == 1);
+ COUNT_TEST(String("Testa").count("Test") == 1);
+ COUNT_TEST(String("TestTestTest").count("Test") == 3);
+ COUNT_TEST(String("TestTestTest").count("TestTest") == 1);
+ COUNT_TEST(String("TestGodotTestGodotTestGodot").count("Test") == 3);
+
+ COUNT_TEST(String("TestTestTestTest").count("Test", 4, 8) == 1);
+ COUNT_TEST(String("TestTestTestTest").count("Test", 4, 12) == 2);
+ COUNT_TEST(String("TestTestTestTest").count("Test", 4, 16) == 3);
+ COUNT_TEST(String("TestTestTestTest").count("Test", 4) == 3);
+
+ COUNT_TEST(String("Test").countn("test") == 1);
+ COUNT_TEST(String("Test").countn("TEST") == 1);
+ COUNT_TEST(String("testTest-Testatest").countn("tEst") == 4);
+ COUNT_TEST(String("testTest-TeStatest").countn("tEsT", 4, 16) == 2);
+
+ CHECK(state);
+
+#undef COUNT_TEST
+}
+
+TEST_CASE("[String] Bigrams") {
+ String s = "abcd";
+ Vector<String> bigr = s.bigrams();
+
+ CHECK(bigr.size() == 3);
+ CHECK(bigr[0] == "ab");
+ CHECK(bigr[1] == "bc");
+ CHECK(bigr[2] == "cd");
+}
+
+TEST_CASE("[String] c-escape/unescape") {
+ String s = "\\1\a2\b\f3\n45\r6\t7\v8\'9\?0\"";
+ CHECK(s.c_escape().c_unescape() == s);
+}
+
+TEST_CASE("[String] indent") {
+ static const char *input[] = {
+ "",
+ "aaa\nbbb",
+ "\tcontains\n\tindent",
+ "empty\n\nline",
+ };
+ static const char *expected[] = {
+ "",
+ "\taaa\n\tbbb",
+ "\t\tcontains\n\t\tindent",
+ "\tempty\n\n\tline",
+ };
+
+ for (int i = 0; i < 3; i++) {
+ CHECK(String(input[i]).indent("\t") == expected[i]);
+ }
+}
+
+TEST_CASE("[String] dedent") {
+ String s = " aaa\n bbb";
+ String t = "aaa\nbbb";
+ CHECK(s.dedent() == t);
+}
+
+TEST_CASE("[String] Path functions") {
+ static const char *path[7] = { "C:\\Godot\\project\\test.tscn", "/Godot/project/test.xscn", "../Godot/project/test.scn", "Godot\\test.doc", "C:\\test.", "res://test", "/.test" };
+ static const char *base_dir[7] = { "C:\\Godot\\project", "/Godot/project", "../Godot/project", "Godot", "C:\\", "res://", "/" };
+ static const char *base_name[7] = { "C:\\Godot\\project\\test", "/Godot/project/test", "../Godot/project/test", "Godot\\test", "C:\\test", "res://test", "/" };
+ static const char *ext[7] = { "tscn", "xscn", "scn", "doc", "", "", "test" };
+ static const char *file[7] = { "test.tscn", "test.xscn", "test.scn", "test.doc", "test.", "test", ".test" };
+ static const bool abs[7] = { true, true, false, false, true, true, true };
+
+ for (int i = 0; i < 7; i++) {
+ CHECK(String(path[i]).get_base_dir() == base_dir[i]);
+ CHECK(String(path[i]).get_basename() == base_name[i]);
+ CHECK(String(path[i]).get_extension() == ext[i]);
+ CHECK(String(path[i]).get_file() == file[i]);
+ CHECK(String(path[i]).is_absolute_path() == abs[i]);
+ CHECK(String(path[i]).is_relative_path() != abs[i]);
+ CHECK(String(path[i]).simplify_path().get_base_dir().plus_file(file[i]) == String(path[i]).simplify_path());
+ }
+
+ static const char *file_name[3] = { "test.tscn", "test://.xscn", "?tes*t.scn" };
+ static const bool valid[3] = { true, false, false };
+ for (int i = 0; i < 3; i++) {
+ CHECK(String(file_name[i]).is_valid_filename() == valid[i]);
+ }
+}
+
+TEST_CASE("[String] hash") {
+ String a = "Test";
+ String b = "Test";
+ String c = "West";
+ CHECK(a.hash() == b.hash());
+ CHECK(a.hash() != c.hash());
+
+ CHECK(a.hash64() == b.hash64());
+ CHECK(a.hash64() != c.hash64());
+}
+
+TEST_CASE("[String] uri_encode/unescape") {
+ String s = "Godot Engine:'docs'";
+ String t = "Godot%20Engine%3A%27docs%27";
+
+ String x1 = "T%C4%93%C5%A1t";
+ static const uint8_t u8str[] = { 0x54, 0xC4, 0x93, 0xC5, 0xA1, 0x74, 0x00 };
+ String x2 = String::utf8((const char *)u8str);
+ String x3 = U"Tēšt";
+
+ CHECK(x1.uri_decode() == x2);
+ CHECK(x1.uri_decode() == x3);
+ CHECK((x1 + x3).uri_decode() == (x2 + x3)); // Mixed unicode and URL encoded string, e.g. GTK+ bookmark.
+ CHECK(x2.uri_encode() == x1);
+ CHECK(x3.uri_encode() == x1);
+
+ CHECK(s.uri_encode() == t);
+ CHECK(t.uri_decode() == s);
+}
+
+TEST_CASE("[String] xml_escape/unescape") {
+ String s = "\"Test\" <test@test&'test'>";
+ CHECK(s.xml_escape(true).xml_unescape() == s);
+ CHECK(s.xml_escape(false).xml_unescape() == s);
+}
+
+TEST_CASE("[String] xml_unescape") {
+ // Named entities
+ String input = "&quot;&amp;&apos;&lt;&gt;";
+ CHECK(input.xml_unescape() == "\"&\'<>");
+
+ // Numeric entities
+ input = "&#x41;&#66;";
+ CHECK(input.xml_unescape() == "AB");
+
+ input = "&#0;&x#0;More text";
+ String result = input.xml_unescape();
+ // Didn't put in a leading NUL and terminate the string
+ CHECK(input.length() > 0);
+ CHECK(input[0] != '\0');
+ // Entity should be left as-is if invalid
+ CHECK(input.xml_unescape() == input);
+
+ // Check near char32_t range
+ input = "&#xFFFFFFFF;";
+ result = input.xml_unescape();
+ CHECK(result.length() == 1);
+ CHECK(result[0] == 0xFFFFFFFF);
+ input = "&#4294967295;";
+ result = input.xml_unescape();
+ CHECK(result.length() == 1);
+ CHECK(result[0] == 0xFFFFFFFF);
+
+ // Check out of range of char32_t
+ input = "&#xFFFFFFFFF;";
+ CHECK(input.xml_unescape() == input);
+ input = "&#4294967296;";
+ CHECK(input.xml_unescape() == input);
+
+ // Shouldn't consume without ending in a ';'
+ input = "&#66";
+ CHECK(input.xml_unescape() == input);
+ input = "&#x41";
+ CHECK(input.xml_unescape() == input);
+
+ // Invalid characters should make the entity ignored
+ input = "&#x41SomeIrrelevantText;";
+ CHECK(input.xml_unescape() == input);
+ input = "&#66SomeIrrelevantText;";
+ CHECK(input.xml_unescape() == input);
+}
+
+TEST_CASE("[String] Strip escapes") {
+ String s = "\t\tTest Test\r\n Test";
+ CHECK(s.strip_escapes() == "Test Test Test");
+}
+
+TEST_CASE("[String] Similarity") {
+ String a = "Test";
+ String b = "West";
+ String c = "Toad";
+ CHECK(a.similarity(b) > a.similarity(c));
+}
+
+TEST_CASE("[String] Strip edges") {
+ String s = "\t Test Test ";
+ CHECK(s.strip_edges(true, false) == "Test Test ");
+ CHECK(s.strip_edges(false, true) == "\t Test Test");
+ CHECK(s.strip_edges(true, true) == "Test Test");
+}
+
+TEST_CASE("[String] Trim") {
+ String s = "aaaTestbbb";
+ CHECK(s.trim_prefix("aaa") == "Testbbb");
+ CHECK(s.trim_suffix("bbb") == "aaaTest");
+ CHECK(s.trim_suffix("Test") == s);
+}
+
+TEST_CASE("[String] Right/Left") {
+ String s = "aaaTestbbb";
+ // ^
+ CHECK(s.right(6) == "estbbb");
+ CHECK(s.right(-6) == "tbbb");
+ CHECK(s.left(6) == "aaaTes");
+ CHECK(s.left(-6) == "aaaT");
+}
+
+TEST_CASE("[String] Repeat") {
+ String s = "abababab";
+ String x = "ab";
+ String t = x.repeat(4);
+ CHECK(t == s);
+}
+
+TEST_CASE("[String] SHA1/SHA256/MD5") {
+ String s = "Godot";
+ String sha1 = "a1e91f39b9fce6a9998b14bdbe2aa2b39dc2d201";
+ static uint8_t sha1_buf[20] = {
+ 0xA1, 0xE9, 0x1F, 0x39, 0xB9, 0xFC, 0xE6, 0xA9, 0x99, 0x8B, 0x14, 0xBD, 0xBE, 0x2A, 0xA2, 0xB3,
+ 0x9D, 0xC2, 0xD2, 0x01
+ };
+ String sha256 = "2a02b2443f7985d89d09001086ae3dcfa6eb0f55c6ef170715d42328e16e6cb8";
+ static uint8_t sha256_buf[32] = {
+ 0x2A, 0x02, 0xB2, 0x44, 0x3F, 0x79, 0x85, 0xD8, 0x9D, 0x09, 0x00, 0x10, 0x86, 0xAE, 0x3D, 0xCF,
+ 0xA6, 0xEB, 0x0F, 0x55, 0xC6, 0xEF, 0x17, 0x07, 0x15, 0xD4, 0x23, 0x28, 0xE1, 0x6E, 0x6C, 0xB8
+ };
+ String md5 = "4a336d087aeb0390da10ee2ea7cb87f8";
+ static uint8_t md5_buf[16] = {
+ 0x4A, 0x33, 0x6D, 0x08, 0x7A, 0xEB, 0x03, 0x90, 0xDA, 0x10, 0xEE, 0x2E, 0xA7, 0xCB, 0x87, 0xF8
+ };
+
+ PackedByteArray buf = s.sha1_buffer();
+ CHECK(memcmp(sha1_buf, buf.ptr(), 20) == 0);
+ CHECK(s.sha1_text() == sha1);
+
+ buf = s.sha256_buffer();
+ CHECK(memcmp(sha256_buf, buf.ptr(), 32) == 0);
+ CHECK(s.sha256_text() == sha256);
+
+ buf = s.md5_buffer();
+ CHECK(memcmp(md5_buf, buf.ptr(), 16) == 0);
+ CHECK(s.md5_text() == md5);
+}
+
+TEST_CASE("[String] Join") {
+ String s = ", ";
+ Vector<String> parts;
+ parts.push_back("One");
+ parts.push_back("B");
+ parts.push_back("C");
+ String t = s.join(parts);
+ CHECK(t == "One, B, C");
+}
+
+TEST_CASE("[String] Is_*") {
+ static const char *data[12] = { "-30", "100", "10.1", "10,1", "1e2", "1e-2", "1e2e3", "0xAB", "AB", "Test1", "1Test", "Test*1" };
+ static bool isnum[12] = { true, true, true, false, false, false, false, false, false, false, false, false };
+ static bool isint[12] = { true, true, false, false, false, false, false, false, false, false, false, false };
+ static bool ishex[12] = { true, true, false, false, true, false, true, false, true, false, false, false };
+ static bool ishex_p[12] = { false, false, false, false, false, false, false, true, false, false, false, false };
+ static bool isflt[12] = { true, true, true, false, true, true, false, false, false, false, false, false };
+ static bool isid[12] = { false, false, false, false, false, false, false, false, true, true, false, false };
+ for (int i = 0; i < 12; i++) {
+ String s = String(data[i]);
+ CHECK(s.is_numeric() == isnum[i]);
+ CHECK(s.is_valid_int() == isint[i]);
+ CHECK(s.is_valid_hex_number(false) == ishex[i]);
+ CHECK(s.is_valid_hex_number(true) == ishex_p[i]);
+ CHECK(s.is_valid_float() == isflt[i]);
+ CHECK(s.is_valid_identifier() == isid[i]);
+ }
+}
+
+TEST_CASE("[String] humanize_size") {
+ CHECK(String::humanize_size(1000) == "1000 B");
+ CHECK(String::humanize_size(1025) == "1.00 KiB");
+ CHECK(String::humanize_size(1025300) == "1001.2 KiB");
+ CHECK(String::humanize_size(100523550) == "95.86 MiB");
+ CHECK(String::humanize_size(5345555000) == "4.97 GiB");
+}
+
+TEST_CASE("[String] validate_node_name") {
+ String numeric_only = "12345";
+ CHECK(numeric_only.validate_node_name() == "12345");
+
+ String name_with_spaces = "Name with spaces";
+ CHECK(name_with_spaces.validate_node_name() == "Name with spaces");
+
+ String name_with_kana = "Name with kana ゴドツ";
+ CHECK(name_with_kana.validate_node_name() == "Name with kana ゴドツ");
+
+ String name_with_invalid_chars = "Name with invalid characters :.@removed!";
+ CHECK(name_with_invalid_chars.validate_node_name() == "Name with invalid characters removed!");
+}
+
+TEST_CASE("[String] Variant indexed get") {
+ Variant s = String("abcd");
+ bool valid = false;
+ bool oob = true;
+
+ String r = s.get_indexed(1, valid, oob);
+
+ CHECK(valid);
+ CHECK_FALSE(oob);
+ CHECK_EQ(r, String("b"));
+}
+
+TEST_CASE("[String] Variant validated indexed get") {
+ Variant s = String("abcd");
+
+ Variant::ValidatedIndexedGetter getter = Variant::get_member_validated_indexed_getter(Variant::STRING);
+
+ Variant r;
+ bool oob = true;
+ getter(&s, 1, &r, &oob);
+
+ CHECK_FALSE(oob);
+ CHECK_EQ(r, String("b"));
+}
+
+TEST_CASE("[String] Variant ptr indexed get") {
+ String s("abcd");
+
+ Variant::PTRIndexedGetter getter = Variant::get_member_ptr_indexed_getter(Variant::STRING);
+
+ String r;
+ getter(&s, 1, &r);
+
+ CHECK_EQ(r, String("b"));
+}
+
+TEST_CASE("[String] Variant indexed set") {
+ Variant s = String("abcd");
+ bool valid = false;
+ bool oob = true;
+
+ s.set_indexed(1, String("z"), valid, oob);
+
+ CHECK(valid);
+ CHECK_FALSE(oob);
+ CHECK_EQ(s, String("azcd"));
+}
+
+TEST_CASE("[String] Variant validated indexed set") {
+ Variant s = String("abcd");
+
+ Variant::ValidatedIndexedSetter setter = Variant::get_member_validated_indexed_setter(Variant::STRING);
+
+ Variant v = String("z");
+ bool oob = true;
+ setter(&s, 1, &v, &oob);
+
+ CHECK_FALSE(oob);
+ CHECK_EQ(s, String("azcd"));
+}
+
+TEST_CASE("[String] Variant ptr indexed set") {
+ String s("abcd");
+
+ Variant::PTRIndexedSetter setter = Variant::get_member_ptr_indexed_setter(Variant::STRING);
+
+ String v("z");
+ setter(&s, 1, &v);
+
+ CHECK_EQ(s, String("azcd"));
+}
+
+TEST_CASE("[Stress][String] Empty via ' == String()'") {
+ for (int i = 0; i < 100000; ++i) {
+ String str = "Hello World!";
+ if (str.is_empty()) {
+ continue;
+ }
+ }
+}
+
+TEST_CASE("[Stress][String] Empty via `is_empty()`") {
+ for (int i = 0; i < 100000; ++i) {
+ String str = "Hello World!";
+ if (str.is_empty()) {
+ continue;
+ }
+ }
+}
+} // namespace TestString
+
+#endif // TEST_STRING_H
diff --git a/tests/core/string/test_translation.h b/tests/core/string/test_translation.h
new file mode 100644
index 0000000000..85ac639bec
--- /dev/null
+++ b/tests/core/string/test_translation.h
@@ -0,0 +1,182 @@
+/*************************************************************************/
+/* test_translation.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_TRANSLATION_H
+#define TEST_TRANSLATION_H
+
+#include "core/string/optimized_translation.h"
+#include "core/string/translation.h"
+#include "core/string/translation_po.h"
+
+#ifdef TOOLS_ENABLED
+#include "editor/import/resource_importer_csv_translation.h"
+#endif
+
+#include "tests/test_macros.h"
+#include "tests/test_utils.h"
+
+namespace TestTranslation {
+
+TEST_CASE("[Translation] Messages") {
+ Ref<Translation> translation = memnew(Translation);
+ translation->set_locale("fr");
+ translation->add_message("Hello", "Bonjour");
+ CHECK(translation->get_message("Hello") == "Bonjour");
+
+ translation->erase_message("Hello");
+ // The message no longer exists, so it returns an empty string instead.
+ CHECK(translation->get_message("Hello") == "");
+
+ List<StringName> messages;
+ translation->get_message_list(&messages);
+ CHECK(translation->get_message_count() == 0);
+ CHECK(messages.size() == 0);
+
+ translation->add_message("Hello2", "Bonjour2");
+ translation->add_message("Hello3", "Bonjour3");
+ messages.clear();
+ translation->get_message_list(&messages);
+ CHECK(translation->get_message_count() == 2);
+ CHECK(messages.size() == 2);
+ // Messages are stored in a Map, don't assume ordering.
+ CHECK(messages.find("Hello2"));
+ CHECK(messages.find("Hello3"));
+}
+
+TEST_CASE("[TranslationPO] Messages with context") {
+ Ref<TranslationPO> translation = memnew(TranslationPO);
+ translation->set_locale("fr");
+ translation->add_message("Hello", "Bonjour");
+ translation->add_message("Hello", "Salut", "friendly");
+ CHECK(translation->get_message("Hello") == "Bonjour");
+ CHECK(translation->get_message("Hello", "friendly") == "Salut");
+ CHECK(translation->get_message("Hello", "nonexistent_context") == "");
+
+ // Only remove the message for the default context, not the "friendly" context.
+ translation->erase_message("Hello");
+ // The message no longer exists, so it returns an empty string instead.
+ CHECK(translation->get_message("Hello") == "");
+ CHECK(translation->get_message("Hello", "friendly") == "Salut");
+ CHECK(translation->get_message("Hello", "nonexistent_context") == "");
+
+ List<StringName> messages;
+ translation->get_message_list(&messages);
+
+ // `get_message_count()` takes all contexts into account.
+ CHECK(translation->get_message_count() == 1);
+ // Only the default context is taken into account.
+ // Since "Hello" is now only present in a non-default context, it is not counted in the list of messages.
+ CHECK(messages.size() == 0);
+
+ translation->add_message("Hello2", "Bonjour2");
+ translation->add_message("Hello2", "Salut2", "friendly");
+ translation->add_message("Hello3", "Bonjour3");
+ messages.clear();
+ translation->get_message_list(&messages);
+
+ // `get_message_count()` takes all contexts into account.
+ CHECK(translation->get_message_count() == 4);
+ // Only the default context is taken into account.
+ CHECK(messages.size() == 2);
+ // Messages are stored in a Map, don't assume ordering.
+ CHECK(messages.find("Hello2"));
+ CHECK(messages.find("Hello3"));
+}
+
+TEST_CASE("[TranslationPO] Plural messages") {
+ Ref<TranslationPO> translation = memnew(TranslationPO);
+ translation->set_locale("fr");
+ translation->set_plural_rule("Plural-Forms: nplurals=2; plural=(n >= 2);");
+ CHECK(translation->get_plural_forms() == 2);
+
+ PackedStringArray plurals;
+ plurals.push_back("Il y a %d pomme");
+ plurals.push_back("Il y a %d pommes");
+ translation->add_plural_message("There are %d apples", plurals);
+ ERR_PRINT_OFF;
+ // This is invalid, as the number passed to `get_plural_message()` may not be negative.
+ CHECK(vformat(translation->get_plural_message("There are %d apples", "", -1), -1) == "");
+ ERR_PRINT_ON;
+ CHECK(vformat(translation->get_plural_message("There are %d apples", "", 0), 0) == "Il y a 0 pomme");
+ CHECK(vformat(translation->get_plural_message("There are %d apples", "", 1), 1) == "Il y a 1 pomme");
+ CHECK(vformat(translation->get_plural_message("There are %d apples", "", 2), 2) == "Il y a 2 pommes");
+}
+
+TEST_CASE("[OptimizedTranslation] Generate from Translation and read messages") {
+ Ref<Translation> translation = memnew(Translation);
+ translation->set_locale("fr");
+ translation->add_message("Hello", "Bonjour");
+ translation->add_message("Hello2", "Bonjour2");
+ translation->add_message("Hello3", "Bonjour3");
+
+ Ref<OptimizedTranslation> optimized_translation = memnew(OptimizedTranslation);
+ optimized_translation->generate(translation);
+ CHECK(optimized_translation->get_message("Hello") == "Bonjour");
+ CHECK(optimized_translation->get_message("Hello2") == "Bonjour2");
+ CHECK(optimized_translation->get_message("Hello3") == "Bonjour3");
+ CHECK(optimized_translation->get_message("DoesNotExist") == "");
+
+ List<StringName> messages;
+ // `get_message_list()` can't return the list of messages stored in an OptimizedTranslation.
+ optimized_translation->get_message_list(&messages);
+ CHECK(optimized_translation->get_message_count() == 0);
+ CHECK(messages.size() == 0);
+}
+
+#ifdef TOOLS_ENABLED
+TEST_CASE("[Translation] CSV import") {
+ Ref<ResourceImporterCSVTranslation> import_csv_translation = memnew(ResourceImporterCSVTranslation);
+
+ Map<StringName, Variant> options;
+ options["compress"] = false;
+ options["delimiter"] = 0;
+
+ List<String> gen_files;
+
+ Error result = import_csv_translation->import(TestUtils::get_data_path("translations.csv"),
+ "", options, nullptr, &gen_files);
+ CHECK(result == OK);
+ CHECK(gen_files.size() == 2);
+
+ for (const String &file : gen_files) {
+ Ref<Translation> translation = ResourceLoader::load(file);
+ CHECK(translation.is_valid());
+ TranslationServer::get_singleton()->add_translation(translation);
+ }
+
+ TranslationServer::get_singleton()->set_locale("de");
+
+ CHECK(Object().tr("GOOD_MORNING", "") == "Guten Morgen");
+}
+#endif // TOOLS_ENABLED
+
+} // namespace TestTranslation
+
+#endif // TEST_TRANSLATION_H
diff --git a/tests/core/templates/test_command_queue.h b/tests/core/templates/test_command_queue.h
new file mode 100644
index 0000000000..0d016f5d06
--- /dev/null
+++ b/tests/core/templates/test_command_queue.h
@@ -0,0 +1,431 @@
+/*************************************************************************/
+/* test_command_queue.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_COMMAND_QUEUE_H
+#define TEST_COMMAND_QUEUE_H
+
+#include "core/config/project_settings.h"
+#include "core/math/random_number_generator.h"
+#include "core/os/os.h"
+#include "core/os/thread.h"
+#include "core/templates/command_queue_mt.h"
+#include "tests/test_macros.h"
+
+#if !defined(NO_THREADS)
+
+namespace TestCommandQueue {
+
+class ThreadWork {
+ Semaphore thread_sem;
+ Semaphore main_sem;
+ Mutex mut;
+ int threading_errors = 0;
+ enum State {
+ MAIN_START,
+ MAIN_DONE,
+ THREAD_START,
+ THREAD_DONE,
+ } state;
+
+public:
+ ThreadWork() {
+ mut.lock();
+ state = MAIN_START;
+ }
+ ~ThreadWork() {
+ CHECK_MESSAGE(threading_errors == 0, "threads did not lock/unlock correctly");
+ }
+ void thread_wait_for_work() {
+ thread_sem.wait();
+ mut.lock();
+ if (state != MAIN_DONE) {
+ threading_errors++;
+ }
+ state = THREAD_START;
+ }
+ void thread_done_work() {
+ if (state != THREAD_START) {
+ threading_errors++;
+ }
+ state = THREAD_DONE;
+ mut.unlock();
+ main_sem.post();
+ }
+
+ void main_wait_for_done() {
+ main_sem.wait();
+ mut.lock();
+ if (state != THREAD_DONE) {
+ threading_errors++;
+ }
+ state = MAIN_START;
+ }
+ void main_start_work() {
+ if (state != MAIN_START) {
+ threading_errors++;
+ }
+ state = MAIN_DONE;
+ mut.unlock();
+ thread_sem.post();
+ }
+};
+
+class SharedThreadState {
+public:
+ ThreadWork reader_threadwork;
+ ThreadWork writer_threadwork;
+
+ CommandQueueMT command_queue = CommandQueueMT(true);
+
+ enum TestMsgType {
+ TEST_MSG_FUNC1_TRANSFORM,
+ TEST_MSG_FUNC2_TRANSFORM_FLOAT,
+ TEST_MSG_FUNC3_TRANSFORMx6,
+ TEST_MSGSYNC_FUNC1_TRANSFORM,
+ TEST_MSGSYNC_FUNC2_TRANSFORM_FLOAT,
+ TEST_MSGRET_FUNC1_TRANSFORM,
+ TEST_MSGRET_FUNC2_TRANSFORM_FLOAT,
+ TEST_MSG_MAX
+ };
+
+ Vector<TestMsgType> message_types_to_write;
+ bool during_writing = false;
+ int message_count_to_read = 0;
+ bool exit_threads = false;
+
+ Thread reader_thread;
+ Thread writer_thread;
+
+ int func1_count = 0;
+
+ void func1(Transform3D t) {
+ func1_count++;
+ }
+ void func2(Transform3D t, float f) {
+ func1_count++;
+ }
+ void func3(Transform3D t1, Transform3D t2, Transform3D t3, Transform3D t4, Transform3D t5, Transform3D t6) {
+ func1_count++;
+ }
+ Transform3D func1r(Transform3D t) {
+ func1_count++;
+ return t;
+ }
+ Transform3D func2r(Transform3D t, float f) {
+ func1_count++;
+ return t;
+ }
+
+ void add_msg_to_write(TestMsgType type) {
+ message_types_to_write.push_back(type);
+ }
+
+ void reader_thread_loop() {
+ reader_threadwork.thread_wait_for_work();
+ while (!exit_threads) {
+ if (message_count_to_read < 0) {
+ command_queue.flush_all();
+ }
+ for (int i = 0; i < message_count_to_read; i++) {
+ command_queue.wait_and_flush();
+ }
+ message_count_to_read = 0;
+
+ reader_threadwork.thread_done_work();
+ reader_threadwork.thread_wait_for_work();
+ }
+ command_queue.flush_all();
+ reader_threadwork.thread_done_work();
+ }
+ static void static_reader_thread_loop(void *stsvoid) {
+ SharedThreadState *sts = static_cast<SharedThreadState *>(stsvoid);
+ sts->reader_thread_loop();
+ }
+
+ void writer_thread_loop() {
+ during_writing = false;
+ writer_threadwork.thread_wait_for_work();
+ while (!exit_threads) {
+ Transform3D tr;
+ Transform3D otr;
+ float f = 1;
+ during_writing = true;
+ for (int i = 0; i < message_types_to_write.size(); i++) {
+ TestMsgType msg_type = message_types_to_write[i];
+ switch (msg_type) {
+ case TEST_MSG_FUNC1_TRANSFORM:
+ command_queue.push(this, &SharedThreadState::func1, tr);
+ break;
+ case TEST_MSG_FUNC2_TRANSFORM_FLOAT:
+ command_queue.push(this, &SharedThreadState::func2, tr, f);
+ break;
+ case TEST_MSG_FUNC3_TRANSFORMx6:
+ command_queue.push(this, &SharedThreadState::func3, tr, tr, tr, tr, tr, tr);
+ break;
+ case TEST_MSGSYNC_FUNC1_TRANSFORM:
+ command_queue.push_and_sync(this, &SharedThreadState::func1, tr);
+ break;
+ case TEST_MSGSYNC_FUNC2_TRANSFORM_FLOAT:
+ command_queue.push_and_sync(this, &SharedThreadState::func2, tr, f);
+ break;
+ case TEST_MSGRET_FUNC1_TRANSFORM:
+ command_queue.push_and_ret(this, &SharedThreadState::func1r, tr, &otr);
+ break;
+ case TEST_MSGRET_FUNC2_TRANSFORM_FLOAT:
+ command_queue.push_and_ret(this, &SharedThreadState::func2r, tr, f, &otr);
+ break;
+ default:
+ break;
+ }
+ }
+ message_types_to_write.clear();
+ during_writing = false;
+
+ writer_threadwork.thread_done_work();
+ writer_threadwork.thread_wait_for_work();
+ }
+ writer_threadwork.thread_done_work();
+ }
+ static void static_writer_thread_loop(void *stsvoid) {
+ SharedThreadState *sts = static_cast<SharedThreadState *>(stsvoid);
+ sts->writer_thread_loop();
+ }
+
+ void init_threads() {
+ reader_thread.start(&SharedThreadState::static_reader_thread_loop, this);
+ writer_thread.start(&SharedThreadState::static_writer_thread_loop, this);
+ }
+ void destroy_threads() {
+ exit_threads = true;
+ reader_threadwork.main_start_work();
+ writer_threadwork.main_start_work();
+
+ reader_thread.wait_to_finish();
+ writer_thread.wait_to_finish();
+ }
+};
+
+TEST_CASE("[CommandQueue] Test Queue Basics") {
+ const char *COMMAND_QUEUE_SETTING = "memory/limits/command_queue/multithreading_queue_size_kb";
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING, 1);
+ SharedThreadState sts;
+ sts.init_threads();
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC1_TRANSFORM);
+ sts.writer_threadwork.main_start_work();
+ sts.writer_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 0,
+ "Control: no messages read before reader has run.");
+
+ sts.message_count_to_read = 1;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 1,
+ "Reader should have read one message");
+
+ sts.message_count_to_read = -1;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 1,
+ "Reader should have read no additional messages from flush_all");
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC1_TRANSFORM);
+ sts.writer_threadwork.main_start_work();
+ sts.writer_threadwork.main_wait_for_done();
+
+ sts.message_count_to_read = -1;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 2,
+ "Reader should have read one additional message from flush_all");
+
+ sts.destroy_threads();
+
+ CHECK_MESSAGE(sts.func1_count == 2,
+ "Reader should have read no additional messages after join");
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING,
+ ProjectSettings::get_singleton()->property_get_revert(COMMAND_QUEUE_SETTING));
+}
+
+TEST_CASE("[CommandQueue] Test Queue Wrapping to same spot.") {
+ const char *COMMAND_QUEUE_SETTING = "memory/limits/command_queue/multithreading_queue_size_kb";
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING, 1);
+ SharedThreadState sts;
+ sts.init_threads();
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC1_TRANSFORM);
+ sts.writer_threadwork.main_start_work();
+ sts.writer_threadwork.main_wait_for_done();
+
+ sts.message_count_to_read = -1;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 3,
+ "Reader should have read at least three messages");
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.writer_threadwork.main_start_work();
+ sts.writer_threadwork.main_wait_for_done();
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC1_TRANSFORM);
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.writer_threadwork.main_start_work();
+ OS::get_singleton()->delay_usec(1000);
+
+ sts.message_count_to_read = -1;
+ sts.reader_threadwork.main_start_work();
+ OS::get_singleton()->delay_usec(1000);
+
+ sts.writer_threadwork.main_wait_for_done();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count >= 3,
+ "Reader should have read at least three messages");
+
+ sts.message_count_to_read = 6 - sts.func1_count;
+ sts.reader_threadwork.main_start_work();
+
+ // The following will fail immediately.
+ // The reason it hangs indefinitely in engine, is all subsequent calls to
+ // CommandQueue.wait_and_flush_one will also fail.
+ sts.reader_threadwork.main_wait_for_done();
+
+ // Because looping around uses an extra message, easiest to consume all.
+ sts.message_count_to_read = -1;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 6,
+ "Reader should have read both message sets");
+
+ sts.destroy_threads();
+
+ CHECK_MESSAGE(sts.func1_count == 6,
+ "Reader should have read no additional messages after join");
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING,
+ ProjectSettings::get_singleton()->property_get_revert(COMMAND_QUEUE_SETTING));
+}
+
+TEST_CASE("[CommandQueue] Test Queue Lapping") {
+ const char *COMMAND_QUEUE_SETTING = "memory/limits/command_queue/multithreading_queue_size_kb";
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING, 1);
+ SharedThreadState sts;
+ sts.init_threads();
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC1_TRANSFORM);
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.writer_threadwork.main_start_work();
+ sts.writer_threadwork.main_wait_for_done();
+
+ // We need to read an extra message so that it triggers the dealloc logic once.
+ // Otherwise, the queue will be considered full.
+ sts.message_count_to_read = 3;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ CHECK_MESSAGE(sts.func1_count == 3,
+ "Reader should have read first set of messages");
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC3_TRANSFORMx6);
+ sts.writer_threadwork.main_start_work();
+ // Don't wait for these, because the queue isn't big enough.
+ sts.writer_threadwork.main_wait_for_done();
+
+ sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC2_TRANSFORM_FLOAT);
+ sts.writer_threadwork.main_start_work();
+ OS::get_singleton()->delay_usec(1000);
+
+ sts.message_count_to_read = 3;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+
+ sts.writer_threadwork.main_wait_for_done();
+
+ sts.message_count_to_read = -1;
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+
+ CHECK_MESSAGE(sts.func1_count == 6,
+ "Reader should have read rest of the messages after lapping writers.");
+
+ sts.destroy_threads();
+
+ CHECK_MESSAGE(sts.func1_count == 6,
+ "Reader should have read no additional messages after join");
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING,
+ ProjectSettings::get_singleton()->property_get_revert(COMMAND_QUEUE_SETTING));
+}
+
+TEST_CASE("[Stress][CommandQueue] Stress test command queue") {
+ const char *COMMAND_QUEUE_SETTING = "memory/limits/command_queue/multithreading_queue_size_kb";
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING, 1);
+ SharedThreadState sts;
+ sts.init_threads();
+
+ RandomNumberGenerator rng;
+
+ rng.set_seed(1837267);
+
+ int msgs_to_add = 2048;
+
+ for (int i = 0; i < msgs_to_add; i++) {
+ // randi_range is inclusive, so allow any enum value except MAX.
+ sts.add_msg_to_write((SharedThreadState::TestMsgType)rng.randi_range(0, SharedThreadState::TEST_MSG_MAX - 1));
+ }
+ sts.writer_threadwork.main_start_work();
+
+ int max_loop_iters = msgs_to_add * 2;
+ int loop_iters = 0;
+ while (sts.func1_count < msgs_to_add && loop_iters < max_loop_iters) {
+ int remaining = (msgs_to_add - sts.func1_count);
+ sts.message_count_to_read = rng.randi_range(1, remaining < 128 ? remaining : 128);
+ if (loop_iters % 3 == 0) {
+ sts.message_count_to_read = -1;
+ }
+ sts.reader_threadwork.main_start_work();
+ sts.reader_threadwork.main_wait_for_done();
+ loop_iters++;
+ }
+ CHECK_MESSAGE(loop_iters < max_loop_iters,
+ "Reader needed too many iterations to read messages!");
+ sts.writer_threadwork.main_wait_for_done();
+
+ sts.destroy_threads();
+
+ CHECK_MESSAGE(sts.func1_count == msgs_to_add,
+ "Reader should have read no additional messages after join");
+ ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING,
+ ProjectSettings::get_singleton()->property_get_revert(COMMAND_QUEUE_SETTING));
+}
+} // namespace TestCommandQueue
+
+#endif // !defined(NO_THREADS)
+
+#endif // TEST_COMMAND_QUEUE_H
diff --git a/tests/core/templates/test_list.h b/tests/core/templates/test_list.h
new file mode 100644
index 0000000000..49da0b8aad
--- /dev/null
+++ b/tests/core/templates/test_list.h
@@ -0,0 +1,548 @@
+/*************************************************************************/
+/* test_list.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_LIST_H
+#define TEST_LIST_H
+
+#include "core/templates/list.h"
+
+#include "tests/test_macros.h"
+
+namespace TestList {
+
+static void populate_integers(List<int> &p_list, List<int>::Element *r_elements[], int num_elements) {
+ p_list.clear();
+ for (int i = 0; i < num_elements; ++i) {
+ List<int>::Element *n = p_list.push_back(i);
+ r_elements[i] = n;
+ }
+}
+
+TEST_CASE("[List] Push/pop back") {
+ List<String> list;
+
+ List<String>::Element *n;
+ n = list.push_back("A");
+ CHECK(n->get() == "A");
+ n = list.push_back("B");
+ CHECK(n->get() == "B");
+ n = list.push_back("C");
+ CHECK(n->get() == "C");
+
+ CHECK(list.size() == 3);
+ CHECK(!list.is_empty());
+
+ String v;
+ v = list.back()->get();
+ list.pop_back();
+ CHECK(v == "C");
+ v = list.back()->get();
+ list.pop_back();
+ CHECK(v == "B");
+ v = list.back()->get();
+ list.pop_back();
+ CHECK(v == "A");
+
+ CHECK(list.size() == 0);
+ CHECK(list.is_empty());
+
+ CHECK(list.back() == nullptr);
+ CHECK(list.front() == nullptr);
+}
+
+TEST_CASE("[List] Push/pop front") {
+ List<String> list;
+
+ List<String>::Element *n;
+ n = list.push_front("A");
+ CHECK(n->get() == "A");
+ n = list.push_front("B");
+ CHECK(n->get() == "B");
+ n = list.push_front("C");
+ CHECK(n->get() == "C");
+
+ CHECK(list.size() == 3);
+ CHECK(!list.is_empty());
+
+ String v;
+ v = list.front()->get();
+ list.pop_front();
+ CHECK(v == "C");
+ v = list.front()->get();
+ list.pop_front();
+ CHECK(v == "B");
+ v = list.front()->get();
+ list.pop_front();
+ CHECK(v == "A");
+
+ CHECK(list.size() == 0);
+ CHECK(list.is_empty());
+
+ CHECK(list.back() == nullptr);
+ CHECK(list.front() == nullptr);
+}
+
+TEST_CASE("[List] Set and get") {
+ List<String> list;
+ list.push_back("A");
+
+ List<String>::Element *n = list.front();
+ CHECK(n->get() == "A");
+
+ n->set("X");
+ CHECK(n->get() == "X");
+}
+
+TEST_CASE("[List] Insert before") {
+ List<String> list;
+ List<String>::Element *a = list.push_back("A");
+ List<String>::Element *b = list.push_back("B");
+ List<String>::Element *c = list.push_back("C");
+
+ list.insert_before(b, "I");
+
+ CHECK(a->next()->get() == "I");
+ CHECK(c->prev()->prev()->get() == "I");
+ CHECK(list.front()->next()->get() == "I");
+ CHECK(list.back()->prev()->prev()->get() == "I");
+}
+
+TEST_CASE("[List] Insert after") {
+ List<String> list;
+ List<String>::Element *a = list.push_back("A");
+ List<String>::Element *b = list.push_back("B");
+ List<String>::Element *c = list.push_back("C");
+
+ list.insert_after(b, "I");
+
+ CHECK(a->next()->next()->get() == "I");
+ CHECK(c->prev()->get() == "I");
+ CHECK(list.front()->next()->next()->get() == "I");
+ CHECK(list.back()->prev()->get() == "I");
+}
+
+TEST_CASE("[List] Insert before null") {
+ List<String> list;
+ List<String>::Element *a = list.push_back("A");
+ List<String>::Element *b = list.push_back("B");
+ List<String>::Element *c = list.push_back("C");
+
+ list.insert_before(nullptr, "I");
+
+ CHECK(a->next()->get() == "B");
+ CHECK(b->get() == "B");
+ CHECK(c->prev()->prev()->get() == "A");
+ CHECK(list.front()->next()->get() == "B");
+ CHECK(list.back()->prev()->prev()->get() == "B");
+ CHECK(list.back()->get() == "I");
+}
+
+TEST_CASE("[List] Insert after null") {
+ List<String> list;
+ List<String>::Element *a = list.push_back("A");
+ List<String>::Element *b = list.push_back("B");
+ List<String>::Element *c = list.push_back("C");
+
+ list.insert_after(nullptr, "I");
+
+ CHECK(a->next()->get() == "B");
+ CHECK(b->get() == "B");
+ CHECK(c->prev()->prev()->get() == "A");
+ CHECK(list.front()->next()->get() == "B");
+ CHECK(list.back()->prev()->prev()->get() == "B");
+ CHECK(list.back()->get() == "I");
+}
+
+TEST_CASE("[List] Find") {
+ List<int> list;
+ List<int>::Element *n[10];
+ // Indices match values.
+ populate_integers(list, n, 10);
+
+ for (int i = 0; i < 10; ++i) {
+ CHECK(n[i]->get() == list.find(i)->get());
+ }
+}
+
+TEST_CASE("[List] Erase (by value)") {
+ List<int> list;
+ List<int>::Element *n[4];
+ // Indices match values.
+ populate_integers(list, n, 4);
+
+ CHECK(list.front()->next()->next()->get() == 2);
+ bool erased = list.erase(2); // 0, 1, 3.
+ CHECK(erased);
+ CHECK(list.size() == 3);
+
+ // The pointer n[2] points to the freed memory which is not reset to zero,
+ // so the below assertion may pass, but this relies on undefined behavior.
+ // CHECK(n[2]->get() == 2);
+
+ CHECK(list.front()->get() == 0);
+ CHECK(list.front()->next()->next()->get() == 3);
+ CHECK(list.back()->get() == 3);
+ CHECK(list.back()->prev()->get() == 1);
+
+ CHECK(n[1]->next()->get() == 3);
+ CHECK(n[3]->prev()->get() == 1);
+
+ erased = list.erase(9000); // Doesn't exist.
+ CHECK(!erased);
+}
+
+TEST_CASE("[List] Erase (by element)") {
+ List<int> list;
+ List<int>::Element *n[4];
+ // Indices match values.
+ populate_integers(list, n, 4);
+
+ bool erased = list.erase(n[2]);
+ CHECK(erased);
+ CHECK(list.size() == 3);
+ CHECK(n[1]->next()->get() == 3);
+ CHECK(n[3]->prev()->get() == 1);
+}
+
+TEST_CASE("[List] Element erase") {
+ List<int> list;
+ List<int>::Element *n[4];
+ // Indices match values.
+ populate_integers(list, n, 4);
+
+ n[2]->erase();
+
+ CHECK(list.size() == 3);
+ CHECK(n[1]->next()->get() == 3);
+ CHECK(n[3]->prev()->get() == 1);
+}
+
+TEST_CASE("[List] Clear") {
+ List<int> list;
+ List<int>::Element *n[100];
+ populate_integers(list, n, 100);
+
+ list.clear();
+
+ CHECK(list.size() == 0);
+ CHECK(list.is_empty());
+}
+
+TEST_CASE("[List] Invert") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.reverse();
+
+ CHECK(list.front()->get() == 3);
+ CHECK(list.front()->next()->get() == 2);
+ CHECK(list.back()->prev()->get() == 1);
+ CHECK(list.back()->get() == 0);
+}
+
+TEST_CASE("[List] Move to front") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.move_to_front(n[3]);
+
+ CHECK(list.front()->get() == 3);
+ CHECK(list.back()->get() == 2);
+}
+
+TEST_CASE("[List] Move to back") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.move_to_back(n[0]);
+
+ CHECK(list.back()->get() == 0);
+ CHECK(list.front()->get() == 1);
+}
+
+TEST_CASE("[List] Move before") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.move_before(n[3], n[1]);
+
+ CHECK(list.front()->next()->get() == n[3]->get());
+}
+
+TEST_CASE("[List] Sort") {
+ List<String> list;
+ list.push_back("D");
+ list.push_back("B");
+ list.push_back("A");
+ list.push_back("C");
+
+ list.sort();
+
+ CHECK(list.front()->get() == "A");
+ CHECK(list.front()->next()->get() == "B");
+ CHECK(list.back()->prev()->get() == "C");
+ CHECK(list.back()->get() == "D");
+}
+
+TEST_CASE("[List] Swap adjacent front and back") {
+ List<int> list;
+ List<int>::Element *n[2];
+ populate_integers(list, n, 2);
+
+ list.swap(list.front(), list.back());
+
+ CHECK(list.front()->prev() == nullptr);
+ CHECK(list.front() != list.front()->next());
+
+ CHECK(list.front() == n[1]);
+ CHECK(list.back() == n[0]);
+
+ CHECK(list.back()->next() == nullptr);
+ CHECK(list.back() != list.back()->prev());
+}
+
+TEST_CASE("[List] Swap first adjacent pair") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.swap(n[0], n[1]);
+
+ CHECK(list.front()->prev() == nullptr);
+ CHECK(list.front() != list.front()->next());
+
+ CHECK(list.front() == n[1]);
+ CHECK(list.front()->next() == n[0]);
+ CHECK(list.back()->prev() == n[2]);
+ CHECK(list.back() == n[3]);
+
+ CHECK(list.back()->next() == nullptr);
+ CHECK(list.back() != list.back()->prev());
+}
+
+TEST_CASE("[List] Swap middle adjacent pair") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.swap(n[1], n[2]);
+
+ CHECK(list.front()->prev() == nullptr);
+
+ CHECK(list.front() == n[0]);
+ CHECK(list.front()->next() == n[2]);
+ CHECK(list.back()->prev() == n[1]);
+ CHECK(list.back() == n[3]);
+
+ CHECK(list.back()->next() == nullptr);
+}
+
+TEST_CASE("[List] Swap last adjacent pair") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.swap(n[2], n[3]);
+
+ CHECK(list.front()->prev() == nullptr);
+
+ CHECK(list.front() == n[0]);
+ CHECK(list.front()->next() == n[1]);
+ CHECK(list.back()->prev() == n[3]);
+ CHECK(list.back() == n[2]);
+
+ CHECK(list.back()->next() == nullptr);
+}
+
+TEST_CASE("[List] Swap first cross pair") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.swap(n[0], n[2]);
+
+ CHECK(list.front()->prev() == nullptr);
+
+ CHECK(list.front() == n[2]);
+ CHECK(list.front()->next() == n[1]);
+ CHECK(list.back()->prev() == n[0]);
+ CHECK(list.back() == n[3]);
+
+ CHECK(list.back()->next() == nullptr);
+}
+
+TEST_CASE("[List] Swap last cross pair") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.swap(n[1], n[3]);
+
+ CHECK(list.front()->prev() == nullptr);
+
+ CHECK(list.front() == n[0]);
+ CHECK(list.front()->next() == n[3]);
+ CHECK(list.back()->prev() == n[2]);
+ CHECK(list.back() == n[1]);
+
+ CHECK(list.back()->next() == nullptr);
+}
+
+TEST_CASE("[List] Swap edges") {
+ List<int> list;
+ List<int>::Element *n[4];
+ populate_integers(list, n, 4);
+
+ list.swap(n[1], n[3]);
+
+ CHECK(list.front()->prev() == nullptr);
+
+ CHECK(list.front() == n[0]);
+ CHECK(list.front()->next() == n[3]);
+ CHECK(list.back()->prev() == n[2]);
+ CHECK(list.back() == n[1]);
+
+ CHECK(list.back()->next() == nullptr);
+}
+
+TEST_CASE("[List] Swap middle (values check)") {
+ List<String> list;
+ List<String>::Element *n_str1 = list.push_back("Still");
+ List<String>::Element *n_str2 = list.push_back("waiting");
+ List<String>::Element *n_str3 = list.push_back("for");
+ List<String>::Element *n_str4 = list.push_back("Godot.");
+
+ CHECK(n_str1->get() == "Still");
+ CHECK(n_str4->get() == "Godot.");
+
+ CHECK(list.front()->get() == "Still");
+ CHECK(list.front()->next()->get() == "waiting");
+ CHECK(list.back()->prev()->get() == "for");
+ CHECK(list.back()->get() == "Godot.");
+
+ list.swap(n_str2, n_str3);
+
+ CHECK(list.front()->next()->get() == "for");
+ CHECK(list.back()->prev()->get() == "waiting");
+}
+
+TEST_CASE("[List] Swap front and back (values check)") {
+ List<Variant> list;
+ Variant str = "Godot";
+ List<Variant>::Element *n_str = list.push_back(str);
+ Variant color = Color(0, 0, 1);
+ List<Variant>::Element *n_color = list.push_back(color);
+
+ CHECK(list.front()->get() == "Godot");
+ CHECK(list.back()->get() == Color(0, 0, 1));
+
+ list.swap(n_str, n_color);
+
+ CHECK(list.front()->get() == Color(0, 0, 1));
+ CHECK(list.back()->get() == "Godot");
+}
+
+TEST_CASE("[List] Swap adjacent back and front (reverse order of elements)") {
+ List<int> list;
+ List<int>::Element *n[2];
+ populate_integers(list, n, 2);
+
+ list.swap(n[1], n[0]);
+
+ List<int>::Element *it = list.front();
+ while (it) {
+ List<int>::Element *prev_it = it;
+ it = it->next();
+ if (it == prev_it) {
+ FAIL_CHECK("Infinite loop detected.");
+ break;
+ }
+ }
+}
+
+static void swap_random(List<int> &p_list, List<int>::Element *r_elements[], size_t p_size, size_t p_iterations) {
+ Math::seed(0);
+
+ for (size_t test_i = 0; test_i < p_iterations; ++test_i) {
+ // A and B elements have corresponding indices as values.
+ const int a_idx = static_cast<int>(Math::rand() % p_size);
+ const int b_idx = static_cast<int>(Math::rand() % p_size);
+ List<int>::Element *a = p_list.find(a_idx); // via find.
+ List<int>::Element *b = r_elements[b_idx]; // via pointer.
+
+ int va = a->get();
+ int vb = b->get();
+
+ p_list.swap(a, b);
+
+ CHECK(va == a->get());
+ CHECK(vb == b->get());
+
+ size_t element_count = 0;
+
+ // Fully traversable after swap?
+ List<int>::Element *it = p_list.front();
+ while (it) {
+ element_count += 1;
+ List<int>::Element *prev_it = it;
+ it = it->next();
+ if (it == prev_it) {
+ FAIL_CHECK("Infinite loop detected.");
+ break;
+ }
+ }
+ // We should not lose anything in the process.
+ if (element_count != p_size) {
+ FAIL_CHECK("Element count mismatch.");
+ break;
+ }
+ }
+}
+
+TEST_CASE("[Stress][List] Swap random 100 elements, 500 iterations.") {
+ List<int> list;
+ List<int>::Element *n[100];
+ populate_integers(list, n, 100);
+ swap_random(list, n, 100, 500);
+}
+
+TEST_CASE("[Stress][List] Swap random 10 elements, 1000 iterations.") {
+ List<int> list;
+ List<int>::Element *n[10];
+ populate_integers(list, n, 10);
+ swap_random(list, n, 10, 1000);
+}
+} // namespace TestList
+
+#endif // TEST_LIST_H
diff --git a/tests/core/templates/test_local_vector.h b/tests/core/templates/test_local_vector.h
new file mode 100644
index 0000000000..b2464c3914
--- /dev/null
+++ b/tests/core/templates/test_local_vector.h
@@ -0,0 +1,240 @@
+/*************************************************************************/
+/* test_local_vector.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_LOCAL_VECTOR_H
+#define TEST_LOCAL_VECTOR_H
+
+#include "core/templates/local_vector.h"
+
+#include "tests/test_macros.h"
+
+namespace TestLocalVector {
+
+TEST_CASE("[LocalVector] List Initialization.") {
+ LocalVector<int> vector{ 0, 1, 2, 3, 4 };
+
+ CHECK(vector.size() == 5);
+ CHECK(vector[0] == 0);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 3);
+ CHECK(vector[4] == 4);
+}
+
+TEST_CASE("[LocalVector] Push Back.") {
+ LocalVector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ CHECK(vector[0] == 0);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 3);
+ CHECK(vector[4] == 4);
+}
+
+TEST_CASE("[LocalVector] Find.") {
+ LocalVector<int> vector;
+ vector.push_back(3);
+ vector.push_back(1);
+ vector.push_back(4);
+ vector.push_back(0);
+ vector.push_back(2);
+
+ CHECK(vector[0] == 3);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 4);
+ CHECK(vector[3] == 0);
+ CHECK(vector[4] == 2);
+
+ CHECK(vector.find(0) == 3);
+ CHECK(vector.find(1) == 1);
+ CHECK(vector.find(2) == 4);
+ CHECK(vector.find(3) == 0);
+ CHECK(vector.find(4) == 2);
+
+ CHECK(vector.find(-1) == -1);
+ CHECK(vector.find(5) == -1);
+}
+
+TEST_CASE("[LocalVector] Remove.") {
+ LocalVector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ vector.remove_at(0);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 3);
+ CHECK(vector[3] == 4);
+
+ vector.remove_at(2);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 4);
+
+ vector.remove_at(1);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 4);
+
+ vector.remove_at(0);
+
+ CHECK(vector[0] == 4);
+}
+
+TEST_CASE("[LocalVector] Remove Unordered.") {
+ LocalVector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ CHECK(vector.size() == 5);
+
+ vector.remove_at_unordered(0);
+
+ CHECK(vector.size() == 4);
+
+ CHECK(vector.find(0) == -1);
+ CHECK(vector.find(1) != -1);
+ CHECK(vector.find(2) != -1);
+ CHECK(vector.find(3) != -1);
+ CHECK(vector.find(4) != -1);
+
+ // Now the vector is no more ordered.
+ vector.remove_at_unordered(vector.find(3));
+
+ CHECK(vector.size() == 3);
+
+ CHECK(vector.find(3) == -1);
+ CHECK(vector.find(1) != -1);
+ CHECK(vector.find(2) != -1);
+ CHECK(vector.find(4) != -1);
+
+ vector.remove_at_unordered(vector.find(2));
+
+ CHECK(vector.size() == 2);
+
+ CHECK(vector.find(2) == -1);
+ CHECK(vector.find(1) != -1);
+ CHECK(vector.find(4) != -1);
+
+ vector.remove_at_unordered(vector.find(4));
+
+ CHECK(vector.size() == 1);
+
+ CHECK(vector.find(4) == -1);
+ CHECK(vector.find(1) != -1);
+
+ // Remove the last one.
+ vector.remove_at_unordered(0);
+
+ CHECK(vector.is_empty());
+ CHECK(vector.size() == 0);
+}
+
+TEST_CASE("[LocalVector] Erase.") {
+ LocalVector<int> vector;
+ vector.push_back(1);
+ vector.push_back(3);
+ vector.push_back(0);
+ vector.push_back(2);
+ vector.push_back(4);
+
+ CHECK(vector.find(2) == 3);
+
+ vector.erase(2);
+
+ CHECK(vector.find(2) == -1);
+ CHECK(vector.size() == 4);
+}
+
+TEST_CASE("[LocalVector] Size / Resize / Reserve.") {
+ LocalVector<int> vector;
+
+ CHECK(vector.is_empty());
+ CHECK(vector.size() == 0);
+ CHECK(vector.get_capacity() == 0);
+
+ vector.resize(10);
+
+ CHECK(vector.size() == 10);
+ CHECK(vector.get_capacity() >= 10);
+
+ vector.resize(5);
+
+ CHECK(vector.size() == 5);
+ // Capacity is supposed to change only when the size increase.
+ CHECK(vector.get_capacity() >= 10);
+
+ vector.remove_at(0);
+ vector.remove_at(0);
+ vector.remove_at(0);
+
+ CHECK(vector.size() == 2);
+ // Capacity is supposed to change only when the size increase.
+ CHECK(vector.get_capacity() >= 10);
+
+ vector.reset();
+
+ CHECK(vector.size() == 0);
+ CHECK(vector.get_capacity() == 0);
+
+ vector.reserve(3);
+
+ CHECK(vector.is_empty());
+ CHECK(vector.size() == 0);
+ CHECK(vector.get_capacity() >= 3);
+
+ vector.push_back(0);
+ vector.push_back(0);
+ vector.push_back(0);
+
+ CHECK(vector.size() == 3);
+ CHECK(vector.get_capacity() >= 3);
+
+ vector.push_back(0);
+
+ CHECK(vector.size() == 4);
+ CHECK(vector.get_capacity() >= 4);
+}
+} // namespace TestLocalVector
+
+#endif // TEST_LOCAL_VECTOR_H
diff --git a/tests/core/templates/test_lru.h b/tests/core/templates/test_lru.h
new file mode 100644
index 0000000000..354f53e164
--- /dev/null
+++ b/tests/core/templates/test_lru.h
@@ -0,0 +1,99 @@
+/*************************************************************************/
+/* test_lru.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_LRU_H
+#define TEST_LRU_H
+
+#include "core/templates/lru.h"
+
+#include "tests/test_macros.h"
+
+namespace TestLRU {
+
+TEST_CASE("[LRU] Store and read") {
+ LRUCache<int, int> lru;
+
+ lru.set_capacity(3);
+ lru.insert(1, 1);
+ lru.insert(50, 2);
+ lru.insert(100, 5);
+
+ CHECK(lru.has(1));
+ CHECK(lru.has(50));
+ CHECK(lru.has(100));
+ CHECK(!lru.has(200));
+
+ CHECK(lru.get(1) == 1);
+ CHECK(lru.get(50) == 2);
+ CHECK(lru.get(100) == 5);
+
+ CHECK(lru.getptr(1) != nullptr);
+ CHECK(lru.getptr(1000) == nullptr);
+
+ lru.insert(600, 600); // Erase <50>
+ CHECK(lru.has(600));
+ CHECK(!lru.has(50));
+}
+
+TEST_CASE("[LRU] Resize and clear") {
+ LRUCache<int, int> lru;
+
+ lru.set_capacity(3);
+ lru.insert(1, 1);
+ lru.insert(2, 2);
+ lru.insert(3, 3);
+
+ CHECK(lru.get_capacity() == 3);
+
+ lru.set_capacity(5);
+ CHECK(lru.get_capacity() == 5);
+
+ CHECK(lru.has(1));
+ CHECK(lru.has(2));
+ CHECK(lru.has(3));
+ CHECK(!lru.has(4));
+
+ lru.set_capacity(2);
+ CHECK(lru.get_capacity() == 2);
+
+ CHECK(!lru.has(1));
+ CHECK(lru.has(2));
+ CHECK(lru.has(3));
+ CHECK(!lru.has(4));
+
+ lru.clear();
+ CHECK(!lru.has(1));
+ CHECK(!lru.has(2));
+ CHECK(!lru.has(3));
+ CHECK(!lru.has(4));
+}
+} // namespace TestLRU
+
+#endif // TEST_LRU_H
diff --git a/tests/core/templates/test_oa_hash_map.cpp b/tests/core/templates/test_oa_hash_map.cpp
new file mode 100644
index 0000000000..87bf9feb83
--- /dev/null
+++ b/tests/core/templates/test_oa_hash_map.cpp
@@ -0,0 +1,301 @@
+/*************************************************************************/
+/* test_oa_hash_map.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "test_oa_hash_map.h"
+
+#include "core/os/os.h"
+#include "core/templates/oa_hash_map.h"
+
+namespace TestOAHashMap {
+
+struct CountedItem {
+ static int count;
+
+ int id = -1;
+ bool destroyed = false;
+
+ CountedItem() {
+ count++;
+ }
+
+ CountedItem(int p_id) :
+ id(p_id) {
+ count++;
+ }
+
+ CountedItem(const CountedItem &p_other) :
+ id(p_other.id) {
+ count++;
+ }
+
+ void operator=(const CountedItem &p_other) {
+ id = p_other.id;
+ count++;
+ }
+
+ ~CountedItem() {
+ CRASH_COND(destroyed);
+ count--;
+ destroyed = true;
+ }
+};
+
+int CountedItem::count;
+
+MainLoop *test() {
+ OS::get_singleton()->print("\n\n\nHello from test\n");
+
+ // test element tracking.
+ {
+ OAHashMap<int, int> map;
+
+ map.set(42, 1337);
+ map.set(1337, 21);
+ map.set(42, 11880);
+
+ int value = 0;
+ map.lookup(42, value);
+
+ OS::get_singleton()->print("capacity %d\n", map.get_capacity());
+ OS::get_singleton()->print("elements %d\n", map.get_num_elements());
+
+ OS::get_singleton()->print("map[42] = %d\n", value);
+ }
+
+ // rehashing and deletion
+ {
+ OAHashMap<int, int> map;
+
+ for (int i = 0; i < 500; i++) {
+ map.set(i, i * 2);
+ }
+
+ for (int i = 0; i < 500; i += 2) {
+ map.remove(i);
+ }
+
+ uint32_t num_elems = 0;
+ for (int i = 0; i < 500; i++) {
+ int tmp;
+ if (map.lookup(i, tmp) && tmp == i * 2) {
+ num_elems++;
+ }
+ }
+
+ OS::get_singleton()->print("elements %d == %d.\n", map.get_num_elements(), num_elems);
+ }
+
+ // iteration
+ {
+ OAHashMap<String, int> map;
+
+ map.set("Hello", 1);
+ map.set("World", 2);
+ map.set("Godot rocks", 42);
+
+ for (OAHashMap<String, int>::Iterator it = map.iter(); it.valid; it = map.next_iter(it)) {
+ OS::get_singleton()->print("map[\"%s\"] = %d\n", it.key->utf8().get_data(), *it.value);
+ }
+ }
+
+ // stress test / test for issue #22928
+ {
+ OAHashMap<int, int> map;
+ int dummy = 0;
+ const int N = 1000;
+ uint32_t *keys = new uint32_t[N];
+
+ Math::seed(0);
+
+ // insert a couple of random keys (with a dummy value, which is ignored)
+ for (int i = 0; i < N; i++) {
+ keys[i] = Math::rand();
+ map.set(keys[i], dummy);
+
+ if (!map.lookup(keys[i], dummy)) {
+ OS::get_singleton()->print("could not find 0x%X despite it was just inserted!\n", unsigned(keys[i]));
+ }
+ }
+
+ // check whether the keys are still present
+ for (int i = 0; i < N; i++) {
+ if (!map.lookup(keys[i], dummy)) {
+ OS::get_singleton()->print("could not find 0x%X despite it has been inserted previously! (not checking the other keys, breaking...)\n", unsigned(keys[i]));
+ break;
+ }
+ }
+
+ delete[] keys;
+ }
+
+ // regression test / test for issue related to #31402
+ {
+ OS::get_singleton()->print("test for issue #31402 started...\n");
+
+ const int num_test_values = 12;
+ int test_values[num_test_values] = { 0, 24, 48, 72, 96, 120, 144, 168, 192, 216, 240, 264 };
+
+ int dummy = 0;
+ OAHashMap<int, int> map;
+ map.clear();
+
+ for (int i = 0; i < num_test_values; ++i) {
+ map.set(test_values[i], dummy);
+ }
+
+ OS::get_singleton()->print("test for issue #31402 passed.\n");
+ }
+
+ // test collision resolution, should not crash or run indefinitely
+ {
+ OAHashMap<int, int> map(4);
+ map.set(1, 1);
+ map.set(5, 1);
+ map.set(9, 1);
+ map.set(13, 1);
+ map.remove(5);
+ map.remove(9);
+ map.remove(13);
+ map.set(5, 1);
+ }
+
+ // test memory management of items, should not crash or leak items
+ {
+ // Exercise different patterns of removal
+ for (int i = 0; i < 4; ++i) {
+ {
+ OAHashMap<String, CountedItem> map;
+ int id = 0;
+ for (int j = 0; j < 100; ++j) {
+ map.insert(itos(j), CountedItem(id));
+ }
+ if (i <= 1) {
+ for (int j = 0; j < 100; ++j) {
+ map.remove(itos(j));
+ }
+ }
+ if (i % 2 == 0) {
+ map.clear();
+ }
+ }
+
+ if (CountedItem::count != 0) {
+ OS::get_singleton()->print("%d != 0 (not performing the other test sub-cases, breaking...)\n", CountedItem::count);
+ break;
+ }
+ }
+ }
+
+ // Test map with 0 capacity.
+ {
+ OAHashMap<int, String> original_map(0);
+ original_map.set(1, "1");
+ OS::get_singleton()->print("OAHashMap 0 capacity initialization passed.\n");
+ }
+
+ // Test copy constructor.
+ {
+ OAHashMap<int, String> original_map;
+ original_map.set(1, "1");
+ original_map.set(2, "2");
+ original_map.set(3, "3");
+ original_map.set(4, "4");
+ original_map.set(5, "5");
+
+ OAHashMap<int, String> map_copy(original_map);
+
+ bool pass = true;
+ for (
+ OAHashMap<int, String>::Iterator it = original_map.iter();
+ it.valid;
+ it = original_map.next_iter(it)) {
+ if (map_copy.lookup_ptr(*it.key) == nullptr) {
+ pass = false;
+ }
+ if (*it.value != *map_copy.lookup_ptr(*it.key)) {
+ pass = false;
+ }
+ }
+ if (pass) {
+ OS::get_singleton()->print("OAHashMap copy constructor test passed.\n");
+ } else {
+ OS::get_singleton()->print("OAHashMap copy constructor test FAILED.\n");
+ }
+
+ map_copy.set(1, "Random String");
+ if (*map_copy.lookup_ptr(1) == *original_map.lookup_ptr(1)) {
+ OS::get_singleton()->print("OAHashMap copy constructor, atomic copy test FAILED.\n");
+ } else {
+ OS::get_singleton()->print("OAHashMap copy constructor, atomic copy test passed.\n");
+ }
+ }
+
+ // Test assign operator.
+ {
+ OAHashMap<int, String> original_map;
+ original_map.set(1, "1");
+ original_map.set(2, "2");
+ original_map.set(3, "3");
+ original_map.set(4, "4");
+ original_map.set(5, "5");
+
+ OAHashMap<int, String> map_copy(100000);
+ map_copy.set(1, "Just a string.");
+ map_copy = original_map;
+
+ bool pass = true;
+ for (
+ OAHashMap<int, String>::Iterator it = map_copy.iter();
+ it.valid;
+ it = map_copy.next_iter(it)) {
+ if (original_map.lookup_ptr(*it.key) == nullptr) {
+ pass = false;
+ }
+ if (*it.value != *original_map.lookup_ptr(*it.key)) {
+ pass = false;
+ }
+ }
+ if (pass) {
+ OS::get_singleton()->print("OAHashMap assign operation test passed.\n");
+ } else {
+ OS::get_singleton()->print("OAHashMap assign operation test FAILED.\n");
+ }
+
+ map_copy.set(1, "Random String");
+ if (*map_copy.lookup_ptr(1) == *original_map.lookup_ptr(1)) {
+ OS::get_singleton()->print("OAHashMap assign operation atomic copy test FAILED.\n");
+ } else {
+ OS::get_singleton()->print("OAHashMap assign operation atomic copy test passed.\n");
+ }
+ }
+
+ return nullptr;
+}
+} // namespace TestOAHashMap
diff --git a/tests/core/templates/test_oa_hash_map.h b/tests/core/templates/test_oa_hash_map.h
new file mode 100644
index 0000000000..d4b72af2ac
--- /dev/null
+++ b/tests/core/templates/test_oa_hash_map.h
@@ -0,0 +1,41 @@
+/*************************************************************************/
+/* test_oa_hash_map.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_OA_HASH_MAP_H
+#define TEST_OA_HASH_MAP_H
+
+class MainLoop;
+
+namespace TestOAHashMap {
+
+MainLoop *test();
+}
+
+#endif // TEST_OA_HASH_MAP_H
diff --git a/tests/core/templates/test_ordered_hash_map.h b/tests/core/templates/test_ordered_hash_map.h
new file mode 100644
index 0000000000..08c5c9b72a
--- /dev/null
+++ b/tests/core/templates/test_ordered_hash_map.h
@@ -0,0 +1,135 @@
+/*************************************************************************/
+/* test_ordered_hash_map.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_ORDERED_HASH_MAP_H
+#define TEST_ORDERED_HASH_MAP_H
+
+#include "core/templates/ordered_hash_map.h"
+
+#include "tests/test_macros.h"
+
+namespace TestOrderedHashMap {
+
+TEST_CASE("[OrderedHashMap] Insert element") {
+ OrderedHashMap<int, int> map;
+ OrderedHashMap<int, int>::Element e = map.insert(42, 84);
+
+ CHECK(e);
+ CHECK(e.key() == 42);
+ CHECK(e.get() == 84);
+ CHECK(e.value() == 84);
+ CHECK(map[42] == 84);
+ CHECK(map.has(42));
+ CHECK(map.find(42));
+}
+
+TEST_CASE("[OrderedHashMap] Overwrite element") {
+ OrderedHashMap<int, int> map;
+ map.insert(42, 84);
+ map.insert(42, 1234);
+
+ CHECK(map[42] == 1234);
+}
+
+TEST_CASE("[OrderedHashMap] Erase via element") {
+ OrderedHashMap<int, int> map;
+ OrderedHashMap<int, int>::Element e = map.insert(42, 84);
+
+ map.erase(e);
+ CHECK(!e);
+ CHECK(!map.has(42));
+ CHECK(!map.find(42));
+}
+
+TEST_CASE("[OrderedHashMap] Erase via key") {
+ OrderedHashMap<int, int> map;
+ map.insert(42, 84);
+ map.erase(42);
+ CHECK(!map.has(42));
+ CHECK(!map.find(42));
+}
+
+TEST_CASE("[OrderedHashMap] Size") {
+ OrderedHashMap<int, int> map;
+ map.insert(42, 84);
+ map.insert(123, 84);
+ map.insert(123, 84);
+ map.insert(0, 84);
+ map.insert(123485, 84);
+
+ CHECK(map.size() == 4);
+}
+
+TEST_CASE("[OrderedHashMap] Iteration") {
+ OrderedHashMap<int, int> map;
+ map.insert(42, 84);
+ map.insert(123, 12385);
+ map.insert(0, 12934);
+ map.insert(123485, 1238888);
+ map.insert(123, 111111);
+
+ Vector<Pair<int, int>> expected;
+ expected.push_back(Pair<int, int>(42, 84));
+ expected.push_back(Pair<int, int>(123, 111111));
+ expected.push_back(Pair<int, int>(0, 12934));
+ expected.push_back(Pair<int, int>(123485, 1238888));
+
+ int idx = 0;
+ for (OrderedHashMap<int, int>::Element E = map.front(); E; E = E.next()) {
+ CHECK(expected[idx] == Pair<int, int>(E.key(), E.value()));
+ ++idx;
+ }
+}
+
+TEST_CASE("[OrderedHashMap] Const iteration") {
+ OrderedHashMap<int, int> map;
+ map.insert(42, 84);
+ map.insert(123, 12385);
+ map.insert(0, 12934);
+ map.insert(123485, 1238888);
+ map.insert(123, 111111);
+
+ const OrderedHashMap<int, int> const_map = map;
+
+ Vector<Pair<int, int>> expected;
+ expected.push_back(Pair<int, int>(42, 84));
+ expected.push_back(Pair<int, int>(123, 111111));
+ expected.push_back(Pair<int, int>(0, 12934));
+ expected.push_back(Pair<int, int>(123485, 1238888));
+
+ int idx = 0;
+ for (OrderedHashMap<int, int>::ConstElement E = const_map.front(); E; E = E.next()) {
+ CHECK(expected[idx] == Pair<int, int>(E.key(), E.value()));
+ ++idx;
+ }
+}
+} // namespace TestOrderedHashMap
+
+#endif // TEST_ORDERED_HASH_MAP_H
diff --git a/tests/core/templates/test_paged_array.h b/tests/core/templates/test_paged_array.h
new file mode 100644
index 0000000000..86cf3a2dfc
--- /dev/null
+++ b/tests/core/templates/test_paged_array.h
@@ -0,0 +1,153 @@
+/*************************************************************************/
+/* test_paged_array.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_PAGED_ARRAY_H
+#define TEST_PAGED_ARRAY_H
+
+#include "core/templates/paged_array.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestPagedArray {
+
+// PagedArray
+
+TEST_CASE("[PagedArray] Simple fill and refill") {
+ PagedArrayPool<uint32_t> pool;
+ PagedArray<uint32_t> array;
+ array.set_page_pool(&pool);
+
+ for (uint32_t i = 0; i < 123456; i++) {
+ array.push_back(i);
+ }
+ CHECK_MESSAGE(
+ array.size() == 123456,
+ "PagedArray should have 123456 elements.");
+
+ bool all_match = true;
+ for (uint32_t i = 0; i < 123456; i++) {
+ if (array[i] != i) {
+ all_match = false;
+ break;
+ }
+ }
+
+ CHECK_MESSAGE(
+ all_match,
+ "PagedArray elements should match from 0 to 123455.");
+
+ array.clear();
+
+ CHECK_MESSAGE(
+ array.size() == 0,
+ "PagedArray elements should be 0 after clear.");
+
+ for (uint32_t i = 0; i < 999; i++) {
+ array.push_back(i);
+ }
+ CHECK_MESSAGE(
+ array.size() == 999,
+ "PagedArray should have 999 elements.");
+
+ all_match = true;
+ for (uint32_t i = 0; i < 999; i++) {
+ if (array[i] != i) {
+ all_match = false;
+ }
+ }
+
+ CHECK_MESSAGE(
+ all_match,
+ "PagedArray elements should match from 0 to 998.");
+
+ array.reset(); //reset so pagepool can be reset
+ pool.reset();
+}
+
+TEST_CASE("[PagedArray] Shared pool fill, including merging") {
+ PagedArrayPool<uint32_t> pool;
+ PagedArray<uint32_t> array1;
+ PagedArray<uint32_t> array2;
+ array1.set_page_pool(&pool);
+ array2.set_page_pool(&pool);
+
+ for (uint32_t i = 0; i < 123456; i++) {
+ array1.push_back(i);
+ }
+ CHECK_MESSAGE(
+ array1.size() == 123456,
+ "PagedArray #1 should have 123456 elements.");
+
+ bool all_match = true;
+ for (uint32_t i = 0; i < 123456; i++) {
+ if (array1[i] != i) {
+ all_match = false;
+ }
+ }
+
+ CHECK_MESSAGE(
+ all_match,
+ "PagedArray #1 elements should match from 0 to 123455.");
+
+ for (uint32_t i = 0; i < 999; i++) {
+ array2.push_back(i);
+ }
+ CHECK_MESSAGE(
+ array2.size() == 999,
+ "PagedArray #2 should have 999 elements.");
+
+ all_match = true;
+ for (uint32_t i = 0; i < 999; i++) {
+ if (array2[i] != i) {
+ all_match = false;
+ }
+ }
+
+ CHECK_MESSAGE(
+ all_match,
+ "PagedArray #2 elements should match from 0 to 998.");
+
+ array1.merge_unordered(array2);
+
+ CHECK_MESSAGE(
+ array1.size() == 123456 + 999,
+ "PagedArray #1 should now be 123456 + 999 elements.");
+
+ CHECK_MESSAGE(
+ array2.size() == 0,
+ "PagedArray #2 should now be 0 elements.");
+
+ array1.reset(); //reset so pagepool can be reset
+ array2.reset(); //reset so pagepool can be reset
+ pool.reset();
+}
+} // namespace TestPagedArray
+
+#endif // TEST_PAGED_ARRAY_H
diff --git a/tests/core/templates/test_vector.h b/tests/core/templates/test_vector.h
new file mode 100644
index 0000000000..24b3547256
--- /dev/null
+++ b/tests/core/templates/test_vector.h
@@ -0,0 +1,520 @@
+/*************************************************************************/
+/* test_vector.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_VECTOR_H
+#define TEST_VECTOR_H
+
+#include "core/templates/vector.h"
+
+#include "tests/test_macros.h"
+
+namespace TestVector {
+
+TEST_CASE("[Vector] List initialization") {
+ Vector<int> vector{ 0, 1, 2, 3, 4 };
+
+ CHECK(vector.size() == 5);
+ CHECK(vector[0] == 0);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 3);
+ CHECK(vector[4] == 4);
+}
+
+TEST_CASE("[Vector] Push back and append") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ // Alias for `push_back`.
+ vector.append(4);
+
+ CHECK(vector[0] == 0);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 3);
+ CHECK(vector[4] == 4);
+}
+
+TEST_CASE("[Vector] Append array") {
+ Vector<int> vector;
+ vector.push_back(1);
+ vector.push_back(2);
+
+ Vector<int> vector_other;
+ vector_other.push_back(128);
+ vector_other.push_back(129);
+ vector.append_array(vector_other);
+
+ CHECK(vector.size() == 4);
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 128);
+ CHECK(vector[3] == 129);
+}
+
+TEST_CASE("[Vector] Insert") {
+ Vector<int> vector;
+ vector.insert(0, 2);
+ vector.insert(0, 8);
+ vector.insert(2, 5);
+ vector.insert(1, 5);
+ vector.insert(0, -2);
+
+ CHECK(vector.size() == 5);
+ CHECK(vector[0] == -2);
+ CHECK(vector[1] == 8);
+ CHECK(vector[2] == 5);
+ CHECK(vector[3] == 2);
+ CHECK(vector[4] == 5);
+}
+
+TEST_CASE("[Vector] Ordered insert") {
+ Vector<int> vector;
+ vector.ordered_insert(2);
+ vector.ordered_insert(8);
+ vector.ordered_insert(5);
+ vector.ordered_insert(5);
+ vector.ordered_insert(-2);
+
+ CHECK(vector.size() == 5);
+ CHECK(vector[0] == -2);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 5);
+ CHECK(vector[3] == 5);
+ CHECK(vector[4] == 8);
+}
+
+TEST_CASE("[Vector] Insert + Ordered insert") {
+ Vector<int> vector;
+ vector.ordered_insert(2);
+ vector.ordered_insert(8);
+ vector.insert(0, 5);
+ vector.ordered_insert(5);
+ vector.insert(1, -2);
+
+ CHECK(vector.size() == 5);
+ CHECK(vector[0] == 5);
+ CHECK(vector[1] == -2);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 5);
+ CHECK(vector[4] == 8);
+}
+
+TEST_CASE("[Vector] Fill large array and modify it") {
+ Vector<int> vector;
+ vector.resize(1'000'000);
+ vector.fill(0x60d07);
+
+ vector.write[200] = 0;
+ CHECK(vector.size() == 1'000'000);
+ CHECK(vector[0] == 0x60d07);
+ CHECK(vector[200] == 0);
+ CHECK(vector[499'999] == 0x60d07);
+ CHECK(vector[999'999] == 0x60d07);
+ vector.remove_at(200);
+ CHECK(vector[200] == 0x60d07);
+
+ vector.clear();
+ CHECK(vector.size() == 0);
+}
+
+TEST_CASE("[Vector] Copy creation") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ Vector<int> vector_other = Vector<int>(vector);
+ vector_other.remove_at(0);
+ CHECK(vector_other[0] == 1);
+ CHECK(vector_other[1] == 2);
+ CHECK(vector_other[2] == 3);
+ CHECK(vector_other[3] == 4);
+
+ // Make sure the original vector isn't modified.
+ CHECK(vector[0] == 0);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 3);
+ CHECK(vector[4] == 4);
+}
+
+TEST_CASE("[Vector] Duplicate") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ Vector<int> vector_other = vector.duplicate();
+ vector_other.remove_at(0);
+ CHECK(vector_other[0] == 1);
+ CHECK(vector_other[1] == 2);
+ CHECK(vector_other[2] == 3);
+ CHECK(vector_other[3] == 4);
+
+ // Make sure the original vector isn't modified.
+ CHECK(vector[0] == 0);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 2);
+ CHECK(vector[3] == 3);
+ CHECK(vector[4] == 4);
+}
+
+TEST_CASE("[Vector] Get, set") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ CHECK(vector.get(0) == 0);
+ CHECK(vector.get(1) == 1);
+ vector.set(2, 256);
+ CHECK(vector.get(2) == 256);
+ CHECK(vector.get(3) == 3);
+
+ ERR_PRINT_OFF;
+ // Invalid (but should not crash): setting out of bounds.
+ vector.set(6, 500);
+ ERR_PRINT_ON;
+
+ CHECK(vector.get(4) == 4);
+}
+
+TEST_CASE("[Vector] To byte array") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(-1);
+ vector.push_back(2008);
+ vector.push_back(999999999);
+
+ Vector<uint8_t> byte_array = vector.to_byte_array();
+ CHECK(byte_array.size() == 16);
+ // vector[0]
+ CHECK(byte_array[0] == 0);
+ CHECK(byte_array[1] == 0);
+ CHECK(byte_array[2] == 0);
+ CHECK(byte_array[3] == 0);
+
+ // vector[1]
+ CHECK(byte_array[4] == 255);
+ CHECK(byte_array[5] == 255);
+ CHECK(byte_array[6] == 255);
+ CHECK(byte_array[7] == 255);
+
+ // vector[2]
+ CHECK(byte_array[8] == 216);
+ CHECK(byte_array[9] == 7);
+ CHECK(byte_array[10] == 0);
+ CHECK(byte_array[11] == 0);
+
+ // vector[3]
+ CHECK(byte_array[12] == 255);
+ CHECK(byte_array[13] == 201);
+ CHECK(byte_array[14] == 154);
+ CHECK(byte_array[15] == 59);
+}
+
+TEST_CASE("[Vector] Slice") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ Vector<int> slice1 = vector.slice(1, 3);
+ CHECK(slice1.size() == 2);
+ CHECK(slice1[0] == 1);
+ CHECK(slice1[1] == 2);
+
+ Vector<int> slice2 = vector.slice(1, -1);
+ CHECK(slice2.size() == 4);
+ CHECK(slice2[0] == 1);
+ CHECK(slice2[1] == 2);
+ CHECK(slice2[2] == 3);
+ CHECK(slice2[3] == 4);
+
+ Vector<int> slice3 = vector.slice(3, -1);
+ CHECK(slice3.size() == 2);
+ CHECK(slice3[0] == 3);
+ CHECK(slice3[1] == 4);
+
+ Vector<int> slice4 = vector.slice(2, -2);
+ CHECK(slice4.size() == 2);
+ CHECK(slice4[0] == 2);
+ CHECK(slice4[1] == 3);
+}
+
+TEST_CASE("[Vector] Find, has") {
+ Vector<int> vector;
+ vector.push_back(3);
+ vector.push_back(1);
+ vector.push_back(4);
+ vector.push_back(0);
+ vector.push_back(2);
+
+ CHECK(vector[0] == 3);
+ CHECK(vector[1] == 1);
+ CHECK(vector[2] == 4);
+ CHECK(vector[3] == 0);
+ CHECK(vector[4] == 2);
+
+ CHECK(vector.find(0) == 3);
+ CHECK(vector.find(1) == 1);
+ CHECK(vector.find(2) == 4);
+ CHECK(vector.find(3) == 0);
+ CHECK(vector.find(4) == 2);
+
+ CHECK(vector.find(-1) == -1);
+ CHECK(vector.find(5) == -1);
+
+ CHECK(vector.has(0));
+ CHECK(vector.has(1));
+ CHECK(vector.has(2));
+ CHECK(vector.has(3));
+ CHECK(vector.has(4));
+
+ CHECK(!vector.has(-1));
+ CHECK(!vector.has(5));
+}
+
+TEST_CASE("[Vector] Remove at") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ vector.remove_at(0);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 3);
+ CHECK(vector[3] == 4);
+
+ vector.remove_at(2);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 4);
+
+ vector.remove_at(1);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 4);
+
+ vector.remove_at(0);
+
+ CHECK(vector[0] == 4);
+}
+
+TEST_CASE("[Vector] Remove at and find") {
+ Vector<int> vector;
+ vector.push_back(0);
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(4);
+
+ CHECK(vector.size() == 5);
+
+ vector.remove_at(0);
+
+ CHECK(vector.size() == 4);
+
+ CHECK(vector.find(0) == -1);
+ CHECK(vector.find(1) != -1);
+ CHECK(vector.find(2) != -1);
+ CHECK(vector.find(3) != -1);
+ CHECK(vector.find(4) != -1);
+
+ vector.remove_at(vector.find(3));
+
+ CHECK(vector.size() == 3);
+
+ CHECK(vector.find(3) == -1);
+ CHECK(vector.find(1) != -1);
+ CHECK(vector.find(2) != -1);
+ CHECK(vector.find(4) != -1);
+
+ vector.remove_at(vector.find(2));
+
+ CHECK(vector.size() == 2);
+
+ CHECK(vector.find(2) == -1);
+ CHECK(vector.find(1) != -1);
+ CHECK(vector.find(4) != -1);
+
+ vector.remove_at(vector.find(4));
+
+ CHECK(vector.size() == 1);
+
+ CHECK(vector.find(4) == -1);
+ CHECK(vector.find(1) != -1);
+
+ vector.remove_at(0);
+
+ CHECK(vector.is_empty());
+ CHECK(vector.size() == 0);
+}
+
+TEST_CASE("[Vector] Erase") {
+ Vector<int> vector;
+ vector.push_back(1);
+ vector.push_back(3);
+ vector.push_back(0);
+ vector.push_back(2);
+ vector.push_back(4);
+
+ CHECK(vector.find(2) == 3);
+
+ vector.erase(2);
+
+ CHECK(vector.find(2) == -1);
+ CHECK(vector.size() == 4);
+}
+
+TEST_CASE("[Vector] Size, resize, reserve") {
+ Vector<int> vector;
+ CHECK(vector.is_empty());
+ CHECK(vector.size() == 0);
+
+ vector.resize(10);
+
+ CHECK(vector.size() == 10);
+
+ vector.resize(5);
+
+ CHECK(vector.size() == 5);
+
+ vector.remove_at(0);
+ vector.remove_at(0);
+ vector.remove_at(0);
+
+ CHECK(vector.size() == 2);
+
+ vector.clear();
+
+ CHECK(vector.size() == 0);
+ CHECK(vector.is_empty());
+
+ vector.push_back(0);
+ vector.push_back(0);
+ vector.push_back(0);
+
+ CHECK(vector.size() == 3);
+
+ vector.push_back(0);
+
+ CHECK(vector.size() == 4);
+}
+
+TEST_CASE("[Vector] Sort") {
+ Vector<int> vector;
+ vector.push_back(2);
+ vector.push_back(8);
+ vector.push_back(-4);
+ vector.push_back(5);
+ vector.sort();
+
+ CHECK(vector.size() == 4);
+ CHECK(vector[0] == -4);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 5);
+ CHECK(vector[3] == 8);
+}
+
+TEST_CASE("[Vector] Sort custom") {
+ Vector<String> vector;
+ vector.push_back("world");
+ vector.push_back("World");
+ vector.push_back("Hello");
+ vector.push_back("10Hello");
+ vector.push_back("12Hello");
+ vector.push_back("01Hello");
+ vector.push_back("1Hello");
+ vector.push_back(".Hello");
+ vector.sort_custom<NaturalNoCaseComparator>();
+
+ CHECK(vector.size() == 8);
+ CHECK(vector[0] == ".Hello");
+ CHECK(vector[1] == "01Hello");
+ CHECK(vector[2] == "1Hello");
+ CHECK(vector[3] == "10Hello");
+ CHECK(vector[4] == "12Hello");
+ CHECK(vector[5] == "Hello");
+ CHECK(vector[6] == "world");
+ CHECK(vector[7] == "World");
+}
+
+TEST_CASE("[Vector] Search") {
+ Vector<int> vector;
+ vector.push_back(1);
+ vector.push_back(2);
+ vector.push_back(3);
+ vector.push_back(5);
+ vector.push_back(8);
+ CHECK(vector.bsearch(2, true) == 1);
+ CHECK(vector.bsearch(2, false) == 2);
+ CHECK(vector.bsearch(5, true) == 3);
+ CHECK(vector.bsearch(5, false) == 4);
+}
+
+TEST_CASE("[Vector] Operators") {
+ Vector<int> vector;
+ vector.push_back(2);
+ vector.push_back(8);
+ vector.push_back(-4);
+ vector.push_back(5);
+
+ Vector<int> vector_other;
+ vector_other.push_back(2);
+ vector_other.push_back(8);
+ vector_other.push_back(-4);
+ vector_other.push_back(5);
+
+ CHECK(vector == vector_other);
+
+ vector_other.push_back(10);
+ CHECK(vector != vector_other);
+}
+
+} // namespace TestVector
+
+#endif // TEST_VECTOR_H
diff --git a/tests/core/test_crypto.h b/tests/core/test_crypto.h
new file mode 100644
index 0000000000..ce4edc71ae
--- /dev/null
+++ b/tests/core/test_crypto.h
@@ -0,0 +1,73 @@
+/*************************************************************************/
+/* test_crypto.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_CRYPTO_H
+#define TEST_CRYPTO_H
+
+#include "core/crypto/crypto.h"
+#include "tests/test_macros.h"
+
+namespace TestCrypto {
+
+class _MockCrypto : public Crypto {
+ virtual PackedByteArray generate_random_bytes(int p_bytes) { return PackedByteArray(); }
+ virtual Ref<CryptoKey> generate_rsa(int p_bytes) { return nullptr; }
+ virtual Ref<X509Certificate> generate_self_signed_certificate(Ref<CryptoKey> p_key, String p_issuer_name, String p_not_before, String p_not_after) { return nullptr; }
+
+ virtual Vector<uint8_t> sign(HashingContext::HashType p_hash_type, Vector<uint8_t> p_hash, Ref<CryptoKey> p_key) { return Vector<uint8_t>(); }
+ virtual bool verify(HashingContext::HashType p_hash_type, Vector<uint8_t> p_hash, Vector<uint8_t> p_signature, Ref<CryptoKey> p_key) { return false; }
+ virtual Vector<uint8_t> encrypt(Ref<CryptoKey> p_key, Vector<uint8_t> p_plaintext) { return Vector<uint8_t>(); }
+ virtual Vector<uint8_t> decrypt(Ref<CryptoKey> p_key, Vector<uint8_t> p_ciphertext) { return Vector<uint8_t>(); }
+ virtual PackedByteArray hmac_digest(HashingContext::HashType p_hash_type, PackedByteArray p_key, PackedByteArray p_msg) { return PackedByteArray(); }
+};
+
+PackedByteArray raw_to_pba(const uint8_t *arr, size_t len) {
+ PackedByteArray pba;
+ pba.resize(len);
+ for (size_t i = 0; i < len; i++) {
+ pba.set(i, arr[i]);
+ }
+ return pba;
+}
+
+TEST_CASE("[Crypto] PackedByteArray constant time compare") {
+ const uint8_t hm1[] = { 144, 140, 176, 38, 88, 113, 101, 45, 71, 105, 10, 91, 248, 16, 117, 244, 189, 30, 238, 29, 219, 134, 82, 130, 212, 114, 161, 166, 188, 169, 200, 106 };
+ const uint8_t hm2[] = { 80, 30, 144, 228, 108, 38, 188, 125, 150, 64, 165, 127, 221, 118, 144, 232, 45, 100, 15, 248, 193, 244, 245, 34, 116, 147, 132, 200, 110, 27, 38, 75 };
+ PackedByteArray p1 = raw_to_pba(hm1, sizeof(hm1) / sizeof(hm1[0]));
+ PackedByteArray p2 = raw_to_pba(hm2, sizeof(hm2) / sizeof(hm2[0]));
+ _MockCrypto crypto;
+ bool equal = crypto.constant_time_compare(p1, p1);
+ CHECK(equal);
+ equal = crypto.constant_time_compare(p1, p2);
+ CHECK(!equal);
+}
+} // namespace TestCrypto
+
+#endif // TEST_CRYPTO_H
diff --git a/tests/core/test_hashing_context.h b/tests/core/test_hashing_context.h
new file mode 100644
index 0000000000..4795d24103
--- /dev/null
+++ b/tests/core/test_hashing_context.h
@@ -0,0 +1,165 @@
+/*************************************************************************/
+/* test_hashing_context.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_HASHING_CONTEXT_H
+#define TEST_HASHING_CONTEXT_H
+
+#include "core/crypto/hashing_context.h"
+
+#include "tests/test_macros.h"
+
+namespace TestHashingContext {
+
+TEST_CASE("[HashingContext] Default - MD5/SHA1/SHA256") {
+ HashingContext ctx;
+
+ static const uint8_t md5_expected[] = {
+ 0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8, 0x42, 0x7e
+ };
+ static const uint8_t sha1_expected[] = {
+ 0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60, 0x18, 0x90,
+ 0xaf, 0xd8, 0x07, 0x09
+ };
+ static const uint8_t sha256_expected[] = {
+ 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
+ 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55
+ };
+
+ CHECK(ctx.start(HashingContext::HASH_MD5) == OK);
+ PackedByteArray result = ctx.finish();
+ REQUIRE(result.size() == 16);
+ CHECK(memcmp(result.ptr(), md5_expected, 16) == 0);
+
+ CHECK(ctx.start(HashingContext::HASH_SHA1) == OK);
+ result = ctx.finish();
+ REQUIRE(result.size() == 20);
+ CHECK(memcmp(result.ptr(), sha1_expected, 20) == 0);
+
+ CHECK(ctx.start(HashingContext::HASH_SHA256) == OK);
+ result = ctx.finish();
+ REQUIRE(result.size() == 32);
+ CHECK(memcmp(result.ptr(), sha256_expected, 32) == 0);
+}
+
+TEST_CASE("[HashingContext] Multiple updates - MD5/SHA1/SHA256") {
+ HashingContext ctx;
+ const String s = "xyz";
+
+ const PackedByteArray s_byte_parts[] = {
+ String("x").to_ascii_buffer(),
+ String("y").to_ascii_buffer(),
+ String("z").to_ascii_buffer()
+ };
+
+ static const uint8_t md5_expected[] = {
+ 0xd1, 0x6f, 0xb3, 0x6f, 0x09, 0x11, 0xf8, 0x78, 0x99, 0x8c, 0x13, 0x61, 0x91, 0xaf, 0x70, 0x5e
+ };
+ static const uint8_t sha1_expected[] = {
+ 0x66, 0xb2, 0x74, 0x17, 0xd3, 0x7e, 0x02, 0x4c, 0x46, 0x52, 0x6c, 0x2f, 0x6d, 0x35, 0x8a, 0x75,
+ 0x4f, 0xc5, 0x52, 0xf3
+ };
+ static const uint8_t sha256_expected[] = {
+ 0x36, 0x08, 0xbc, 0xa1, 0xe4, 0x4e, 0xa6, 0xc4, 0xd2, 0x68, 0xeb, 0x6d, 0xb0, 0x22, 0x60, 0x26,
+ 0x98, 0x92, 0xc0, 0xb4, 0x2b, 0x86, 0xbb, 0xf1, 0xe7, 0x7a, 0x6f, 0xa1, 0x6c, 0x3c, 0x92, 0x82
+ };
+
+ CHECK(ctx.start(HashingContext::HASH_MD5) == OK);
+ CHECK(ctx.update(s_byte_parts[0]) == OK);
+ CHECK(ctx.update(s_byte_parts[1]) == OK);
+ CHECK(ctx.update(s_byte_parts[2]) == OK);
+ PackedByteArray result = ctx.finish();
+ REQUIRE(result.size() == 16);
+ CHECK(memcmp(result.ptr(), md5_expected, 16) == 0);
+
+ CHECK(ctx.start(HashingContext::HASH_SHA1) == OK);
+ CHECK(ctx.update(s_byte_parts[0]) == OK);
+ CHECK(ctx.update(s_byte_parts[1]) == OK);
+ CHECK(ctx.update(s_byte_parts[2]) == OK);
+ result = ctx.finish();
+ REQUIRE(result.size() == 20);
+ CHECK(memcmp(result.ptr(), sha1_expected, 20) == 0);
+
+ CHECK(ctx.start(HashingContext::HASH_SHA256) == OK);
+ CHECK(ctx.update(s_byte_parts[0]) == OK);
+ CHECK(ctx.update(s_byte_parts[1]) == OK);
+ CHECK(ctx.update(s_byte_parts[2]) == OK);
+ result = ctx.finish();
+ REQUIRE(result.size() == 32);
+ CHECK(memcmp(result.ptr(), sha256_expected, 32) == 0);
+}
+
+TEST_CASE("[HashingContext] Invalid use of start") {
+ HashingContext ctx;
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ ctx.start(static_cast<HashingContext::HashType>(-1)) == ERR_UNAVAILABLE,
+ "Using invalid hash types should fail.");
+ ERR_PRINT_ON;
+
+ REQUIRE(ctx.start(HashingContext::HASH_MD5) == OK);
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ ctx.start(HashingContext::HASH_MD5) == ERR_ALREADY_IN_USE,
+ "Calling 'start' twice before 'finish' should fail.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[HashingContext] Invalid use of update") {
+ HashingContext ctx;
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ ctx.update(PackedByteArray()) == ERR_UNCONFIGURED,
+ "Calling 'update' before 'start' should fail.");
+ ERR_PRINT_ON;
+
+ REQUIRE(ctx.start(HashingContext::HASH_MD5) == OK);
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ ctx.update(PackedByteArray()) == FAILED,
+ "Calling 'update' with an empty byte array should fail.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[HashingContext] Invalid use of finish") {
+ HashingContext ctx;
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ ctx.finish() == PackedByteArray(),
+ "Calling 'finish' before 'start' should return an empty byte array.");
+ ERR_PRINT_ON;
+}
+} // namespace TestHashingContext
+
+#endif // TEST_HASHING_CONTEXT_H
diff --git a/tests/core/test_time.h b/tests/core/test_time.h
new file mode 100644
index 0000000000..903ca9c001
--- /dev/null
+++ b/tests/core/test_time.h
@@ -0,0 +1,145 @@
+/*************************************************************************/
+/* test_time.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_TIME_H
+#define TEST_TIME_H
+
+#include "core/os/time.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+#define YEAR_KEY "year"
+#define MONTH_KEY "month"
+#define DAY_KEY "day"
+#define WEEKDAY_KEY "weekday"
+#define HOUR_KEY "hour"
+#define MINUTE_KEY "minute"
+#define SECOND_KEY "second"
+#define DST_KEY "dst"
+
+namespace TestTime {
+
+TEST_CASE("[Time] Unix time conversion to/from datetime string") {
+ const Time *time = Time::get_singleton();
+
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1970-01-01T00:00:00") == 0, "Time get_unix_time_from_datetime_string: The timestamp for Unix epoch is zero.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1970-01-01 00:00:00") == 0, "Time get_unix_time_from_datetime_string: The timestamp for Unix epoch with space is zero.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1970-01-01") == 0, "Time get_unix_time_from_datetime_string: The timestamp for Unix epoch without time is zero.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("00:00:00") == 0, "Time get_unix_time_from_datetime_string: The timestamp for zero time without date is zero.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1969-12-31T23:59:59") == -1, "Time get_unix_time_from_datetime_string: The timestamp for just before Unix epoch is negative one.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1234-05-06T07:08:09") == -23215049511, "Time get_unix_time_from_datetime_string: The timestamp for an arbitrary datetime is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1234-05-06 07:08:09") == -23215049511, "Time get_unix_time_from_datetime_string: The timestamp for an arbitrary datetime with space is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1234-05-06") == -23215075200, "Time get_unix_time_from_datetime_string: The timestamp for an arbitrary date without time is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("07:08:09") == 25689, "Time get_unix_time_from_datetime_string: The timestamp for an arbitrary time without date is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("2014-02-09T22:10:30") == 1391983830, "Time get_unix_time_from_datetime_string: The timestamp for GODOT IS OPEN SOURCE is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("2014-02-09 22:10:30") == 1391983830, "Time get_unix_time_from_datetime_string: The timestamp for GODOT IS OPEN SOURCE with space is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("2014-02-09") == 1391904000, "Time get_unix_time_from_datetime_string: The date for GODOT IS OPEN SOURCE without time is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("22:10:30") == 79830, "Time get_unix_time_from_datetime_string: The time for GODOT IS OPEN SOURCE without date is as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("-1000000000-01-01T00:00:00") == -31557014167219200, "Time get_unix_time_from_datetime_string: In the year negative a billion, Japan might not have been here.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1000000-01-01T00:00:00") == 31494784780800, "Time get_unix_time_from_datetime_string: The timestamp for the year a million is as expected.");
+
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(0) == "1970-01-01T00:00:00", "Time get_datetime_string_from_unix_time: The timestamp string for Unix epoch is zero.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(0, true) == "1970-01-01 00:00:00", "Time get_datetime_string_from_unix_time: The timestamp string for Unix epoch with space is zero.");
+ CHECK_MESSAGE(time->get_date_string_from_unix_time(0) == "1970-01-01", "Time get_date_string_from_unix_time: The date string for zero is Unix epoch date.");
+ CHECK_MESSAGE(time->get_time_string_from_unix_time(0) == "00:00:00", "Time get_time_string_from_unix_time: The date for zero zero is Unix epoch date.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(-1) == "1969-12-31T23:59:59", "Time get_time_string_from_unix_time: The timestamp string for just before Unix epoch is as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(-23215049511) == "1234-05-06T07:08:09", "Time get_datetime_string_from_unix_time: The timestamp for an arbitrary datetime is as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(-23215049511, true) == "1234-05-06 07:08:09", "Time get_datetime_string_from_unix_time: The timestamp for an arbitrary datetime with space is as expected.");
+ CHECK_MESSAGE(time->get_date_string_from_unix_time(-23215075200) == "1234-05-06", "Time get_date_string_from_unix_time: The timestamp for an arbitrary date without time is as expected.");
+ CHECK_MESSAGE(time->get_time_string_from_unix_time(25689) == "07:08:09", "Time get_time_string_from_unix_time: The timestamp for an arbitrary time without date is as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(1391983830) == "2014-02-09T22:10:30", "Time get_datetime_string_from_unix_time: The timestamp for GODOT IS OPEN SOURCE is as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(1391983830, true) == "2014-02-09 22:10:30", "Time get_datetime_string_from_unix_time: The timestamp for GODOT IS OPEN SOURCE with space is as expected.");
+ CHECK_MESSAGE(time->get_date_string_from_unix_time(1391904000) == "2014-02-09", "Time get_date_string_from_unix_time: The date for GODOT IS OPEN SOURCE without time is as expected.");
+ CHECK_MESSAGE(time->get_time_string_from_unix_time(79830) == "22:10:30", "Time get_time_string_from_unix_time: The time for GODOT IS OPEN SOURCE without date is as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_unix_time(31494784780800) == "1000000-01-01T00:00:00", "Time get_datetime_string_from_unix_time: The timestamp for the year a million is as expected.");
+}
+
+TEST_CASE("[Time] Datetime dictionary conversion methods") {
+ const Time *time = Time::get_singleton();
+
+ Dictionary datetime;
+ datetime[YEAR_KEY] = 2014;
+ datetime[MONTH_KEY] = 2;
+ datetime[DAY_KEY] = 9;
+ datetime[WEEKDAY_KEY] = Time::Weekday::WEEKDAY_SUNDAY;
+ datetime[HOUR_KEY] = 22;
+ datetime[MINUTE_KEY] = 10;
+ datetime[SECOND_KEY] = 30;
+
+ Dictionary date_only;
+ date_only[YEAR_KEY] = 2014;
+ date_only[MONTH_KEY] = 2;
+ date_only[DAY_KEY] = 9;
+ date_only[WEEKDAY_KEY] = Time::Weekday::WEEKDAY_SUNDAY;
+
+ Dictionary time_only;
+ time_only[HOUR_KEY] = 22;
+ time_only[MINUTE_KEY] = 10;
+ time_only[SECOND_KEY] = 30;
+
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(datetime) == 1391983830, "Time get_unix_time_from_datetime_dict: The datetime dictionary for GODOT IS OPEN SOURCE is converted to a timestamp as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(date_only) == 1391904000, "Time get_unix_time_from_datetime_dict: The date dictionary for GODOT IS OPEN SOURCE is converted to a timestamp as expected.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(time_only) == 79830, "Time get_unix_time_from_datetime_dict: The time dictionary for GODOT IS OPEN SOURCE is converted to a timestamp as expected.");
+
+ CHECK_MESSAGE(time->get_datetime_dict_from_unix_time(1391983830).hash() == datetime.hash(), "Time get_datetime_dict_from_unix_time: The datetime timestamp for GODOT IS OPEN SOURCE is converted to a dictionary as expected.");
+ CHECK_MESSAGE(time->get_date_dict_from_unix_time(1391904000).hash() == date_only.hash(), "Time get_date_dict_from_unix_time: The date timestamp for GODOT IS OPEN SOURCE is converted to a dictionary as expected.");
+ CHECK_MESSAGE(time->get_time_dict_from_unix_time(79830).hash() == time_only.hash(), "Time get_time_dict_from_unix_time: The time timestamp for GODOT IS OPEN SOURCE is converted to a dictionary as expected.");
+
+ CHECK_MESSAGE((Time::Weekday)(int)time->get_datetime_dict_from_unix_time(0)[WEEKDAY_KEY] == Time::Weekday::WEEKDAY_THURSDAY, "Time get_datetime_dict_from_unix_time: The weekday for the Unix epoch is a Thursday as expected.");
+ CHECK_MESSAGE((Time::Weekday)(int)time->get_datetime_dict_from_unix_time(1391983830)[WEEKDAY_KEY] == Time::Weekday::WEEKDAY_SUNDAY, "Time get_datetime_dict_from_unix_time: The weekday for GODOT IS OPEN SOURCE is a Sunday as expected.");
+
+ CHECK_MESSAGE(time->get_datetime_dict_from_string("2014-02-09T22:10:30").hash() == datetime.hash(), "Time get_datetime_dict_from_string: The dictionary from string for GODOT IS OPEN SOURCE works as expected.");
+ CHECK_MESSAGE(!time->get_datetime_dict_from_string("2014-02-09T22:10:30", false).has(WEEKDAY_KEY), "Time get_datetime_dict_from_string: The dictionary from string for GODOT IS OPEN SOURCE without weekday doesn't contain the weekday key as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_dict(datetime) == "2014-02-09T22:10:30", "Time get_datetime_string_from_dict: The string from dictionary for GODOT IS OPEN SOURCE works as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_dict(time->get_datetime_dict_from_string("2014-02-09T22:10:30")) == "2014-02-09T22:10:30", "Time get_datetime_string_from_dict: The round-trip string to dict to string GODOT IS OPEN SOURCE works as expected.");
+ CHECK_MESSAGE(time->get_datetime_string_from_dict(time->get_datetime_dict_from_string("2014-02-09 22:10:30"), true) == "2014-02-09 22:10:30", "Time get_datetime_string_from_dict: The round-trip string to dict to string GODOT IS OPEN SOURCE with spaces works as expected.");
+}
+
+TEST_CASE("[Time] System time methods") {
+ const Time *time = Time::get_singleton();
+
+ const uint64_t ticks_msec = time->get_ticks_msec();
+ const uint64_t ticks_usec = time->get_ticks_usec();
+
+ CHECK_MESSAGE(time->get_unix_time_from_system() > 1000000000, "Time get_unix_time_from_system: The timestamp from system time doesn't fail and is very positive.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(time->get_datetime_dict_from_system()) > 1000000000, "Time get_datetime_string_from_system: The timestamp from system time doesn't fail and is very positive.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(time->get_date_dict_from_system()) > 1000000000, "Time get_datetime_string_from_system: The date from system time doesn't fail and is very positive.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(time->get_time_dict_from_system()) < 86400, "Time get_datetime_string_from_system: The time from system time doesn't fail and is within the acceptable range.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string(time->get_datetime_string_from_system()) > 1000000000, "Time get_datetime_string_from_system: The timestamp from system time doesn't fail and is very positive.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string(time->get_date_string_from_system()) > 1000000000, "Time get_datetime_string_from_system: The date from system time doesn't fail and is very positive.");
+ CHECK_MESSAGE(time->get_unix_time_from_datetime_string(time->get_time_string_from_system()) < 86400, "Time get_datetime_string_from_system: The time from system time doesn't fail and is within the acceptable range.");
+
+ CHECK_MESSAGE(time->get_ticks_msec() >= ticks_msec, "Time get_ticks_msec: The value has not decreased.");
+ CHECK_MESSAGE(time->get_ticks_usec() > ticks_usec, "Time get_ticks_usec: The value has increased.");
+}
+
+} // namespace TestTime
+
+#endif // TEST_TIME_H
diff --git a/tests/core/variant/test_array.h b/tests/core/variant/test_array.h
new file mode 100644
index 0000000000..205e34daea
--- /dev/null
+++ b/tests/core/variant/test_array.h
@@ -0,0 +1,496 @@
+/*************************************************************************/
+/* test_array.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_ARRAY_H
+#define TEST_ARRAY_H
+
+#include "core/variant/array.h"
+#include "tests/test_macros.h"
+#include "tests/test_tools.h"
+
+namespace TestArray {
+
+static inline Array build_array() {
+ return Array();
+}
+template <typename... Targs>
+static inline Array build_array(Variant item, Targs... Fargs) {
+ Array a = build_array(Fargs...);
+ a.push_front(item);
+ return a;
+}
+static inline Dictionary build_dictionary() {
+ return Dictionary();
+}
+template <typename... Targs>
+static inline Dictionary build_dictionary(Variant key, Variant item, Targs... Fargs) {
+ Dictionary d = build_dictionary(Fargs...);
+ d[key] = item;
+ return d;
+}
+
+TEST_CASE("[Array] size(), clear(), and is_empty()") {
+ Array arr;
+ CHECK(arr.size() == 0);
+ CHECK(arr.is_empty());
+ arr.push_back(1);
+ CHECK(arr.size() == 1);
+ arr.clear();
+ CHECK(arr.is_empty());
+ CHECK(arr.size() == 0);
+}
+
+TEST_CASE("[Array] Assignment and comparison operators") {
+ Array arr1;
+ Array arr2;
+ arr1.push_back(1);
+ CHECK(arr1 != arr2);
+ CHECK(arr1 > arr2);
+ CHECK(arr1 >= arr2);
+ arr2.push_back(2);
+ CHECK(arr1 != arr2);
+ CHECK(arr1 < arr2);
+ CHECK(arr1 <= arr2);
+ CHECK(arr2 > arr1);
+ CHECK(arr2 >= arr1);
+ Array arr3 = arr2;
+ CHECK(arr3 == arr2);
+}
+
+TEST_CASE("[Array] append_array()") {
+ Array arr1;
+ Array arr2;
+ arr1.push_back(1);
+ arr1.append_array(arr2);
+ CHECK(arr1.size() == 1);
+ arr2.push_back(2);
+ arr1.append_array(arr2);
+ CHECK(arr1.size() == 2);
+ CHECK(int(arr1[0]) == 1);
+ CHECK(int(arr1[1]) == 2);
+}
+
+TEST_CASE("[Array] resize(), insert(), and erase()") {
+ Array arr;
+ arr.resize(2);
+ CHECK(arr.size() == 2);
+ arr.insert(0, 1);
+ CHECK(int(arr[0]) == 1);
+ arr.insert(0, 2);
+ CHECK(int(arr[0]) == 2);
+ arr.erase(2);
+ CHECK(int(arr[0]) == 1);
+}
+
+TEST_CASE("[Array] front() and back()") {
+ Array arr;
+ arr.push_back(1);
+ CHECK(int(arr.front()) == 1);
+ CHECK(int(arr.back()) == 1);
+ arr.push_back(3);
+ CHECK(int(arr.front()) == 1);
+ CHECK(int(arr.back()) == 3);
+}
+
+TEST_CASE("[Array] has() and count()") {
+ Array arr;
+ arr.push_back(1);
+ arr.push_back(1);
+ CHECK(arr.has(1));
+ CHECK(!arr.has(2));
+ CHECK(arr.count(1) == 2);
+ CHECK(arr.count(2) == 0);
+}
+
+TEST_CASE("[Array] remove_at()") {
+ Array arr;
+ arr.push_back(1);
+ arr.push_back(2);
+ arr.remove_at(0);
+ CHECK(arr.size() == 1);
+ CHECK(int(arr[0]) == 2);
+ arr.remove_at(0);
+ CHECK(arr.size() == 0);
+
+ // The array is now empty; try to use `remove_at()` again.
+ // Normally, this prints an error message so we silence it.
+ ERR_PRINT_OFF;
+ arr.remove_at(0);
+ ERR_PRINT_ON;
+
+ CHECK(arr.size() == 0);
+}
+
+TEST_CASE("[Array] get()") {
+ Array arr;
+ arr.push_back(1);
+ CHECK(int(arr.get(0)) == 1);
+}
+
+TEST_CASE("[Array] sort()") {
+ Array arr;
+
+ arr.push_back(3);
+ arr.push_back(4);
+ arr.push_back(2);
+ arr.push_back(1);
+ arr.sort();
+ int val = 1;
+ for (int i = 0; i < arr.size(); i++) {
+ CHECK(int(arr[i]) == val);
+ val++;
+ }
+}
+
+TEST_CASE("[Array] push_front(), pop_front(), pop_back()") {
+ Array arr;
+ arr.push_front(1);
+ arr.push_front(2);
+ CHECK(int(arr[0]) == 2);
+ arr.pop_front();
+ CHECK(int(arr[0]) == 1);
+ CHECK(arr.size() == 1);
+ arr.push_front(2);
+ arr.push_front(3);
+ arr.pop_back();
+ CHECK(int(arr[1]) == 2);
+ CHECK(arr.size() == 2);
+}
+
+TEST_CASE("[Array] pop_at()") {
+ ErrorDetector ed;
+
+ Array arr;
+ arr.push_back(2);
+ arr.push_back(4);
+ arr.push_back(6);
+ arr.push_back(8);
+ arr.push_back(10);
+
+ REQUIRE(int(arr.pop_at(2)) == 6);
+ REQUIRE(arr.size() == 4);
+ CHECK(int(arr[0]) == 2);
+ CHECK(int(arr[1]) == 4);
+ CHECK(int(arr[2]) == 8);
+ CHECK(int(arr[3]) == 10);
+
+ REQUIRE(int(arr.pop_at(2)) == 8);
+ REQUIRE(arr.size() == 3);
+ CHECK(int(arr[0]) == 2);
+ CHECK(int(arr[1]) == 4);
+ CHECK(int(arr[2]) == 10);
+
+ // Negative index.
+ REQUIRE(int(arr.pop_at(-1)) == 10);
+ REQUIRE(arr.size() == 2);
+ CHECK(int(arr[0]) == 2);
+ CHECK(int(arr[1]) == 4);
+
+ // Invalid pop.
+ ed.clear();
+ ERR_PRINT_OFF;
+ const Variant ret = arr.pop_at(-15);
+ ERR_PRINT_ON;
+ REQUIRE(ret.is_null());
+ CHECK(ed.has_error);
+
+ REQUIRE(int(arr.pop_at(0)) == 2);
+ REQUIRE(arr.size() == 1);
+ CHECK(int(arr[0]) == 4);
+
+ REQUIRE(int(arr.pop_at(0)) == 4);
+ REQUIRE(arr.is_empty());
+
+ // Pop from empty array.
+ ed.clear();
+ REQUIRE(arr.pop_at(24).is_null());
+ CHECK_FALSE(ed.has_error);
+}
+
+TEST_CASE("[Array] max() and min()") {
+ Array arr;
+ arr.push_back(3);
+ arr.push_front(4);
+ arr.push_back(5);
+ arr.push_back(2);
+ int max = int(arr.max());
+ int min = int(arr.min());
+ CHECK(max == 5);
+ CHECK(min == 2);
+}
+
+TEST_CASE("[Array] slice()") {
+ Array array;
+ array.push_back(0);
+ array.push_back(1);
+ array.push_back(2);
+ array.push_back(3);
+ array.push_back(4);
+
+ Array slice1 = array.slice(1, 3);
+ CHECK(slice1.size() == 2);
+ CHECK(slice1[0] == Variant(1));
+ CHECK(slice1[1] == Variant(2));
+
+ Array slice2 = array.slice(1, -1);
+ CHECK(slice2.size() == 4);
+ CHECK(slice2[0] == Variant(1));
+ CHECK(slice2[1] == Variant(2));
+ CHECK(slice2[2] == Variant(3));
+ CHECK(slice2[3] == Variant(4));
+
+ Array slice3 = array.slice(3, -1);
+ CHECK(slice3.size() == 2);
+ CHECK(slice3[0] == Variant(3));
+ CHECK(slice3[1] == Variant(4));
+
+ Array slice4 = array.slice(2, -2);
+ CHECK(slice4.size() == 2);
+ CHECK(slice4[0] == Variant(2));
+ CHECK(slice4[1] == Variant(3));
+}
+
+TEST_CASE("[Array] Duplicate array") {
+ // a = [1, [2, 2], {3: 3}]
+ Array a = build_array(1, build_array(2, 2), build_dictionary(3, 3));
+
+ // Deep copy
+ Array deep_a = a.duplicate(true);
+ CHECK_MESSAGE(deep_a.id() != a.id(), "Should create a new array");
+ CHECK_MESSAGE(Array(deep_a[1]).id() != Array(a[1]).id(), "Should clone nested array");
+ CHECK_MESSAGE(Dictionary(deep_a[2]).id() != Dictionary(a[2]).id(), "Should clone nested dictionary");
+ CHECK_EQ(deep_a, a);
+ deep_a.push_back(1);
+ CHECK_NE(deep_a, a);
+ deep_a.pop_back();
+ Array(deep_a[1]).push_back(1);
+ CHECK_NE(deep_a, a);
+ Array(deep_a[1]).pop_back();
+ CHECK_EQ(deep_a, a);
+
+ // Shallow copy
+ Array shallow_a = a.duplicate(false);
+ CHECK_MESSAGE(shallow_a.id() != a.id(), "Should create a new array");
+ CHECK_MESSAGE(Array(shallow_a[1]).id() == Array(a[1]).id(), "Should keep nested array");
+ CHECK_MESSAGE(Dictionary(shallow_a[2]).id() == Dictionary(a[2]).id(), "Should keep nested dictionary");
+ CHECK_EQ(shallow_a, a);
+ Array(shallow_a).push_back(1);
+ CHECK_NE(shallow_a, a);
+}
+
+TEST_CASE("[Array] Duplicate recursive array") {
+ // Self recursive
+ Array a;
+ a.push_back(a);
+
+ Array a_shallow = a.duplicate(false);
+ CHECK_EQ(a, a_shallow);
+
+ // Deep copy of recursive array endup with recursion limit and return
+ // an invalid result (multiple nested arrays), the point is we should
+ // not end up with a segfault and an error log should be printed
+ ERR_PRINT_OFF;
+ a.duplicate(true);
+ ERR_PRINT_ON;
+
+ // Nested recursive
+ Array a1;
+ Array a2;
+ a2.push_back(a1);
+ a1.push_back(a2);
+
+ Array a1_shallow = a1.duplicate(false);
+ CHECK_EQ(a1, a1_shallow);
+
+ // Same deep copy issue as above
+ ERR_PRINT_OFF;
+ a1.duplicate(true);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Array teardown will leak memory
+ a.clear();
+ a1.clear();
+ a2.clear();
+}
+
+TEST_CASE("[Array] Hash array") {
+ // a = [1, [2, 2], {3: 3}]
+ Array a = build_array(1, build_array(2, 2), build_dictionary(3, 3));
+ uint32_t original_hash = a.hash();
+
+ a.push_back(1);
+ CHECK_NE(a.hash(), original_hash);
+
+ a.pop_back();
+ CHECK_EQ(a.hash(), original_hash);
+
+ Array(a[1]).push_back(1);
+ CHECK_NE(a.hash(), original_hash);
+ Array(a[1]).pop_back();
+ CHECK_EQ(a.hash(), original_hash);
+
+ (Dictionary(a[2]))[1] = 1;
+ CHECK_NE(a.hash(), original_hash);
+ Dictionary(a[2]).erase(1);
+ CHECK_EQ(a.hash(), original_hash);
+
+ Array a2 = a.duplicate(true);
+ CHECK_EQ(a2.hash(), a.hash());
+}
+
+TEST_CASE("[Array] Hash recursive array") {
+ Array a1;
+ a1.push_back(a1);
+
+ Array a2;
+ a2.push_back(a2);
+
+ // Hash should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(a1.hash(), a2.hash());
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Array teardown will leak memory
+ a1.clear();
+ a2.clear();
+}
+
+TEST_CASE("[Array] Empty comparison") {
+ Array a1;
+ Array a2;
+
+ // test both operator== and operator!=
+ CHECK_EQ(a1, a2);
+ CHECK_FALSE(a1 != a2);
+}
+
+TEST_CASE("[Array] Flat comparison") {
+ Array a1 = build_array(1);
+ Array a2 = build_array(1);
+ Array other_a = build_array(2);
+
+ // test both operator== and operator!=
+ CHECK_EQ(a1, a1); // compare self
+ CHECK_FALSE(a1 != a1);
+ CHECK_EQ(a1, a2); // different equivalent arrays
+ CHECK_FALSE(a1 != a2);
+ CHECK_NE(a1, other_a); // different arrays with different content
+ CHECK_FALSE(a1 == other_a);
+}
+
+TEST_CASE("[Array] Nested array comparison") {
+ // a1 = [[[1], 2], 3]
+ Array a1 = build_array(build_array(build_array(1), 2), 3);
+
+ Array a2 = a1.duplicate(true);
+
+ // other_a = [[[1, 0], 2], 3]
+ Array other_a = build_array(build_array(build_array(1, 0), 2), 3);
+
+ // test both operator== and operator!=
+ CHECK_EQ(a1, a1); // compare self
+ CHECK_FALSE(a1 != a1);
+ CHECK_EQ(a1, a2); // different equivalent arrays
+ CHECK_FALSE(a1 != a2);
+ CHECK_NE(a1, other_a); // different arrays with different content
+ CHECK_FALSE(a1 == other_a);
+}
+
+TEST_CASE("[Array] Nested dictionary comparison") {
+ // a1 = [{1: 2}, 3]
+ Array a1 = build_array(build_dictionary(1, 2), 3);
+
+ Array a2 = a1.duplicate(true);
+
+ // other_a = [{1: 0}, 3]
+ Array other_a = build_array(build_dictionary(1, 0), 3);
+
+ // test both operator== and operator!=
+ CHECK_EQ(a1, a1); // compare self
+ CHECK_FALSE(a1 != a1);
+ CHECK_EQ(a1, a2); // different equivalent arrays
+ CHECK_FALSE(a1 != a2);
+ CHECK_NE(a1, other_a); // different arrays with different content
+ CHECK_FALSE(a1 == other_a);
+}
+
+TEST_CASE("[Array] Recursive comparison") {
+ Array a1;
+ a1.push_back(a1);
+
+ Array a2;
+ a2.push_back(a2);
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(a1, a2);
+ CHECK_FALSE(a1 != a2);
+ ERR_PRINT_ON;
+
+ a1.push_back(1);
+ a2.push_back(1);
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(a1, a2);
+ CHECK_FALSE(a1 != a2);
+ ERR_PRINT_ON;
+
+ a1.push_back(1);
+ a2.push_back(2);
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_NE(a1, a2);
+ CHECK_FALSE(a1 == a2);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Array tearndown will leak memory
+ a1.clear();
+ a2.clear();
+}
+
+TEST_CASE("[Array] Recursive self comparison") {
+ Array a1;
+ Array a2;
+ a2.push_back(a1);
+ a1.push_back(a2);
+
+ CHECK_EQ(a1, a1);
+ CHECK_FALSE(a1 != a1);
+
+ // Break the recursivity otherwise Array tearndown will leak memory
+ a1.clear();
+ a2.clear();
+}
+
+} // namespace TestArray
+
+#endif // TEST_ARRAY_H
diff --git a/tests/core/variant/test_dictionary.h b/tests/core/variant/test_dictionary.h
new file mode 100644
index 0000000000..79d53fa64e
--- /dev/null
+++ b/tests/core/variant/test_dictionary.h
@@ -0,0 +1,505 @@
+/*************************************************************************/
+/* test_dictionary.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_DICTIONARY_H
+#define TEST_DICTIONARY_H
+
+#include "core/variant/dictionary.h"
+#include "tests/test_macros.h"
+
+namespace TestDictionary {
+
+static inline Array build_array() {
+ return Array();
+}
+template <typename... Targs>
+static inline Array build_array(Variant item, Targs... Fargs) {
+ Array a = build_array(Fargs...);
+ a.push_front(item);
+ return a;
+}
+static inline Dictionary build_dictionary() {
+ return Dictionary();
+}
+template <typename... Targs>
+static inline Dictionary build_dictionary(Variant key, Variant item, Targs... Fargs) {
+ Dictionary d = build_dictionary(Fargs...);
+ d[key] = item;
+ return d;
+}
+
+TEST_CASE("[Dictionary] Assignment using bracket notation ([])") {
+ Dictionary map;
+ map["Hello"] = 0;
+ CHECK(int(map["Hello"]) == 0);
+ map["Hello"] = 3;
+ CHECK(int(map["Hello"]) == 3);
+ map["World!"] = 4;
+ CHECK(int(map["World!"]) == 4);
+
+ // Test non-string keys, since keys can be of any Variant type.
+ map[12345] = -5;
+ CHECK(int(map[12345]) == -5);
+ map[false] = 128;
+ CHECK(int(map[false]) == 128);
+ map[Vector2(10, 20)] = 30;
+ CHECK(int(map[Vector2(10, 20)]) == 30);
+ map[0] = 400;
+ CHECK(int(map[0]) == 400);
+ // Check that assigning 0 doesn't overwrite the value for `false`.
+ CHECK(int(map[false]) == 128);
+}
+
+TEST_CASE("[Dictionary] get_key_lists()") {
+ Dictionary map;
+ List<Variant> keys;
+ List<Variant> *ptr = &keys;
+ map.get_key_list(ptr);
+ CHECK(keys.is_empty());
+ map[1] = 3;
+ map.get_key_list(ptr);
+ CHECK(keys.size() == 1);
+ CHECK(int(keys[0]) == 1);
+ map[2] = 4;
+ map.get_key_list(ptr);
+ CHECK(keys.size() == 3);
+}
+
+TEST_CASE("[Dictionary] get_key_at_index()") {
+ Dictionary map;
+ map[4] = 3;
+ Variant val = map.get_key_at_index(0);
+ CHECK(int(val) == 4);
+ map[3] = 1;
+ val = map.get_key_at_index(0);
+ CHECK(int(val) == 4);
+ val = map.get_key_at_index(1);
+ CHECK(int(val) == 3);
+}
+
+TEST_CASE("[Dictionary] getptr()") {
+ Dictionary map;
+ map[1] = 3;
+ Variant *key = map.getptr(1);
+ CHECK(int(*key) == 3);
+ key = map.getptr(2);
+ CHECK(key == nullptr);
+}
+
+TEST_CASE("[Dictionary] get_valid()") {
+ Dictionary map;
+ map[1] = 3;
+ Variant val = map.get_valid(1);
+ CHECK(int(val) == 3);
+}
+TEST_CASE("[Dictionary] get()") {
+ Dictionary map;
+ map[1] = 3;
+ Variant val = map.get(1, -1);
+ CHECK(int(val) == 3);
+}
+
+TEST_CASE("[Dictionary] size(), empty() and clear()") {
+ Dictionary map;
+ CHECK(map.size() == 0);
+ CHECK(map.is_empty());
+ map[1] = 3;
+ CHECK(map.size() == 1);
+ CHECK(!map.is_empty());
+ map.clear();
+ CHECK(map.size() == 0);
+ CHECK(map.is_empty());
+}
+
+TEST_CASE("[Dictionary] has() and has_all()") {
+ Dictionary map;
+ CHECK(map.has(1) == false);
+ map[1] = 3;
+ CHECK(map.has(1));
+ Array keys;
+ keys.push_back(1);
+ CHECK(map.has_all(keys));
+ keys.push_back(2);
+ CHECK(map.has_all(keys) == false);
+}
+
+TEST_CASE("[Dictionary] keys() and values()") {
+ Dictionary map;
+ Array keys = map.keys();
+ Array values = map.values();
+ CHECK(keys.is_empty());
+ CHECK(values.is_empty());
+ map[1] = 3;
+ keys = map.keys();
+ values = map.values();
+ CHECK(int(keys[0]) == 1);
+ CHECK(int(values[0]) == 3);
+}
+
+TEST_CASE("[Dictionary] Duplicate dictionary") {
+ // d = {1: {1: 1}, {2: 2}: [2], [3]: 3}
+ Dictionary k2 = build_dictionary(2, 2);
+ Array k3 = build_array(3);
+ Dictionary d = build_dictionary(1, build_dictionary(1, 1), k2, build_array(2), k3, 3);
+
+ // Deep copy
+ Dictionary deep_d = d.duplicate(true);
+ CHECK_MESSAGE(deep_d.id() != d.id(), "Should create a new dictionary");
+ CHECK_MESSAGE(Dictionary(deep_d[1]).id() != Dictionary(d[1]).id(), "Should clone nested dictionary");
+ CHECK_MESSAGE(Array(deep_d[k2]).id() != Array(d[k2]).id(), "Should clone nested array");
+ CHECK_EQ(deep_d, d);
+ deep_d[0] = 0;
+ CHECK_NE(deep_d, d);
+ deep_d.erase(0);
+ Dictionary(deep_d[1]).operator[](0) = 0;
+ CHECK_NE(deep_d, d);
+ Dictionary(deep_d[1]).erase(0);
+ CHECK_EQ(deep_d, d);
+ // Keys should also be copied
+ k2[0] = 0;
+ CHECK_NE(deep_d, d);
+ k2.erase(0);
+ CHECK_EQ(deep_d, d);
+ k3.push_back(0);
+ CHECK_NE(deep_d, d);
+ k3.pop_back();
+ CHECK_EQ(deep_d, d);
+
+ // Shallow copy
+ Dictionary shallow_d = d.duplicate(false);
+ CHECK_MESSAGE(shallow_d.id() != d.id(), "Should create a new array");
+ CHECK_MESSAGE(Dictionary(shallow_d[1]).id() == Dictionary(d[1]).id(), "Should keep nested dictionary");
+ CHECK_MESSAGE(Array(shallow_d[2]).id() == Array(d[2]).id(), "Should keep nested array");
+ CHECK_EQ(shallow_d, d);
+ shallow_d[0] = 0;
+ CHECK_NE(shallow_d, d);
+ shallow_d.erase(0);
+#if 0 // TODO: recursion in dict key currently is buggy
+ // Keys should also be shallowed
+ k2[0] = 0;
+ CHECK_EQ(shallow_d, d);
+ k2.erase(0);
+ k3.push_back(0);
+ CHECK_EQ(shallow_d, d);
+#endif
+}
+
+TEST_CASE("[Dictionary] Duplicate recursive dictionary") {
+ // Self recursive
+ Dictionary d;
+ d[1] = d;
+
+ Dictionary d_shallow = d.duplicate(false);
+ CHECK_EQ(d, d_shallow);
+
+ // Deep copy of recursive dictionary endup with recursion limit and return
+ // an invalid result (multiple nested dictionaries), the point is we should
+ // not end up with a segfault and an error log should be printed
+ ERR_PRINT_OFF;
+ d.duplicate(true);
+ ERR_PRINT_ON;
+
+ // Nested recursive
+ Dictionary d1;
+ Dictionary d2;
+ d1[2] = d2;
+ d2[1] = d1;
+
+ Dictionary d1_shallow = d1.duplicate(false);
+ CHECK_EQ(d1, d1_shallow);
+
+ // Same deep copy issue as above
+ ERR_PRINT_OFF;
+ d1.duplicate(true);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d.clear();
+ d1.clear();
+ d2.clear();
+}
+
+#if 0 // TODO: duplicate recursion in dict key is currently buggy
+TEST_CASE("[Dictionary] Duplicate recursive dictionary on keys") {
+ // Self recursive
+ Dictionary d;
+ d[d] = d;
+
+ Dictionary d_shallow = d.duplicate(false);
+ CHECK_EQ(d, d_shallow);
+
+ // Deep copy of recursive dictionary endup with recursion limit and return
+ // an invalid result (multiple nested dictionaries), the point is we should
+ // not end up with a segfault and an error log should be printed
+ ERR_PRINT_OFF;
+ d.duplicate(true);
+ ERR_PRINT_ON;
+
+ // Nested recursive
+ Dictionary d1;
+ Dictionary d2;
+ d1[d2] = d2;
+ d2[d1] = d1;
+
+ Dictionary d1_shallow = d1.duplicate(false);
+ CHECK_EQ(d1, d1_shallow);
+
+ // Same deep copy issue as above
+ ERR_PRINT_OFF;
+ d1.duplicate(true);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d.clear();
+ d1.clear();
+ d2.clear();
+}
+#endif
+
+TEST_CASE("[Dictionary] Hash dictionary") {
+ // d = {1: {1: 1}, {2: 2}: [2], [3]: 3}
+ Dictionary k2 = build_dictionary(2, 2);
+ Array k3 = build_array(3);
+ Dictionary d = build_dictionary(1, build_dictionary(1, 1), k2, build_array(2), k3, 3);
+ uint32_t original_hash = d.hash();
+
+ // Modify dict change the hash
+ d[0] = 0;
+ CHECK_NE(d.hash(), original_hash);
+ d.erase(0);
+ CHECK_EQ(d.hash(), original_hash);
+
+ // Modify nested item change the hash
+ Dictionary(d[1]).operator[](0) = 0;
+ CHECK_NE(d.hash(), original_hash);
+ Dictionary(d[1]).erase(0);
+ Array(d[k2]).push_back(0);
+ CHECK_NE(d.hash(), original_hash);
+ Array(d[k2]).pop_back();
+
+ // Modify a key change the hash
+ k2[0] = 0;
+ CHECK_NE(d.hash(), original_hash);
+ k2.erase(0);
+ CHECK_EQ(d.hash(), original_hash);
+ k3.push_back(0);
+ CHECK_NE(d.hash(), original_hash);
+ k3.pop_back();
+ CHECK_EQ(d.hash(), original_hash);
+
+ // Duplication doesn't change the hash
+ Dictionary d2 = d.duplicate(true);
+ CHECK_EQ(d2.hash(), original_hash);
+}
+
+TEST_CASE("[Dictionary] Hash recursive dictionary") {
+ Dictionary d;
+ d[1] = d;
+
+ // Hash should reach recursion limit, we just make sure this doesn't blow up
+ ERR_PRINT_OFF;
+ d.hash();
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d.clear();
+}
+
+#if 0 // TODO: recursion in dict key is currently buggy
+TEST_CASE("[Dictionary] Hash recursive dictionary on keys") {
+ Dictionary d;
+ d[d] = 1;
+
+ // Hash should reach recursion limit, we just make sure this doesn't blow up
+ ERR_PRINT_OFF;
+ d.hash();
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d.clear();
+}
+#endif
+
+TEST_CASE("[Dictionary] Empty comparison") {
+ Dictionary d1;
+ Dictionary d2;
+
+ // test both operator== and operator!=
+ CHECK_EQ(d1, d2);
+ CHECK_FALSE(d1 != d2);
+}
+
+TEST_CASE("[Dictionary] Flat comparison") {
+ Dictionary d1 = build_dictionary(1, 1);
+ Dictionary d2 = build_dictionary(1, 1);
+ Dictionary other_d = build_dictionary(2, 1);
+
+ // test both operator== and operator!=
+ CHECK_EQ(d1, d1); // compare self
+ CHECK_FALSE(d1 != d1);
+ CHECK_EQ(d1, d2); // different equivalent arrays
+ CHECK_FALSE(d1 != d2);
+ CHECK_NE(d1, other_d); // different arrays with different content
+ CHECK_FALSE(d1 == other_d);
+}
+
+TEST_CASE("[Dictionary] Nested dictionary comparison") {
+ // d1 = {1: {2: {3: 4}}}
+ Dictionary d1 = build_dictionary(1, build_dictionary(2, build_dictionary(3, 4)));
+
+ Dictionary d2 = d1.duplicate(true);
+
+ // other_d = {1: {2: {3: 0}}}
+ Dictionary other_d = build_dictionary(1, build_dictionary(2, build_dictionary(3, 0)));
+
+ // test both operator== and operator!=
+ CHECK_EQ(d1, d1); // compare self
+ CHECK_FALSE(d1 != d1);
+ CHECK_EQ(d1, d2); // different equivalent arrays
+ CHECK_FALSE(d1 != d2);
+ CHECK_NE(d1, other_d); // different arrays with different content
+ CHECK_FALSE(d1 == other_d);
+}
+
+TEST_CASE("[Dictionary] Nested array comparison") {
+ // d1 = {1: [2, 3]}
+ Dictionary d1 = build_dictionary(1, build_array(2, 3));
+
+ Dictionary d2 = d1.duplicate(true);
+
+ // other_d = {1: [2, 0]}
+ Dictionary other_d = build_dictionary(1, build_array(2, 0));
+
+ // test both operator== and operator!=
+ CHECK_EQ(d1, d1); // compare self
+ CHECK_FALSE(d1 != d1);
+ CHECK_EQ(d1, d2); // different equivalent arrays
+ CHECK_FALSE(d1 != d2);
+ CHECK_NE(d1, other_d); // different arrays with different content
+ CHECK_FALSE(d1 == other_d);
+}
+
+TEST_CASE("[Dictionary] Recursive comparison") {
+ Dictionary d1;
+ d1[1] = d1;
+
+ Dictionary d2;
+ d2[1] = d2;
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(d1, d2);
+ CHECK_FALSE(d1 != d2);
+ ERR_PRINT_ON;
+
+ d1[2] = 2;
+ d2[2] = 2;
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(d1, d2);
+ CHECK_FALSE(d1 != d2);
+ ERR_PRINT_ON;
+
+ d1[3] = 3;
+ d2[3] = 0;
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_NE(d1, d2);
+ CHECK_FALSE(d1 == d2);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d1.clear();
+ d2.clear();
+}
+
+#if 0 // TODO: recursion in dict key is currently buggy
+TEST_CASE("[Dictionary] Recursive comparison on keys") {
+ Dictionary d1;
+ // Hash computation should reach recursion limit
+ ERR_PRINT_OFF;
+ d1[d1] = 1;
+ ERR_PRINT_ON;
+
+ Dictionary d2;
+ // Hash computation should reach recursion limit
+ ERR_PRINT_OFF;
+ d2[d2] = 1;
+ ERR_PRINT_ON;
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(d1, d2);
+ CHECK_FALSE(d1 != d2);
+ ERR_PRINT_ON;
+
+ d1[2] = 2;
+ d2[2] = 2;
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_EQ(d1, d2);
+ CHECK_FALSE(d1 != d2);
+ ERR_PRINT_ON;
+
+ d1[3] = 3;
+ d2[3] = 0;
+
+ // Comparison should reach recursion limit
+ ERR_PRINT_OFF;
+ CHECK_NE(d1, d2);
+ CHECK_FALSE(d1 == d2);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d1.clear();
+ d2.clear();
+}
+#endif
+
+TEST_CASE("[Dictionary] Recursive self comparison") {
+ Dictionary d1;
+ Dictionary d2;
+ d1[1] = d2;
+ d2[1] = d1;
+
+ CHECK_EQ(d1, d1);
+ CHECK_FALSE(d1 != d1);
+
+ // Break the recursivity otherwise Dictionary teardown will leak memory
+ d1.clear();
+ d2.clear();
+}
+
+} // namespace TestDictionary
+
+#endif // TEST_DICTIONARY_H
diff --git a/tests/core/variant/test_variant.h b/tests/core/variant/test_variant.h
new file mode 100644
index 0000000000..916686d7c1
--- /dev/null
+++ b/tests/core/variant/test_variant.h
@@ -0,0 +1,916 @@
+/*************************************************************************/
+/* test_variant.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_VARIANT_H
+#define TEST_VARIANT_H
+
+#include "core/variant/variant.h"
+#include "core/variant/variant_parser.h"
+
+#include "tests/test_macros.h"
+
+namespace TestVariant {
+
+static inline Array build_array() {
+ return Array();
+}
+template <typename... Targs>
+static inline Array build_array(Variant item, Targs... Fargs) {
+ Array a = build_array(Fargs...);
+ a.push_front(item);
+ return a;
+}
+static inline Dictionary build_dictionary() {
+ return Dictionary();
+}
+template <typename... Targs>
+static inline Dictionary build_dictionary(Variant key, Variant item, Targs... Fargs) {
+ Dictionary d = build_dictionary(Fargs...);
+ d[key] = item;
+ return d;
+}
+
+TEST_CASE("[Variant] Writer and parser integer") {
+ int64_t a32 = 2147483648; // 2^31, so out of bounds for 32-bit signed int [-2^31, +2^31-1].
+ String a32_str;
+ VariantWriter::write_to_string(a32, a32_str);
+
+ CHECK_MESSAGE(a32_str != "-2147483648", "Should not wrap around");
+
+ int64_t b64 = 9223372036854775807; // 2^63-1, upper bound for signed 64-bit int.
+ String b64_str;
+ VariantWriter::write_to_string(b64, b64_str);
+
+ CHECK_MESSAGE(b64_str == "9223372036854775807", "Should not wrap around.");
+
+ VariantParser::StreamString ss;
+ String errs;
+ int line;
+ Variant b64_parsed;
+ int64_t b64_int_parsed;
+
+ ss.s = b64_str;
+ VariantParser::parse(&ss, b64_parsed, errs, line);
+ b64_int_parsed = b64_parsed;
+
+ CHECK_MESSAGE(b64_int_parsed == 9223372036854775807, "Should parse back.");
+
+ ss.s = "9223372036854775808"; // Overflowed by one.
+ VariantParser::parse(&ss, b64_parsed, errs, line);
+ b64_int_parsed = b64_parsed;
+
+ CHECK_MESSAGE(b64_int_parsed == 9223372036854775807, "The result should be clamped to max value.");
+
+ ss.s = "1e100"; // Googol! Scientific notation.
+ VariantParser::parse(&ss, b64_parsed, errs, line);
+ b64_int_parsed = b64_parsed;
+
+ CHECK_MESSAGE(b64_int_parsed == 9223372036854775807, "The result should be clamped to max value.");
+}
+
+TEST_CASE("[Variant] Writer and parser Variant::FLOAT") {
+ // Variant::FLOAT is always 64-bit (C++ double).
+ // This is the maximum non-infinity double-precision float.
+ double a64 = 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0;
+ String a64_str;
+ VariantWriter::write_to_string(a64, a64_str);
+
+ CHECK_MESSAGE(a64_str == "1.79769e+308", "Writes in scientific notation.");
+ CHECK_MESSAGE(a64_str != "inf", "Should not overflow.");
+ CHECK_MESSAGE(a64_str != "nan", "The result should be defined.");
+
+ String errs;
+ int line;
+ Variant variant_parsed;
+ double float_parsed;
+
+ VariantParser::StreamString bss;
+ bss.s = a64_str;
+ VariantParser::parse(&bss, variant_parsed, errs, line);
+ float_parsed = variant_parsed;
+ // Loses precision, but that's alright.
+ CHECK_MESSAGE(float_parsed == 1.79769e+308, "Should parse back.");
+
+ // Approximation of Googol with a double-precision float.
+ VariantParser::StreamString css;
+ css.s = "1.0e+100";
+ VariantParser::parse(&css, variant_parsed, errs, line);
+ float_parsed = variant_parsed;
+ CHECK_MESSAGE(float_parsed == 1.0e+100, "Should match the double literal.");
+}
+
+TEST_CASE("[Variant] Assignment To Bool from Int,Float,String,Vec2,Vec2i,Vec3,Vec3i and Color") {
+ Variant int_v = 0;
+ Variant bool_v = true;
+ int_v = bool_v; // int_v is now a bool
+ CHECK(int_v == Variant(true));
+ bool_v = false;
+ int_v = bool_v;
+ CHECK(int_v.get_type() == Variant::BOOL);
+
+ Variant float_v = 0.0f;
+ bool_v = true;
+ float_v = bool_v;
+ CHECK(float_v == Variant(true));
+ bool_v = false;
+ float_v = bool_v;
+ CHECK(float_v.get_type() == Variant::BOOL);
+
+ Variant string_v = "";
+ bool_v = true;
+ string_v = bool_v;
+ CHECK(string_v == Variant(true));
+ bool_v = false;
+ string_v = bool_v;
+ CHECK(string_v.get_type() == Variant::BOOL);
+
+ Variant vec2_v = Vector2(0, 0);
+ bool_v = true;
+ vec2_v = bool_v;
+ CHECK(vec2_v == Variant(true));
+ bool_v = false;
+ vec2_v = bool_v;
+ CHECK(vec2_v.get_type() == Variant::BOOL);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ bool_v = true;
+ vec2i_v = bool_v;
+ CHECK(vec2i_v == Variant(true));
+ bool_v = false;
+ vec2i_v = bool_v;
+ CHECK(vec2i_v.get_type() == Variant::BOOL);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ bool_v = true;
+ vec3_v = bool_v;
+ CHECK(vec3_v == Variant(true));
+ bool_v = false;
+ vec3_v = bool_v;
+ CHECK(vec3_v.get_type() == Variant::BOOL);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ bool_v = true;
+ vec3i_v = bool_v;
+ CHECK(vec3i_v == Variant(true));
+ bool_v = false;
+ vec3i_v = bool_v;
+ CHECK(vec3i_v.get_type() == Variant::BOOL);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ bool_v = true;
+ col_v = bool_v;
+ CHECK(col_v == Variant(true));
+ bool_v = false;
+ col_v = bool_v;
+ CHECK(col_v.get_type() == Variant::BOOL);
+}
+
+TEST_CASE("[Variant] Assignment To Int from Bool,Float,String,Vec2,Vec2i,Vec3,Vec3i and Color") {
+ Variant bool_v = false;
+ Variant int_v = 2;
+ bool_v = int_v; // Now bool_v is int
+ CHECK(bool_v == Variant(2));
+ int_v = -3;
+ bool_v = int_v;
+ CHECK(bool_v.get_type() == Variant::INT);
+
+ Variant float_v = 0.0f;
+ int_v = 2;
+ float_v = int_v;
+ CHECK(float_v == Variant(2));
+ int_v = -3;
+ float_v = int_v;
+ CHECK(float_v.get_type() == Variant::INT);
+
+ Variant string_v = "";
+ int_v = 2;
+ string_v = int_v;
+ CHECK(string_v == Variant(2));
+ int_v = -3;
+ string_v = int_v;
+ CHECK(string_v.get_type() == Variant::INT);
+
+ Variant vec2_v = Vector2(0, 0);
+ int_v = 2;
+ vec2_v = int_v;
+ CHECK(vec2_v == Variant(2));
+ int_v = -3;
+ vec2_v = int_v;
+ CHECK(vec2_v.get_type() == Variant::INT);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ int_v = 2;
+ vec2i_v = int_v;
+ CHECK(vec2i_v == Variant(2));
+ int_v = -3;
+ vec2i_v = int_v;
+ CHECK(vec2i_v.get_type() == Variant::INT);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ int_v = 2;
+ vec3_v = int_v;
+ CHECK(vec3_v == Variant(2));
+ int_v = -3;
+ vec3_v = int_v;
+ CHECK(vec3_v.get_type() == Variant::INT);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ int_v = 2;
+ vec3i_v = int_v;
+ CHECK(vec3i_v == Variant(2));
+ int_v = -3;
+ vec3i_v = int_v;
+ CHECK(vec3i_v.get_type() == Variant::INT);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ int_v = 2;
+ col_v = int_v;
+ CHECK(col_v == Variant(2));
+ int_v = -3;
+ col_v = int_v;
+ CHECK(col_v.get_type() == Variant::INT);
+}
+
+TEST_CASE("[Variant] Assignment To Float from Bool,Int,String,Vec2,Vec2i,Vec3,Vec3i and Color") {
+ Variant bool_v = false;
+ Variant float_v = 1.5f;
+ bool_v = float_v; // Now bool_v is float
+ CHECK(bool_v == Variant(1.5f));
+ float_v = -4.6f;
+ bool_v = float_v;
+ CHECK(bool_v.get_type() == Variant::FLOAT);
+
+ Variant int_v = 1;
+ float_v = 1.5f;
+ int_v = float_v;
+ CHECK(int_v == Variant(1.5f));
+ float_v = -4.6f;
+ int_v = float_v;
+ CHECK(int_v.get_type() == Variant::FLOAT);
+
+ Variant string_v = "";
+ float_v = 1.5f;
+ string_v = float_v;
+ CHECK(string_v == Variant(1.5f));
+ float_v = -4.6f;
+ string_v = float_v;
+ CHECK(string_v.get_type() == Variant::FLOAT);
+
+ Variant vec2_v = Vector2(0, 0);
+ float_v = 1.5f;
+ vec2_v = float_v;
+ CHECK(vec2_v == Variant(1.5f));
+ float_v = -4.6f;
+ vec2_v = float_v;
+ CHECK(vec2_v.get_type() == Variant::FLOAT);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ float_v = 1.5f;
+ vec2i_v = float_v;
+ CHECK(vec2i_v == Variant(1.5f));
+ float_v = -4.6f;
+ vec2i_v = float_v;
+ CHECK(vec2i_v.get_type() == Variant::FLOAT);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ float_v = 1.5f;
+ vec3_v = float_v;
+ CHECK(vec3_v == Variant(1.5f));
+ float_v = -4.6f;
+ vec3_v = float_v;
+ CHECK(vec3_v.get_type() == Variant::FLOAT);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ float_v = 1.5f;
+ vec3i_v = float_v;
+ CHECK(vec3i_v == Variant(1.5f));
+ float_v = -4.6f;
+ vec3i_v = float_v;
+ CHECK(vec3i_v.get_type() == Variant::FLOAT);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ float_v = 1.5f;
+ col_v = float_v;
+ CHECK(col_v == Variant(1.5f));
+ float_v = -4.6f;
+ col_v = float_v;
+ CHECK(col_v.get_type() == Variant::FLOAT);
+}
+
+TEST_CASE("[Variant] Assignment To String from Bool,Int,Float,Vec2,Vec2i,Vec3,Vec3i and Color") {
+ Variant bool_v = false;
+ Variant string_v = "Hello";
+ bool_v = string_v; // Now bool_v is string
+ CHECK(bool_v == Variant("Hello"));
+ string_v = "Hello there";
+ bool_v = string_v;
+ CHECK(bool_v.get_type() == Variant::STRING);
+
+ Variant int_v = 0;
+ string_v = "Hello";
+ int_v = string_v;
+ CHECK(int_v == Variant("Hello"));
+ string_v = "Hello there";
+ int_v = string_v;
+ CHECK(int_v.get_type() == Variant::STRING);
+
+ Variant float_v = 0.0f;
+ string_v = "Hello";
+ float_v = string_v;
+ CHECK(float_v == Variant("Hello"));
+ string_v = "Hello there";
+ float_v = string_v;
+ CHECK(float_v.get_type() == Variant::STRING);
+
+ Variant vec2_v = Vector2(0, 0);
+ string_v = "Hello";
+ vec2_v = string_v;
+ CHECK(vec2_v == Variant("Hello"));
+ string_v = "Hello there";
+ vec2_v = string_v;
+ CHECK(vec2_v.get_type() == Variant::STRING);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ string_v = "Hello";
+ vec2i_v = string_v;
+ CHECK(vec2i_v == Variant("Hello"));
+ string_v = "Hello there";
+ vec2i_v = string_v;
+ CHECK(vec2i_v.get_type() == Variant::STRING);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ string_v = "Hello";
+ vec3_v = string_v;
+ CHECK(vec3_v == Variant("Hello"));
+ string_v = "Hello there";
+ vec3_v = string_v;
+ CHECK(vec3_v.get_type() == Variant::STRING);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ string_v = "Hello";
+ vec3i_v = string_v;
+ CHECK(vec3i_v == Variant("Hello"));
+ string_v = "Hello there";
+ vec3i_v = string_v;
+ CHECK(vec3i_v.get_type() == Variant::STRING);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ string_v = "Hello";
+ col_v = string_v;
+ CHECK(col_v == Variant("Hello"));
+ string_v = "Hello there";
+ col_v = string_v;
+ CHECK(col_v.get_type() == Variant::STRING);
+}
+
+TEST_CASE("[Variant] Assignment To Vec2 from Bool,Int,Float,String,Vec2i,Vec3,Vec3i and Color") {
+ Variant bool_v = false;
+ Variant vec2_v = Vector2(2.2f, 3.5f);
+ bool_v = vec2_v; // Now bool_v is Vector2
+ CHECK(bool_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ bool_v = vec2_v;
+ CHECK(bool_v.get_type() == Variant::VECTOR2);
+
+ Variant int_v = 0;
+ vec2_v = Vector2(2.2f, 3.5f);
+ int_v = vec2_v;
+ CHECK(int_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ int_v = vec2_v;
+ CHECK(int_v.get_type() == Variant::VECTOR2);
+
+ Variant float_v = 0.0f;
+ vec2_v = Vector2(2.2f, 3.5f);
+ float_v = vec2_v;
+ CHECK(float_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ float_v = vec2_v;
+ CHECK(float_v.get_type() == Variant::VECTOR2);
+
+ Variant string_v = "";
+ vec2_v = Vector2(2.2f, 3.5f);
+ string_v = vec2_v;
+ CHECK(string_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ string_v = vec2_v;
+ CHECK(string_v.get_type() == Variant::VECTOR2);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ vec2_v = Vector2(2.2f, 3.5f);
+ vec2i_v = vec2_v;
+ CHECK(vec2i_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ vec2i_v = vec2_v;
+ CHECK(vec2i_v.get_type() == Variant::VECTOR2);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ vec2_v = Vector2(2.2f, 3.5f);
+ vec3_v = vec2_v;
+ CHECK(vec3_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ vec3_v = vec2_v;
+ CHECK(vec3_v.get_type() == Variant::VECTOR2);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ vec2_v = Vector2(2.2f, 3.5f);
+ vec3i_v = vec2_v;
+ CHECK(vec3i_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ vec3i_v = vec2_v;
+ CHECK(vec3i_v.get_type() == Variant::VECTOR2);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ vec2_v = Vector2(2.2f, 3.5f);
+ col_v = vec2_v;
+ CHECK(col_v == Variant(Vector2(2.2f, 3.5f)));
+ vec2_v = Vector2(-5.4f, -7.9f);
+ col_v = vec2_v;
+ CHECK(col_v.get_type() == Variant::VECTOR2);
+}
+
+TEST_CASE("[Variant] Assignment To Vec2i from Bool,Int,Float,String,Vec2,Vec3,Vec3i and Color") {
+ Variant bool_v = false;
+ Variant vec2i_v = Vector2i(2, 3);
+ bool_v = vec2i_v; // Now bool_v is Vector2i
+ CHECK(bool_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ bool_v = vec2i_v;
+ CHECK(bool_v.get_type() == Variant::VECTOR2I);
+
+ Variant int_v = 0;
+ vec2i_v = Vector2i(2, 3);
+ int_v = vec2i_v;
+ CHECK(int_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ int_v = vec2i_v;
+ CHECK(int_v.get_type() == Variant::VECTOR2I);
+
+ Variant float_v = 0.0f;
+ vec2i_v = Vector2i(2, 3);
+ float_v = vec2i_v;
+ CHECK(float_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ float_v = vec2i_v;
+ CHECK(float_v.get_type() == Variant::VECTOR2I);
+
+ Variant string_v = "";
+ vec2i_v = Vector2i(2, 3);
+ string_v = vec2i_v;
+ CHECK(string_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ string_v = vec2i_v;
+ CHECK(string_v.get_type() == Variant::VECTOR2I);
+
+ Variant vec2_v = Vector2(0, 0);
+ vec2i_v = Vector2i(2, 3);
+ vec2_v = vec2i_v;
+ CHECK(vec2_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ vec2_v = vec2i_v;
+ CHECK(vec2i_v.get_type() == Variant::VECTOR2I);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ vec2i_v = Vector2i(2, 3);
+ vec3_v = vec2i_v;
+ CHECK(vec3_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ vec3_v = vec2i_v;
+ CHECK(vec3_v.get_type() == Variant::VECTOR2I);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ vec2i_v = Vector2i(2, 3);
+ vec3i_v = vec2i_v;
+ CHECK(vec3i_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ vec3i_v = vec2i_v;
+ CHECK(vec3i_v.get_type() == Variant::VECTOR2I);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ vec2i_v = Vector2i(2, 3);
+ col_v = vec2i_v;
+ CHECK(col_v == Variant(Vector2i(2, 3)));
+ vec2i_v = Vector2i(-5, -7);
+ col_v = vec2i_v;
+ CHECK(col_v.get_type() == Variant::VECTOR2I);
+}
+
+TEST_CASE("[Variant] Assignment To Vec3 from Bool,Int,Float,String,Vec2,Vec2i,Vec3i and Color") {
+ Variant bool_v = false;
+ Variant vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ bool_v = vec3_v; // Now bool_v is Vector3
+ CHECK(bool_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ bool_v = vec3_v;
+ CHECK(bool_v.get_type() == Variant::VECTOR3);
+
+ Variant int_v = 0;
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ int_v = vec3_v;
+ CHECK(int_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ int_v = vec3_v;
+ CHECK(int_v.get_type() == Variant::VECTOR3);
+
+ Variant float_v = 0.0f;
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ float_v = vec3_v;
+ CHECK(float_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ float_v = vec3_v;
+ CHECK(float_v.get_type() == Variant::VECTOR3);
+
+ Variant string_v = "";
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ string_v = vec3_v;
+ CHECK(string_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ string_v = vec3_v;
+ CHECK(string_v.get_type() == Variant::VECTOR3);
+
+ Variant vec2_v = Vector2(0, 0);
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ vec2_v = vec3_v;
+ CHECK(vec2_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ vec2_v = vec3_v;
+ CHECK(vec2_v.get_type() == Variant::VECTOR3);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ vec2i_v = vec3_v;
+ CHECK(vec2i_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ vec2i_v = vec3_v;
+ CHECK(vec2i_v.get_type() == Variant::VECTOR3);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ vec3i_v = vec3_v;
+ CHECK(vec3i_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ vec3i_v = vec3_v;
+ CHECK(vec3i_v.get_type() == Variant::VECTOR3);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ vec3_v = Vector3(2.2f, 3.5f, 5.3f);
+ col_v = vec3_v;
+ CHECK(col_v == Variant(Vector3(2.2f, 3.5f, 5.3f)));
+ vec3_v = Vector3(-5.4f, -7.9f, -2.1f);
+ col_v = vec3_v;
+ CHECK(col_v.get_type() == Variant::VECTOR3);
+}
+
+TEST_CASE("[Variant] Assignment To Vec3i from Bool,Int,Float,String,Vec2,Vec2i,Vec3 and Color") {
+ Variant bool_v = false;
+ Variant vec3i_v = Vector3i(2, 3, 5);
+ bool_v = vec3i_v; // Now bool_v is Vector3i
+ CHECK(bool_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ bool_v = vec3i_v;
+ CHECK(bool_v.get_type() == Variant::VECTOR3I);
+
+ Variant int_v = 0;
+ vec3i_v = Vector3i(2, 3, 5);
+ int_v = vec3i_v;
+ CHECK(int_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ int_v = vec3i_v;
+ CHECK(int_v.get_type() == Variant::VECTOR3I);
+
+ Variant float_v = 0.0f;
+ vec3i_v = Vector3i(2, 3, 5);
+ float_v = vec3i_v;
+ CHECK(float_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ float_v = vec3i_v;
+ CHECK(float_v.get_type() == Variant::VECTOR3I);
+
+ Variant string_v = "";
+ vec3i_v = Vector3i(2, 3, 5);
+ string_v = vec3i_v;
+ CHECK(string_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ string_v = vec3i_v;
+ CHECK(string_v.get_type() == Variant::VECTOR3I);
+
+ Variant vec2_v = Vector2(0, 0);
+ vec3i_v = Vector3i(2, 3, 5);
+ vec2_v = vec3i_v;
+ CHECK(vec2_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ vec2_v = vec3i_v;
+ CHECK(vec2_v.get_type() == Variant::VECTOR3I);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ vec3i_v = Vector3i(2, 3, 5);
+ vec2i_v = vec3i_v;
+ CHECK(vec2i_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ vec2i_v = vec3i_v;
+ CHECK(vec2i_v.get_type() == Variant::VECTOR3I);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ vec3i_v = Vector3i(2, 3, 5);
+ vec3_v = vec3i_v;
+ CHECK(vec3_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ vec3_v = vec3i_v;
+ CHECK(vec3_v.get_type() == Variant::VECTOR3I);
+
+ Variant col_v = Color(0.5f, 0.2f, 0.75f);
+ vec3i_v = Vector3i(2, 3, 5);
+ col_v = vec3i_v;
+ CHECK(col_v == Variant(Vector3i(2, 3, 5)));
+ vec3i_v = Vector3i(-5, -7, -2);
+ col_v = vec3i_v;
+ CHECK(col_v.get_type() == Variant::VECTOR3I);
+}
+
+TEST_CASE("[Variant] Assignment To Color from Bool,Int,Float,String,Vec2,Vec2i,Vec3 and Vec3i") {
+ Variant bool_v = false;
+ Variant col_v = Color(0.25f, 0.4f, 0.78f);
+ bool_v = col_v; // Now bool_v is Color
+ CHECK(bool_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ bool_v = col_v;
+ CHECK(bool_v.get_type() == Variant::COLOR);
+
+ Variant int_v = 0;
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ int_v = col_v;
+ CHECK(int_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ int_v = col_v;
+ CHECK(int_v.get_type() == Variant::COLOR);
+
+ Variant float_v = 0.0f;
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ float_v = col_v;
+ CHECK(float_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ float_v = col_v;
+ CHECK(float_v.get_type() == Variant::COLOR);
+
+ Variant string_v = "";
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ string_v = col_v;
+ CHECK(string_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ string_v = col_v;
+ CHECK(string_v.get_type() == Variant::COLOR);
+
+ Variant vec2_v = Vector2(0, 0);
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ vec2_v = col_v;
+ CHECK(vec2_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ vec2_v = col_v;
+ CHECK(vec2_v.get_type() == Variant::COLOR);
+
+ Variant vec2i_v = Vector2i(0, 0);
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ vec2i_v = col_v;
+ CHECK(vec2i_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ vec2i_v = col_v;
+ CHECK(vec2i_v.get_type() == Variant::COLOR);
+
+ Variant vec3_v = Vector3(0, 0, 0);
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ vec3_v = col_v;
+ CHECK(vec3_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ vec3_v = col_v;
+ CHECK(vec3_v.get_type() == Variant::COLOR);
+
+ Variant vec3i_v = Vector3i(0, 0, 0);
+ col_v = Color(0.25f, 0.4f, 0.78f);
+ vec3i_v = col_v;
+ CHECK(vec3i_v == Variant(Color(0.25f, 0.4f, 0.78f)));
+ col_v = Color(0.33f, 0.75f, 0.21f);
+ vec3i_v = col_v;
+ CHECK(vec3i_v.get_type() == Variant::COLOR);
+}
+TEST_CASE("[Variant] Writer and parser array") {
+ Array a = build_array(1, String("hello"), build_array(Variant()));
+ String a_str;
+ VariantWriter::write_to_string(a, a_str);
+
+ CHECK_EQ(a_str, "[1, \"hello\", [null]]");
+
+ VariantParser::StreamString ss;
+ String errs;
+ int line;
+ Variant a_parsed;
+
+ ss.s = a_str;
+ VariantParser::parse(&ss, a_parsed, errs, line);
+
+ CHECK_MESSAGE(a_parsed == Variant(a), "Should parse back.");
+}
+
+TEST_CASE("[Variant] Writer recursive array") {
+ // There is no way to accurately represent a recursive array,
+ // the only thing we can do is make sure the writer doesn't blow up
+
+ // Self recursive
+ Array a;
+ a.push_back(a);
+
+ // Writer should it recursion limit while visiting the array
+ ERR_PRINT_OFF;
+ String a_str;
+ VariantWriter::write_to_string(a, a_str);
+ ERR_PRINT_ON;
+
+ // Nested recursive
+ Array a1;
+ Array a2;
+ a1.push_back(a2);
+ a2.push_back(a1);
+
+ // Writer should it recursion limit while visiting the array
+ ERR_PRINT_OFF;
+ String a1_str;
+ VariantWriter::write_to_string(a1, a1_str);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary tearndown will leak memory
+ a.clear();
+ a1.clear();
+ a2.clear();
+}
+
+TEST_CASE("[Variant] Writer and parser dictionary") {
+ // d = {{1: 2}: 3, 4: "hello", 5: {null: []}}
+ Dictionary d = build_dictionary(build_dictionary(1, 2), 3, 4, String("hello"), 5, build_dictionary(Variant(), build_array()));
+ String d_str;
+ VariantWriter::write_to_string(d, d_str);
+
+ CHECK_EQ(d_str, "{\n4: \"hello\",\n5: {\nnull: []\n},\n{\n1: 2\n}: 3\n}");
+
+ VariantParser::StreamString ss;
+ String errs;
+ int line;
+ Variant d_parsed;
+
+ ss.s = d_str;
+ VariantParser::parse(&ss, d_parsed, errs, line);
+
+ CHECK_MESSAGE(d_parsed == Variant(d), "Should parse back.");
+}
+
+TEST_CASE("[Variant] Writer recursive dictionary") {
+ // There is no way to accurately represent a recursive dictionary,
+ // the only thing we can do is make sure the writer doesn't blow up
+
+ // Self recursive
+ Dictionary d;
+ d[1] = d;
+
+ // Writer should it recursion limit while visiting the dictionary
+ ERR_PRINT_OFF;
+ String d_str;
+ VariantWriter::write_to_string(d, d_str);
+ ERR_PRINT_ON;
+
+ // Nested recursive
+ Dictionary d1;
+ Dictionary d2;
+ d1[2] = d2;
+ d2[1] = d1;
+
+ // Writer should it recursion limit while visiting the dictionary
+ ERR_PRINT_OFF;
+ String d1_str;
+ VariantWriter::write_to_string(d1, d1_str);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary tearndown will leak memory
+ d.clear();
+ d1.clear();
+ d2.clear();
+}
+
+#if 0 // TODO: recursion in dict key is currently buggy
+TEST_CASE("[Variant] Writer recursive dictionary on keys") {
+ // There is no way to accurately represent a recursive dictionary,
+ // the only thing we can do is make sure the writer doesn't blow up
+
+ // Self recursive
+ Dictionary d;
+ d[d] = 1;
+
+ // Writer should it recursion limit while visiting the dictionary
+ ERR_PRINT_OFF;
+ String d_str;
+ VariantWriter::write_to_string(d, d_str);
+ ERR_PRINT_ON;
+
+ // Nested recursive
+ Dictionary d1;
+ Dictionary d2;
+ d1[d2] = 2;
+ d2[d1] = 1;
+
+ // Writer should it recursion limit while visiting the dictionary
+ ERR_PRINT_OFF;
+ String d1_str;
+ VariantWriter::write_to_string(d1, d1_str);
+ ERR_PRINT_ON;
+
+ // Break the recursivity otherwise Dictionary tearndown will leak memory
+ d.clear();
+ d1.clear();
+ d2.clear();
+}
+#endif
+
+TEST_CASE("[Variant] Basic comparison") {
+ CHECK_EQ(Variant(1), Variant(1));
+ CHECK_FALSE(Variant(1) != Variant(1));
+ CHECK_NE(Variant(1), Variant(2));
+ CHECK_EQ(Variant(String("foo")), Variant(String("foo")));
+ CHECK_NE(Variant(String("foo")), Variant(String("bar")));
+ // Check "empty" version of different types are not equivalents
+ CHECK_NE(Variant(0), Variant());
+ CHECK_NE(Variant(String()), Variant());
+ CHECK_NE(Variant(Array()), Variant());
+ CHECK_NE(Variant(Dictionary()), Variant());
+}
+
+TEST_CASE("[Variant] Nested array comparison") {
+ Array a1 = build_array(1, build_array(2, 3));
+ Array a2 = build_array(1, build_array(2, 3));
+ Array a_other = build_array(1, build_array(2, 4));
+ Variant v_a1 = a1;
+ Variant v_a1_ref2 = a1;
+ Variant v_a2 = a2;
+ Variant v_a_other = a_other;
+
+ // test both operator== and operator!=
+ CHECK_EQ(v_a1, v_a1);
+ CHECK_FALSE(v_a1 != v_a1);
+ CHECK_EQ(v_a1, v_a1_ref2);
+ CHECK_FALSE(v_a1 != v_a1_ref2);
+ CHECK_EQ(v_a1, v_a2);
+ CHECK_FALSE(v_a1 != v_a2);
+ CHECK_NE(v_a1, v_a_other);
+ CHECK_FALSE(v_a1 == v_a_other);
+}
+
+TEST_CASE("[Variant] Nested dictionary comparison") {
+ Dictionary d1 = build_dictionary(build_dictionary(1, 2), build_dictionary(3, 4));
+ Dictionary d2 = build_dictionary(build_dictionary(1, 2), build_dictionary(3, 4));
+ Dictionary d_other_key = build_dictionary(build_dictionary(1, 0), build_dictionary(3, 4));
+ Dictionary d_other_val = build_dictionary(build_dictionary(1, 2), build_dictionary(3, 0));
+ Variant v_d1 = d1;
+ Variant v_d1_ref2 = d1;
+ Variant v_d2 = d2;
+ Variant v_d_other_key = d_other_key;
+ Variant v_d_other_val = d_other_val;
+
+ // test both operator== and operator!=
+ CHECK_EQ(v_d1, v_d1);
+ CHECK_FALSE(v_d1 != v_d1);
+ CHECK_EQ(v_d1, v_d1_ref2);
+ CHECK_FALSE(v_d1 != v_d1_ref2);
+ CHECK_EQ(v_d1, v_d2);
+ CHECK_FALSE(v_d1 != v_d2);
+ CHECK_NE(v_d1, v_d_other_key);
+ CHECK_FALSE(v_d1 == v_d_other_key);
+ CHECK_NE(v_d1, v_d_other_val);
+ CHECK_FALSE(v_d1 == v_d_other_val);
+}
+
+} // namespace TestVariant
+
+#endif // TEST_VARIANT_H
diff --git a/tests/data/images/icon.bmp b/tests/data/images/icon.bmp
new file mode 100644
index 0000000000..e006f7ebdd
--- /dev/null
+++ b/tests/data/images/icon.bmp
Binary files differ
diff --git a/tests/data/images/icon.jpg b/tests/data/images/icon.jpg
new file mode 100644
index 0000000000..b45bfa8d9b
--- /dev/null
+++ b/tests/data/images/icon.jpg
Binary files differ
diff --git a/tests/data/images/icon.png b/tests/data/images/icon.png
new file mode 100644
index 0000000000..45aaaf584f
--- /dev/null
+++ b/tests/data/images/icon.png
Binary files differ
diff --git a/tests/data/images/icon.tga b/tests/data/images/icon.tga
new file mode 100644
index 0000000000..dcacdc5c67
--- /dev/null
+++ b/tests/data/images/icon.tga
Binary files differ
diff --git a/tests/data/images/icon.webp b/tests/data/images/icon.webp
new file mode 100644
index 0000000000..6c4707e858
--- /dev/null
+++ b/tests/data/images/icon.webp
Binary files differ
diff --git a/tests/data/translations.csv b/tests/data/translations.csv
new file mode 100644
index 0000000000..8cb7b800c5
--- /dev/null
+++ b/tests/data/translations.csv
@@ -0,0 +1,8 @@
+keys,en,de
+GOOD_MORNING,"Good Morning","Guten Morgen"
+GOOD_EVENING,"Good Evening",""
+Without quotes,"With, comma","With ""inner"" quotes","With ""inner"", quotes"","" and comma","With ""inner
+split"" quotes and
+line breaks","With \nnewline chars"
+Some other~delimiter~should still work, shouldn't it?
+What about tab separated lines, good?
diff --git a/tests/scene/test_code_edit.h b/tests/scene/test_code_edit.h
new file mode 100644
index 0000000000..52d3d5c340
--- /dev/null
+++ b/tests/scene/test_code_edit.h
@@ -0,0 +1,3253 @@
+/*************************************************************************/
+/* test_code_edit.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_CODE_EDIT_H
+#define TEST_CODE_EDIT_H
+
+#include "scene/gui/code_edit.h"
+
+#include "tests/test_macros.h"
+
+namespace TestCodeEdit {
+
+TEST_CASE("[SceneTree][CodeEdit] line gutters") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+ SUBCASE("[CodeEdit] breakpoints") {
+ SIGNAL_WATCH(code_edit, "breakpoint_toggled");
+
+ SUBCASE("[CodeEdit] draw breakpoints gutter") {
+ code_edit->set_draw_breakpoints_gutter(false);
+ CHECK_FALSE(code_edit->is_drawing_breakpoints_gutter());
+
+ code_edit->set_draw_breakpoints_gutter(true);
+ CHECK(code_edit->is_drawing_breakpoints_gutter());
+ }
+
+ SUBCASE("[CodeEdit] set line as breakpoint") {
+ /* Out of bounds. */
+ ERR_PRINT_OFF;
+
+ code_edit->set_line_as_breakpoint(-1, true);
+ CHECK_FALSE(code_edit->is_line_breakpointed(-1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ ERR_PRINT_ON;
+
+ Array arg1;
+ arg1.push_back(0);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_line_as_breakpoint(0, true);
+ CHECK(code_edit->is_line_breakpointed(0));
+ CHECK(code_edit->get_breakpointed_lines()[0] == Variant(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->set_line_as_breakpoint(0, false);
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] clear breakpointed lines") {
+ code_edit->clear_breakpointed_lines();
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ Array arg1;
+ arg1.push_back(0);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_line_as_breakpoint(0, true);
+ CHECK(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->clear_breakpointed_lines();
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and set text") {
+ Array arg1;
+ arg1.push_back(0);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_breakpoint(0, true);
+ CHECK(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* breakpoint on lines that still exist are kept. */
+ code_edit->set_text("");
+ MessageQueue::get_singleton()->flush();
+ CHECK(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ /* breakpoint on lines that are removed should also be removed. */
+ code_edit->clear_breakpointed_lines();
+ SIGNAL_DISCARD("breakpoint_toggled")
+
+ ((Array)args[0])[0] = 1;
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->set_text("");
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ ERR_PRINT_ON;
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and clear") {
+ Array arg1;
+ arg1.push_back(0);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_breakpoint(0, true);
+ CHECK(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* breakpoint on lines that still exist are removed. */
+ code_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* breakpoint on lines that are removed should also be removed. */
+ code_edit->clear_breakpointed_lines();
+ SIGNAL_DISCARD("breakpoint_toggled")
+
+ ((Array)args[0])[0] = 1;
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ ERR_PRINT_ON;
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and new lines no text") {
+ Array arg1;
+ arg1.push_back(0);
+ Array args;
+ args.push_back(arg1);
+
+ /* No text moves breakpoint. */
+ code_edit->set_line_as_breakpoint(0, true);
+ CHECK(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Normal. */
+ ((Array)args[0])[0] = 0;
+ Array arg2;
+ arg2.push_back(1);
+ args.push_back(arg2);
+
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Non-Breaking. */
+ ((Array)args[0])[0] = 1;
+ ((Array)args[1])[0] = 2;
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line_count() == 3);
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ CHECK(code_edit->is_line_breakpointed(2));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Above. */
+ ((Array)args[0])[0] = 2;
+ ((Array)args[1])[0] = 3;
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line_count() == 4);
+ CHECK_FALSE(code_edit->is_line_breakpointed(2));
+ CHECK(code_edit->is_line_breakpointed(3));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and new lines with text") {
+ Array arg1;
+ arg1.push_back(0);
+ Array args;
+ args.push_back(arg1);
+
+ /* Having text does not move breakpoint. */
+ code_edit->insert_text_at_caret("text");
+ code_edit->set_line_as_breakpoint(0, true);
+ CHECK(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Normal. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK(code_edit->is_line_breakpointed(0));
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ /* Non-Breaking. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line_count() == 3);
+ CHECK(code_edit->is_line_breakpointed(0));
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ /* Above does move. */
+ ((Array)args[0])[0] = 0;
+ Array arg2;
+ arg2.push_back(1);
+ args.push_back(arg2);
+
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line_count() == 4);
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and backspace") {
+ Array arg1;
+ arg1.push_back(1);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->set_caret_line(2);
+
+ /* backspace onto line does not remove breakpoint */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ /* backspace on breakpointed line removes it */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ ERR_PRINT_ON;
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Backspace above breakpointed line moves it. */
+ ((Array)args[0])[0] = 2;
+
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_breakpoint(2, true);
+ CHECK(code_edit->is_line_breakpointed(2));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->set_caret_line(1);
+
+ Array arg2;
+ arg2.push_back(1);
+ args.push_back(arg2);
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(2));
+ ERR_PRINT_ON;
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and delete") {
+ Array arg1;
+ arg1.push_back(1);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ code_edit->set_caret_line(1);
+
+ /* Delete onto breakpointed lines does not remove it. */
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+
+ /* Delete moving breakpointed line up removes it. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ CHECK(code_edit->get_line_count() == 1);
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ ERR_PRINT_ON;
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Delete above breakpointed line moves it. */
+ ((Array)args[0])[0] = 2;
+
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_breakpoint(2, true);
+ CHECK(code_edit->is_line_breakpointed(2));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->set_caret_line(0);
+
+ Array arg2;
+ arg2.push_back(1);
+ args.push_back(arg2);
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(2));
+ ERR_PRINT_ON;
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and delete selection") {
+ Array arg1;
+ arg1.push_back(1);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->select(0, 0, 2, 0);
+ code_edit->delete_selection();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Should handle breakpoint move when deleting selection by adding less text then removed. */
+ ((Array)args[0])[0] = 9;
+
+ code_edit->set_text("\n\n\n\n\n\n\n\n\n");
+ code_edit->set_line_as_breakpoint(9, true);
+ CHECK(code_edit->is_line_breakpointed(9));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->select(0, 0, 6, 0);
+
+ Array arg2;
+ arg2.push_back(4);
+ args.push_back(arg2);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_breakpointed(9));
+ ERR_PRINT_ON;
+ CHECK(code_edit->is_line_breakpointed(4));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Should handle breakpoint move when deleting selection by adding more text then removed. */
+ ((Array)args[0])[0] = 9;
+ ((Array)args[1])[0] = 14;
+
+ code_edit->insert_text_at_caret("\n\n\n\n\n");
+ MessageQueue::get_singleton()->flush();
+ SIGNAL_DISCARD("breakpoint_toggled")
+ CHECK(code_edit->is_line_breakpointed(9));
+
+ code_edit->select(0, 0, 6, 0);
+ code_edit->insert_text_at_caret("\n\n\n\n\n\n\n\n\n\n\n");
+ MessageQueue::get_singleton()->flush();
+ CHECK(code_edit->is_line_breakpointed(14));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+ }
+
+ SUBCASE("[CodeEdit] breakpoints and undo") {
+ Array arg1;
+ arg1.push_back(1);
+ Array args;
+ args.push_back(arg1);
+
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_breakpoint(1, true);
+ CHECK(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ code_edit->select(0, 0, 2, 0);
+ code_edit->delete_selection();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_breakpointed(0));
+ SIGNAL_CHECK("breakpoint_toggled", args);
+
+ /* Undo does not restore breakpoint. */
+ code_edit->undo();
+ CHECK_FALSE(code_edit->is_line_breakpointed(1));
+ SIGNAL_CHECK_FALSE("breakpoint_toggled");
+ }
+
+ SIGNAL_UNWATCH(code_edit, "breakpoint_toggled");
+ }
+
+ SUBCASE("[CodeEdit] bookmarks") {
+ SUBCASE("[CodeEdit] draw bookmarks gutter") {
+ code_edit->set_draw_bookmarks_gutter(false);
+ CHECK_FALSE(code_edit->is_drawing_bookmarks_gutter());
+
+ code_edit->set_draw_bookmarks_gutter(true);
+ CHECK(code_edit->is_drawing_bookmarks_gutter());
+ }
+
+ SUBCASE("[CodeEdit] set line as bookmarks") {
+ /* Out of bounds. */
+ ERR_PRINT_OFF;
+
+ code_edit->set_line_as_bookmarked(-1, true);
+ CHECK_FALSE(code_edit->is_line_bookmarked(-1));
+
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+
+ ERR_PRINT_ON;
+
+ code_edit->set_line_as_bookmarked(0, true);
+ CHECK(code_edit->get_bookmarked_lines()[0] == Variant(0));
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ code_edit->set_line_as_bookmarked(0, false);
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ }
+
+ SUBCASE("[CodeEdit] clear bookmarked lines") {
+ code_edit->clear_bookmarked_lines();
+
+ code_edit->set_line_as_bookmarked(0, true);
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ code_edit->clear_bookmarked_lines();
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and set text") {
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_bookmarked(0, true);
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ /* bookmarks on lines that still exist are kept. */
+ code_edit->set_text("");
+ MessageQueue::get_singleton()->flush();
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ /* bookmarks on lines that are removed should also be removed. */
+ code_edit->clear_bookmarked_lines();
+
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ code_edit->set_text("");
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and clear") {
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_bookmarked(0, true);
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ /* bookmarks on lines that still exist are removed. */
+ code_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+
+ /* bookmarks on lines that are removed should also be removed. */
+ code_edit->clear_bookmarked_lines();
+
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ code_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and new lines no text") {
+ /* No text moves bookmarks. */
+ code_edit->set_line_as_bookmarked(0, true);
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ /* Normal. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ /* Non-Breaking. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line_count() == 3);
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+ CHECK(code_edit->is_line_bookmarked(2));
+
+ /* Above. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line_count() == 4);
+ CHECK_FALSE(code_edit->is_line_bookmarked(2));
+ CHECK(code_edit->is_line_bookmarked(3));
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and new lines with text") {
+ /* Having text does not move bookmark. */
+ code_edit->insert_text_at_caret("text");
+ code_edit->set_line_as_bookmarked(0, true);
+ CHECK(code_edit->is_line_bookmarked(0));
+
+ /* Normal. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK(code_edit->is_line_bookmarked(0));
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+
+ /* Non-Breaking. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line_count() == 3);
+ CHECK(code_edit->is_line_bookmarked(0));
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+
+ /* Above does move. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line_count() == 4);
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ CHECK(code_edit->is_line_bookmarked(1));
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and backspace") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ code_edit->set_caret_line(2);
+
+ /* backspace onto line does not remove bookmark */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ /* backspace on bookmarked line removes it */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and delete") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK(code_edit->is_line_bookmarked(1));
+ code_edit->set_caret_line(1);
+
+ /* Delete onto bookmarked lines does not remove it. */
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ /* Delete moving bookmarked line up removes it. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ CHECK(code_edit->get_line_count() == 1);
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and delete selection") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ code_edit->select(0, 0, 2, 0);
+ code_edit->delete_selection();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+ }
+
+ SUBCASE("[CodeEdit] bookmarks and undo") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_bookmarked(1, true);
+ CHECK(code_edit->is_line_bookmarked(1));
+
+ code_edit->select(0, 0, 2, 0);
+ code_edit->delete_selection();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_bookmarked(0));
+
+ /* Undo does not restore bookmark. */
+ code_edit->undo();
+ CHECK_FALSE(code_edit->is_line_bookmarked(1));
+ }
+ }
+
+ SUBCASE("[CodeEdit] executing lines") {
+ SUBCASE("[CodeEdit] draw executing lines gutter") {
+ code_edit->set_draw_executing_lines_gutter(false);
+ CHECK_FALSE(code_edit->is_drawing_executing_lines_gutter());
+
+ code_edit->set_draw_executing_lines_gutter(true);
+ CHECK(code_edit->is_drawing_executing_lines_gutter());
+ }
+
+ SUBCASE("[CodeEdit] set line as executing lines") {
+ /* Out of bounds. */
+ ERR_PRINT_OFF;
+
+ code_edit->set_line_as_executing(-1, true);
+ CHECK_FALSE(code_edit->is_line_executing(-1));
+
+ code_edit->set_line_as_executing(1, true);
+ CHECK_FALSE(code_edit->is_line_executing(1));
+
+ ERR_PRINT_ON;
+
+ code_edit->set_line_as_executing(0, true);
+ CHECK(code_edit->get_executing_lines()[0] == Variant(0));
+ CHECK(code_edit->is_line_executing(0));
+
+ code_edit->set_line_as_executing(0, false);
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ }
+
+ SUBCASE("[CodeEdit] clear executing lines lines") {
+ code_edit->clear_executing_lines();
+
+ code_edit->set_line_as_executing(0, true);
+ CHECK(code_edit->is_line_executing(0));
+
+ code_edit->clear_executing_lines();
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ }
+
+ SUBCASE("[CodeEdit] executing lines and set text") {
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_executing(0, true);
+ CHECK(code_edit->is_line_executing(0));
+
+ /* executing on lines that still exist are kept. */
+ code_edit->set_text("");
+ MessageQueue::get_singleton()->flush();
+ CHECK(code_edit->is_line_executing(0));
+
+ /* executing on lines that are removed should also be removed. */
+ code_edit->clear_executing_lines();
+
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_executing(1, true);
+ CHECK(code_edit->is_line_executing(1));
+
+ code_edit->set_text("");
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_executing(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] executing lines and clear") {
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_executing(0, true);
+ CHECK(code_edit->is_line_executing(0));
+
+ /* executing on lines that still exist are removed. */
+ code_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_executing(0));
+
+ /* executing on lines that are removed should also be removed. */
+ code_edit->clear_executing_lines();
+
+ code_edit->set_text("test\nline");
+ code_edit->set_line_as_executing(1, true);
+ CHECK(code_edit->is_line_executing(1));
+
+ code_edit->clear();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_executing(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] executing lines and new lines no text") {
+ /* No text moves executing lines. */
+ code_edit->set_line_as_executing(0, true);
+ CHECK(code_edit->is_line_executing(0));
+
+ /* Normal. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ CHECK(code_edit->is_line_executing(1));
+
+ /* Non-Breaking. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line_count() == 3);
+ CHECK_FALSE(code_edit->is_line_executing(1));
+ CHECK(code_edit->is_line_executing(2));
+
+ /* Above. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line_count() == 4);
+ CHECK_FALSE(code_edit->is_line_executing(2));
+ CHECK(code_edit->is_line_executing(3));
+ }
+
+ SUBCASE("[CodeEdit] executing lines and new lines with text") {
+ /* Having text does not move executing lines. */
+ code_edit->insert_text_at_caret("text");
+ code_edit->set_line_as_executing(0, true);
+ CHECK(code_edit->is_line_executing(0));
+
+ /* Normal. */
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK(code_edit->is_line_executing(0));
+ CHECK_FALSE(code_edit->is_line_executing(1));
+
+ /* Non-Breaking. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line_count() == 3);
+ CHECK(code_edit->is_line_executing(0));
+ CHECK_FALSE(code_edit->is_line_executing(1));
+
+ /* Above does move. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line_count() == 4);
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ CHECK(code_edit->is_line_executing(1));
+ }
+
+ SUBCASE("[CodeEdit] executing lines and backspace") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_executing(1, true);
+ CHECK(code_edit->is_line_executing(1));
+
+ code_edit->set_caret_line(2);
+
+ /* backspace onto line does not remove executing lines. */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK(code_edit->is_line_executing(1));
+
+ /* backspace on executing line removes it */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_executing(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] executing lines and delete") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_executing(1, true);
+ CHECK(code_edit->is_line_executing(1));
+ code_edit->set_caret_line(1);
+
+ /* Delete onto executing lines does not remove it. */
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ CHECK(code_edit->get_line_count() == 2);
+ CHECK(code_edit->is_line_executing(1));
+
+ /* Delete moving executing line up removes it. */
+ code_edit->set_caret_line(0);
+ SEND_GUI_ACTION(code_edit, "ui_text_delete");
+ CHECK(code_edit->get_line_count() == 1);
+ ERR_PRINT_OFF;
+ CHECK_FALSE(code_edit->is_line_executing(1));
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("[CodeEdit] executing lines and delete selection") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_executing(1, true);
+ CHECK(code_edit->is_line_executing(1));
+
+ code_edit->select(0, 0, 2, 0);
+ code_edit->delete_selection();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_executing(0));
+ }
+
+ SUBCASE("[CodeEdit] executing lines and undo") {
+ code_edit->set_text("\n\n");
+ code_edit->set_line_as_executing(1, true);
+ CHECK(code_edit->is_line_executing(1));
+
+ code_edit->select(0, 0, 2, 0);
+ code_edit->delete_selection();
+ MessageQueue::get_singleton()->flush();
+ CHECK_FALSE(code_edit->is_line_executing(0));
+
+ /* Undo does not restore executing lines. */
+ code_edit->undo();
+ CHECK_FALSE(code_edit->is_line_executing(1));
+ }
+ }
+
+ SUBCASE("[CodeEdit] line numbers") {
+ SUBCASE("[CodeEdit] draw line numbers gutter and padding") {
+ code_edit->set_draw_line_numbers(false);
+ CHECK_FALSE(code_edit->is_draw_line_numbers_enabled());
+
+ code_edit->set_draw_line_numbers(true);
+ CHECK(code_edit->is_draw_line_numbers_enabled());
+
+ code_edit->set_line_numbers_zero_padded(false);
+ CHECK_FALSE(code_edit->is_line_numbers_zero_padded());
+
+ code_edit->set_line_numbers_zero_padded(true);
+ CHECK(code_edit->is_line_numbers_zero_padded());
+
+ code_edit->set_line_numbers_zero_padded(false);
+ CHECK_FALSE(code_edit->is_line_numbers_zero_padded());
+
+ code_edit->set_draw_line_numbers(false);
+ CHECK_FALSE(code_edit->is_draw_line_numbers_enabled());
+
+ code_edit->set_line_numbers_zero_padded(true);
+ CHECK(code_edit->is_line_numbers_zero_padded());
+ }
+ }
+
+ SUBCASE("[CodeEdit] line folding") {
+ SUBCASE("[CodeEdit] draw line folding gutter") {
+ code_edit->set_draw_fold_gutter(false);
+ CHECK_FALSE(code_edit->is_drawing_fold_gutter());
+
+ code_edit->set_draw_fold_gutter(true);
+ CHECK(code_edit->is_drawing_fold_gutter());
+ }
+ }
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] delimiters") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+ const Point2 OUTSIDE_DELIMETER = Point2(-1, -1);
+
+ code_edit->clear_string_delimiters();
+ code_edit->clear_comment_delimiters();
+
+ SUBCASE("[CodeEdit] add and remove delimiters") {
+ SUBCASE("[CodeEdit] add and remove string delimiters") {
+ /* Add a delimiter.*/
+ code_edit->add_string_delimiter("\"", "\"", false);
+ CHECK(code_edit->has_string_delimiter("\""));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ ERR_PRINT_OFF;
+
+ /* Adding a duplicate start key is not allowed. */
+ code_edit->add_string_delimiter("\"", "\'", false);
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* Adding a duplicate end key is allowed. */
+ code_edit->add_string_delimiter("'", "\"", false);
+ CHECK(code_edit->has_string_delimiter("'"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ /* Both start and end keys have to be symbols. */
+ code_edit->add_string_delimiter("f", "\"", false);
+ CHECK_FALSE(code_edit->has_string_delimiter("f"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ code_edit->add_string_delimiter("f", "\"", false);
+ CHECK_FALSE(code_edit->has_string_delimiter("f"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ code_edit->add_string_delimiter("@", "f", false);
+ CHECK_FALSE(code_edit->has_string_delimiter("@"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ code_edit->add_string_delimiter("f", "f", false);
+ CHECK_FALSE(code_edit->has_string_delimiter("f"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ /* Blank start keys are not allowed */
+ code_edit->add_string_delimiter("", "#", false);
+ CHECK_FALSE(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ ERR_PRINT_ON;
+
+ /* Blank end keys are allowed. */
+ code_edit->add_string_delimiter("#", "", false);
+ CHECK(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 3);
+
+ /* Remove a delimiter. */
+ code_edit->remove_string_delimiter("#");
+ CHECK_FALSE(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ /* Set should override existing, and test multiline */
+ TypedArray<String> delimiters;
+ delimiters.push_back("^^ ^^");
+
+ code_edit->set_string_delimiters(delimiters);
+ CHECK_FALSE(code_edit->has_string_delimiter("\""));
+ CHECK(code_edit->has_string_delimiter("^^"));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* clear should remove all. */
+ code_edit->clear_string_delimiters();
+ CHECK_FALSE(code_edit->has_string_delimiter("^^"));
+ CHECK(code_edit->get_string_delimiters().size() == 0);
+ }
+
+ SUBCASE("[CodeEdit] add and remove comment delimiters") {
+ /* Add a delimiter.*/
+ code_edit->add_comment_delimiter("\"", "\"", false);
+ CHECK(code_edit->has_comment_delimiter("\""));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ ERR_PRINT_OFF;
+
+ /* Adding a duplicate start key is not allowed. */
+ code_edit->add_comment_delimiter("\"", "\'", false);
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* Adding a duplicate end key is allowed. */
+ code_edit->add_comment_delimiter("'", "\"", false);
+ CHECK(code_edit->has_comment_delimiter("'"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ /* Both start and end keys have to be symbols. */
+ code_edit->add_comment_delimiter("f", "\"", false);
+ CHECK_FALSE(code_edit->has_comment_delimiter("f"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ code_edit->add_comment_delimiter("f", "\"", false);
+ CHECK_FALSE(code_edit->has_comment_delimiter("f"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ code_edit->add_comment_delimiter("@", "f", false);
+ CHECK_FALSE(code_edit->has_comment_delimiter("@"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ code_edit->add_comment_delimiter("f", "f", false);
+ CHECK_FALSE(code_edit->has_comment_delimiter("f"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ /* Blank start keys are not allowed. */
+ code_edit->add_comment_delimiter("", "#", false);
+ CHECK_FALSE(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ ERR_PRINT_ON;
+
+ /* Blank end keys are allowed. */
+ code_edit->add_comment_delimiter("#", "", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 3);
+
+ /* Remove a delimiter. */
+ code_edit->remove_comment_delimiter("#");
+ CHECK_FALSE(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ /* Set should override existing, and test multiline. */
+ TypedArray<String> delimiters;
+ delimiters.push_back("^^ ^^");
+
+ code_edit->set_comment_delimiters(delimiters);
+ CHECK_FALSE(code_edit->has_comment_delimiter("\""));
+ CHECK(code_edit->has_comment_delimiter("^^"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* clear should remove all. */
+ code_edit->clear_comment_delimiters();
+ CHECK_FALSE(code_edit->has_comment_delimiter("^^"));
+ CHECK(code_edit->get_comment_delimiters().size() == 0);
+ }
+
+ SUBCASE("[CodeEdit] add and remove mixed delimiters") {
+ code_edit->add_comment_delimiter("#", "", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ ERR_PRINT_OFF;
+
+ /* Disallow adding a string with the same start key as comment. */
+ code_edit->add_string_delimiter("#", "", false);
+ CHECK_FALSE(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 0);
+
+ code_edit->add_string_delimiter("\"", "\"", false);
+ CHECK(code_edit->has_string_delimiter("\""));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* Disallow adding a comment with the same start key as string. */
+ code_edit->add_comment_delimiter("\"", "", false);
+ CHECK_FALSE(code_edit->has_comment_delimiter("\""));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ ERR_PRINT_ON;
+
+ /* Cannot remove string with remove comment. */
+ code_edit->remove_comment_delimiter("\"");
+ CHECK(code_edit->has_string_delimiter("\""));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* Cannot remove comment with remove string. */
+ code_edit->remove_string_delimiter("#");
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* Clear comments leave strings. */
+ code_edit->clear_comment_delimiters();
+ CHECK(code_edit->has_string_delimiter("\""));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* Clear string leave comments. */
+ code_edit->add_comment_delimiter("#", "", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ code_edit->clear_string_delimiters();
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+ }
+ }
+
+ SUBCASE("[CodeEdit] single line delimiters") {
+ SUBCASE("[CodeEdit] single line string delimiters") {
+ /* Blank end key should set lineonly to true. */
+ code_edit->add_string_delimiter("#", "", false);
+ CHECK(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* Insert line above, line with string then line below. */
+ code_edit->insert_text_at_caret(" \n#\n ");
+
+ /* Check line above is not in string. */
+ CHECK(code_edit->is_in_string(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in string. */
+ CHECK(code_edit->is_in_string(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column after start key is in string and start / end positions are correct. */
+ CHECK(code_edit->is_in_string(1, 1) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(2, 1));
+
+ /* Check line after is not in string. */
+ CHECK(code_edit->is_in_string(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Check region metadata. */
+ int idx = code_edit->is_in_string(1, 1);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "");
+
+ /* Check nested strings are handled correctly. */
+ code_edit->set_text(" \n# # \n ");
+
+ /* Check line above is not in string. */
+ CHECK(code_edit->is_in_string(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before first start key is not in string. */
+ CHECK(code_edit->is_in_string(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column after the first start key is in string and start / end positions are correct. */
+ CHECK(code_edit->is_in_string(1, 1) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1));
+
+ /* Check column after the second start key returns data for the first. */
+ CHECK(code_edit->is_in_string(1, 5) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1));
+
+ /* Check line after is not in string. */
+ CHECK(code_edit->is_in_string(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Check is in string with no column returns true if entire line is comment excluding whitespace. */
+ code_edit->set_text(" \n # # \n ");
+ CHECK(code_edit->is_in_string(1) != -1);
+
+ code_edit->set_text(" \n text # # \n ");
+ CHECK(code_edit->is_in_string(1) == -1);
+
+ /* Removing delimiter should update. */
+ code_edit->set_text(" \n # # \n ");
+
+ code_edit->remove_string_delimiter("#");
+ CHECK_FALSE(code_edit->has_string_delimiter("$"));
+ CHECK(code_edit->get_string_delimiters().size() == 0);
+
+ CHECK(code_edit->is_in_string(1) == -1);
+
+ /* Adding and clear should update. */
+ code_edit->add_string_delimiter("#", "", false);
+ CHECK(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+ CHECK(code_edit->is_in_string(1) != -1);
+
+ code_edit->clear_string_delimiters();
+ CHECK_FALSE(code_edit->has_string_delimiter("$"));
+ CHECK(code_edit->get_string_delimiters().size() == 0);
+
+ CHECK(code_edit->is_in_string(1) == -1);
+ }
+
+ SUBCASE("[CodeEdit] single line comment delimiters") {
+ /* Blank end key should set lineonly to true. */
+ code_edit->add_comment_delimiter("#", "", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* Insert line above, line with comment then line below. */
+ code_edit->insert_text_at_caret(" \n#\n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column after start key is in comment and start / end positions are correct. */
+ CHECK(code_edit->is_in_comment(1, 1) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(2, 1));
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Check region metadata. */
+ int idx = code_edit->is_in_comment(1, 1);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "");
+
+ /* Check nested comments are handled correctly. */
+ code_edit->set_text(" \n# # \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before first start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column after the first start key is in comment and start / end positions are correct. */
+ CHECK(code_edit->is_in_comment(1, 1) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1));
+
+ /* Check column after the second start key returns data for the first. */
+ CHECK(code_edit->is_in_comment(1, 5) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1));
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Check is in comment with no column returns true if entire line is comment excluding whitespace. */
+ code_edit->set_text(" \n # # \n ");
+ CHECK(code_edit->is_in_comment(1) != -1);
+
+ code_edit->set_text(" \n text # # \n ");
+ CHECK(code_edit->is_in_comment(1) == -1);
+
+ /* Removing delimiter should update. */
+ code_edit->set_text(" \n # # \n ");
+
+ code_edit->remove_comment_delimiter("#");
+ CHECK_FALSE(code_edit->has_comment_delimiter("$"));
+ CHECK(code_edit->get_comment_delimiters().size() == 0);
+
+ CHECK(code_edit->is_in_comment(1) == -1);
+
+ /* Adding and clear should update. */
+ code_edit->add_comment_delimiter("#", "", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+ CHECK(code_edit->is_in_comment(1) != -1);
+
+ code_edit->clear_comment_delimiters();
+ CHECK_FALSE(code_edit->has_comment_delimiter("$"));
+ CHECK(code_edit->get_comment_delimiters().size() == 0);
+
+ CHECK(code_edit->is_in_comment(1) == -1);
+ }
+
+ SUBCASE("[CodeEdit] single line mixed delimiters") {
+ /* Blank end key should set lineonly to true. */
+ /* Add string delimiter. */
+ code_edit->add_string_delimiter("&", "", false);
+ CHECK(code_edit->has_string_delimiter("&"));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* Add comment delimiter. */
+ code_edit->add_comment_delimiter("#", "", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* Nest a string delimiter inside a comment. */
+ code_edit->set_text(" \n# & \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before first start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column after the first start key is in comment and start / end positions are correct. */
+ CHECK(code_edit->is_in_comment(1, 1) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1));
+
+ /* Check column after the second start key returns data for the first, and does not state string. */
+ CHECK(code_edit->is_in_comment(1, 5) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1));
+ CHECK(code_edit->is_in_string(1, 5) == -1);
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Remove the comment delimiter. */
+ code_edit->remove_comment_delimiter("#");
+ CHECK_FALSE(code_edit->has_comment_delimiter("$"));
+ CHECK(code_edit->get_comment_delimiters().size() == 0);
+
+ /* The "first" comment region is no longer valid. */
+ CHECK(code_edit->is_in_comment(1, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 1) == OUTSIDE_DELIMETER);
+
+ /* The "second" region as string is now valid. */
+ CHECK(code_edit->is_in_string(1, 5) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(4, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1));
+ }
+ }
+
+ SUBCASE("[CodeEdit] multiline delimiters") {
+ SUBCASE("[CodeEdit] multiline string delimiters") {
+ code_edit->clear_string_delimiters();
+ code_edit->clear_comment_delimiters();
+
+ /* Add string delimiter. */
+ code_edit->add_string_delimiter("#", "#", false);
+ CHECK(code_edit->has_string_delimiter("#"));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* First test over a single line. */
+ code_edit->set_text(" \n # # \n ");
+
+ /* Check line above is not in string. */
+ CHECK(code_edit->is_in_string(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in string. */
+ CHECK(code_edit->is_in_string(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column before closing delimiter is in string. */
+ CHECK(code_edit->is_in_string(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(5, 1));
+
+ /* Check column after end key is not in string. */
+ CHECK(code_edit->is_in_string(1, 6) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 6) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 6) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in string. */
+ CHECK(code_edit->is_in_string(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Check the region metadata. */
+ int idx = code_edit->is_in_string(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Next test over a multiple blank lines. */
+ code_edit->set_text(" \n # \n\n # \n ");
+
+ /* Check line above is not in string. */
+ CHECK(code_edit->is_in_string(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in string. */
+ CHECK(code_edit->is_in_string(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in string. */
+ CHECK(code_edit->is_in_string(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3));
+
+ /* Check blank middle line. */
+ CHECK(code_edit->is_in_string(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3));
+
+ /* Check column just before end key is in string. */
+ CHECK(code_edit->is_in_string(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3));
+
+ /* Check column after end key is not in string. */
+ CHECK(code_edit->is_in_string(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in string. */
+ CHECK(code_edit->is_in_string(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* Next test over a multiple non-blank lines. */
+ code_edit->set_text(" \n # \n \n # \n ");
+
+ /* Check line above is not in string. */
+ CHECK(code_edit->is_in_string(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in string. */
+ CHECK(code_edit->is_in_string(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in string. */
+ CHECK(code_edit->is_in_string(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3));
+
+ /* Check middle line. */
+ CHECK(code_edit->is_in_string(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3));
+
+ /* Check column just before end key is in string. */
+ CHECK(code_edit->is_in_string(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3));
+
+ /* Check column after end key is not in string. */
+ CHECK(code_edit->is_in_string(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in string. */
+ CHECK(code_edit->is_in_string(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* check the region metadata. */
+ idx = code_edit->is_in_string(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Next test nested strings. */
+ code_edit->add_string_delimiter("^", "^", false);
+ CHECK(code_edit->has_string_delimiter("^"));
+ CHECK(code_edit->get_string_delimiters().size() == 2);
+
+ code_edit->set_text(" \n # ^\n \n^ # \n ");
+
+ /* Check line above is not in string. */
+ CHECK(code_edit->is_in_string(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in string. */
+ CHECK(code_edit->is_in_string(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in string. */
+ CHECK(code_edit->is_in_string(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3));
+
+ /* Check middle line. */
+ CHECK(code_edit->is_in_string(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3));
+
+ /* Check column just before end key is in string. */
+ CHECK(code_edit->is_in_string(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3));
+
+ /* Check column after end key is not in string. */
+ CHECK(code_edit->is_in_string(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in string. */
+ CHECK(code_edit->is_in_string(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* check the region metadata. */
+ idx = code_edit->is_in_string(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Next test no end key. */
+ code_edit->set_text(" \n # \n ");
+
+ /* check the region metadata. */
+ idx = code_edit->is_in_string(1, 2);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(-1, -1));
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Check is in string with no column returns true if entire line is string excluding whitespace. */
+ code_edit->set_text(" \n # \n\n #\n ");
+ CHECK(code_edit->is_in_string(1) != -1);
+ CHECK(code_edit->is_in_string(2) != -1);
+ CHECK(code_edit->is_in_string(3) != -1);
+
+ code_edit->set_text(" \n test # \n\n # test \n ");
+ CHECK(code_edit->is_in_string(1) == -1);
+ CHECK(code_edit->is_in_string(2) != -1);
+ CHECK(code_edit->is_in_string(3) == -1);
+ }
+
+ SUBCASE("[CodeEdit] multiline comment delimiters") {
+ /* Add comment delimiter. */
+ code_edit->add_comment_delimiter("#", "#", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* First test over a single line. */
+ code_edit->set_text(" \n # # \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column before closing delimiter is in comment. */
+ CHECK(code_edit->is_in_comment(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(5, 1));
+
+ /* Check column after end key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 6) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 6) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 6) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(2, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+ /* Check the region metadata. */
+ int idx = code_edit->is_in_comment(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Next test over a multiple blank lines. */
+ code_edit->set_text(" \n # \n\n # \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in comment. */
+ CHECK(code_edit->is_in_comment(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3));
+
+ /* Check blank middle line. */
+ CHECK(code_edit->is_in_comment(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3));
+
+ /* Check column just before end key is in comment. */
+ CHECK(code_edit->is_in_comment(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3));
+
+ /* Check column after end key is not in comment. */
+ CHECK(code_edit->is_in_comment(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* Next test over a multiple non-blank lines. */
+ code_edit->set_text(" \n # \n \n # \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in comment. */
+ CHECK(code_edit->is_in_comment(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3));
+
+ /* Check middle line. */
+ CHECK(code_edit->is_in_comment(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3));
+
+ /* Check column just before end key is in comment. */
+ CHECK(code_edit->is_in_comment(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3));
+
+ /* Check column after end key is not in comment. */
+ CHECK(code_edit->is_in_comment(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* check the region metadata. */
+ idx = code_edit->is_in_comment(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Next test nested comments. */
+ code_edit->add_comment_delimiter("^", "^", false);
+ CHECK(code_edit->has_comment_delimiter("^"));
+ CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+ code_edit->set_text(" \n # ^\n \n^ # \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in comment. */
+ CHECK(code_edit->is_in_comment(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3));
+
+ /* Check middle line. */
+ CHECK(code_edit->is_in_comment(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3));
+
+ /* Check column just before end key is in comment. */
+ CHECK(code_edit->is_in_comment(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3));
+
+ /* Check column after end key is not in comment. */
+ CHECK(code_edit->is_in_comment(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* check the region metadata. */
+ idx = code_edit->is_in_comment(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Next test no end key. */
+ code_edit->set_text(" \n # \n ");
+
+ /* check the region metadata. */
+ idx = code_edit->is_in_comment(1, 2);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(-1, -1));
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Check is in comment with no column returns true if entire line is comment excluding whitespace. */
+ code_edit->set_text(" \n # \n\n #\n ");
+ CHECK(code_edit->is_in_comment(1) != -1);
+ CHECK(code_edit->is_in_comment(2) != -1);
+ CHECK(code_edit->is_in_comment(3) != -1);
+
+ code_edit->set_text(" \n test # \n\n # test \n ");
+ CHECK(code_edit->is_in_comment(1) == -1);
+ CHECK(code_edit->is_in_comment(2) != -1);
+ CHECK(code_edit->is_in_comment(3) == -1);
+ }
+
+ SUBCASE("[CodeEdit] multiline mixed delimiters") {
+ /* Add comment delimiter. */
+ code_edit->add_comment_delimiter("#", "#", false);
+ CHECK(code_edit->has_comment_delimiter("#"));
+ CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+ /* Add string delimiter. */
+ code_edit->add_string_delimiter("^", "^", false);
+ CHECK(code_edit->has_string_delimiter("^"));
+ CHECK(code_edit->get_string_delimiters().size() == 1);
+
+ /* Nest a string inside a comment. */
+ code_edit->set_text(" \n # ^\n \n^ # \n ");
+
+ /* Check line above is not in comment. */
+ CHECK(code_edit->is_in_comment(0, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+ /* Check column before start key is not in comment. */
+ CHECK(code_edit->is_in_comment(1, 0) == -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+ /* Check column just after start key is in comment. */
+ CHECK(code_edit->is_in_comment(1, 2) != -1);
+ CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3));
+
+ /* Check middle line. */
+ CHECK(code_edit->is_in_comment(2, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3));
+
+ /* Check column just before end key is in comment. */
+ CHECK(code_edit->is_in_comment(3, 0) != -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+ CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3));
+
+ /* Check column after end key is not in comment. */
+ CHECK(code_edit->is_in_comment(3, 3) == -1);
+ CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+ /* Check line after is not in comment. */
+ CHECK(code_edit->is_in_comment(4, 1) == -1);
+ CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+ CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+ /* check the region metadata. */
+ int idx = code_edit->is_in_comment(1, 2);
+ CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+ CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+ /* Check is in comment with no column returns true as inner delimiter should not be counted. */
+ CHECK(code_edit->is_in_comment(1) != -1);
+ CHECK(code_edit->is_in_comment(2) != -1);
+ CHECK(code_edit->is_in_comment(3) != -1);
+ }
+ }
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] indent") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+ SUBCASE("[CodeEdit] indent settings") {
+ code_edit->set_indent_size(10);
+ CHECK(code_edit->get_indent_size() == 10);
+ CHECK(code_edit->get_tab_size() == 10);
+
+ code_edit->set_auto_indent_enabled(false);
+ CHECK_FALSE(code_edit->is_auto_indent_enabled());
+
+ code_edit->set_auto_indent_enabled(true);
+ CHECK(code_edit->is_auto_indent_enabled());
+
+ code_edit->set_indent_using_spaces(false);
+ CHECK_FALSE(code_edit->is_indent_using_spaces());
+
+ code_edit->set_indent_using_spaces(true);
+ CHECK(code_edit->is_indent_using_spaces());
+
+ /* Only the first char is registered. */
+ TypedArray<String> auto_indent_prefixes;
+ auto_indent_prefixes.push_back("::");
+ auto_indent_prefixes.push_back("s");
+ auto_indent_prefixes.push_back("1");
+ code_edit->set_auto_indent_prefixes(auto_indent_prefixes);
+
+ auto_indent_prefixes = code_edit->get_auto_indent_prefixes();
+ CHECK(auto_indent_prefixes.has(":"));
+ CHECK(auto_indent_prefixes.has("s"));
+ CHECK(auto_indent_prefixes.has("1"));
+ }
+
+ SUBCASE("[CodeEdit] indent tabs") {
+ code_edit->set_indent_size(4);
+ code_edit->set_auto_indent_enabled(true);
+ code_edit->set_indent_using_spaces(false);
+
+ /* Do nothing if not editable. */
+ code_edit->set_editable(false);
+
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0).is_empty());
+
+ code_edit->indent_lines();
+ CHECK(code_edit->get_line(0).is_empty());
+
+ code_edit->set_editable(true);
+
+ /* Simple indent. */
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "\t");
+
+ /* Check input action. */
+ SEND_GUI_ACTION(code_edit, "ui_text_indent");
+ CHECK(code_edit->get_line(0) == "\t\t");
+
+ /* Insert in place. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test");
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "test\t");
+
+ /* Indent lines does entire line and works without selection. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test");
+ code_edit->indent_lines();
+ CHECK(code_edit->get_line(0) == "\ttest");
+
+ /* Selection does entire line. */
+ code_edit->set_text("test");
+ code_edit->select_all();
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+
+ /* Handles multiple lines. */
+ code_edit->set_text("test\ntext");
+ code_edit->select_all();
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_line(1) == "\ttext");
+
+ /* Do not indent line if last col is zero. */
+ code_edit->set_text("test\ntext");
+ code_edit->select(0, 0, 1, 0);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Indent even if last column of first line. */
+ code_edit->set_text("test\ntext");
+ code_edit->select(0, 4, 1, 0);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Check selection is adjusted. */
+ code_edit->set_text("test");
+ code_edit->select(0, 1, 0, 2);
+ code_edit->do_indent();
+ CHECK(code_edit->get_selection_from_column() == 2);
+ CHECK(code_edit->get_selection_to_column() == 3);
+ CHECK(code_edit->get_line(0) == "\ttest");
+ code_edit->undo();
+ }
+
+ SUBCASE("[CodeEdit] indent spaces") {
+ code_edit->set_indent_size(4);
+ code_edit->set_auto_indent_enabled(true);
+ code_edit->set_indent_using_spaces(true);
+
+ /* Do nothing if not editable. */
+ code_edit->set_editable(false);
+
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0).is_empty());
+
+ code_edit->indent_lines();
+ CHECK(code_edit->get_line(0).is_empty());
+
+ code_edit->set_editable(true);
+
+ /* Simple indent. */
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " ");
+
+ /* Check input action. */
+ SEND_GUI_ACTION(code_edit, "ui_text_indent");
+ CHECK(code_edit->get_line(0) == " ");
+
+ /* Insert in place. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test");
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == "test ");
+
+ /* Indent lines does entire line and works without selection. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test");
+ code_edit->indent_lines();
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* Selection does entire line. */
+ code_edit->set_text("test");
+ code_edit->select_all();
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* single indent only add required spaces. */
+ code_edit->set_text(" test");
+ code_edit->select_all();
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* Handles multiple lines. */
+ code_edit->set_text("test\ntext");
+ code_edit->select_all();
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_line(1) == " text");
+
+ /* Do not indent line if last col is zero. */
+ code_edit->set_text("test\ntext");
+ code_edit->select(0, 0, 1, 0);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Indent even if last column of first line. */
+ code_edit->set_text("test\ntext");
+ code_edit->select(0, 4, 1, 0);
+ code_edit->do_indent();
+ CHECK(code_edit->get_line(0) == " test");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Check selection is adjusted. */
+ code_edit->set_text("test");
+ code_edit->select(0, 1, 0, 2);
+ code_edit->do_indent();
+ CHECK(code_edit->get_selection_from_column() == 5);
+ CHECK(code_edit->get_selection_to_column() == 6);
+ CHECK(code_edit->get_line(0) == " test");
+ }
+
+ SUBCASE("[CodeEdit] unindent tabs") {
+ code_edit->set_indent_size(4);
+ code_edit->set_auto_indent_enabled(true);
+ code_edit->set_indent_using_spaces(false);
+
+ /* Do nothing if not editable. */
+ code_edit->set_text("\t");
+
+ code_edit->set_editable(false);
+
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "\t");
+
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == "\t");
+
+ code_edit->set_editable(true);
+
+ /* Simple unindent. */
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "");
+
+ /* Should inindent inplace. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test\t");
+
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+
+ /* Backspace does a simple unindent. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("\t");
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == "");
+
+ /* Unindent lines does entire line and works without selection. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("\ttest");
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == "test");
+
+ /* Caret on col zero unindent line. */
+ code_edit->set_text("\t\ttest");
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+
+ /* Check input action. */
+ code_edit->set_text("\t\ttest");
+ SEND_GUI_ACTION(code_edit, "ui_text_dedent");
+ CHECK(code_edit->get_line(0) == "\ttest");
+
+ /* Selection does entire line. */
+ code_edit->set_text("\t\ttest");
+ code_edit->select_all();
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "\ttest");
+
+ /* Handles multiple lines. */
+ code_edit->set_text("\ttest\n\ttext");
+ code_edit->select_all();
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Do not unindent line if last col is zero. */
+ code_edit->set_text("\ttest\n\ttext");
+ code_edit->select(0, 0, 1, 0);
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == "\ttext");
+
+ /* Unindent even if last column of first line. */
+ code_edit->set_text("\ttest\n\ttext");
+ code_edit->select(0, 5, 1, 1);
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Check selection is adjusted. */
+ code_edit->set_text("\ttest");
+ code_edit->select(0, 1, 0, 2);
+ code_edit->do_unindent();
+ CHECK(code_edit->get_selection_from_column() == 0);
+ CHECK(code_edit->get_selection_to_column() == 1);
+ CHECK(code_edit->get_line(0) == "test");
+ }
+
+ SUBCASE("[CodeEdit] unindent spaces") {
+ code_edit->set_indent_size(4);
+ code_edit->set_auto_indent_enabled(true);
+ code_edit->set_indent_using_spaces(true);
+
+ /* Do nothing if not editable. */
+ code_edit->set_text(" ");
+
+ code_edit->set_editable(false);
+
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == " ");
+
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == " ");
+
+ code_edit->set_editable(true);
+
+ /* Simple unindent. */
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "");
+
+ /* Should inindent inplace. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test ");
+
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+
+ /* Backspace does a simple unindent. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret(" ");
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == "");
+
+ /* Backspace with letter. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret(" a");
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == " ");
+
+ /* Unindent lines does entire line and works without selection. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret(" test");
+ code_edit->unindent_lines();
+ CHECK(code_edit->get_line(0) == "test");
+
+ /* Caret on col zero unindent line. */
+ code_edit->set_text(" test");
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* Only as far as needed */
+ code_edit->set_text(" test");
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* Check input action. */
+ code_edit->set_text(" test");
+ SEND_GUI_ACTION(code_edit, "ui_text_dedent");
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* Selection does entire line. */
+ code_edit->set_text(" test");
+ code_edit->select_all();
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == " test");
+
+ /* Handles multiple lines. */
+ code_edit->set_text(" test\n text");
+ code_edit->select_all();
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Do not unindent line if last col is zero. */
+ code_edit->set_text(" test\n text");
+ code_edit->select(0, 0, 1, 0);
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == " text");
+
+ /* Unindent even if last column of first line. */
+ code_edit->set_text(" test\n text");
+ code_edit->select(0, 5, 1, 1);
+ code_edit->do_unindent();
+ CHECK(code_edit->get_line(0) == "test");
+ CHECK(code_edit->get_line(1) == "text");
+
+ /* Check selection is adjusted. */
+ code_edit->set_text(" test");
+ code_edit->select(0, 4, 0, 5);
+ code_edit->do_unindent();
+ CHECK(code_edit->get_selection_from_column() == 0);
+ CHECK(code_edit->get_selection_to_column() == 1);
+ CHECK(code_edit->get_line(0) == "test");
+ }
+
+ SUBCASE("[CodeEdit] auto indent") {
+ SUBCASE("[CodeEdit] auto indent tabs") {
+ code_edit->set_indent_size(4);
+ code_edit->set_auto_indent_enabled(true);
+ code_edit->set_indent_using_spaces(false);
+
+ /* Simple indent on new line. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test:");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test:");
+ CHECK(code_edit->get_line(1) == "\t");
+
+ /* new blank line should still indent. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test:");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == "test:");
+ CHECK(code_edit->get_line(1) == "\t");
+
+ /* new line above should not indent. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test:");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line(0) == "");
+ CHECK(code_edit->get_line(1) == "test:");
+
+ /* Whitespace between symbol and caret is okay. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test: ");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test: ");
+ CHECK(code_edit->get_line(1) == "\t");
+
+ /* Comment between symbol and caret is okay. */
+ code_edit->add_comment_delimiter("#", "");
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test: # comment");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test: # comment");
+ CHECK(code_edit->get_line(1) == "\t");
+ code_edit->remove_comment_delimiter("#");
+
+ /* Strings between symbol and caret are not okay. */
+ code_edit->add_string_delimiter("#", "");
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test: # string");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test: # string");
+ CHECK(code_edit->get_line(1) == "");
+ code_edit->remove_comment_delimiter("#");
+
+ /* If between brace pairs an extra line is added. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test{}");
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test{");
+ CHECK(code_edit->get_line(1) == "\t");
+ CHECK(code_edit->get_line(2) == "}");
+
+ /* Except when we are going above. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test{}");
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line(0) == "");
+ CHECK(code_edit->get_line(1) == "test{}");
+
+ /* or below. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test{}");
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == "test{}");
+ CHECK(code_edit->get_line(1) == "");
+ }
+
+ SUBCASE("[CodeEdit] auto indent spaces") {
+ code_edit->set_indent_size(4);
+ code_edit->set_auto_indent_enabled(true);
+ code_edit->set_indent_using_spaces(true);
+
+ /* Simple indent on new line. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test:");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test:");
+ CHECK(code_edit->get_line(1) == " ");
+
+ /* new blank line should still indent. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test:");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == "test:");
+ CHECK(code_edit->get_line(1) == " ");
+
+ /* new line above should not indent. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test:");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line(0) == "");
+ CHECK(code_edit->get_line(1) == "test:");
+
+ /* Whitespace between symbol and caret is okay. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test: ");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test: ");
+ CHECK(code_edit->get_line(1) == " ");
+
+ /* Comment between symbol and caret is okay. */
+ code_edit->add_comment_delimiter("#", "");
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test: # comment");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test: # comment");
+ CHECK(code_edit->get_line(1) == " ");
+ code_edit->remove_comment_delimiter("#");
+
+ /* Strings between symbol and caret are not okay. */
+ code_edit->add_string_delimiter("#", "");
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test: # string");
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test: # string");
+ CHECK(code_edit->get_line(1) == "");
+ code_edit->remove_comment_delimiter("#");
+
+ /* If between brace pairs an extra line is added. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test{}");
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline");
+ CHECK(code_edit->get_line(0) == "test{");
+ CHECK(code_edit->get_line(1) == " ");
+ CHECK(code_edit->get_line(2) == "}");
+
+ /* Except when we are going above. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test{}");
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+ CHECK(code_edit->get_line(0) == "");
+ CHECK(code_edit->get_line(1) == "test{}");
+
+ /* or below. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test{}");
+ code_edit->set_caret_column(5);
+ SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+ CHECK(code_edit->get_line(0) == "test{}");
+ CHECK(code_edit->get_line(1) == "");
+ }
+ }
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] folding") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+ SUBCASE("[CodeEdit] folding settings") {
+ code_edit->set_line_folding_enabled(true);
+ CHECK(code_edit->is_line_folding_enabled());
+
+ code_edit->set_line_folding_enabled(false);
+ CHECK_FALSE(code_edit->is_line_folding_enabled());
+ }
+
+ SUBCASE("[CodeEdit] folding") {
+ code_edit->set_line_folding_enabled(true);
+
+ // No indent.
+ code_edit->set_text("line1\nline2\nline3");
+ for (int i = 0; i < 2; i++) {
+ CHECK_FALSE(code_edit->can_fold_line(i));
+ code_edit->fold_line(i);
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Indented lines.
+ code_edit->set_text("\tline1\n\tline2\n\tline3");
+ for (int i = 0; i < 2; i++) {
+ CHECK_FALSE(code_edit->can_fold_line(i));
+ code_edit->fold_line(i);
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Indent.
+ code_edit->set_text("line1\n\tline2\nline3");
+ CHECK(code_edit->can_fold_line(0));
+ for (int i = 1; i < 2; i++) {
+ CHECK_FALSE(code_edit->can_fold_line(i));
+ code_edit->fold_line(i);
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2);
+
+ // Indent with blank lines.
+ code_edit->set_text("line1\n\tline2\n\n\nline3");
+ CHECK(code_edit->can_fold_line(0));
+ for (int i = 1; i < 2; i++) {
+ CHECK_FALSE(code_edit->can_fold_line(i));
+ code_edit->fold_line(i);
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2);
+
+ // Nested indents.
+ code_edit->set_text("line1\n\tline2\n\t\tline3\nline4");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK(code_edit->can_fold_line(1));
+ for (int i = 2; i < 3; i++) {
+ CHECK_FALSE(code_edit->can_fold_line(i));
+ code_edit->fold_line(i);
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK(code_edit->is_line_folded(1));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ CHECK_FALSE(code_edit->is_line_folded(3));
+ CHECK(code_edit->get_next_visible_line_offset_from(2, 1) == 2);
+
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ CHECK_FALSE(code_edit->is_line_folded(3));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3);
+
+ // Check metadata.
+ CHECK(code_edit->get_folded_lines().size() == 1);
+ CHECK((int)code_edit->get_folded_lines()[0] == 0);
+
+ // Cannot unfold nested.
+ code_edit->unfold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // (un)Fold all / toggle.
+ code_edit->unfold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Check metadata.
+ CHECK(code_edit->get_folded_lines().size() == 0);
+
+ code_edit->fold_all_lines();
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3);
+
+ code_edit->unfold_all_lines();
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ code_edit->toggle_foldable_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3);
+
+ // Can also unfold from hidden line.
+ code_edit->unfold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Blank lines.
+ code_edit->set_text("line1\n\tline2\n\n\n\ttest\n\nline3");
+ CHECK(code_edit->can_fold_line(0));
+ for (int i = 1; i < code_edit->get_line_count(); i++) {
+ CHECK_FALSE(code_edit->can_fold_line(i));
+ code_edit->fold_line(i);
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ for (int i = 1; i < code_edit->get_line_count(); i++) {
+ CHECK_FALSE(code_edit->is_line_folded(i));
+ }
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 5);
+
+ // End of file.
+ code_edit->set_text("line1\n\tline2");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Comment & string blocks.
+ // Single line block
+ code_edit->add_comment_delimiter("#", "", true);
+ code_edit->set_text("#line1\n#\tline2");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Has to be full line.
+ code_edit->set_text("test #line1\n#\tline2");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ code_edit->set_text("#line1\ntest #\tline2");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // String.
+ code_edit->add_string_delimiter("^", "", true);
+ code_edit->set_text("^line1\n^\tline2");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Has to be full line.
+ code_edit->set_text("test ^line1\n^\tline2");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ code_edit->set_text("^line1\ntest ^\tline2");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Multiline blocks.
+ code_edit->add_comment_delimiter("&", "&", false);
+ code_edit->set_text("&line1\n\tline2&\nline3");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2);
+
+ // Multiline comment before last line.
+ code_edit->set_text("&line1\nline2&\ntest");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(2));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2);
+
+ // Has to be full line.
+ code_edit->set_text("test &line1\n\tline2&");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ code_edit->set_text("&line1\n\tline2& test");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Strings.
+ code_edit->add_string_delimiter("$", "$", false);
+ code_edit->set_text("$line1\n\tline2$");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Has to be full line.
+ code_edit->set_text("test $line1\n\tline2$");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ code_edit->set_text("$line1\n\tline2$ test");
+ CHECK_FALSE(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK_FALSE(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+ // Non-indented comments/strings.
+ // Single line
+ code_edit->set_text("test\n\tline1\n#line1\n#line2\n\ttest");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4);
+
+ code_edit->set_text("test\n\tline1\n^line1\n^line2\n\ttest");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4);
+
+ // Indent level 0->1, comment after lines
+ code_edit->set_text("line1\n\tline2\n#test");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2);
+
+ // Indent level 0->1, comment between lines
+ code_edit->set_text("line1\n#test\n\tline2\nline3");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(2));
+ code_edit->fold_line(2);
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3);
+
+ // Indent level 1->2, comment after lines
+ code_edit->set_text("\tline1\n\t\tline2\n#test");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2);
+
+ // Indent level 1->2, comment between lines
+ code_edit->set_text("\tline1\n#test\n\t\tline2\nline3");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(2));
+ code_edit->fold_line(2);
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(2));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3);
+
+ // Multiline
+ code_edit->set_text("test\n\tline1\n&line1\nline2&\n\ttest");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4);
+
+ code_edit->set_text("test\n\tline1\n$line1\nline2$\n\ttest");
+ CHECK(code_edit->can_fold_line(0));
+ CHECK_FALSE(code_edit->can_fold_line(1));
+ code_edit->fold_line(1);
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ code_edit->fold_line(0);
+ CHECK(code_edit->is_line_folded(0));
+ CHECK_FALSE(code_edit->is_line_folded(1));
+ CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4);
+ }
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] completion") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+ SUBCASE("[CodeEdit] auto brace completion") {
+ code_edit->set_auto_brace_completion_enabled(true);
+ CHECK(code_edit->is_auto_brace_completion_enabled());
+
+ code_edit->set_highlight_matching_braces_enabled(true);
+ CHECK(code_edit->is_highlight_matching_braces_enabled());
+
+ /* Try setters, any length. */
+ Dictionary auto_brace_completion_pairs;
+ auto_brace_completion_pairs["["] = "]";
+ auto_brace_completion_pairs["'"] = "'";
+ auto_brace_completion_pairs[";"] = "'";
+ auto_brace_completion_pairs["'''"] = "'''";
+ code_edit->set_auto_brace_completion_pairs(auto_brace_completion_pairs);
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+ CHECK(code_edit->get_auto_brace_completion_pairs()["["] == "]");
+ CHECK(code_edit->get_auto_brace_completion_pairs()["'"] == "'");
+ CHECK(code_edit->get_auto_brace_completion_pairs()[";"] == "'");
+ CHECK(code_edit->get_auto_brace_completion_pairs()["'''"] == "'''");
+
+ ERR_PRINT_OFF;
+
+ /* No duplicate start keys. */
+ code_edit->add_auto_brace_completion_pair("[", "]");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ /* No empty keys. */
+ code_edit->add_auto_brace_completion_pair("[", "");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ code_edit->add_auto_brace_completion_pair("", "]");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ code_edit->add_auto_brace_completion_pair("", "");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ /* Must be a symbol. */
+ code_edit->add_auto_brace_completion_pair("a", "]");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ code_edit->add_auto_brace_completion_pair("[", "a");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ code_edit->add_auto_brace_completion_pair("a", "a");
+ CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+ ERR_PRINT_ON;
+
+ /* Check metadata. */
+ CHECK(code_edit->has_auto_brace_completion_open_key("["));
+ CHECK(code_edit->has_auto_brace_completion_open_key("'"));
+ CHECK(code_edit->has_auto_brace_completion_open_key(";"));
+ CHECK(code_edit->has_auto_brace_completion_open_key("'''"));
+ CHECK_FALSE(code_edit->has_auto_brace_completion_open_key("("));
+
+ CHECK(code_edit->has_auto_brace_completion_close_key("]"));
+ CHECK(code_edit->has_auto_brace_completion_close_key("'"));
+ CHECK(code_edit->has_auto_brace_completion_close_key("'''"));
+ CHECK_FALSE(code_edit->has_auto_brace_completion_close_key(")"));
+
+ CHECK(code_edit->get_auto_brace_completion_close_key("[") == "]");
+ CHECK(code_edit->get_auto_brace_completion_close_key("'") == "'");
+ CHECK(code_edit->get_auto_brace_completion_close_key(";") == "'");
+ CHECK(code_edit->get_auto_brace_completion_close_key("'''") == "'''");
+ CHECK(code_edit->get_auto_brace_completion_close_key("(").is_empty());
+
+ /* Check typing inserts closing pair. */
+ code_edit->clear();
+ SEND_GUI_KEY_EVENT(code_edit, Key::BRACKETLEFT);
+ CHECK(code_edit->get_line(0) == "[]");
+
+ /* Should first match and insert smaller key. */
+ code_edit->clear();
+ SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE);
+ CHECK(code_edit->get_line(0) == "''");
+ CHECK(code_edit->get_caret_column() == 1);
+
+ /* Move out from centre, Should match and insert larger key. */
+ SEND_GUI_ACTION(code_edit, "ui_text_caret_right");
+ SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE);
+ CHECK(code_edit->get_line(0) == "''''''");
+ CHECK(code_edit->get_caret_column() == 3);
+
+ /* Backspace should remove all. */
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK(code_edit->get_line(0).is_empty());
+
+ /* If in between and typing close key should "skip". */
+ SEND_GUI_KEY_EVENT(code_edit, Key::BRACKETLEFT);
+ CHECK(code_edit->get_line(0) == "[]");
+ CHECK(code_edit->get_caret_column() == 1);
+ SEND_GUI_KEY_EVENT(code_edit, Key::BRACKETRIGHT);
+ CHECK(code_edit->get_line(0) == "[]");
+ CHECK(code_edit->get_caret_column() == 2);
+
+ /* If current is char and inserting a string, do not autocomplete. */
+ code_edit->clear();
+ SEND_GUI_KEY_EVENT(code_edit, Key::A);
+ SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE);
+ CHECK(code_edit->get_line(0) == "A'");
+
+ /* If in comment, do not complete. */
+ code_edit->add_comment_delimiter("#", "");
+ code_edit->clear();
+ SEND_GUI_KEY_EVENT(code_edit, Key::NUMBERSIGN);
+ SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE);
+ CHECK(code_edit->get_line(0) == "#'");
+
+ /* If in string, and inserting string do not complete. */
+ code_edit->clear();
+ SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE);
+ SEND_GUI_KEY_EVENT(code_edit, Key::QUOTEDBL);
+ CHECK(code_edit->get_line(0) == "'\"'");
+ }
+
+ SUBCASE("[CodeEdit] autocomplete") {
+ code_edit->set_code_completion_enabled(true);
+ CHECK(code_edit->is_code_completion_enabled());
+
+ /* Set prefixes, single char only, disallow empty. */
+ TypedArray<String> completion_prefixes;
+ completion_prefixes.push_back("");
+ completion_prefixes.push_back(".");
+ completion_prefixes.push_back(".");
+ completion_prefixes.push_back(",,");
+
+ ERR_PRINT_OFF;
+ code_edit->set_code_completion_prefixes(completion_prefixes);
+ ERR_PRINT_ON;
+ completion_prefixes = code_edit->get_code_completion_prefixes();
+ CHECK(completion_prefixes.size() == 2);
+ CHECK(completion_prefixes.has("."));
+ CHECK(completion_prefixes.has(","));
+
+ code_edit->set_text("test\ntest");
+ CHECK(code_edit->get_text_for_code_completion() == String::chr(0xFFFF) + "test\ntest");
+ }
+
+ SUBCASE("[CodeEdit] autocomplete request") {
+ SIGNAL_WATCH(code_edit, "request_code_completion");
+ code_edit->set_code_completion_enabled(true);
+
+ Array signal_args;
+ signal_args.push_back(Array());
+
+ /* Force request. */
+ code_edit->request_code_completion();
+ SIGNAL_CHECK_FALSE("request_code_completion");
+ code_edit->request_code_completion(true);
+ SIGNAL_CHECK("request_code_completion", signal_args);
+
+ /* Manual request should force. */
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_query");
+ SIGNAL_CHECK("request_code_completion", signal_args);
+
+ /* Insert prefix. */
+ TypedArray<String> completion_prefixes;
+ completion_prefixes.push_back(".");
+ code_edit->set_code_completion_prefixes(completion_prefixes);
+
+ code_edit->insert_text_at_caret(".");
+ code_edit->request_code_completion();
+ SIGNAL_CHECK("request_code_completion", signal_args);
+
+ /* Should work with space too. */
+ code_edit->insert_text_at_caret(" ");
+ code_edit->request_code_completion();
+ SIGNAL_CHECK("request_code_completion", signal_args);
+
+ /* Should work when complete ends with prefix. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("t");
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "test.", "test.");
+ code_edit->update_code_completion_options();
+ code_edit->confirm_code_completion();
+ CHECK(code_edit->get_line(0) == "test.");
+ SIGNAL_CHECK("request_code_completion", signal_args);
+
+ SIGNAL_UNWATCH(code_edit, "request_code_completion");
+ }
+
+ SUBCASE("[CodeEdit] autocomplete completion") {
+ CHECK(code_edit->get_code_completion_selected_index() == -1);
+ code_edit->set_code_completion_enabled(true);
+ CHECK(code_edit->get_code_completion_selected_index() == -1);
+
+ code_edit->update_code_completion_options();
+ code_edit->set_code_completion_selected_index(1);
+ CHECK(code_edit->get_code_completion_selected_index() == -1);
+ CHECK(code_edit->get_code_completion_option(0).size() == 0);
+ CHECK(code_edit->get_code_completion_options().size() == 0);
+
+ /* Adding does not update the list. */
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_0.", "item_0");
+
+ code_edit->set_code_completion_selected_index(1);
+ CHECK(code_edit->get_code_completion_selected_index() == -1);
+ CHECK(code_edit->get_code_completion_option(0).size() == 0);
+ CHECK(code_edit->get_code_completion_options().size() == 0);
+
+ /* After update, pending add should not be counted, */
+ /* also does not work on col 0 */
+ code_edit->insert_text_at_caret("i");
+ code_edit->update_code_completion_options();
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0", Color(1, 0, 0), RES(), Color(1, 0, 0));
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_1.", "item_1");
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_2.", "item_2");
+
+ ERR_PRINT_OFF;
+ code_edit->set_code_completion_selected_index(1);
+ ERR_PRINT_ON;
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+ CHECK(code_edit->get_code_completion_option(0).size() == 6);
+ CHECK(code_edit->get_code_completion_options().size() == 1);
+
+ /* Check cancel closes completion. */
+ SEND_GUI_ACTION(code_edit, "ui_cancel");
+ CHECK(code_edit->get_code_completion_selected_index() == -1);
+
+ code_edit->update_code_completion_options();
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+ code_edit->set_code_completion_selected_index(1);
+ CHECK(code_edit->get_code_completion_selected_index() == 1);
+ CHECK(code_edit->get_code_completion_option(0).size() == 6);
+ CHECK(code_edit->get_code_completion_options().size() == 3);
+
+ /* Check data. */
+ Dictionary option = code_edit->get_code_completion_option(0);
+ CHECK((int)option["kind"] == (int)CodeEdit::CodeCompletionKind::KIND_CLASS);
+ CHECK(option["display_text"] == "item_0.");
+ CHECK(option["insert_text"] == "item_0");
+ CHECK(option["font_color"] == Color(1, 0, 0));
+ CHECK(option["icon"] == RES());
+ CHECK(option["default_value"] == Color(1, 0, 0));
+
+ /* Set size for mouse input. */
+ code_edit->set_size(Size2(100, 100));
+
+ /* Check input. */
+ SEND_GUI_ACTION(code_edit, "ui_end");
+ CHECK(code_edit->get_code_completion_selected_index() == 2);
+
+ SEND_GUI_ACTION(code_edit, "ui_home");
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ SEND_GUI_ACTION(code_edit, "ui_page_down");
+ CHECK(code_edit->get_code_completion_selected_index() == 2);
+
+ SEND_GUI_ACTION(code_edit, "ui_page_up");
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ SEND_GUI_ACTION(code_edit, "ui_up");
+ CHECK(code_edit->get_code_completion_selected_index() == 2);
+
+ SEND_GUI_ACTION(code_edit, "ui_down");
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ SEND_GUI_KEY_EVENT(code_edit, Key::T);
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ SEND_GUI_ACTION(code_edit, "ui_left");
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ SEND_GUI_ACTION(code_edit, "ui_right");
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ Point2 caret_pos = code_edit->get_caret_draw_pos();
+ caret_pos.y -= code_edit->get_line_height();
+ SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MouseButton::WHEEL_DOWN, MouseButton::NONE);
+ CHECK(code_edit->get_code_completion_selected_index() == 1);
+
+ SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MouseButton::WHEEL_UP, MouseButton::NONE);
+ CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+ /* Single click selects. */
+ SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MouseButton::LEFT, MouseButton::MASK_LEFT);
+ CHECK(code_edit->get_code_completion_selected_index() == 2);
+
+ /* Double click inserts. */
+ SEND_GUI_DOUBLE_CLICK(code_edit, caret_pos);
+ CHECK(code_edit->get_code_completion_selected_index() == -1);
+ CHECK(code_edit->get_line(0) == "item_2");
+
+ code_edit->set_auto_brace_completion_enabled(false);
+
+ /* Does nothing in readonly. */
+ code_edit->undo();
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+ code_edit->update_code_completion_options();
+ code_edit->set_editable(false);
+ code_edit->confirm_code_completion();
+ code_edit->set_editable(true);
+ CHECK(code_edit->get_line(0) == "i");
+
+ /* Replace */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0 test");
+
+ /* Replace string. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("\"item_1 test\"");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "\"item_0\"");
+
+ /* Normal replace if no end is given. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("\"item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "\"item_0\" test");
+
+ /* Insert at completion. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_accept");
+ CHECK(code_edit->get_line(0) == "item_01 test");
+
+ /* Insert at completion with string should have same output. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("\"item_1 test\"");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_accept");
+ CHECK(code_edit->get_line(0) == "\"item_0\"1 test\"");
+
+ /* Merge symbol at end on insert text. */
+ /* End on completion entry. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0(");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0( test");
+ CHECK(code_edit->get_caret_column() == 7);
+
+ /* End of text*/
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1( test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0( test");
+ CHECK(code_edit->get_caret_column() == 6);
+
+ /* End of both. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1( test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0(");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0( test");
+ CHECK(code_edit->get_caret_column() == 7);
+
+ /* Full set. */
+ /* End on completion entry. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 8);
+
+ /* End of text*/
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1() test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 6);
+
+ /* End of both. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1() test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 8);
+
+ /* Autobrace completion. */
+ code_edit->set_auto_brace_completion_enabled(true);
+
+ /* End on completion entry. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0(");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 7);
+
+ /* End of text*/
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1( test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0( test");
+ CHECK(code_edit->get_caret_column() == 6);
+
+ /* End of both. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1( test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0(");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0( test");
+ CHECK(code_edit->get_caret_column() == 7);
+
+ /* Full set. */
+ /* End on completion entry. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1 test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 8);
+
+ /* End of text*/
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1() test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 6);
+
+ /* End of both. */
+ code_edit->clear();
+ code_edit->insert_text_at_caret("item_1() test");
+ code_edit->set_caret_column(2);
+ code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()");
+ code_edit->update_code_completion_options();
+ SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+ CHECK(code_edit->get_line(0) == "item_0() test");
+ CHECK(code_edit->get_caret_column() == 8);
+ }
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] symbol lookup") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+ code_edit->set_symbol_lookup_on_click_enabled(true);
+ CHECK(code_edit->is_symbol_lookup_on_click_enabled());
+
+ /* Set size for mouse input. */
+ code_edit->set_size(Size2(100, 100));
+
+ code_edit->set_text("this is some text");
+
+ Point2 caret_pos = code_edit->get_caret_draw_pos();
+ caret_pos.x += 55;
+ SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MouseButton::NONE, MouseButton::NONE);
+ CHECK(code_edit->get_text_for_symbol_lookup() == "this is s" + String::chr(0xFFFF) + "ome text");
+
+ SIGNAL_WATCH(code_edit, "symbol_validate");
+
+#ifdef OSX_ENABLED
+ SEND_GUI_KEY_EVENT(code_edit, Key::META);
+#else
+ SEND_GUI_KEY_EVENT(code_edit, Key::CTRL);
+#endif
+
+ Array signal_args;
+ Array arg;
+ arg.push_back("some");
+ signal_args.push_back(arg);
+ SIGNAL_CHECK("symbol_validate", signal_args);
+
+ SIGNAL_UNWATCH(code_edit, "symbol_validate");
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] line length guidelines") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+ TypedArray<int> guide_lines;
+
+ code_edit->set_line_length_guidelines(guide_lines);
+ CHECK(code_edit->get_line_length_guidelines().size() == 0);
+
+ guide_lines.push_back(80);
+ guide_lines.push_back(120);
+
+ /* Order should be preserved. */
+ code_edit->set_line_length_guidelines(guide_lines);
+ CHECK((int)code_edit->get_line_length_guidelines()[0] == 80);
+ CHECK((int)code_edit->get_line_length_guidelines()[1] == 120);
+
+ memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] Backspace delete") {
+ CodeEdit *code_edit = memnew(CodeEdit);
+ SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+ /* Backspace with selection on first line. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test backspace");
+ code_edit->select(0, 0, 0, 5);
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == "backspace");
+
+ /* Backspace with selection on first line and caret at the beginning of file. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("test backspace");
+ code_edit->select(0, 0, 0, 5);
+ code_edit->set_caret_column(0);
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == "backspace");
+
+ /* Move caret up to the previous line on backspace if carret is at the first column. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("line 1\nline 2");
+ code_edit->set_caret_line(1);
+ code_edit->set_caret_column(0);
+ code_edit->backspace();
+ CHECK(code_edit->get_line(0) == "line 1line 2");
+ CHECK(code_edit->get_caret_line() == 0);
+ CHECK(code_edit->get_caret_column() == 6);
+
+ /* Backspace delete all text if all text is selected. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("line 1\nline 2\nline 3");
+ code_edit->select_all();
+ code_edit->backspace();
+ CHECK(code_edit->get_text().is_empty());
+
+ /* Backspace at the beginning without selection has no effect. */
+ code_edit->set_text("");
+ code_edit->insert_text_at_caret("line 1\nline 2\nline 3");
+ code_edit->set_caret_line(0);
+ code_edit->set_caret_column(0);
+ code_edit->backspace();
+ CHECK(code_edit->get_text() == "line 1\nline 2\nline 3");
+
+ memdelete(code_edit);
+}
+
+} // namespace TestCodeEdit
+
+#endif // TEST_CODE_EDIT_H
diff --git a/tests/scene/test_curve.h b/tests/scene/test_curve.h
new file mode 100644
index 0000000000..0370ab15fd
--- /dev/null
+++ b/tests/scene/test_curve.h
@@ -0,0 +1,254 @@
+/*************************************************************************/
+/* test_curve.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_CURVE_H
+#define TEST_CURVE_H
+
+#include "scene/resources/curve.h"
+
+#include "tests/test_macros.h"
+
+namespace TestCurve {
+
+TEST_CASE("[Curve] Default curve") {
+ const Ref<Curve> curve = memnew(Curve);
+
+ CHECK_MESSAGE(
+ curve->get_point_count() == 0,
+ "Default curve should contain the expected number of points.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->interpolate(0)),
+ "Default curve should return the expected value at offset 0.0.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->interpolate(0.5)),
+ "Default curve should return the expected value at offset 0.5.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->interpolate(1)),
+ "Default curve should return the expected value at offset 1.0.");
+}
+
+TEST_CASE("[Curve] Custom curve with free tangents") {
+ Ref<Curve> curve = memnew(Curve);
+ // "Sawtooth" curve with an open ending towards the 1.0 offset.
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(0.25, 1));
+ curve->add_point(Vector2(0.5, 0));
+ curve->add_point(Vector2(0.75, 1));
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->get_point_left_tangent(0)),
+ "get_point_left_tangent() should return the expected value for point index 0.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->get_point_right_tangent(0)),
+ "get_point_right_tangent() should return the expected value for point index 0.");
+ CHECK_MESSAGE(
+ curve->get_point_left_mode(0) == Curve::TangentMode::TANGENT_FREE,
+ "get_point_left_mode() should return the expected value for point index 0.");
+ CHECK_MESSAGE(
+ curve->get_point_right_mode(0) == Curve::TangentMode::TANGENT_FREE,
+ "get_point_right_mode() should return the expected value for point index 0.");
+
+ CHECK_MESSAGE(
+ curve->get_point_count() == 4,
+ "Custom free curve should contain the expected number of points.");
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->interpolate(-0.1)),
+ "Custom free curve should return the expected value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate(0.1), (real_t)0.352),
+ "Custom free curve should return the expected value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate(0.4), (real_t)0.352),
+ "Custom free curve should return the expected value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate(0.7), (real_t)0.896),
+ "Custom free curve should return the expected value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate(1), 1),
+ "Custom free curve should return the expected value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate(2), 1),
+ "Custom free curve should return the expected value at offset 0.1.");
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->interpolate_baked(-0.1)),
+ "Custom free curve should return the expected baked value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate_baked(0.1), (real_t)0.352),
+ "Custom free curve should return the expected baked value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate_baked(0.4), (real_t)0.352),
+ "Custom free curve should return the expected baked value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate_baked(0.7), (real_t)0.896),
+ "Custom free curve should return the expected baked value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate_baked(1), 1),
+ "Custom free curve should return the expected baked value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate_baked(2), 1),
+ "Custom free curve should return the expected baked value at offset 0.1.");
+
+ curve->remove_point(1);
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate(0.1), 0),
+ "Custom free curve should return the expected value at offset 0.1 after removing point at index 1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate_baked(0.1), 0),
+ "Custom free curve should return the expected baked value at offset 0.1 after removing point at index 1.");
+
+ curve->clear_points();
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate(0.6), 0),
+ "Custom free curve should return the expected value at offset 0.6 after clearing all points.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate_baked(0.6), 0),
+ "Custom free curve should return the expected baked value at offset 0.6 after clearing all points.");
+}
+
+TEST_CASE("[Curve] Custom curve with linear tangents") {
+ Ref<Curve> curve = memnew(Curve);
+ // "Sawtooth" curve with an open ending towards the 1.0 offset.
+ curve->add_point(Vector2(0, 0), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR);
+ curve->add_point(Vector2(0.25, 1), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR);
+ curve->add_point(Vector2(0.5, 0), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR);
+ curve->add_point(Vector2(0.75, 1), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR);
+
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->get_point_left_tangent(3), 4),
+ "get_point_left_tangent() should return the expected value for point index 3.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->get_point_right_tangent(3)),
+ "get_point_right_tangent() should return the expected value for point index 3.");
+ CHECK_MESSAGE(
+ curve->get_point_left_mode(3) == Curve::TangentMode::TANGENT_LINEAR,
+ "get_point_left_mode() should return the expected value for point index 3.");
+ CHECK_MESSAGE(
+ curve->get_point_right_mode(3) == Curve::TangentMode::TANGENT_LINEAR,
+ "get_point_right_mode() should return the expected value for point index 3.");
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->get_point_right_tangent(300)),
+ "get_point_right_tangent() should return the expected value for invalid point index 300.");
+ CHECK_MESSAGE(
+ curve->get_point_left_mode(-12345) == Curve::TangentMode::TANGENT_FREE,
+ "get_point_left_mode() should return the expected value for invalid point index -12345.");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ curve->get_point_count() == 4,
+ "Custom linear curve should contain the expected number of points.");
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->interpolate(-0.1)),
+ "Custom linear curve should return the expected value at offset -0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate(0.1), (real_t)0.4),
+ "Custom linear curve should return the expected value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate(0.4), (real_t)0.4),
+ "Custom linear curve should return the expected value at offset 0.4.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate(0.7), (real_t)0.8),
+ "Custom linear curve should return the expected value at offset 0.7.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate(1), 1),
+ "Custom linear curve should return the expected value at offset 1.0.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate(2), 1),
+ "Custom linear curve should return the expected value at offset 2.0.");
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->interpolate_baked(-0.1)),
+ "Custom linear curve should return the expected baked value at offset -0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate_baked(0.1), (real_t)0.4),
+ "Custom linear curve should return the expected baked value at offset 0.1.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate_baked(0.4), (real_t)0.4),
+ "Custom linear curve should return the expected baked value at offset 0.4.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate_baked(0.7), (real_t)0.8),
+ "Custom linear curve should return the expected baked value at offset 0.7.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate_baked(1), 1),
+ "Custom linear curve should return the expected baked value at offset 1.0.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate_baked(2), 1),
+ "Custom linear curve should return the expected baked value at offset 2.0.");
+
+ ERR_PRINT_OFF;
+ curve->remove_point(10);
+ ERR_PRINT_ON;
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate(0.7), (real_t)0.8),
+ "Custom free curve should return the expected value at offset 0.7 after removing point at invalid index 10.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(curve->interpolate_baked(0.7), (real_t)0.8),
+ "Custom free curve should return the expected baked value at offset 0.7 after removing point at invalid index 10.");
+}
+
+TEST_CASE("[Curve2D] Linear sampling should return exact value") {
+ Ref<Curve2D> curve = memnew(Curve2D);
+ real_t len = 2048.0;
+
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(len, 0));
+
+ real_t baked_length = curve->get_baked_length();
+ CHECK(len == baked_length);
+
+ for (int i = 0; i < len; i++) {
+ Vector2 pos = curve->interpolate_baked(i);
+ CHECK_MESSAGE(pos.x == i, "interpolate_baked should return exact value");
+ }
+}
+
+TEST_CASE("[Curve3D] Linear sampling should return exact value") {
+ Ref<Curve3D> curve = memnew(Curve3D);
+ real_t len = 2048.0;
+
+ curve->add_point(Vector3(0, 0, 0));
+ curve->add_point(Vector3(len, 0, 0));
+
+ real_t baked_length = curve->get_baked_length();
+ CHECK(len == baked_length);
+
+ for (int i = 0; i < len; i++) {
+ Vector3 pos = curve->interpolate_baked(i);
+ CHECK_MESSAGE(pos.x == i, "interpolate_baked should return exact value");
+ }
+}
+
+} // namespace TestCurve
+
+#endif // TEST_CURVE_H
diff --git a/tests/scene/test_gradient.h b/tests/scene/test_gradient.h
new file mode 100644
index 0000000000..b0e6128932
--- /dev/null
+++ b/tests/scene/test_gradient.h
@@ -0,0 +1,149 @@
+/*************************************************************************/
+/* test_gradient.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_GRADIENT_H
+#define TEST_GRADIENT_H
+
+#include "scene/resources/gradient.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestGradient {
+
+TEST_CASE("[Gradient] Default gradient") {
+ // Black-white gradient.
+ Ref<Gradient> gradient = memnew(Gradient);
+
+ CHECK_MESSAGE(
+ gradient->get_points_count() == 2,
+ "Default gradient should contain the expected number of points.");
+
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(0.0).is_equal_approx(Color(0, 0, 0)),
+ "Default gradient should return the expected interpolated value at offset 0.0.");
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(0.4).is_equal_approx(Color(0.4, 0.4, 0.4)),
+ "Default gradient should return the expected interpolated value at offset 0.4.");
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(0.8).is_equal_approx(Color(0.8, 0.8, 0.8)),
+ "Default gradient should return the expected interpolated value at offset 0.8.");
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(1.0).is_equal_approx(Color(1, 1, 1)),
+ "Default gradient should return the expected interpolated value at offset 1.0.");
+
+ // Out of bounds checks.
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(-1.0).is_equal_approx(Color(0, 0, 0)),
+ "Default gradient should return the expected interpolated value at offset -1.0.");
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(1234.0).is_equal_approx(Color(1, 1, 1)),
+ "Default gradient should return the expected interpolated value at offset 1234.0.");
+}
+
+TEST_CASE("[Gradient] Custom gradient (points specified in order)") {
+ // Red-yellow-green gradient (with overbright green).
+ Ref<Gradient> gradient = memnew(Gradient);
+ Vector<Gradient::Point> points;
+ points.push_back({ 0.0, Color(1, 0, 0) });
+ points.push_back({ 0.5, Color(1, 1, 0) });
+ points.push_back({ 1.0, Color(0, 2, 0) });
+ gradient->set_points(points);
+
+ CHECK_MESSAGE(
+ gradient->get_points_count() == 3,
+ "Custom gradient should contain the expected number of points.");
+
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(0.0).is_equal_approx(Color(1, 0, 0)),
+ "Custom gradient should return the expected interpolated value at offset 0.0.");
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(0.25).is_equal_approx(Color(1, 0.5, 0)),
+ "Custom gradient should return the expected interpolated value at offset 0.25.");
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(0.5).is_equal_approx(Color(1, 1, 0)),
+ "Custom gradient should return the expected interpolated value at offset 0.5.");
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(0.75).is_equal_approx(Color(0.5, 1.5, 0)),
+ "Custom gradient should return the expected interpolated value at offset 0.75.");
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(1.0).is_equal_approx(Color(0, 2, 0)),
+ "Custom gradient should return the expected interpolated value at offset 1.0.");
+
+ gradient->remove_point(1);
+ CHECK_MESSAGE(
+ gradient->get_points_count() == 2,
+ "Custom gradient should contain the expected number of points after removing one point.");
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(0.5).is_equal_approx(Color(0.5, 1, 0)),
+ "Custom gradient should return the expected interpolated value at offset 0.5 after removing point at index 1.");
+}
+
+TEST_CASE("[Gradient] Custom gradient (points specified out-of-order)") {
+ // HSL rainbow with points specified out of order.
+ // These should be sorted automatically when adding points.
+ Ref<Gradient> gradient = memnew(Gradient);
+ Vector<Gradient::Point> points;
+ points.push_back({ 0.2, Color(1, 0, 0) });
+ points.push_back({ 0.0, Color(1, 1, 0) });
+ points.push_back({ 0.8, Color(0, 1, 0) });
+ points.push_back({ 0.4, Color(0, 1, 1) });
+ points.push_back({ 1.0, Color(0, 0, 1) });
+ points.push_back({ 0.6, Color(1, 0, 1) });
+ gradient->set_points(points);
+
+ CHECK_MESSAGE(
+ gradient->get_points_count() == 6,
+ "Custom out-of-order gradient should contain the expected number of points.");
+
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(0.0).is_equal_approx(Color(1, 1, 0)),
+ "Custom out-of-order gradient should return the expected interpolated value at offset 0.0.");
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(0.3).is_equal_approx(Color(0.5, 0.5, 0.5)),
+ "Custom out-of-order gradient should return the expected interpolated value at offset 0.3.");
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(0.6).is_equal_approx(Color(1, 0, 1)),
+ "Custom out-of-order gradient should return the expected interpolated value at offset 0.6.");
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(1.0).is_equal_approx(Color(0, 0, 1)),
+ "Custom out-of-order gradient should return the expected interpolated value at offset 1.0.");
+
+ gradient->remove_point(0);
+ CHECK_MESSAGE(
+ gradient->get_points_count() == 5,
+ "Custom out-of-order gradient should contain the expected number of points after removing one point.");
+ // The color will be clamped to the nearest point (which is at offset 0.2).
+ CHECK_MESSAGE(
+ gradient->get_color_at_offset(0.1).is_equal_approx(Color(1, 0, 0)),
+ "Custom out-of-order gradient should return the expected interpolated value at offset 0.1 after removing point at index 0.");
+}
+} // namespace TestGradient
+
+#endif // TEST_GRADIENT_H
diff --git a/tests/scene/test_gui.cpp b/tests/scene/test_gui.cpp
new file mode 100644
index 0000000000..cd5624b70c
--- /dev/null
+++ b/tests/scene/test_gui.cpp
@@ -0,0 +1,259 @@
+/*************************************************************************/
+/* test_gui.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef _3D_DISABLED
+
+#include "test_gui.h"
+
+#include "scene/gui/button.h"
+#include "scene/gui/label.h"
+#include "scene/gui/line_edit.h"
+#include "scene/gui/menu_button.h"
+#include "scene/gui/option_button.h"
+#include "scene/gui/panel.h"
+#include "scene/gui/progress_bar.h"
+#include "scene/gui/rich_text_label.h"
+#include "scene/gui/scroll_bar.h"
+#include "scene/gui/spin_box.h"
+#include "scene/gui/tab_container.h"
+#include "scene/gui/tree.h"
+
+namespace TestGUI {
+
+class TestMainLoop : public SceneTree {
+public:
+ virtual void request_quit() {
+ quit();
+ }
+ virtual void initialize() {
+ SceneTree::initialize();
+
+ Panel *frame = memnew(Panel);
+ frame->set_anchor(SIDE_RIGHT, Control::ANCHOR_END);
+ frame->set_anchor(SIDE_BOTTOM, Control::ANCHOR_END);
+ frame->set_end(Point2(0, 0));
+
+ Ref<Theme> t = memnew(Theme);
+ frame->set_theme(t);
+
+ get_root()->add_child(frame);
+
+ Label *label = memnew(Label);
+
+ label->set_position(Point2(80, 90));
+ label->set_size(Point2(170, 80));
+ label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_FILL);
+ label->set_text("There was once upon a time a beautiful unicorn that loved to play with little girls...");
+
+ frame->add_child(label);
+
+ Button *button = memnew(Button);
+
+ button->set_position(Point2(20, 20));
+ button->set_size(Point2(1, 1));
+ button->set_text("This is a biggie button");
+
+ frame->add_child(button);
+
+ Tree *tree = memnew(Tree);
+ tree->set_columns(2);
+
+ tree->set_position(Point2(230, 210));
+ tree->set_size(Point2(150, 250));
+
+ TreeItem *item = tree->create_item();
+ item->set_editable(0, true);
+ item->set_text(0, "root");
+ item = tree->create_item(tree->get_root());
+ item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
+ item->set_editable(0, true);
+ item->set_text(0, "check");
+ item->set_cell_mode(1, TreeItem::CELL_MODE_CHECK);
+ item->set_editable(1, true);
+ item->set_text(1, "check2");
+ item = tree->create_item(tree->get_root());
+ item->set_cell_mode(0, TreeItem::CELL_MODE_RANGE);
+ item->set_editable(0, true);
+ item->set_range_config(0, 0, 20, 0.1);
+ item->set_range(0, 2);
+ item->add_button(0, Theme::get_default()->get_icon("folder", "FileDialog"));
+ item->set_cell_mode(1, TreeItem::CELL_MODE_RANGE);
+ item->set_editable(1, true);
+ item->set_range_config(1, 0, 20, 0.1);
+ item->set_range(1, 3);
+
+ item = tree->create_item(tree->get_root());
+ item->set_cell_mode(0, TreeItem::CELL_MODE_RANGE);
+ item->set_editable(0, true);
+ item->set_text(0, "Have,Many,Several,Options!");
+ item->set_range(0, 2);
+
+ item = tree->create_item(item);
+ item->set_editable(0, true);
+ item->set_text(0, "Gershwin!");
+
+ frame->add_child(tree);
+
+ LineEdit *line_edit = memnew(LineEdit);
+
+ line_edit->set_position(Point2(30, 190));
+ line_edit->set_size(Point2(180, 1));
+
+ frame->add_child(line_edit);
+
+ HScrollBar *hscroll = memnew(HScrollBar);
+
+ hscroll->set_position(Point2(30, 290));
+ hscroll->set_size(Point2(180, 1));
+ hscroll->set_max(10);
+ hscroll->set_page(4);
+
+ frame->add_child(hscroll);
+
+ SpinBox *spin = memnew(SpinBox);
+
+ spin->set_position(Point2(30, 260));
+ spin->set_size(Point2(120, 1));
+
+ frame->add_child(spin);
+ hscroll->share(spin);
+
+ ProgressBar *progress = memnew(ProgressBar);
+
+ progress->set_position(Point2(30, 330));
+ progress->set_size(Point2(120, 1));
+
+ frame->add_child(progress);
+ hscroll->share(progress);
+
+ MenuButton *menu_button = memnew(MenuButton);
+
+ menu_button->set_text("I'm a menu!");
+ menu_button->set_position(Point2(30, 380));
+ menu_button->set_size(Point2(1, 1));
+
+ frame->add_child(menu_button);
+
+ PopupMenu *popup = menu_button->get_popup();
+
+ popup->add_item("Hello, testing");
+ popup->add_item("My Dearest");
+ popup->add_separator();
+ popup->add_item("Popup");
+ popup->add_check_item("Check Popup");
+ popup->set_item_checked(4, true);
+ popup->add_separator();
+ popup->add_radio_check_item("Option A");
+ popup->set_item_checked(6, true);
+ popup->add_radio_check_item("Option B");
+
+ OptionButton *options = memnew(OptionButton);
+
+ options->add_item("Hello, testing");
+ options->add_item("My Dearest");
+
+ options->set_position(Point2(230, 180));
+ options->set_size(Point2(1, 1));
+
+ frame->add_child(options);
+
+ RichTextLabel *richtext = memnew(RichTextLabel);
+
+ richtext->set_position(Point2(600, 210));
+ richtext->set_size(Point2(180, 250));
+ richtext->set_anchor_and_offset(SIDE_RIGHT, Control::ANCHOR_END, -20);
+
+ frame->add_child(richtext);
+
+ richtext->add_text("Hello, My Friends!\n\nWelcome to the amazing world of ");
+
+ richtext->add_newline();
+ richtext->add_newline();
+
+ richtext->push_color(Color(1, 0.5, 0.5));
+ richtext->add_text("leprechauns");
+ richtext->pop();
+
+ richtext->add_text(" and ");
+ richtext->push_color(Color(0, 1.0, 0.5));
+ richtext->add_text("faeries.\n");
+ richtext->pop();
+ richtext->add_text("In this new episode, we will attempt to ");
+ richtext->push_font(richtext->get_theme_font(SNAME("mono_font"), SNAME("Fonts")));
+ richtext->push_color(Color(0.7, 0.5, 1.0));
+ richtext->add_text("deliver something nice");
+ richtext->pop();
+ richtext->pop();
+ richtext->add_text(" to all the viewers! Unfortunately, I need to ");
+ richtext->push_underline();
+ richtext->add_text("keep writing a lot of text");
+ richtext->pop();
+ richtext->add_text(" so the label control overflows and the scrollbar appears.\n");
+ richtext->push_meta("http://www.scrollingcapabilities.xz");
+ richtext->add_text("This allows to test for the scrolling capabilities ");
+ richtext->pop();
+ richtext->add_text("of the rich text label for huge text (not like this text will really be huge but, you know).\nAs long as it is so long that it will work nicely for a test/demo, then it's welcomed in my book...\nChanging subject, the day is cloudy today and I'm wondering if I'll get che chance to travel somewhere nice. Sometimes, watching the clouds from satellite images may give a nice insight about how pressure zones in our planet work, although it also makes it pretty obvious to see why most weather forecasts get it wrong so often.\nClouds are so difficult to predict!\nBut it's pretty cool how our civilization has adapted to having water falling from the sky each time it rains...");
+
+ TabContainer *tabc = memnew(TabContainer);
+
+ Control *ctl = memnew(Control);
+ ctl->set_name("tab 1");
+ tabc->add_child(ctl);
+
+ ctl = memnew(Control);
+ ctl->set_name("tab 2");
+ tabc->add_child(ctl);
+ label = memnew(Label);
+ label->set_text("Some Label");
+ label->set_position(Point2(20, 20));
+ ctl->add_child(label);
+
+ ctl = memnew(Control);
+ ctl->set_name("tab 3");
+ button = memnew(Button);
+ button->set_text("Some Button");
+ button->set_position(Point2(30, 50));
+ ctl->add_child(button);
+
+ tabc->add_child(ctl);
+
+ frame->add_child(tabc);
+
+ tabc->set_position(Point2(400, 210));
+ tabc->set_size(Point2(180, 250));
+ }
+};
+
+MainLoop *test() {
+ return memnew(TestMainLoop);
+}
+} // namespace TestGUI
+
+#endif // _3D_DISABLED
diff --git a/tests/scene/test_gui.h b/tests/scene/test_gui.h
new file mode 100644
index 0000000000..a1807ed15c
--- /dev/null
+++ b/tests/scene/test_gui.h
@@ -0,0 +1,41 @@
+/*************************************************************************/
+/* test_gui.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_GUI_H
+#define TEST_GUI_H
+
+class MainLoop;
+
+namespace TestGUI {
+
+MainLoop *test();
+}
+
+#endif
diff --git a/tests/scene/test_path_3d.h b/tests/scene/test_path_3d.h
new file mode 100644
index 0000000000..78f4e97f03
--- /dev/null
+++ b/tests/scene/test_path_3d.h
@@ -0,0 +1,84 @@
+/*************************************************************************/
+/* test_path_3d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_PATH_3D_H
+#define TEST_PATH_3D_H
+
+#include "scene/3d/path_3d.h"
+
+#include "tests/test_macros.h"
+
+namespace TestPath3D {
+
+TEST_CASE("[Path3D] Initialization") {
+ SUBCASE("Path should be empty right after initialization") {
+ Path3D *test_path = memnew(Path3D);
+ CHECK(test_path->get_curve() == nullptr);
+ memdelete(test_path);
+ }
+}
+
+TEST_CASE("[Path3D] Curve setter and getter") {
+ SUBCASE("Curve passed to the class should remain the same") {
+ Path3D *test_path = memnew(Path3D);
+ const Ref<Curve3D> &curve = memnew(Curve3D);
+
+ test_path->set_curve(curve);
+ CHECK(test_path->get_curve() == curve);
+ memdelete(test_path);
+ }
+ SUBCASE("Curve passed many times to the class should remain the same") {
+ Path3D *test_path = memnew(Path3D);
+ const Ref<Curve3D> &curve = memnew(Curve3D);
+
+ test_path->set_curve(curve);
+ test_path->set_curve(curve);
+ test_path->set_curve(curve);
+ CHECK(test_path->get_curve() == curve);
+ memdelete(test_path);
+ }
+ SUBCASE("Curve rewrite testing") {
+ Path3D *test_path = memnew(Path3D);
+ const Ref<Curve3D> &curve1 = memnew(Curve3D);
+ const Ref<Curve3D> &curve2 = memnew(Curve3D);
+
+ test_path->set_curve(curve1);
+ test_path->set_curve(curve2);
+ CHECK_MESSAGE(test_path->get_curve() != curve1,
+ "After rewrite, second curve should be in class");
+ CHECK_MESSAGE(test_path->get_curve() == curve2,
+ "After rewrite, second curve should be in class");
+ memdelete(test_path);
+ }
+}
+
+} // namespace TestPath3D
+
+#endif // TEST_PATH_3D
diff --git a/tests/scene/test_path_follow_2d.h b/tests/scene/test_path_follow_2d.h
new file mode 100644
index 0000000000..abd12fe862
--- /dev/null
+++ b/tests/scene/test_path_follow_2d.h
@@ -0,0 +1,240 @@
+/*************************************************************************/
+/* test_path_follow_2d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_PATH_FOLLOW_2D_H
+#define TEST_PATH_FOLLOW_2D_H
+
+#include "scene/2d/path_2d.h"
+
+#include "tests/test_macros.h"
+
+namespace TestPathFollow2D {
+
+TEST_CASE("[PathFollow2D] Sampling with unit offset") {
+ const Ref<Curve2D> &curve = memnew(Curve2D());
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(100, 0));
+ curve->add_point(Vector2(100, 100));
+ curve->add_point(Vector2(0, 100));
+ curve->add_point(Vector2(0, 0));
+ const Path2D *path = memnew(Path2D);
+ path->set_curve(curve);
+ const PathFollow2D *path_follow_2d = memnew(PathFollow2D);
+ path->add_child(path_follow_2d);
+
+ path_follow_2d->set_unit_offset(0);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0)));
+
+ path_follow_2d->set_unit_offset(0.125);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 0)));
+
+ path_follow_2d->set_unit_offset(0.25);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 0)));
+
+ path_follow_2d->set_unit_offset(0.375);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 50)));
+
+ path_follow_2d->set_unit_offset(0.5);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 100)));
+
+ path_follow_2d->set_unit_offset(0.625);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 100)));
+
+ path_follow_2d->set_unit_offset(0.75);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 100)));
+
+ path_follow_2d->set_unit_offset(0.875);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 50)));
+
+ path_follow_2d->set_unit_offset(1);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0)));
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow2D] Sampling with offset") {
+ const Ref<Curve2D> &curve = memnew(Curve2D());
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(100, 0));
+ curve->add_point(Vector2(100, 100));
+ curve->add_point(Vector2(0, 100));
+ curve->add_point(Vector2(0, 0));
+ const Path2D *path = memnew(Path2D);
+ path->set_curve(curve);
+ const PathFollow2D *path_follow_2d = memnew(PathFollow2D);
+ path->add_child(path_follow_2d);
+
+ path_follow_2d->set_offset(0);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0)));
+
+ path_follow_2d->set_offset(50);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 0)));
+
+ path_follow_2d->set_offset(100);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 0)));
+
+ path_follow_2d->set_offset(150);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 50)));
+
+ path_follow_2d->set_offset(200);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 100)));
+
+ path_follow_2d->set_offset(250);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 100)));
+
+ path_follow_2d->set_offset(300);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 100)));
+
+ path_follow_2d->set_offset(350);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 50)));
+
+ path_follow_2d->set_offset(400);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0)));
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow2D] Removal of a point in curve") {
+ const Ref<Curve2D> &curve = memnew(Curve2D());
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(100, 0));
+ curve->add_point(Vector2(100, 100));
+ const Path2D *path = memnew(Path2D);
+ path->set_curve(curve);
+ const PathFollow2D *path_follow_2d = memnew(PathFollow2D);
+ path->add_child(path_follow_2d);
+
+ path_follow_2d->set_unit_offset(0.5);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 0)));
+
+ curve->remove_point(1);
+
+ CHECK_MESSAGE(
+ path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 50)),
+ "Path follow's position should be updated after removing a point from the curve");
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow2D] Setting h_offset and v_offset") {
+ const Ref<Curve2D> &curve = memnew(Curve2D());
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(100, 0));
+ const Path2D *path = memnew(Path2D);
+ path->set_curve(curve);
+ const PathFollow2D *path_follow_2d = memnew(PathFollow2D);
+ path->add_child(path_follow_2d);
+
+ path_follow_2d->set_unit_offset(0.5);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 0)));
+
+ path_follow_2d->set_h_offset(25);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(75, 0)));
+
+ path_follow_2d->set_v_offset(25);
+ CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(75, 25)));
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow2D] Unit offset out of range") {
+ const Ref<Curve2D> &curve = memnew(Curve2D());
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(100, 0));
+ const Path2D *path = memnew(Path2D);
+ path->set_curve(curve);
+ const PathFollow2D *path_follow_2d = memnew(PathFollow2D);
+ path->add_child(path_follow_2d);
+
+ path_follow_2d->set_loop(true);
+
+ path_follow_2d->set_unit_offset(-0.3);
+ CHECK_MESSAGE(
+ path_follow_2d->get_unit_offset() == 0.7,
+ "Unit Offset should loop back from the end in the opposite direction");
+
+ path_follow_2d->set_unit_offset(1.3);
+ CHECK_MESSAGE(
+ path_follow_2d->get_unit_offset() == 0.3,
+ "Unit Offset should loop back from the end in the opposite direction");
+
+ path_follow_2d->set_loop(false);
+
+ path_follow_2d->set_unit_offset(-0.3);
+ CHECK_MESSAGE(
+ path_follow_2d->get_unit_offset() == 0,
+ "Unit Offset should be clamped at 0");
+
+ path_follow_2d->set_unit_offset(1.3);
+ CHECK_MESSAGE(
+ path_follow_2d->get_unit_offset() == 1,
+ "Unit Offset should be clamped at 1");
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow2D] Offset out of range") {
+ const Ref<Curve2D> &curve = memnew(Curve2D());
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(100, 0));
+ const Path2D *path = memnew(Path2D);
+ path->set_curve(curve);
+ const PathFollow2D *path_follow_2d = memnew(PathFollow2D);
+ path->add_child(path_follow_2d);
+
+ path_follow_2d->set_loop(true);
+
+ path_follow_2d->set_offset(-50);
+ CHECK_MESSAGE(
+ path_follow_2d->get_offset() == 50,
+ "Offset should loop back from the end in the opposite direction");
+
+ path_follow_2d->set_offset(150);
+ CHECK_MESSAGE(
+ path_follow_2d->get_offset() == 50,
+ "Offset should loop back from the end in the opposite direction");
+
+ path_follow_2d->set_loop(false);
+
+ path_follow_2d->set_offset(-50);
+ CHECK_MESSAGE(
+ path_follow_2d->get_offset() == 0,
+ "Offset should be clamped at 0");
+
+ path_follow_2d->set_offset(150);
+ CHECK_MESSAGE(
+ path_follow_2d->get_offset() == 100,
+ "Offset should be clamped at 1");
+
+ memdelete(path);
+}
+} // namespace TestPathFollow2D
+
+#endif // TEST_PATH_FOLLOW_2D_H
diff --git a/tests/scene/test_path_follow_3d.h b/tests/scene/test_path_follow_3d.h
new file mode 100644
index 0000000000..9ffe49e3d6
--- /dev/null
+++ b/tests/scene/test_path_follow_3d.h
@@ -0,0 +1,219 @@
+/*************************************************************************/
+/* test_path_follow_3d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_PATH_FOLLOW_3D_H
+#define TEST_PATH_FOLLOW_3D_H
+
+#include "scene/3d/path_3d.h"
+
+#include "tests/test_macros.h"
+
+namespace TestPathFollow3D {
+
+TEST_CASE("[PathFollow3D] Sampling with unit offset") {
+ const Ref<Curve3D> &curve = memnew(Curve3D());
+ curve->add_point(Vector3(0, 0, 0));
+ curve->add_point(Vector3(100, 0, 0));
+ curve->add_point(Vector3(100, 100, 0));
+ curve->add_point(Vector3(100, 100, 100));
+ curve->add_point(Vector3(100, 0, 100));
+ const Path3D *path = memnew(Path3D);
+ path->set_curve(curve);
+ const PathFollow3D *path_follow_3d = memnew(PathFollow3D);
+ path->add_child(path_follow_3d);
+
+ path_follow_3d->set_unit_offset(0);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(0, 0, 0));
+
+ path_follow_3d->set_unit_offset(0.125);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(50, 0, 0));
+
+ path_follow_3d->set_unit_offset(0.25);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 0, 0);
+
+ path_follow_3d->set_unit_offset(0.375);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 50, 0)));
+
+ path_follow_3d->set_unit_offset(0.5);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 0)));
+
+ path_follow_3d->set_unit_offset(0.625);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 50)));
+
+ path_follow_3d->set_unit_offset(0.75);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 100)));
+
+ path_follow_3d->set_unit_offset(0.875);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 50, 100)));
+
+ path_follow_3d->set_unit_offset(1);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 0, 100)));
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow3D] Sampling with offset") {
+ const Ref<Curve3D> &curve = memnew(Curve3D());
+ curve->add_point(Vector3(0, 0, 0));
+ curve->add_point(Vector3(100, 0, 0));
+ curve->add_point(Vector3(100, 100, 0));
+ curve->add_point(Vector3(100, 100, 100));
+ curve->add_point(Vector3(100, 0, 100));
+ const Path3D *path = memnew(Path3D);
+ path->set_curve(curve);
+ const PathFollow3D *path_follow_3d = memnew(PathFollow3D);
+ path->add_child(path_follow_3d);
+
+ path_follow_3d->set_offset(0);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(0, 0, 0));
+
+ path_follow_3d->set_offset(50);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(50, 0, 0));
+
+ path_follow_3d->set_offset(100);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 0, 0);
+
+ path_follow_3d->set_offset(150);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 50, 0)));
+
+ path_follow_3d->set_offset(200);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 0)));
+
+ path_follow_3d->set_offset(250);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 50)));
+
+ path_follow_3d->set_offset(300);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 100)));
+
+ path_follow_3d->set_offset(350);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 50, 100)));
+
+ path_follow_3d->set_offset(400);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 0, 100)));
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow3D] Removal of a point in curve") {
+ const Ref<Curve3D> &curve = memnew(Curve3D());
+ curve->add_point(Vector3(0, 0, 0));
+ curve->add_point(Vector3(100, 0, 0));
+ curve->add_point(Vector3(100, 100, 0));
+ const Path3D *path = memnew(Path3D);
+ path->set_curve(curve);
+ const PathFollow3D *path_follow_3d = memnew(PathFollow3D);
+ path->add_child(path_follow_3d);
+
+ path_follow_3d->set_unit_offset(0.5);
+ CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector2(100, 0, 0)));
+
+ curve->remove_point(1);
+
+ CHECK_MESSAGE(
+ path_follow_3d->get_transform().get_origin().is_equal_approx(Vector2(50, 50, 0)),
+ "Path follow's position should be updated after removing a point from the curve");
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow3D] Unit offset out of range") {
+ const Ref<Curve3D> &curve = memnew(Curve3D());
+ curve->add_point(Vector3(0, 0, 0));
+ curve->add_point(Vector3(100, 0, 0));
+ const Path3D *path = memnew(Path3D);
+ path->set_curve(curve);
+ const PathFollow3D *path_follow_3d = memnew(PathFollow3D);
+ path->add_child(path_follow_3d);
+
+ path_follow_3d->set_loop(true);
+
+ path_follow_3d->set_unit_offset(-0.3);
+ CHECK_MESSAGE(
+ path_follow_3d->get_unit_offset() == 0.7,
+ "Unit Offset should loop back from the end in the opposite direction");
+
+ path_follow_3d->set_unit_offset(1.3);
+ CHECK_MESSAGE(
+ path_follow_3d->get_unit_offset() == 0.3,
+ "Unit Offset should loop back from the end in the opposite direction");
+
+ path_follow_3d->set_loop(false);
+
+ path_follow_3d->set_unit_offset(-0.3);
+ CHECK_MESSAGE(
+ path_follow_3d->get_unit_offset() == 0,
+ "Unit Offset should be clamped at 0");
+
+ path_follow_3d->set_unit_offset(1.3);
+ CHECK_MESSAGE(
+ path_follow_3d->get_unit_offset() == 1,
+ "Unit Offset should be clamped at 1");
+
+ memdelete(path);
+}
+
+TEST_CASE("[PathFollow3D] Offset out of range") {
+ const Ref<Curve3D> &curve = memnew(Curve3D());
+ curve->add_point(Vector3(0, 0, 0));
+ curve->add_point(Vector3(100, 0, 0));
+ const Path3D *path = memnew(Path3D);
+ path->set_curve(curve);
+ const PathFollow3D *path_follow_3d = memnew(PathFollow3D);
+ path->add_child(path_follow_3d);
+
+ path_follow_3d->set_loop(true);
+
+ path_follow_3d->set_offset(-50);
+ CHECK_MESSAGE(
+ path_follow_3d->get_offset() == 50,
+ "Offset should loop back from the end in the opposite direction");
+
+ path_follow_3d->set_offset(150);
+ CHECK_MESSAGE(
+ path_follow_3d->get_offset() == 50,
+ "Offset should loop back from the end in the opposite direction");
+
+ path_follow_3d->set_loop(false);
+
+ path_follow_3d->set_offset(-50);
+ CHECK_MESSAGE(
+ path_follow_3d->get_offset() == 0,
+ "Offset should be clamped at 0");
+
+ path_follow_3d->set_offset(150);
+ CHECK_MESSAGE(
+ path_follow_3d->get_offset() == 100,
+ "Offset should be clamped at max value of curve");
+
+ memdelete(path);
+}
+} // namespace TestPathFollow3D
+
+#endif // TEST_PATH_FOLLOW_3D_H
diff --git a/tests/servers/test_physics_2d.cpp b/tests/servers/test_physics_2d.cpp
new file mode 100644
index 0000000000..8b77458a33
--- /dev/null
+++ b/tests/servers/test_physics_2d.cpp
@@ -0,0 +1,398 @@
+/*************************************************************************/
+/* test_physics_2d.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "test_physics_2d.h"
+
+#include "core/os/main_loop.h"
+#include "servers/physics_server_2d.h"
+#include "servers/rendering_server.h"
+
+static const unsigned char convex_png[] = {
+ 0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa, 0x0, 0x0, 0x0, 0xd, 0x49, 0x48, 0x44, 0x52, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x40, 0x8, 0x6, 0x0, 0x0, 0x0, 0xaa, 0x69, 0x71, 0xde, 0x0, 0x0, 0x0, 0x1, 0x73, 0x52, 0x47, 0x42, 0x0, 0xae, 0xce, 0x1c, 0xe9, 0x0, 0x0, 0x0, 0x6, 0x62, 0x4b, 0x47, 0x44, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf9, 0x43, 0xbb, 0x7f, 0x0, 0x0, 0x0, 0x9, 0x70, 0x48, 0x59, 0x73, 0x0, 0x0, 0xb, 0x13, 0x0, 0x0, 0xb, 0x13, 0x1, 0x0, 0x9a, 0x9c, 0x18, 0x0, 0x0, 0x0, 0x7, 0x74, 0x49, 0x4d, 0x45, 0x7, 0xdb, 0x6, 0xa, 0x3, 0x13, 0x31, 0x66, 0xa7, 0xac, 0x79, 0x0, 0x0, 0x4, 0xef, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0xed, 0x9b, 0xdd, 0x4e, 0x2a, 0x57, 0x14, 0xc7, 0xf7, 0x1e, 0xc0, 0x19, 0x38, 0x32, 0x80, 0xa, 0x6a, 0xda, 0x18, 0xa3, 0xc6, 0x47, 0x50, 0x7b, 0xa1, 0xd9, 0x36, 0x27, 0x7e, 0x44, 0xed, 0x45, 0x4d, 0x93, 0x3e, 0x40, 0x1f, 0x64, 0x90, 0xf4, 0x1, 0xbc, 0xf0, 0xc2, 0x9c, 0x57, 0x30, 0x4d, 0xbc, 0xa8, 0x6d, 0xc, 0x69, 0x26, 0xb5, 0x68, 0x8b, 0x35, 0x7e, 0x20, 0xb4, 0xf5, 0x14, 0xbf, 0x51, 0x3c, 0x52, 0xe, 0xc, 0xe, 0xc8, 0xf0, 0xb1, 0x7a, 0x51, 0x3d, 0xb1, 0x9e, 0x19, 0x1c, 0x54, 0x70, 0x1c, 0xdc, 0x9, 0x17, 0x64, 0x8, 0xc9, 0xff, 0xb7, 0xd6, 0x7f, 0xcd, 0x3f, 0x2b, 0xd9, 0x8, 0xbd, 0x9c, 0xda, 0x3e, 0xf8, 0x31, 0xff, 0xc, 0x0, 0x8, 0x42, 0x88, 0x9c, 0x9f, 0x9f, 0xbf, 0xa, 0x87, 0xc3, 0xad, 0x7d, 0x7d, 0x7d, 0x7f, 0x23, 0x84, 0x78, 0x8c, 0x31, 0xaf, 0x55, 0x0, 0xc6, 0xc7, 0x14, 0x1e, 0x8f, 0xc7, 0xbf, 0x38, 0x3c, 0x3c, 0x6c, 0x9b, 0x9f, 0x9f, 0x6f, 0xb8, 0x82, 0x9b, 0xee, 0xe8, 0xe8, 0xf8, 0x12, 0x0, 0xbe, 0xd3, 0x2a, 0x8, 0xfc, 0x50, 0xd1, 0xf9, 0x7c, 0x9e, 0x8a, 0x46, 0xa3, 0x5f, 0x9d, 0x9e, 0x9e, 0x7e, 0xb2, 0xb0, 0xb0, 0x60, 0xe5, 0x79, 0x1e, 0xf1, 0xfc, 0x7f, 0x3a, 0x9, 0x21, 0x88, 0x10, 0x82, 0x26, 0x26, 0x26, 0xde, 0x77, 0x75, 0x75, 0x85, 0x59, 0x96, 0xfd, 0x5e, 0x6b, 0x20, 0xf0, 0x7d, 0x85, 0x4b, 0x92, 0xf4, 0xfa, 0xe0, 0xe0, 0xe0, 0xd3, 0xb9, 0xb9, 0xb9, 0x46, 0x49, 0x92, 0xea, 0x6f, 0xa, 0xbf, 0x7d, 0x8, 0x21, 0x68, 0x70, 0x70, 0xb0, 0x38, 0x39, 0x39, 0x79, 0xd6, 0xd9, 0xd9, 0xb9, 0xcf, 0x30, 0xcc, 0xa2, 0xd6, 0xad, 0x21, 0x2b, 0x1c, 0x0, 0x38, 0x41, 0x10, 0xfc, 0xdb, 0xdb, 0xdb, 0x27, 0x1e, 0x8f, 0x27, 0x4b, 0x8, 0x1, 0x84, 0x90, 0xea, 0xf, 0x21, 0x4, 0x3c, 0x1e, 0x4f, 0x76, 0x67, 0x67, 0x67, 0x3f, 0x9f, 0xcf, 0xff, 0x7c, 0x5, 0xf3, 0xd9, 0x0, 0xe0, 0x2, 0x81, 0xc0, 0xa9, 0xdb, 0xed, 0x2e, 0x94, 0x2b, 0x5c, 0xe, 0xc4, 0xca, 0xca, 0x8a, 0x18, 0x8d, 0x46, 0x3, 0x0, 0xc0, 0x69, 0x1e, 0x4, 0x0, 0x90, 0x48, 0x24, 0x12, 0xe4, 0x38, 0xee, 0x41, 0xc2, 0x6f, 0x43, 0xe0, 0x38, 0xe, 0xfc, 0x7e, 0xbf, 0x10, 0x8b, 0xc5, 0xd6, 0x35, 0xd, 0x22, 0x9b, 0xcd, 0x7a, 0x96, 0x97, 0x97, 0x33, 0xf, 0xad, 0x7c, 0x29, 0x10, 0x9b, 0x9b, 0x9b, 0xef, 0x2e, 0x2e, 0x2e, 0x7e, 0xd5, 0x1c, 0x8, 0x0, 0x20, 0xe1, 0x70, 0x38, 0xfc, 0x98, 0xd5, 0x57, 0x2, 0xe1, 0x76, 0xbb, 0xf3, 0xa1, 0x50, 0xe8, 0x38, 0x9b, 0xcd, 0xfe, 0xa2, 0x9, 0x8, 0x0, 0x40, 0x2e, 0x2f, 0x2f, 0x7d, 0x4b, 0x4b, 0x4b, 0xb9, 0x4a, 0x54, 0x5f, 0x9, 0xc4, 0xd2, 0xd2, 0x92, 0xb4, 0xb7, 0xb7, 0xf7, 0x36, 0x97, 0xcb, 0x4d, 0x3d, 0x29, 0x8, 0x0, 0xe0, 0x42, 0xa1, 0xd0, 0x71, 0xb5, 0xc4, 0xdf, 0xb6, 0xc5, 0x93, 0xe, 0x4a, 0x0, 0x20, 0xa9, 0x54, 0xea, 0x37, 0xb7, 0xdb, 0x5d, 0xa8, 0xa6, 0x78, 0x39, 0x10, 0x6b, 0x6b, 0x6b, 0xf1, 0x64, 0x32, 0xb9, 0x5a, 0x55, 0x10, 0x0, 0xc0, 0x6d, 0x6c, 0x6c, 0x9c, 0x57, 0xbb, 0xfa, 0x25, 0x40, 0x14, 0x3, 0x81, 0x40, 0x34, 0x93, 0xc9, 0x2c, 0x57, 0x1c, 0x4, 0x0, 0x90, 0x58, 0x2c, 0xb6, 0x5e, 0xe9, 0xc1, 0x77, 0x1f, 0x10, 0x53, 0x53, 0x53, 0x52, 0xc5, 0x83, 0x14, 0x0, 0x70, 0x7e, 0xbf, 0x5f, 0xd0, 0x42, 0xf5, 0x95, 0x40, 0xf8, 0x7c, 0xbe, 0xcb, 0xa3, 0xa3, 0xa3, 0x3f, 0x1e, 0xbd, 0x1b, 0x0, 0x80, 0x1c, 0x1f, 0x1f, 0x87, 0xb4, 0x56, 0xfd, 0xaa, 0x5, 0x29, 0x51, 0x14, 0xbf, 0xf5, 0xf9, 0x7c, 0x97, 0x5a, 0xad, 0xbe, 0x12, 0x88, 0xf5, 0xf5, 0xf5, 0xd8, 0x83, 0x83, 0x54, 0xb5, 0x42, 0x8f, 0x66, 0x83, 0x94, 0xd6, 0xbd, 0x5f, 0xce, 0x7c, 0x38, 0x3c, 0x3c, 0xfc, 0xb3, 0x50, 0x28, 0xb8, 0xcb, 0x2, 0x1, 0x0, 0xdc, 0xf4, 0xf4, 0xf4, 0xfe, 0x73, 0x15, 0x2f, 0x17, 0xa4, 0x22, 0x91, 0x48, 0x50, 0xb5, 0x2d, 0x0, 0x80, 0x9b, 0x99, 0x99, 0x79, 0xfb, 0xdc, 0x1, 0xc8, 0x5, 0xa9, 0x44, 0x22, 0xf1, 0xfb, 0x9d, 0x10, 0x0, 0x80, 0x9b, 0x9d, 0x9d, 0xd, 0xea, 0x5, 0xc0, 0xad, 0xfd, 0x43, 0x1a, 0x0, 0xb8, 0xdb, 0x9a, 0xa9, 0x8f, 0xb6, 0xa4, 0x46, 0xa3, 0xa4, 0xb7, 0xd5, 0x37, 0xcf, 0xf3, 0x68, 0x75, 0x75, 0xf5, 0x4c, 0xee, 0x99, 0x1c, 0x80, 0x9c, 0x1e, 0xf7, 0xff, 0x16, 0x8b, 0x45, 0x50, 0x5, 0xa0, 0xb7, 0xb7, 0xb7, 0x85, 0x10, 0xa2, 0x2b, 0xf1, 0x84, 0x10, 0xd4, 0xdf, 0xdf, 0x6f, 0x57, 0x3, 0x80, 0x37, 0x18, 0xc, 0x5, 0x3d, 0x2, 0xa0, 0x69, 0x3a, 0x8b, 0x10, 0xe2, 0x4b, 0x2, 0xc0, 0x18, 0xf3, 0xc1, 0x60, 0x70, 0x47, 0x8f, 0x16, 0x38, 0x3a, 0x3a, 0x5a, 0x93, 0x5b, 0xc3, 0x7f, 0x64, 0x81, 0xba, 0xba, 0x3a, 0x49, 0x8f, 0x0, 0x1a, 0x1a, 0x1a, 0xd4, 0xcd, 0x0, 0x93, 0xc9, 0xa4, 0xcb, 0x21, 0xe8, 0x74, 0x3a, 0xd5, 0x1, 0xa0, 0x69, 0x5a, 0x77, 0x1d, 0x80, 0x31, 0x2e, 0x38, 0x9d, 0x4e, 0xb1, 0x66, 0x1, 0x30, 0xc, 0x23, 0x28, 0x3d, 0x93, 0x9b, 0x1, 0xb9, 0x9a, 0x6, 0x60, 0x36, 0x9b, 0x75, 0xd7, 0x1, 0x4a, 0x21, 0xa8, 0x26, 0x0, 0x94, 0xa, 0x41, 0xb2, 0x0, 0x18, 0x86, 0xc9, 0xe9, 0xd, 0x80, 0x52, 0x8, 0x92, 0x5, 0x60, 0xb1, 0x58, 0x74, 0x67, 0x1, 0xa5, 0x10, 0xa4, 0x4, 0x40, 0x77, 0x43, 0xd0, 0xe1, 0x70, 0xa8, 0x9f, 0x1, 0x14, 0x45, 0x1, 0x45, 0x51, 0x79, 0x3d, 0x1, 0x68, 0x6e, 0x6e, 0x4e, 0xaa, 0x6, 0x80, 0x10, 0x42, 0x6, 0x83, 0x41, 0x37, 0x36, 0x28, 0x15, 0x82, 0x6a, 0x2, 0x0, 0x4d, 0xd3, 0xa9, 0x52, 0xcf, 0x95, 0x0, 0xe8, 0x66, 0xe, 0x98, 0xcd, 0x66, 0xa1, 0x6c, 0x0, 0x7a, 0x5a, 0x8b, 0x59, 0x2c, 0x96, 0x64, 0xcd, 0x2, 0xb8, 0x2b, 0x4, 0xe9, 0xde, 0x2, 0x77, 0x85, 0xa0, 0x9a, 0xb0, 0x40, 0xa9, 0x10, 0xa4, 0x8, 0xc0, 0x64, 0x32, 0xe9, 0x6, 0x40, 0xa9, 0x10, 0x54, 0xaa, 0x3, 0x74, 0xf3, 0x16, 0x70, 0xb9, 0x5c, 0xe5, 0x3, 0xe8, 0xe9, 0xe9, 0x69, 0xd5, 0xc3, 0x66, 0x18, 0x63, 0x5c, 0x68, 0x6a, 0x6a, 0x12, 0xcb, 0x5, 0xa0, 0x9b, 0xd5, 0x38, 0x4d, 0xd3, 0x29, 0x8a, 0xa2, 0xa0, 0x2c, 0x0, 0x18, 0x63, 0x3e, 0x14, 0xa, 0xfd, 0x55, 0xb, 0x21, 0x48, 0xd1, 0x2, 0x7a, 0x59, 0x8d, 0xdf, 0x1b, 0x80, 0x1e, 0x56, 0xe3, 0x84, 0x10, 0x34, 0x30, 0x30, 0x60, 0xbb, 0xeb, 0x77, 0x46, 0x5, 0xef, 0x48, 0xcf, 0x4d, 0xec, 0x8d, 0x99, 0x5, 0xf5, 0xf5, 0xf5, 0xef, 0x46, 0x47, 0x47, 0xb, 0x2e, 0x97, 0xeb, 0xbc, 0x54, 0x8, 0x52, 0x4, 0xc0, 0x30, 0x8c, 0xf4, 0x5c, 0x4, 0x9b, 0x4c, 0xa6, 0xf4, 0xf8, 0xf8, 0xb8, 0xc8, 0xb2, 0x6c, 0x32, 0x9d, 0x4e, 0xff, 0xd4, 0xdd, 0xdd, 0x7d, 0x66, 0x34, 0x1a, 0x8b, 0xd7, 0x3, 0xfd, 0xae, 0x5b, 0x29, 0xb2, 0x57, 0x66, 0xb6, 0xb6, 0xb6, 0xde, 0xc4, 0xe3, 0xf1, 0x6f, 0xae, 0xaf, 0xc1, 0x28, 0x5d, 0x85, 0x79, 0x2, 0xc1, 0x60, 0xb5, 0x5a, 0xa3, 0xa3, 0xa3, 0xa3, 0x45, 0xab, 0xd5, 0x9a, 0x2a, 0x16, 0x8b, 0x8b, 0x6d, 0x6d, 0x6d, 0xef, 0xd5, 0x8a, 0x55, 0xd, 0x20, 0x91, 0x48, 0xbc, 0x3e, 0x38, 0x38, 0xf8, 0xda, 0x6e, 0xb7, 0xf7, 0x5f, 0x5c, 0x5c, 0xd4, 0x7b, 0xbd, 0xde, 0xbc, 0x20, 0x8, 0xcd, 0x85, 0x42, 0x81, 0xfe, 0xf0, 0xae, 0xac, 0x10, 0x98, 0x9b, 0xd5, 0xc5, 0x18, 0x17, 0x59, 0x96, 0x3d, 0x1d, 0x19, 0x19, 0x1, 0x96, 0x65, 0x5, 0x8a, 0xa2, 0x7e, 0x6c, 0x69, 0x69, 0x49, 0x3d, 0x44, 0xb0, 0x2a, 0x0, 0x1f, 0xcc, 0x74, 0x75, 0x41, 0xea, 0xfa, 0x7b, 0x32, 0x99, 0x64, 0x76, 0x77, 0x77, 0x5d, 0xe, 0x87, 0xa3, 0x5f, 0x14, 0xc5, 0x57, 0x57, 0x60, 0x5a, 0x8b, 0xc5, 0xa2, 0xf1, 0xbe, 0x50, 0x6e, 0xa, 0x66, 0x18, 0x26, 0x31, 0x36, 0x36, 0x96, 0x65, 0x59, 0x36, 0x29, 0x49, 0x92, 0xb7, 0xbd, 0xbd, 0xfd, 0x9f, 0x72, 0xda, 0xf9, 0xd1, 0x1, 0xa8, 0x1, 0x93, 0xcf, 0xe7, 0xa9, 0x93, 0x93, 0x13, 0x1b, 0x4d, 0xd3, 0x9f, 0xb, 0x82, 0x60, 0xf5, 0x7a, 0xbd, 0xd9, 0x54, 0x2a, 0xe5, 0xcc, 0x64, 0x32, 0xe, 0xb9, 0x6e, 0xb9, 0x16, 0x8c, 0x31, 0x2e, 0xda, 0x6c, 0xb6, 0xc8, 0xd0, 0xd0, 0x10, 0x65, 0xb3, 0xd9, 0x92, 0x95, 0xa8, 0x6e, 0xc5, 0x0, 0xa8, 0xe9, 0x96, 0x68, 0x34, 0x6a, 0xdd, 0xdf, 0xdf, 0x6f, 0x76, 0xb9, 0x5c, 0x9f, 0x89, 0xa2, 0x58, 0xbf, 0xb8, 0xb8, 0x8, 0x26, 0x93, 0x29, 0x3b, 0x3c, 0x3c, 0x8c, 0xed, 0x76, 0x7b, 0xd2, 0x68, 0x34, 0xfe, 0xd0, 0xd8, 0xd8, 0x98, 0xae, 0xb6, 0xe0, 0x8a, 0x1, 0x50, 0xb, 0xe6, 0xa9, 0x5, 0xbf, 0x9c, 0x97, 0xf3, 0xff, 0xf3, 0x2f, 0x6a, 0x82, 0x7f, 0xf6, 0x4e, 0xca, 0x1b, 0xf5, 0x0, 0x0, 0x0, 0x0, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
+};
+
+class TestPhysics2DMainLoop : public MainLoop {
+ GDCLASS(TestPhysics2DMainLoop, MainLoop);
+
+ RID circle_img;
+ RID circle_shape;
+ RID space;
+ RID canvas;
+ RID ray;
+ RID ray_query;
+ Transform2D view_xform;
+
+ Vector2 ray_from, ray_to;
+
+ struct BodyShapeData {
+ RID image;
+ RID shape;
+ };
+
+ BodyShapeData body_shape_data[8];
+
+ void _create_body_shape_data() {
+ RenderingServer *vs = RenderingServer::get_singleton();
+ PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
+
+ // SEGMENT
+
+ {
+ Vector<uint8_t> pixels;
+ pixels.resize(32 * 2 * 2);
+ for (int i = 0; i < 2; i++) {
+ for (int j = 0; j < 32; j++) {
+ pixels.set(i * 32 * 2 + j * 2 + 0, (j == 0) ? 255 : 0);
+ pixels.set(i * 32 * 2 + j * 2 + 1, 255);
+ }
+ }
+
+ Ref<Image> image = memnew(Image(32, 2, 0, Image::FORMAT_LA8, pixels));
+
+ body_shape_data[PhysicsServer2D::SHAPE_SEGMENT].image = vs->texture_2d_create(image);
+
+ RID segment_shape = ps->segment_shape_create();
+ Rect2 sg(Point2(-16, 0), Point2(16, 0));
+ ps->shape_set_data(segment_shape, sg);
+
+ body_shape_data[PhysicsServer2D::SHAPE_SEGMENT].shape = segment_shape;
+ }
+ // CIRCLE
+
+ {
+ Vector<uint8_t> pixels;
+ pixels.resize(32 * 32 * 2);
+ for (int i = 0; i < 32; i++) {
+ for (int j = 0; j < 32; j++) {
+ bool black = Vector2(i - 16, j - 16).length_squared() < 16 * 16;
+
+ pixels.set(i * 32 * 2 + j * 2 + 0, (i == 16 || j == 16) ? 255 : 0);
+ pixels.set(i * 32 * 2 + j * 2 + 1, black ? 255 : 0);
+ }
+ }
+
+ Ref<Image> image = memnew(Image(32, 32, 0, Image::FORMAT_LA8, pixels));
+
+ body_shape_data[PhysicsServer2D::SHAPE_CIRCLE].image = vs->texture_2d_create(image);
+
+ RID circle_shape = ps->circle_shape_create();
+ ps->shape_set_data(circle_shape, 16);
+
+ body_shape_data[PhysicsServer2D::SHAPE_CIRCLE].shape = circle_shape;
+ }
+
+ // BOX
+
+ {
+ Vector<uint8_t> pixels;
+ pixels.resize(32 * 32 * 2);
+ for (int i = 0; i < 32; i++) {
+ for (int j = 0; j < 32; j++) {
+ bool black = i > 0 && i < 31 && j > 0 && j < 31;
+
+ pixels.set(i * 32 * 2 + j * 2 + 0, black ? 0 : 255);
+ pixels.set(i * 32 * 2 + j * 2 + 1, 255);
+ }
+ }
+
+ Ref<Image> image = memnew(Image(32, 32, 0, Image::FORMAT_LA8, pixels));
+
+ body_shape_data[PhysicsServer2D::SHAPE_RECTANGLE].image = vs->texture_2d_create(image);
+
+ RID rectangle_shape = ps->rectangle_shape_create();
+ ps->shape_set_data(rectangle_shape, Vector2(16, 16));
+
+ body_shape_data[PhysicsServer2D::SHAPE_RECTANGLE].shape = rectangle_shape;
+ }
+
+ // CAPSULE
+
+ {
+ Vector<uint8_t> pixels;
+ pixels.resize(32 * 64 * 2);
+ for (int i = 0; i < 64; i++) {
+ for (int j = 0; j < 32; j++) {
+ int si = i > 48 ? i - 32 : (i < 16 ? i : 16);
+ bool black = Vector2(si - 16, j - 16).length_squared() < 16 * 16;
+
+ pixels.set(i * 32 * 2 + j * 2 + 0, (i == 16 || j == 16 || i == 48) ? 255 : 0);
+ pixels.set(i * 32 * 2 + j * 2 + 1, black ? 255 : 0);
+ }
+ }
+
+ Ref<Image> image = memnew(Image(32, 64, 0, Image::FORMAT_LA8, pixels));
+
+ body_shape_data[PhysicsServer2D::SHAPE_CAPSULE].image = vs->texture_2d_create(image);
+
+ RID capsule_shape = ps->capsule_shape_create();
+ ps->shape_set_data(capsule_shape, Vector2(16, 32));
+
+ body_shape_data[PhysicsServer2D::SHAPE_CAPSULE].shape = capsule_shape;
+ }
+
+ // CONVEX
+
+ {
+ Ref<Image> image = memnew(Image(convex_png));
+
+ body_shape_data[PhysicsServer2D::SHAPE_CONVEX_POLYGON].image = vs->texture_2d_create(image);
+
+ RID convex_polygon_shape = ps->convex_polygon_shape_create();
+
+ Vector<Vector2> arr;
+ Point2 sb(32, 32);
+ arr.push_back(Point2(20, 3) - sb);
+ arr.push_back(Point2(58, 23) - sb);
+ arr.push_back(Point2(55, 54) - sb);
+ arr.push_back(Point2(27, 60) - sb);
+ arr.push_back(Point2(5, 56) - sb);
+ arr.push_back(Point2(4, 20) - sb);
+ arr.push_back(Point2(11, 7) - sb);
+ ps->shape_set_data(convex_polygon_shape, arr);
+
+ body_shape_data[PhysicsServer2D::SHAPE_CONVEX_POLYGON].shape = convex_polygon_shape;
+ }
+ }
+
+ void _do_ray_query() {
+ /*
+ PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
+ ps->query_intersection_segment(ray_query,ray_from,ray_to);
+ */
+ }
+
+protected:
+ void input_event(const Ref<InputEvent> &p_event) {
+ Ref<InputEventMouseButton> mb = p_event;
+
+ if (mb.is_valid()) {
+ if (mb->is_pressed()) {
+ Point2 p = mb->get_position();
+
+ if (mb->get_button_index() == MouseButton::LEFT) {
+ ray_to = p;
+ _do_ray_query();
+ } else if (mb->get_button_index() == MouseButton::RIGHT) {
+ ray_from = p;
+ _do_ray_query();
+ }
+ }
+ }
+
+ Ref<InputEventMouseMotion> mm = p_event;
+
+ if (mm.is_valid()) {
+ Point2 p = mm->get_position();
+
+ if ((mm->get_button_mask() & MouseButton::MASK_LEFT) != MouseButton::NONE) {
+ ray_to = p;
+ _do_ray_query();
+ } else if ((mm->get_button_mask() & MouseButton::MASK_RIGHT) != MouseButton::NONE) {
+ ray_from = p;
+ _do_ray_query();
+ }
+ }
+ }
+
+ RID _add_body(PhysicsServer2D::ShapeType p_shape, const Transform2D &p_xform) {
+ RenderingServer *vs = RenderingServer::get_singleton();
+ PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
+
+ RID body = ps->body_create();
+ ps->body_add_shape(body, body_shape_data[p_shape].shape);
+ ps->body_set_space(body, space);
+ ps->body_set_continuous_collision_detection_mode(body, PhysicsServer2D::CCD_MODE_CAST_SHAPE);
+ ps->body_set_state(body, PhysicsServer2D::BODY_STATE_TRANSFORM, p_xform);
+
+ //print_line("add body with xform: "+p_xform);
+ RID sprite = vs->canvas_item_create();
+ vs->canvas_item_set_parent(sprite, canvas);
+ vs->canvas_item_set_transform(sprite, p_xform);
+ Size2 imgsize(5, 5); //vs->texture_get_width(body_shape_data[p_shape].image), vs->texture_get_height(body_shape_data[p_shape].image));
+ vs->canvas_item_add_texture_rect(sprite, Rect2(-imgsize / 2.0, imgsize), body_shape_data[p_shape].image);
+
+ ps->body_set_force_integration_callback(body, callable_mp(this, &TestPhysics2DMainLoop::_body_moved), sprite);
+
+ return body;
+ }
+
+ void _add_world_boundary(const Vector2 &p_normal, real_t p_d) {
+ PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
+
+ Array arr;
+ arr.push_back(p_normal);
+ arr.push_back(p_d);
+
+ RID world_boundary = ps->world_boundary_shape_create();
+ ps->shape_set_data(world_boundary, arr);
+
+ RID plane_body = ps->body_create();
+ ps->body_set_mode(plane_body, PhysicsServer2D::BODY_MODE_STATIC);
+ ps->body_set_space(plane_body, space);
+ ps->body_add_shape(plane_body, world_boundary);
+ }
+
+ void _add_concave(const Vector<Vector2> &p_points, const Transform2D &p_xform = Transform2D()) {
+ PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
+ RenderingServer *vs = RenderingServer::get_singleton();
+
+ RID concave = ps->concave_polygon_shape_create();
+ ps->shape_set_data(concave, p_points);
+ RID body = ps->body_create();
+ ps->body_set_mode(body, PhysicsServer2D::BODY_MODE_STATIC);
+ ps->body_set_space(body, space);
+ ps->body_add_shape(body, concave);
+ ps->body_set_state(body, PhysicsServer2D::BODY_STATE_TRANSFORM, p_xform);
+
+ RID sprite = vs->canvas_item_create();
+ vs->canvas_item_set_parent(sprite, canvas);
+ vs->canvas_item_set_transform(sprite, p_xform);
+ for (int i = 0; i < p_points.size(); i += 2) {
+ vs->canvas_item_add_line(sprite, p_points[i], p_points[i + 1], Color(0, 0, 0), 2);
+ }
+ }
+
+ void _body_moved(Object *p_state, RID p_sprite) {
+ PhysicsDirectBodyState2D *state = (PhysicsDirectBodyState2D *)p_state;
+ RenderingServer::get_singleton()->canvas_item_set_transform(p_sprite, state->get_transform());
+ }
+
+ void _ray_query_callback(const RID &p_rid, ObjectID p_id, int p_shape, const Vector2 &p_point, const Vector2 &p_normal) {
+ Vector2 ray_end;
+
+ if (p_rid.is_valid()) {
+ ray_end = p_point;
+ } else {
+ ray_end = ray_to;
+ }
+
+ RenderingServer *vs = RenderingServer::get_singleton();
+
+ vs->canvas_item_clear(ray);
+ vs->canvas_item_add_line(ray, ray_from, ray_end, p_rid.is_valid() ? Color(0, 1, 0.4) : Color(1, 0.4, 0), 2);
+ if (p_rid.is_valid()) {
+ vs->canvas_item_add_line(ray, ray_end, ray_end + p_normal * 20, p_rid.is_valid() ? Color(0, 1, 0.4) : Color(1, 0.4, 0), 2);
+ }
+ }
+
+ static void _bind_methods() {
+ ClassDB::bind_method(D_METHOD("_ray_query_callback"), &TestPhysics2DMainLoop::_ray_query_callback);
+ }
+
+public:
+ virtual void initialize() override {
+ RenderingServer *vs = RenderingServer::get_singleton();
+ PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
+
+ space = ps->space_create();
+ ps->space_set_active(space, true);
+ ps->set_active(true);
+ ps->area_set_param(space, PhysicsServer2D::AREA_PARAM_GRAVITY_VECTOR, Vector2(0, 1));
+ ps->area_set_param(space, PhysicsServer2D::AREA_PARAM_GRAVITY, 980);
+
+ {
+ RID vp = vs->viewport_create();
+ canvas = vs->canvas_create();
+
+ Size2i screen_size = DisplayServer::get_singleton()->window_get_size();
+ vs->viewport_attach_canvas(vp, canvas);
+ vs->viewport_set_size(vp, screen_size.x, screen_size.y);
+ vs->viewport_attach_to_screen(vp, Rect2(Vector2(), screen_size));
+ vs->viewport_set_active(vp, true);
+
+ Transform2D smaller;
+ //smaller.scale(Vector2(0.6,0.6));
+ //smaller.elements[2]=Vector2(100,0);
+
+ //view_xform = smaller;
+ vs->viewport_set_canvas_transform(vp, canvas, view_xform);
+ }
+
+ ray = vs->canvas_item_create();
+ vs->canvas_item_set_parent(ray, canvas);
+ //ray_query = ps->query_create(this,"_ray_query_callback",Variant());
+ //ps->query_intersection(ray_query,space);
+
+ _create_body_shape_data();
+
+ for (int i = 0; i < 32; i++) {
+ PhysicsServer2D::ShapeType types[4] = {
+ PhysicsServer2D::SHAPE_CIRCLE,
+ PhysicsServer2D::SHAPE_CAPSULE,
+ PhysicsServer2D::SHAPE_RECTANGLE,
+ PhysicsServer2D::SHAPE_CONVEX_POLYGON,
+
+ };
+
+ PhysicsServer2D::ShapeType type = types[i % 4];
+ //type=PhysicsServer2D::SHAPE_SEGMENT;
+ _add_body(type, Transform2D(i * 0.8, Point2(152 + i * 40, 100 - 40 * i)));
+ /*
+ if (i==0)
+ ps->body_set_mode(b,PhysicsServer2D::BODY_MODE_STATIC);
+ */
+ }
+
+ //RID b= _add_body(PhysicsServer2D::SHAPE_CIRCLE,Transform2D(0,Point2(101,140)));
+ //ps->body_set_mode(b,PhysicsServer2D::BODY_MODE_STATIC);
+
+ Point2 prev;
+
+ Vector<Point2> parr;
+ for (int i = 0; i < 30; i++) {
+ Point2 p(i * 60, Math::randf() * 70 + 340);
+ if (i > 0) {
+ parr.push_back(prev);
+ parr.push_back(p);
+ }
+ prev = p;
+ }
+
+ _add_concave(parr);
+ //_add_world_boundary(Vector2(0.0,-1).normalized(),-300);
+ //_add_world_boundary(Vector2(1,0).normalized(),50);
+ //_add_world_boundary(Vector2(-1,0).normalized(),-600);
+ }
+
+ virtual bool process(double p_time) override {
+ return false;
+ }
+ virtual void finalize() override {
+ }
+
+ TestPhysics2DMainLoop() {}
+};
+
+namespace TestPhysics2D {
+
+MainLoop *test() {
+ return memnew(TestPhysics2DMainLoop);
+}
+} // namespace TestPhysics2D
diff --git a/tests/servers/test_physics_2d.h b/tests/servers/test_physics_2d.h
new file mode 100644
index 0000000000..b6c47574cd
--- /dev/null
+++ b/tests/servers/test_physics_2d.h
@@ -0,0 +1,41 @@
+/*************************************************************************/
+/* test_physics_2d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_PHYSICS_2D_H
+#define TEST_PHYSICS_2D_H
+
+class MainLoop;
+
+namespace TestPhysics2D {
+
+MainLoop *test();
+}
+
+#endif // TEST_PHYSICS_2D_H
diff --git a/tests/servers/test_physics_3d.cpp b/tests/servers/test_physics_3d.cpp
new file mode 100644
index 0000000000..3d38b9d901
--- /dev/null
+++ b/tests/servers/test_physics_3d.cpp
@@ -0,0 +1,410 @@
+/*************************************************************************/
+/* test_physics_3d.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "test_physics_3d.h"
+
+#include "core/math/convex_hull.h"
+#include "core/math/geometry_3d.h"
+#include "core/os/main_loop.h"
+#include "servers/physics_server_3d.h"
+#include "servers/rendering_server.h"
+
+class TestPhysics3DMainLoop : public MainLoop {
+ GDCLASS(TestPhysics3DMainLoop, MainLoop);
+
+ enum {
+ LINK_COUNT = 20,
+ };
+
+ RID test_cube;
+
+ RID plane;
+ RID sphere;
+ RID light;
+ RID camera;
+ RID mover;
+ RID scenario;
+ RID space;
+
+ RID character;
+
+ real_t ofs_x, ofs_y;
+
+ Point2 joy_direction;
+
+ List<RID> bodies;
+ Map<PhysicsServer3D::ShapeType, RID> type_shape_map;
+ Map<PhysicsServer3D::ShapeType, RID> type_mesh_map;
+
+ void body_changed_transform(Object *p_state, RID p_visual_instance) {
+ PhysicsDirectBodyState3D *state = (PhysicsDirectBodyState3D *)p_state;
+ RenderingServer *vs = RenderingServer::get_singleton();
+ Transform3D t = state->get_transform();
+ vs->instance_set_transform(p_visual_instance, t);
+ }
+
+ bool quit;
+
+protected:
+ RID create_body(PhysicsServer3D::ShapeType p_shape, PhysicsServer3D::BodyMode p_body, const Transform3D p_location, bool p_active_default = true, const Transform3D &p_shape_xform = Transform3D()) {
+ RenderingServer *vs = RenderingServer::get_singleton();
+ PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
+
+ RID mesh_instance = vs->instance_create2(type_mesh_map[p_shape], scenario);
+ RID body = ps->body_create();
+ ps->body_set_mode(body, p_body);
+ ps->body_set_state(body, PhysicsServer3D::BODY_STATE_SLEEPING, !p_active_default);
+ ps->body_set_space(body, space);
+ ps->body_set_param(body, PhysicsServer3D::BODY_PARAM_BOUNCE, 0.0);
+ //todo set space
+ ps->body_add_shape(body, type_shape_map[p_shape]);
+ ps->body_set_force_integration_callback(body, callable_mp(this, &TestPhysics3DMainLoop::body_changed_transform), mesh_instance);
+
+ ps->body_set_state(body, PhysicsServer3D::BODY_STATE_TRANSFORM, p_location);
+ bodies.push_back(body);
+
+ if (p_body == PhysicsServer3D::BODY_MODE_STATIC) {
+ vs->instance_set_transform(mesh_instance, p_location);
+ }
+ return body;
+ }
+
+ RID create_world_boundary(const Plane &p_plane) {
+ PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
+
+ RID world_boundary_shape = ps->shape_create(PhysicsServer3D::SHAPE_WORLD_BOUNDARY);
+ ps->shape_set_data(world_boundary_shape, p_plane);
+
+ RID b = ps->body_create();
+ ps->body_set_mode(b, PhysicsServer3D::BODY_MODE_STATIC);
+
+ ps->body_set_space(b, space);
+ ps->body_add_shape(b, world_boundary_shape);
+ return b;
+ }
+
+ void configure_body(RID p_body, real_t p_mass, real_t p_friction, real_t p_bounce) {
+ PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
+ ps->body_set_param(p_body, PhysicsServer3D::BODY_PARAM_MASS, p_mass);
+ ps->body_set_param(p_body, PhysicsServer3D::BODY_PARAM_FRICTION, p_friction);
+ ps->body_set_param(p_body, PhysicsServer3D::BODY_PARAM_BOUNCE, p_bounce);
+ }
+
+ void initialize_shapes() {
+ RenderingServer *vs = RenderingServer::get_singleton();
+ PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
+
+ /* SPHERE SHAPE */
+ RID sphere_mesh = vs->make_sphere_mesh(10, 20, 0.5);
+ type_mesh_map[PhysicsServer3D::SHAPE_SPHERE] = sphere_mesh;
+
+ RID sphere_shape = ps->shape_create(PhysicsServer3D::SHAPE_SPHERE);
+ ps->shape_set_data(sphere_shape, 0.5);
+ type_shape_map[PhysicsServer3D::SHAPE_SPHERE] = sphere_shape;
+
+ /* BOX SHAPE */
+
+ Vector<Plane> box_planes = Geometry3D::build_box_planes(Vector3(0.5, 0.5, 0.5));
+ RID box_mesh = vs->mesh_create();
+ Geometry3D::MeshData box_data = Geometry3D::build_convex_mesh(box_planes);
+ vs->mesh_add_surface_from_mesh_data(box_mesh, box_data);
+ type_mesh_map[PhysicsServer3D::SHAPE_BOX] = box_mesh;
+
+ RID box_shape = ps->shape_create(PhysicsServer3D::SHAPE_BOX);
+ ps->shape_set_data(box_shape, Vector3(0.5, 0.5, 0.5));
+ type_shape_map[PhysicsServer3D::SHAPE_BOX] = box_shape;
+
+ /* CAPSULE SHAPE */
+
+ Vector<Plane> capsule_planes = Geometry3D::build_capsule_planes(0.5, 0.7, 12, Vector3::AXIS_Z);
+
+ RID capsule_mesh = vs->mesh_create();
+ Geometry3D::MeshData capsule_data = Geometry3D::build_convex_mesh(capsule_planes);
+ vs->mesh_add_surface_from_mesh_data(capsule_mesh, capsule_data);
+
+ type_mesh_map[PhysicsServer3D::SHAPE_CAPSULE] = capsule_mesh;
+
+ RID capsule_shape = ps->shape_create(PhysicsServer3D::SHAPE_CAPSULE);
+ Dictionary capsule_params;
+ capsule_params["radius"] = 0.5;
+ capsule_params["height"] = 1.4;
+ ps->shape_set_data(capsule_shape, capsule_params);
+ type_shape_map[PhysicsServer3D::SHAPE_CAPSULE] = capsule_shape;
+
+ /* CONVEX SHAPE */
+
+ Vector<Plane> convex_planes = Geometry3D::build_cylinder_planes(0.5, 0.7, 5, Vector3::AXIS_Z);
+
+ RID convex_mesh = vs->mesh_create();
+ Geometry3D::MeshData convex_data = Geometry3D::build_convex_mesh(convex_planes);
+ ConvexHullComputer::convex_hull(convex_data.vertices, convex_data);
+ vs->mesh_add_surface_from_mesh_data(convex_mesh, convex_data);
+
+ type_mesh_map[PhysicsServer3D::SHAPE_CONVEX_POLYGON] = convex_mesh;
+
+ RID convex_shape = ps->shape_create(PhysicsServer3D::SHAPE_CONVEX_POLYGON);
+ ps->shape_set_data(convex_shape, convex_data.vertices);
+ type_shape_map[PhysicsServer3D::SHAPE_CONVEX_POLYGON] = convex_shape;
+ }
+
+ void make_trimesh(Vector<Vector3> p_faces, const Transform3D &p_xform = Transform3D()) {
+ RenderingServer *vs = RenderingServer::get_singleton();
+ PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
+ RID trimesh_shape = ps->shape_create(PhysicsServer3D::SHAPE_CONCAVE_POLYGON);
+ Dictionary trimesh_params;
+ trimesh_params["faces"] = p_faces;
+ trimesh_params["backface_collision"] = false;
+ ps->shape_set_data(trimesh_shape, trimesh_params);
+ Vector<Vector3> normals; // for drawing
+ for (int i = 0; i < p_faces.size() / 3; i++) {
+ Plane p(p_faces[i * 3 + 0], p_faces[i * 3 + 1], p_faces[i * 3 + 2]);
+ normals.push_back(p.normal);
+ normals.push_back(p.normal);
+ normals.push_back(p.normal);
+ }
+
+ RID trimesh_mesh = vs->mesh_create();
+ Array d;
+ d.resize(RS::ARRAY_MAX);
+ d[RS::ARRAY_VERTEX] = p_faces;
+ d[RS::ARRAY_NORMAL] = normals;
+ vs->mesh_add_surface_from_arrays(trimesh_mesh, RS::PRIMITIVE_TRIANGLES, d);
+
+ RID triins = vs->instance_create2(trimesh_mesh, scenario);
+
+ RID tribody = ps->body_create();
+ ps->body_set_mode(tribody, PhysicsServer3D::BODY_MODE_STATIC);
+ ps->body_set_space(tribody, space);
+ //todo set space
+ ps->body_add_shape(tribody, trimesh_shape);
+ Transform3D tritrans = p_xform;
+ ps->body_set_state(tribody, PhysicsServer3D::BODY_STATE_TRANSFORM, tritrans);
+ vs->instance_set_transform(triins, tritrans);
+ }
+
+ void make_grid(int p_width, int p_height, real_t p_cellsize, real_t p_cellheight, const Transform3D &p_xform = Transform3D()) {
+ Vector<Vector<real_t>> grid;
+
+ grid.resize(p_width);
+
+ for (int i = 0; i < p_width; i++) {
+ grid.write[i].resize(p_height);
+
+ for (int j = 0; j < p_height; j++) {
+ grid.write[i].write[j] = 1.0 + Math::random(-p_cellheight, p_cellheight);
+ }
+ }
+
+ Vector<Vector3> faces;
+
+ for (int i = 1; i < p_width; i++) {
+ for (int j = 1; j < p_height; j++) {
+#define MAKE_VERTEX(m_x, m_z) \
+ faces.push_back(Vector3((m_x - p_width / 2) * p_cellsize, grid[m_x][m_z], (m_z - p_height / 2) * p_cellsize))
+
+ MAKE_VERTEX(i, j - 1);
+ MAKE_VERTEX(i, j);
+ MAKE_VERTEX(i - 1, j);
+
+ MAKE_VERTEX(i - 1, j - 1);
+ MAKE_VERTEX(i, j - 1);
+ MAKE_VERTEX(i - 1, j);
+ }
+ }
+
+ make_trimesh(faces, p_xform);
+ }
+
+public:
+ virtual void input_event(const Ref<InputEvent> &p_event) {
+ Ref<InputEventMouseMotion> mm = p_event;
+ if (mm.is_valid() && (mm->get_button_mask() & MouseButton::MASK_MIDDLE) != MouseButton::NONE) {
+ ofs_y -= mm->get_relative().y / 200.0;
+ ofs_x += mm->get_relative().x / 200.0;
+ }
+
+ if (mm.is_valid() && (mm->get_button_mask() & MouseButton::MASK_LEFT) != MouseButton::NONE) {
+ real_t y = -mm->get_relative().y / 20.0;
+ real_t x = mm->get_relative().x / 20.0;
+
+ if (mover.is_valid()) {
+ PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
+ Transform3D t = ps->body_get_state(mover, PhysicsServer3D::BODY_STATE_TRANSFORM);
+ t.origin += Vector3(x, y, 0);
+
+ ps->body_set_state(mover, PhysicsServer3D::BODY_STATE_TRANSFORM, t);
+ }
+ }
+ }
+
+ virtual void request_quit() {
+ quit = true;
+ }
+ virtual void initialize() override {
+ ofs_x = ofs_y = 0;
+ initialize_shapes();
+
+ PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
+ space = ps->space_create();
+ ps->space_set_active(space, true);
+
+ RenderingServer *vs = RenderingServer::get_singleton();
+
+ /* LIGHT */
+ RID lightaux = vs->directional_light_create();
+ scenario = vs->scenario_create();
+ vs->light_set_shadow(lightaux, true);
+ light = vs->instance_create2(lightaux, scenario);
+ Transform3D t;
+ t.rotate(Vector3(1.0, 0, 0), 0.6);
+ vs->instance_set_transform(light, t);
+
+ /* CAMERA */
+
+ camera = vs->camera_create();
+
+ RID viewport = vs->viewport_create();
+ Size2i screen_size = DisplayServer::get_singleton()->window_get_size();
+ vs->viewport_set_size(viewport, screen_size.x, screen_size.y);
+ vs->viewport_attach_to_screen(viewport, Rect2(Vector2(), screen_size));
+ vs->viewport_set_active(viewport, true);
+ vs->viewport_attach_camera(viewport, camera);
+ vs->viewport_set_scenario(viewport, scenario);
+
+ vs->camera_set_perspective(camera, 60, 0.1, 40.0);
+ vs->camera_set_transform(camera, Transform3D(Basis(), Vector3(0, 9, 12)));
+
+ Transform3D gxf;
+ gxf.basis.scale(Vector3(1.4, 0.4, 1.4));
+ gxf.origin = Vector3(-2, 1, -2);
+ make_grid(5, 5, 2.5, 1, gxf);
+ test_fall();
+ quit = false;
+ }
+ virtual bool physics_process(double p_time) override {
+ if (mover.is_valid()) {
+ static real_t joy_speed = 10;
+ PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
+ Transform3D t = ps->body_get_state(mover, PhysicsServer3D::BODY_STATE_TRANSFORM);
+ t.origin += Vector3(joy_speed * joy_direction.x * p_time, -joy_speed * joy_direction.y * p_time, 0);
+ ps->body_set_state(mover, PhysicsServer3D::BODY_STATE_TRANSFORM, t);
+ };
+
+ Transform3D cameratr;
+ cameratr.rotate(Vector3(0, 1, 0), ofs_x);
+ cameratr.rotate(Vector3(1, 0, 0), -ofs_y);
+ cameratr.translate(Vector3(0, 2, 8));
+ RenderingServer *vs = RenderingServer::get_singleton();
+ vs->camera_set_transform(camera, cameratr);
+
+ return quit;
+ }
+ virtual void finalize() override {
+ }
+
+ void test_joint() {
+ }
+
+ void test_hinge() {
+ }
+
+ void test_character() {
+ RenderingServer *vs = RenderingServer::get_singleton();
+ PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
+
+ Vector<Plane> capsule_planes = Geometry3D::build_capsule_planes(0.5, 1, 12, 5, Vector3::AXIS_Y);
+
+ RID capsule_mesh = vs->mesh_create();
+ Geometry3D::MeshData capsule_data = Geometry3D::build_convex_mesh(capsule_planes);
+ vs->mesh_add_surface_from_mesh_data(capsule_mesh, capsule_data);
+ type_mesh_map[PhysicsServer3D::SHAPE_CAPSULE] = capsule_mesh;
+
+ RID capsule_shape = ps->shape_create(PhysicsServer3D::SHAPE_CAPSULE);
+ Dictionary capsule_params;
+ capsule_params["radius"] = 0.5;
+ capsule_params["height"] = 1;
+ Transform3D shape_xform;
+ shape_xform.rotate(Vector3(1, 0, 0), Math_PI / 2.0);
+ //shape_xform.origin=Vector3(1,1,1);
+ ps->shape_set_data(capsule_shape, capsule_params);
+
+ RID mesh_instance = vs->instance_create2(capsule_mesh, scenario);
+ character = ps->body_create();
+ ps->body_set_mode(character, PhysicsServer3D::BODY_MODE_DYNAMIC_LINEAR);
+ ps->body_set_space(character, space);
+ //todo add space
+ ps->body_add_shape(character, capsule_shape);
+ ps->body_set_force_integration_callback(character, callable_mp(this, &TestPhysics3DMainLoop::body_changed_transform), mesh_instance);
+
+ ps->body_set_state(character, PhysicsServer3D::BODY_STATE_TRANSFORM, Transform3D(Basis(), Vector3(-2, 5, -2)));
+ bodies.push_back(character);
+ }
+
+ void test_fall() {
+ for (int i = 0; i < 35; i++) {
+ static const PhysicsServer3D::ShapeType shape_idx[] = {
+ PhysicsServer3D::SHAPE_CAPSULE,
+ PhysicsServer3D::SHAPE_BOX,
+ PhysicsServer3D::SHAPE_SPHERE,
+ PhysicsServer3D::SHAPE_CONVEX_POLYGON
+ };
+
+ PhysicsServer3D::ShapeType type = shape_idx[i % 4];
+
+ Transform3D t;
+
+ t.origin = Vector3(0.0 * i, 3.5 + 1.1 * i, 0.7 + 0.0 * i);
+ t.basis.rotate(Vector3(0.2, -1, 0), Math_PI / 2 * 0.6);
+
+ create_body(type, PhysicsServer3D::BODY_MODE_DYNAMIC, t);
+ }
+
+ create_world_boundary(Plane(Vector3(0, 1, 0), -1));
+ }
+
+ void test_activate() {
+ create_body(PhysicsServer3D::SHAPE_BOX, PhysicsServer3D::BODY_MODE_DYNAMIC, Transform3D(Basis(), Vector3(0, 2, 0)), true);
+ create_world_boundary(Plane(Vector3(0, 1, 0), -1));
+ }
+
+ virtual bool process(double p_time) override {
+ return false;
+ }
+
+ TestPhysics3DMainLoop() {
+ }
+};
+
+namespace TestPhysics3D {
+
+MainLoop *test() {
+ return memnew(TestPhysics3DMainLoop);
+}
+} // namespace TestPhysics3D
diff --git a/tests/servers/test_physics_3d.h b/tests/servers/test_physics_3d.h
new file mode 100644
index 0000000000..f618d0fb4f
--- /dev/null
+++ b/tests/servers/test_physics_3d.h
@@ -0,0 +1,41 @@
+/*************************************************************************/
+/* test_physics_3d.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_PHYSICS_H
+#define TEST_PHYSICS_H
+
+class MainLoop;
+
+namespace TestPhysics3D {
+
+MainLoop *test();
+}
+
+#endif
diff --git a/tests/servers/test_render.cpp b/tests/servers/test_render.cpp
new file mode 100644
index 0000000000..44403e3724
--- /dev/null
+++ b/tests/servers/test_render.cpp
@@ -0,0 +1,232 @@
+/*************************************************************************/
+/* test_render.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "test_render.h"
+
+#include "core/math/convex_hull.h"
+#include "core/os/main_loop.h"
+#include "servers/rendering_server.h"
+
+#define OBJECT_COUNT 50
+
+namespace TestRender {
+
+class TestMainLoop : public MainLoop {
+ RID test_cube;
+ RID instance;
+ RID camera;
+ RID viewport;
+ RID light;
+ RID scenario;
+
+ struct InstanceInfo {
+ RID instance;
+ Transform3D base;
+ Vector3 rot_axis;
+ };
+
+ List<InstanceInfo> instances;
+
+ float ofs;
+ bool quit;
+
+protected:
+public:
+ virtual void input_event(const Ref<InputEvent> &p_event) {
+ if (p_event->is_pressed()) {
+ quit = true;
+ }
+ }
+
+ virtual void init() {
+ print_line("INITIALIZING TEST RENDER");
+ RenderingServer *vs = RenderingServer::get_singleton();
+ test_cube = vs->get_test_cube();
+ scenario = vs->scenario_create();
+
+ Vector<Vector3> vts;
+
+ /*
+ Vector<Plane> sp = Geometry3D::build_sphere_planes(2,5,5);
+ Geometry3D::MeshData md2 = Geometry3D::build_convex_mesh(sp);
+ vts=md2.vertices;
+*/
+ /*
+
+ static const int s = 20;
+ for(int i=0;i<s;i++) {
+ Basis rot(Vector3(0,1,0),i*Math_PI/s);
+
+ for(int j=0;j<s;j++) {
+ Vector3 v;
+ v.x=Math::sin(j*Math_PI*2/s);
+ v.y=Math::cos(j*Math_PI*2/s);
+
+ vts.push_back( rot.xform(v*2 ) );
+ }
+ }*/
+ /*for(int i=0;i<100;i++) {
+ vts.push_back( Vector3(Math::randf()*2-1.0,Math::randf()*2-1.0,Math::randf()*2-1.0).normalized()*2);
+ }*/
+ /*
+ vts.push_back(Vector3(0,0,1));
+ vts.push_back(Vector3(0,0,-1));
+ vts.push_back(Vector3(0,1,0));
+ vts.push_back(Vector3(0,-1,0));
+ vts.push_back(Vector3(1,0,0));
+ vts.push_back(Vector3(-1,0,0));*/
+
+ vts.push_back(Vector3(1, 1, 1));
+ vts.push_back(Vector3(1, -1, 1));
+ vts.push_back(Vector3(-1, 1, 1));
+ vts.push_back(Vector3(-1, -1, 1));
+ vts.push_back(Vector3(1, 1, -1));
+ vts.push_back(Vector3(1, -1, -1));
+ vts.push_back(Vector3(-1, 1, -1));
+ vts.push_back(Vector3(-1, -1, -1));
+
+ Geometry3D::MeshData md;
+ Error err = ConvexHullComputer::convex_hull(vts, md);
+ print_line("ERR: " + itos(err));
+ test_cube = vs->mesh_create();
+ vs->mesh_add_surface_from_mesh_data(test_cube, md);
+ //vs->scenario_set_debug(scenario,RS::SCENARIO_DEBUG_WIREFRAME);
+
+ /*
+ RID sm = vs->shader_create();
+ //vs->shader_set_fragment_code(sm,"OUT_ALPHA=mod(TIME,1);");
+ //vs->shader_set_vertex_code(sm,"OUT_VERTEX=IN_VERTEX*mod(TIME,1);");
+ vs->shader_set_fragment_code(sm,"OUT_DIFFUSE=vec3(1,0,1);OUT_GLOW=abs(sin(TIME));");
+ RID tcmat = vs->mesh_surface_get_material(test_cube,0);
+ vs->material_set_shader(tcmat,sm);
+ */
+
+ List<String> cmdline = OS::get_singleton()->get_cmdline_args();
+ int object_count = OBJECT_COUNT;
+ if (cmdline.size() > 0 && cmdline[cmdline.size() - 1].to_int()) {
+ object_count = cmdline[cmdline.size() - 1].to_int();
+ };
+
+ for (int i = 0; i < object_count; i++) {
+ InstanceInfo ii;
+
+ ii.instance = vs->instance_create2(test_cube, scenario);
+
+ ii.base.translate(Math::random(-20, 20), Math::random(-20, 20), Math::random(-20, 18));
+ ii.base.rotate(Vector3(0, 1, 0), Math::randf() * Math_PI);
+ ii.base.rotate(Vector3(1, 0, 0), Math::randf() * Math_PI);
+ vs->instance_set_transform(ii.instance, ii.base);
+
+ ii.rot_axis = Vector3(Math::random(-1, 1), Math::random(-1, 1), Math::random(-1, 1)).normalized();
+
+ instances.push_back(ii);
+ }
+
+ camera = vs->camera_create();
+
+ // vs->camera_set_perspective( camera, 60.0,0.1, 100.0 );
+
+ viewport = vs->viewport_create();
+ Size2i screen_size = DisplayServer::get_singleton()->window_get_size();
+ vs->viewport_set_size(viewport, screen_size.x, screen_size.y);
+ vs->viewport_attach_to_screen(viewport, Rect2(Vector2(), screen_size));
+ vs->viewport_set_active(viewport, true);
+ vs->viewport_attach_camera(viewport, camera);
+ vs->viewport_set_scenario(viewport, scenario);
+ vs->camera_set_transform(camera, Transform3D(Basis(), Vector3(0, 3, 30)));
+ vs->camera_set_perspective(camera, 60, 0.1, 1000);
+
+ /*
+ RID lightaux = vs->light_create( RenderingServer::LIGHT_OMNI );
+ vs->light_set_var( lightaux, RenderingServer::LIGHT_VAR_RADIUS, 80 );
+ vs->light_set_var( lightaux, RenderingServer::LIGHT_VAR_ATTENUATION, 1 );
+ vs->light_set_var( lightaux, RenderingServer::LIGHT_VAR_ENERGY, 1.5 );
+ light = vs->instance_create( lightaux );
+ */
+ RID lightaux;
+
+ lightaux = vs->directional_light_create();
+ //vs->light_set_color( lightaux, RenderingServer::LIGHT_COLOR_AMBIENT, Color(0.0,0.0,0.0) );
+ vs->light_set_color(lightaux, Color(1.0, 1.0, 1.0));
+ //vs->light_set_shadow( lightaux, true );
+ light = vs->instance_create2(lightaux, scenario);
+ Transform3D lla;
+ //lla.set_look_at(Vector3(),Vector3(1, -1, 1));
+ lla.basis = Basis::looking_at(Vector3(0.0, -0.836026, -0.548690));
+
+ vs->instance_set_transform(light, lla);
+
+ lightaux = vs->omni_light_create();
+ //vs->light_set_color( lightaux, RenderingServer::LIGHT_COLOR_AMBIENT, Color(0.0,0.0,1.0) );
+ vs->light_set_color(lightaux, Color(1.0, 1.0, 0.0));
+ vs->light_set_param(lightaux, RenderingServer::LIGHT_PARAM_RANGE, 4);
+ vs->light_set_param(lightaux, RenderingServer::LIGHT_PARAM_ENERGY, 8);
+ //vs->light_set_shadow( lightaux, true );
+ //light = vs->instance_create( lightaux );
+
+ ofs = 0;
+ quit = false;
+ }
+ virtual bool iteration(double p_time) {
+ RenderingServer *vs = RenderingServer::get_singleton();
+ //Transform3D t;
+ //t.rotate(Vector3(0, 1, 0), ofs);
+ //t.translate(Vector3(0,0,20 ));
+ //vs->camera_set_transform(camera, t);
+
+ ofs += p_time * 0.05;
+
+ //return quit;
+
+ for (const InstanceInfo &E : instances) {
+ Transform3D pre(Basis(E.rot_axis, ofs), Vector3());
+ vs->instance_set_transform(E.instance, pre * E.base);
+ /*
+ if( !E->next() ) {
+ vs->free( E.instance );
+ instances.erase(E );
+ }*/
+ }
+
+ return quit;
+ }
+
+ virtual bool idle(double p_time) {
+ return quit;
+ }
+
+ virtual void finish() {
+ }
+};
+
+MainLoop *test() {
+ return memnew(TestMainLoop);
+}
+} // namespace TestRender
diff --git a/tests/servers/test_render.h b/tests/servers/test_render.h
new file mode 100644
index 0000000000..d5a3e01ee5
--- /dev/null
+++ b/tests/servers/test_render.h
@@ -0,0 +1,41 @@
+/*************************************************************************/
+/* test_render.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_RENDER_H
+#define TEST_RENDER_H
+
+class MainLoop;
+
+namespace TestRender {
+
+MainLoop *test();
+}
+
+#endif // TEST_RENDER_H
diff --git a/tests/servers/test_shader_lang.cpp b/tests/servers/test_shader_lang.cpp
new file mode 100644
index 0000000000..acc7e32441
--- /dev/null
+++ b/tests/servers/test_shader_lang.cpp
@@ -0,0 +1,367 @@
+/*************************************************************************/
+/* test_shader_lang.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "test_shader_lang.h"
+
+#include "core/os/main_loop.h"
+#include "core/os/os.h"
+#include "servers/rendering/shader_language.h"
+
+typedef ShaderLanguage SL;
+
+namespace TestShaderLang {
+
+static String _mktab(int p_level) {
+ String tb;
+ for (int i = 0; i < p_level; i++) {
+ tb += "\t";
+ }
+
+ return tb;
+}
+
+static String _typestr(SL::DataType p_type) {
+ return ShaderLanguage::get_datatype_name(p_type);
+}
+
+static String _prestr(SL::DataPrecision p_pres) {
+ switch (p_pres) {
+ case SL::PRECISION_LOWP:
+ return "lowp ";
+ case SL::PRECISION_MEDIUMP:
+ return "mediump ";
+ case SL::PRECISION_HIGHP:
+ return "highp ";
+ case SL::PRECISION_DEFAULT:
+ return "";
+ }
+ return "";
+}
+
+static String _opstr(SL::Operator p_op) {
+ return ShaderLanguage::get_operator_text(p_op);
+}
+
+static String get_constant_text(SL::DataType p_type, const Vector<SL::ConstantNode::Value> &p_values) {
+ switch (p_type) {
+ case SL::TYPE_BOOL:
+ return p_values[0].boolean ? "true" : "false";
+ case SL::TYPE_BVEC2:
+ return String() + "bvec2(" + (p_values[0].boolean ? "true" : "false") + (p_values[1].boolean ? "true" : "false") + ")";
+ case SL::TYPE_BVEC3:
+ return String() + "bvec3(" + (p_values[0].boolean ? "true" : "false") + "," + (p_values[1].boolean ? "true" : "false") + "," + (p_values[2].boolean ? "true" : "false") + ")";
+ case SL::TYPE_BVEC4:
+ return String() + "bvec4(" + (p_values[0].boolean ? "true" : "false") + "," + (p_values[1].boolean ? "true" : "false") + "," + (p_values[2].boolean ? "true" : "false") + "," + (p_values[3].boolean ? "true" : "false") + ")";
+ case SL::TYPE_INT:
+ return rtos(p_values[0].sint);
+ case SL::TYPE_IVEC2:
+ return String() + "ivec2(" + rtos(p_values[0].sint) + "," + rtos(p_values[1].sint) + ")";
+ case SL::TYPE_IVEC3:
+ return String() + "ivec3(" + rtos(p_values[0].sint) + "," + rtos(p_values[1].sint) + "," + rtos(p_values[2].sint) + ")";
+ case SL::TYPE_IVEC4:
+ return String() + "ivec4(" + rtos(p_values[0].sint) + "," + rtos(p_values[1].sint) + "," + rtos(p_values[2].sint) + "," + rtos(p_values[3].sint) + ")";
+ case SL::TYPE_UINT:
+ return rtos(p_values[0].real);
+ case SL::TYPE_UVEC2:
+ return String() + "uvec2(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + ")";
+ case SL::TYPE_UVEC3:
+ return String() + "uvec3(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + "," + rtos(p_values[2].real) + ")";
+ case SL::TYPE_UVEC4:
+ return String() + "uvec4(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + "," + rtos(p_values[2].real) + "," + rtos(p_values[3].real) + ")";
+ case SL::TYPE_FLOAT:
+ return rtos(p_values[0].real);
+ case SL::TYPE_VEC2:
+ return String() + "vec2(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + ")";
+ case SL::TYPE_VEC3:
+ return String() + "vec3(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + "," + rtos(p_values[2].real) + ")";
+ case SL::TYPE_VEC4:
+ return String() + "vec4(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + "," + rtos(p_values[2].real) + "," + rtos(p_values[3].real) + ")";
+ default:
+ ERR_FAIL_V(String());
+ }
+}
+
+static String dump_node_code(SL::Node *p_node, int p_level) {
+ String code;
+
+ switch (p_node->type) {
+ case SL::Node::TYPE_SHADER: {
+ SL::ShaderNode *pnode = (SL::ShaderNode *)p_node;
+
+ for (const KeyValue<StringName, SL::ShaderNode::Uniform> &E : pnode->uniforms) {
+ String ucode = "uniform ";
+ ucode += _prestr(E.value.precision);
+ ucode += _typestr(E.value.type);
+ ucode += " " + String(E.key);
+ if (E.value.array_size > 0) {
+ ucode += "[";
+ ucode += itos(E.value.array_size);
+ ucode += "]";
+ } else {
+ if (E.value.default_value.size()) {
+ ucode += " = " + get_constant_text(E.value.type, E.value.default_value);
+ }
+
+ static const char *hint_name[SL::ShaderNode::Uniform::HINT_MAX] = {
+ "",
+ "color",
+ "range",
+ "albedo",
+ "normal",
+ "black",
+ "white"
+ };
+
+ if (E.value.hint) {
+ ucode += " : " + String(hint_name[E.value.hint]);
+ }
+ }
+
+ code += ucode + "\n";
+ }
+
+ for (const KeyValue<StringName, SL::ShaderNode::Varying> &E : pnode->varyings) {
+ String vcode = "varying ";
+ vcode += _prestr(E.value.precision);
+ vcode += _typestr(E.value.type);
+ vcode += " " + String(E.key);
+
+ code += vcode + "\n";
+ }
+ for (int i = 0; i < pnode->functions.size(); i++) {
+ SL::FunctionNode *fnode = pnode->functions[i].function;
+
+ String header;
+ header = _typestr(fnode->return_type) + " " + fnode->name + "(";
+ for (int j = 0; j < fnode->arguments.size(); j++) {
+ if (j > 0) {
+ header += ", ";
+ }
+ header += _prestr(fnode->arguments[j].precision) + _typestr(fnode->arguments[j].type) + " " + fnode->arguments[j].name;
+ }
+
+ header += ")\n";
+ code += header;
+ code += dump_node_code(fnode->body, p_level + 1);
+ }
+
+ //code+=dump_node_code(pnode->body,p_level);
+ } break;
+ case SL::Node::TYPE_STRUCT: {
+ } break;
+ case SL::Node::TYPE_FUNCTION: {
+ } break;
+ case SL::Node::TYPE_BLOCK: {
+ SL::BlockNode *bnode = (SL::BlockNode *)p_node;
+
+ //variables
+ code += _mktab(p_level - 1) + "{\n";
+ for (const KeyValue<StringName, SL::BlockNode::Variable> &E : bnode->variables) {
+ code += _mktab(p_level) + _prestr(E.value.precision) + _typestr(E.value.type) + " " + E.key + ";\n";
+ }
+
+ for (int i = 0; i < bnode->statements.size(); i++) {
+ String scode = dump_node_code(bnode->statements[i], p_level);
+
+ if (bnode->statements[i]->type == SL::Node::TYPE_CONTROL_FLOW) {
+ code += scode; //use directly
+ } else {
+ code += _mktab(p_level) + scode + ";\n";
+ }
+ }
+ code += _mktab(p_level - 1) + "}\n";
+
+ } break;
+ case SL::Node::TYPE_VARIABLE: {
+ SL::VariableNode *vnode = (SL::VariableNode *)p_node;
+ code = vnode->name;
+
+ } break;
+ case SL::Node::TYPE_VARIABLE_DECLARATION: {
+ // FIXME: Implement
+ } break;
+ case SL::Node::TYPE_ARRAY: {
+ SL::ArrayNode *vnode = (SL::ArrayNode *)p_node;
+ code = vnode->name;
+ } break;
+ case SL::Node::TYPE_ARRAY_DECLARATION: {
+ // FIXME: Implement
+ } break;
+ case SL::Node::TYPE_ARRAY_CONSTRUCT: {
+ // FIXME: Implement
+ } break;
+ case SL::Node::TYPE_CONSTANT: {
+ SL::ConstantNode *cnode = (SL::ConstantNode *)p_node;
+ return get_constant_text(cnode->datatype, cnode->values);
+
+ } break;
+ case SL::Node::TYPE_OPERATOR: {
+ SL::OperatorNode *onode = (SL::OperatorNode *)p_node;
+
+ switch (onode->op) {
+ case SL::OP_ASSIGN:
+ case SL::OP_ASSIGN_ADD:
+ case SL::OP_ASSIGN_SUB:
+ case SL::OP_ASSIGN_MUL:
+ case SL::OP_ASSIGN_DIV:
+ case SL::OP_ASSIGN_SHIFT_LEFT:
+ case SL::OP_ASSIGN_SHIFT_RIGHT:
+ case SL::OP_ASSIGN_MOD:
+ case SL::OP_ASSIGN_BIT_AND:
+ case SL::OP_ASSIGN_BIT_OR:
+ case SL::OP_ASSIGN_BIT_XOR:
+ code = dump_node_code(onode->arguments[0], p_level) + _opstr(onode->op) + dump_node_code(onode->arguments[1], p_level);
+ break;
+ case SL::OP_BIT_INVERT:
+ case SL::OP_NEGATE:
+ case SL::OP_NOT:
+ case SL::OP_DECREMENT:
+ case SL::OP_INCREMENT:
+ code = _opstr(onode->op) + dump_node_code(onode->arguments[0], p_level);
+ break;
+ case SL::OP_POST_DECREMENT:
+ case SL::OP_POST_INCREMENT:
+ code = dump_node_code(onode->arguments[0], p_level) + _opstr(onode->op);
+ break;
+ case SL::OP_CALL:
+ case SL::OP_CONSTRUCT:
+ code = dump_node_code(onode->arguments[0], p_level) + "(";
+ for (int i = 1; i < onode->arguments.size(); i++) {
+ if (i > 1) {
+ code += ", ";
+ }
+ code += dump_node_code(onode->arguments[i], p_level);
+ }
+ code += ")";
+ break;
+ case SL::OP_EMPTY:
+ break;
+ default: {
+ code = "(" + dump_node_code(onode->arguments[0], p_level) + _opstr(onode->op) + dump_node_code(onode->arguments[1], p_level) + ")";
+ break;
+ }
+ }
+
+ } break;
+ case SL::Node::TYPE_CONTROL_FLOW: {
+ SL::ControlFlowNode *cfnode = (SL::ControlFlowNode *)p_node;
+ if (cfnode->flow_op == SL::FLOW_OP_IF) {
+ code += _mktab(p_level) + "if (" + dump_node_code(cfnode->expressions[0], p_level) + ")\n";
+ code += dump_node_code(cfnode->blocks[0], p_level + 1);
+ if (cfnode->blocks.size() == 2) {
+ code += _mktab(p_level) + "else\n";
+ code += dump_node_code(cfnode->blocks[1], p_level + 1);
+ }
+
+ } else if (cfnode->flow_op == SL::FLOW_OP_RETURN) {
+ if (cfnode->blocks.size()) {
+ code = "return " + dump_node_code(cfnode->blocks[0], p_level);
+ } else {
+ code = "return";
+ }
+ }
+
+ } break;
+ case SL::Node::TYPE_MEMBER: {
+ SL::MemberNode *mnode = (SL::MemberNode *)p_node;
+ code = dump_node_code(mnode->owner, p_level) + "." + mnode->name;
+
+ } break;
+ }
+
+ return code;
+}
+
+static Error recreate_code(void *p_str, SL::ShaderNode *p_program) {
+ String *str = (String *)p_str;
+
+ *str = dump_node_code(p_program, 0);
+
+ return OK;
+}
+
+MainLoop *test() {
+ List<String> cmdlargs = OS::get_singleton()->get_cmdline_args();
+
+ if (cmdlargs.is_empty()) {
+ //try editor!
+ print_line("usage: godot -test shader_lang <shader>");
+ return nullptr;
+ }
+
+ String test = cmdlargs.back()->get();
+
+ FileAccess *fa = FileAccess::open(test, FileAccess::READ);
+
+ if (!fa) {
+ ERR_FAIL_V(nullptr);
+ }
+
+ String code;
+
+ while (true) {
+ char32_t c = fa->get_8();
+ if (fa->eof_reached()) {
+ break;
+ }
+ code += c;
+ }
+
+ SL sl;
+ print_line("tokens:\n\n" + sl.token_debug(code));
+
+ Map<StringName, SL::FunctionInfo> dt;
+ dt["fragment"].built_ins["ALBEDO"] = SL::TYPE_VEC3;
+ dt["fragment"].can_discard = true;
+
+ Vector<SL::ModeInfo> rm;
+ rm.push_back({ "popo" });
+ Set<String> types;
+ types.insert("spatial");
+
+ ShaderLanguage::ShaderCompileInfo info;
+ info.functions = dt;
+ info.render_modes = rm;
+ info.shader_types = types;
+
+ Error err = sl.compile(code, info);
+
+ if (err) {
+ print_line("Error at line: " + rtos(sl.get_error_line()) + ": " + sl.get_error_text());
+ return nullptr;
+ } else {
+ String code2;
+ recreate_code(&code2, sl.get_shader());
+ print_line("code:\n\n" + code2);
+ }
+
+ return nullptr;
+}
+} // namespace TestShaderLang
diff --git a/tests/servers/test_shader_lang.h b/tests/servers/test_shader_lang.h
new file mode 100644
index 0000000000..31e1bfbeea
--- /dev/null
+++ b/tests/servers/test_shader_lang.h
@@ -0,0 +1,41 @@
+/*************************************************************************/
+/* test_shader_lang.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_SHADER_LANG_H
+#define TEST_SHADER_LANG_H
+
+class MainLoop;
+
+namespace TestShaderLang {
+
+MainLoop *test();
+}
+
+#endif // TEST_SHADER_LANG_H
diff --git a/tests/servers/test_text_server.h b/tests/servers/test_text_server.h
new file mode 100644
index 0000000000..b06f315bc8
--- /dev/null
+++ b/tests/servers/test_text_server.h
@@ -0,0 +1,296 @@
+/*************************************************************************/
+/* test_text_server.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifdef TOOLS_ENABLED
+
+#ifndef TEST_TEXT_SERVER_H
+#define TEST_TEXT_SERVER_H
+
+#include "editor/builtin_fonts.gen.h"
+#include "servers/text_server.h"
+#include "tests/test_macros.h"
+
+namespace TestTextServer {
+
+TEST_SUITE("[[TextServer]") {
+ TEST_CASE("[TextServer] Init, font loading and shaping") {
+ SUBCASE("[TextServer] Loading fonts") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ TEST_FAIL_COND(ts.is_null(), "Invalid TS interface.");
+
+ RID font = ts->create_font();
+ ts->font_set_data_ptr(font, _font_NotoSans_Regular, _font_NotoSans_Regular_size);
+ TEST_FAIL_COND(font == RID(), "Loading font failed.");
+ ts->free(font);
+ }
+ }
+
+ SUBCASE("[TextServer] Text layout: Font fallback") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ TEST_FAIL_COND(ts.is_null(), "Invalid TS interface.");
+
+ RID font1 = ts->create_font();
+ ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size);
+ RID font2 = ts->create_font();
+ ts->font_set_data_ptr(font2, _font_NotoSansThaiUI_Regular, _font_NotoSansThaiUI_Regular_size);
+
+ Vector<RID> font;
+ font.push_back(font1);
+ font.push_back(font2);
+
+ String test = U"คนอ้วน khon uan ראה";
+ // 6^ 17^
+
+ RID ctx = ts->create_shaped_text();
+ TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed.");
+ bool ok = ts->shaped_text_add_string(ctx, test, font, 16);
+ TEST_FAIL_COND(!ok, "Adding text to the buffer failed.");
+
+ const Glyph *glyphs = ts->shaped_text_get_glyphs(ctx);
+ int gl_size = ts->shaped_text_get_glyph_count(ctx);
+ TEST_FAIL_COND(gl_size == 0, "Shaping failed");
+ for (int j = 0; j < gl_size; j++) {
+ if (glyphs[j].start < 6) {
+ TEST_FAIL_COND(glyphs[j].font_rid != font[1], "Incorrect font selected.");
+ }
+ if ((glyphs[j].start > 6) && (glyphs[j].start < 16)) {
+ TEST_FAIL_COND(glyphs[j].font_rid != font[0], "Incorrect font selected.");
+ }
+ if (glyphs[j].start > 16) {
+ TEST_FAIL_COND(glyphs[j].font_rid != RID(), "Incorrect font selected.");
+ TEST_FAIL_COND(glyphs[j].index != test[glyphs[j].start], "Incorrect glyph index.");
+ }
+ TEST_FAIL_COND((glyphs[j].start < 0 || glyphs[j].end > test.length()), "Incorrect glyph range.");
+ TEST_FAIL_COND(glyphs[j].font_size != 16, "Incorrect glyph font size.");
+ }
+
+ ts->free(ctx);
+
+ for (int j = 0; j < font.size(); j++) {
+ ts->free(font[j]);
+ }
+ font.clear();
+ }
+ }
+
+ SUBCASE("[TextServer] Text layout: BiDi") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ TEST_FAIL_COND(ts.is_null(), "Invalid TS interface.");
+
+ if (!ts->has_feature(TextServer::FEATURE_BIDI_LAYOUT)) {
+ continue;
+ }
+
+ RID font1 = ts->create_font();
+ ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size);
+ RID font2 = ts->create_font();
+ ts->font_set_data_ptr(font2, _font_NotoNaskhArabicUI_Regular, _font_NotoNaskhArabicUI_Regular_size);
+
+ Vector<RID> font;
+ font.push_back(font1);
+ font.push_back(font2);
+
+ String test = U"Arabic (اَلْعَرَبِيَّةُ, al-ʿarabiyyah)";
+ // 7^ 26^
+
+ RID ctx = ts->create_shaped_text();
+ TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed.");
+ bool ok = ts->shaped_text_add_string(ctx, test, font, 16);
+ TEST_FAIL_COND(!ok, "Adding text to the buffer failed.");
+
+ const Glyph *glyphs = ts->shaped_text_get_glyphs(ctx);
+ int gl_size = ts->shaped_text_get_glyph_count(ctx);
+ TEST_FAIL_COND(gl_size == 0, "Shaping failed");
+ for (int j = 0; j < gl_size; j++) {
+ if (glyphs[j].count > 0) {
+ if (glyphs[j].start < 7) {
+ TEST_FAIL_COND(((glyphs[j].flags & TextServer::GRAPHEME_IS_RTL) == TextServer::GRAPHEME_IS_RTL), "Incorrect direction.");
+ }
+ if ((glyphs[j].start > 8) && (glyphs[j].start < 23)) {
+ TEST_FAIL_COND(((glyphs[j].flags & TextServer::GRAPHEME_IS_RTL) != TextServer::GRAPHEME_IS_RTL), "Incorrect direction.");
+ }
+ if (glyphs[j].start > 26) {
+ TEST_FAIL_COND(((glyphs[j].flags & TextServer::GRAPHEME_IS_RTL) == TextServer::GRAPHEME_IS_RTL), "Incorrect direction.");
+ }
+ }
+ }
+
+ ts->free(ctx);
+
+ for (int j = 0; j < font.size(); j++) {
+ ts->free(font[j]);
+ }
+ font.clear();
+ }
+ }
+
+ SUBCASE("[TextServer] Text layout: Line breaking") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ TEST_FAIL_COND(ts.is_null(), "Invalid TS interface.");
+
+ String test_1 = U"test test test";
+ // 5^ 10^
+
+ RID font1 = ts->create_font();
+ ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size);
+ RID font2 = ts->create_font();
+ ts->font_set_data_ptr(font2, _font_NotoSansThaiUI_Regular, _font_NotoSansThaiUI_Regular_size);
+
+ Vector<RID> font;
+ font.push_back(font1);
+ font.push_back(font2);
+
+ RID ctx = ts->create_shaped_text();
+ TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed.");
+ bool ok = ts->shaped_text_add_string(ctx, test_1, font, 16);
+ TEST_FAIL_COND(!ok, "Adding text to the buffer failed.");
+
+ PackedInt32Array brks = ts->shaped_text_get_line_breaks(ctx, 1);
+ TEST_FAIL_COND(brks.size() != 6, "Invalid line breaks number.");
+ if (brks.size() == 6) {
+ TEST_FAIL_COND(brks[0] != 0, "Invalid line break position.");
+ TEST_FAIL_COND(brks[1] != 5, "Invalid line break position.");
+
+ TEST_FAIL_COND(brks[2] != 5, "Invalid line break position.");
+ TEST_FAIL_COND(brks[3] != 10, "Invalid line break position.");
+
+ TEST_FAIL_COND(brks[4] != 10, "Invalid line break position.");
+ TEST_FAIL_COND(brks[5] != 14, "Invalid line break position.");
+ }
+
+ ts->free(ctx);
+
+ for (int j = 0; j < font.size(); j++) {
+ ts->free(font[j]);
+ }
+ font.clear();
+ }
+ }
+
+ SUBCASE("[TextServer] Text layout: Justification") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ TEST_FAIL_COND(ts.is_null(), "Invalid TS interface.");
+
+ RID font1 = ts->create_font();
+ ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size);
+ RID font2 = ts->create_font();
+ ts->font_set_data_ptr(font2, _font_NotoNaskhArabicUI_Regular, _font_NotoNaskhArabicUI_Regular_size);
+
+ Vector<RID> font;
+ font.push_back(font1);
+ font.push_back(font2);
+
+ String test_1 = U"الحمد";
+ String test_2 = U"الحمد test";
+ String test_3 = U"test test";
+ // 7^ 26^
+
+ RID ctx;
+ bool ok;
+ float width_old, width;
+ if (ts->has_feature(TextServer::FEATURE_KASHIDA_JUSTIFICATION)) {
+ ctx = ts->create_shaped_text();
+ TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed.");
+ ok = ts->shaped_text_add_string(ctx, test_1, font, 16);
+ TEST_FAIL_COND(!ok, "Adding text to the buffer failed.");
+
+ width_old = ts->shaped_text_get_width(ctx);
+ width = ts->shaped_text_fit_to_width(ctx, 100, TextServer::JUSTIFICATION_WORD_BOUND);
+ TEST_FAIL_COND((width != width_old), "Invalid fill width.");
+ width = ts->shaped_text_fit_to_width(ctx, 100, TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA);
+ TEST_FAIL_COND((width <= width_old || width > 100), "Invalid fill width.");
+
+ ts->free(ctx);
+
+ ctx = ts->create_shaped_text();
+ TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed.");
+ ok = ts->shaped_text_add_string(ctx, test_2, font, 16);
+ TEST_FAIL_COND(!ok, "Adding text to the buffer failed.");
+
+ width_old = ts->shaped_text_get_width(ctx);
+ width = ts->shaped_text_fit_to_width(ctx, 100, TextServer::JUSTIFICATION_WORD_BOUND);
+ TEST_FAIL_COND((width <= width_old || width > 100), "Invalid fill width.");
+ width = ts->shaped_text_fit_to_width(ctx, 100, TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA);
+ TEST_FAIL_COND((width <= width_old || width > 100), "Invalid fill width.");
+
+ ts->free(ctx);
+ }
+
+ ctx = ts->create_shaped_text();
+ TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed.");
+ ok = ts->shaped_text_add_string(ctx, test_3, font, 16);
+ TEST_FAIL_COND(!ok, "Adding text to the buffer failed.");
+
+ width_old = ts->shaped_text_get_width(ctx);
+ width = ts->shaped_text_fit_to_width(ctx, 100, TextServer::JUSTIFICATION_WORD_BOUND);
+ TEST_FAIL_COND((width <= width_old || width > 100), "Invalid fill width.");
+
+ ts->free(ctx);
+
+ for (int j = 0; j < font.size(); j++) {
+ ts->free(font[j]);
+ }
+ font.clear();
+ }
+ }
+
+ SUBCASE("[TextServer] Strip Diacritics") {
+ for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) {
+ Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i);
+ TEST_FAIL_COND(ts.is_null(), "Invalid TS interface.");
+
+ if (ts->has_feature(TextServer::FEATURE_SHAPING)) {
+ CHECK(ts->strip_diacritics(U"ٱلسَّلَامُ عَلَيْكُمْ") == U"ٱلسلام عليكم");
+ }
+
+ CHECK(ts->strip_diacritics(U"pêches épinards tomates fraises") == U"peches epinards tomates fraises");
+ CHECK(ts->strip_diacritics(U"ΆΈΉΊΌΎΏΪΫϓϔ") == U"ΑΕΗΙΟΥΩΙΥΥΥ");
+ CHECK(ts->strip_diacritics(U"άέήίΐϊΰϋόύώ") == U"αεηιιιυυουω");
+ CHECK(ts->strip_diacritics(U"ЀЁЃ ЇЌЍӢӤЙ ЎӮӰӲ ӐӒӖӚӜӞ ӦӪ Ӭ Ӵ Ӹ") == U"ЕЕГ ІКИИИИ УУУУ ААЕӘЖЗ ОӨ Э Ч Ы");
+ CHECK(ts->strip_diacritics(U"ѐёѓ їќѝӣӥй ўӯӱӳ ӑӓӗӛӝӟ ӧӫ ӭ ӵ ӹ") == U"еег ікииии уууу ааеәжз оө э ч ы");
+ CHECK(ts->strip_diacritics(U"ÀÁÂÃÄÅĀĂĄÇĆĈĊČĎÈÉÊËĒĔĖĘĚĜĞĠĢĤÌÍÎÏĨĪĬĮİĴĶĹĻĽÑŃŅŇŊÒÓÔÕÖØŌŎŐƠŔŖŘŚŜŞŠŢŤÙÚÛÜŨŪŬŮŰŲƯŴÝŶŹŻŽ") == U"AAAAAAAAACCCCCDEEEEEEEEEGGGGHIIIIIIIIIJKLLLNNNNŊOOOOOØOOOORRRSSSSTTUUUUUUUUUUUWYYZZZ");
+ CHECK(ts->strip_diacritics(U"àáâãäåāăąçćĉċčďèéêëēĕėęěĝğġģĥìíîïĩīĭįĵķĺļľñńņňŋòóôõöøōŏőơŕŗřśŝşšţťùúûüũūŭůűųưŵýÿŷźżž") == U"aaaaaaaaacccccdeeeeeeeeegggghiiiiiiiijklllnnnnŋoooooøoooorrrssssttuuuuuuuuuuuwyyyzzz");
+ CHECK(ts->strip_diacritics(U"ǍǏȈǑǪǬȌȎȪȬȮȰǓǕǗǙǛȔȖǞǠǺȀȂȦǢǼǦǴǨǸȆȐȒȘȚȞȨ Ḁ ḂḄḆ Ḉ ḊḌḎḐḒ ḔḖḘḚḜ Ḟ Ḡ ḢḤḦḨḪ ḬḮ ḰḲḴ ḶḸḺḼ ḾṀṂ ṄṆṈṊ ṌṎṐṒ ṔṖ ṘṚṜṞ ṠṢṤṦṨ ṪṬṮṰ ṲṴṶṸṺ") == U"AIIOOOOOOOOOUUUUUUUAAAAAAÆÆGGKNERRSTHE A BBB C DDDDD EEEEE F G HHHHH II KKK LLLL MMM NNNN OOOO PP RRRR SSSSS TTTT UUUUU");
+ CHECK(ts->strip_diacritics(U"ǎǐȉȋǒǫǭȍȏȫȭȯȱǔǖǘǚǜȕȗǟǡǻȁȃȧǣǽǧǵǩǹȇȑȓșțȟȩ ḁ ḃḅḇ ḉ ḋḍḏḑḓ ḟ ḡ ḭḯ ḱḳḵ ḷḹḻḽ ḿṁṃ ṅṇṉṋ ṍṏṑṓ ṗṕ ṙṛṝṟ ṡṣṥṧṩ ṫṭṯṱ ṳṵṷṹṻ") == U"aiiiooooooooouuuuuuuaaaaaaææggknerrsthe a bbb c ddddd f g ii kkk llll mmm nnnn oooo pp rrrr sssss tttt uuuuu");
+ CHECK(ts->strip_diacritics(U"ṼṾ ẀẂẄẆẈ ẊẌ Ẏ ẐẒẔ") == U"VV WWWWW XX Y ZZZ");
+ CHECK(ts->strip_diacritics(U"ṽṿ ẁẃẅẇẉ ẋẍ ẏ ẑẓẕ ẖ ẗẘẙẛ") == U"vv wwwww xx y zzz h twys");
+ }
+ }
+ }
+}
+}; // namespace TestTextServer
+
+#endif // TEST_TEXT_SERVER_H
+#endif // TOOLS_ENABLED
diff --git a/tests/test_macros.cpp b/tests/test_macros.cpp
new file mode 100644
index 0000000000..aa07f8211a
--- /dev/null
+++ b/tests/test_macros.cpp
@@ -0,0 +1,42 @@
+/*************************************************************************/
+/* test_macros.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#define DOCTEST_CONFIG_IMPLEMENT
+#include "test_macros.h"
+
+Map<String, TestFunc> *test_commands = nullptr;
+
+int register_test_command(String p_command, TestFunc p_function) {
+ if (!test_commands) {
+ test_commands = new Map<String, TestFunc>;
+ }
+ test_commands->insert(p_command, p_function);
+ return 0;
+}
diff --git a/tests/test_macros.h b/tests/test_macros.h
new file mode 100644
index 0000000000..ed8a12f155
--- /dev/null
+++ b/tests/test_macros.h
@@ -0,0 +1,350 @@
+/*************************************************************************/
+/* test_macros.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_MACROS_H
+#define TEST_MACROS_H
+
+#include "core/input/input_map.h"
+#include "core/object/message_queue.h"
+#include "core/variant/variant.h"
+
+// See documentation for doctest at:
+// https://github.com/onqtam/doctest/blob/master/doc/markdown/readme.md#reference
+#include "thirdparty/doctest/doctest.h"
+
+// The test is skipped with this, run pending tests with `--test --no-skip`.
+#define TEST_CASE_PENDING(name) TEST_CASE(name *doctest::skip())
+
+// The test case is marked as failed, but does not fail the entire test run.
+#define TEST_CASE_MAY_FAIL(name) TEST_CASE(name *doctest::may_fail())
+
+// Provide aliases to conform with Godot naming conventions (see error macros).
+#define TEST_COND(cond, ...) DOCTEST_CHECK_FALSE_MESSAGE(cond, __VA_ARGS__)
+#define TEST_FAIL(cond, ...) DOCTEST_FAIL(cond, __VA_ARGS__)
+#define TEST_FAIL_COND(cond, ...) DOCTEST_REQUIRE_FALSE_MESSAGE(cond, __VA_ARGS__)
+#define TEST_FAIL_COND_WARN(cond, ...) DOCTEST_WARN_FALSE_MESSAGE(cond, __VA_ARGS__)
+
+// Temporarily disable error prints to test failure paths.
+// This allows to avoid polluting the test summary with error messages.
+// The `_print_error_enabled` boolean is defined in `core/print_string.cpp` and
+// works at global scope. It's used by various loggers in `should_log()` method,
+// which are used by error macros which call into `OS::print_error`, effectively
+// disabling any error messages to be printed from the engine side (not tests).
+#define ERR_PRINT_OFF _print_error_enabled = false;
+#define ERR_PRINT_ON _print_error_enabled = true;
+
+// Stringify all `Variant` compatible types for doctest output by default.
+// https://github.com/onqtam/doctest/blob/master/doc/markdown/stringification.md
+
+#define DOCTEST_STRINGIFY_VARIANT(m_type) \
+ template <> \
+ struct doctest::StringMaker<m_type> { \
+ static doctest::String convert(const m_type &p_val) { \
+ const Variant val = p_val; \
+ return val.get_construct_string().utf8().get_data(); \
+ } \
+ };
+
+#define DOCTEST_STRINGIFY_VARIANT_POINTER(m_type) \
+ template <> \
+ struct doctest::StringMaker<m_type> { \
+ static doctest::String convert(const m_type *p_val) { \
+ const Variant val = p_val; \
+ return val.get_construct_string().utf8().get_data(); \
+ } \
+ };
+
+DOCTEST_STRINGIFY_VARIANT(Variant);
+DOCTEST_STRINGIFY_VARIANT(::String); // Disambiguate from `doctest::String`.
+
+DOCTEST_STRINGIFY_VARIANT(Vector2);
+DOCTEST_STRINGIFY_VARIANT(Vector2i);
+DOCTEST_STRINGIFY_VARIANT(Rect2);
+DOCTEST_STRINGIFY_VARIANT(Rect2i);
+DOCTEST_STRINGIFY_VARIANT(Vector3);
+DOCTEST_STRINGIFY_VARIANT(Vector3i);
+DOCTEST_STRINGIFY_VARIANT(Transform2D);
+DOCTEST_STRINGIFY_VARIANT(Plane);
+DOCTEST_STRINGIFY_VARIANT(Quaternion);
+DOCTEST_STRINGIFY_VARIANT(AABB);
+DOCTEST_STRINGIFY_VARIANT(Basis);
+DOCTEST_STRINGIFY_VARIANT(Transform3D);
+
+DOCTEST_STRINGIFY_VARIANT(::Color); // Disambiguate from `doctest::Color`.
+DOCTEST_STRINGIFY_VARIANT(StringName);
+DOCTEST_STRINGIFY_VARIANT(NodePath);
+DOCTEST_STRINGIFY_VARIANT(RID);
+DOCTEST_STRINGIFY_VARIANT_POINTER(Object);
+DOCTEST_STRINGIFY_VARIANT(Callable);
+DOCTEST_STRINGIFY_VARIANT(Signal);
+DOCTEST_STRINGIFY_VARIANT(Dictionary);
+DOCTEST_STRINGIFY_VARIANT(Array);
+
+DOCTEST_STRINGIFY_VARIANT(PackedByteArray);
+DOCTEST_STRINGIFY_VARIANT(PackedInt32Array);
+DOCTEST_STRINGIFY_VARIANT(PackedInt64Array);
+DOCTEST_STRINGIFY_VARIANT(PackedFloat32Array);
+DOCTEST_STRINGIFY_VARIANT(PackedFloat64Array);
+DOCTEST_STRINGIFY_VARIANT(PackedStringArray);
+DOCTEST_STRINGIFY_VARIANT(PackedVector2Array);
+DOCTEST_STRINGIFY_VARIANT(PackedVector3Array);
+DOCTEST_STRINGIFY_VARIANT(PackedColorArray);
+
+// Register test commands to be launched from the command-line.
+// For instance: REGISTER_TEST_COMMAND("gdscript-parser" &test_parser_func).
+// Example usage: `godot --test gdscript-parser`.
+
+typedef void (*TestFunc)();
+extern Map<String, TestFunc> *test_commands;
+int register_test_command(String p_command, TestFunc p_function);
+
+#define REGISTER_TEST_COMMAND(m_command, m_function) \
+ DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_VAR_)) = \
+ register_test_command(m_command, m_function); \
+ DOCTEST_GLOBAL_NO_WARNINGS_END()
+
+// Utility macros to send an event actions to a given object
+// Requires Message Queue and InputMap to be setup.
+// SEND_GUI_ACTION - takes an object and a input map key. e.g SEND_GUI_ACTION(code_edit, "ui_text_newline").
+// SEND_GUI_KEY_EVENT - takes an object and a keycode set. e.g SEND_GUI_KEY_EVENT(code_edit, Key::A | KeyModifierMask::CMD).
+// SEND_GUI_MOUSE_EVENT - takes an object, position, mouse button and mouse mask e.g SEND_GUI_MOUSE_EVENT(code_edit, Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE);
+// SEND_GUI_DOUBLE_CLICK - takes an object and a position. e.g SEND_GUI_DOUBLE_CLICK(code_edit, Vector2(50, 50));
+
+#define SEND_GUI_ACTION(m_object, m_action) \
+ { \
+ const List<Ref<InputEvent>> *events = InputMap::get_singleton()->action_get_events(m_action); \
+ const List<Ref<InputEvent>>::Element *first_event = events->front(); \
+ Ref<InputEventKey> event = first_event->get(); \
+ event->set_pressed(true); \
+ m_object->gui_input(event); \
+ MessageQueue::get_singleton()->flush(); \
+ }
+
+#define SEND_GUI_KEY_EVENT(m_object, m_input) \
+ { \
+ Ref<InputEventKey> event = InputEventKey::create_reference(m_input); \
+ event->set_pressed(true); \
+ m_object->gui_input(event); \
+ MessageQueue::get_singleton()->flush(); \
+ }
+
+#define _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask) \
+ Ref<InputEventMouseButton> event; \
+ event.instantiate(); \
+ event->set_position(m_local_pos); \
+ event->set_button_index(m_input); \
+ event->set_button_mask(m_mask); \
+ event->set_pressed(true);
+
+#define SEND_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask) \
+ { \
+ _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask); \
+ m_object->get_viewport()->push_input(event); \
+ MessageQueue::get_singleton()->flush(); \
+ }
+
+#define SEND_GUI_DOUBLE_CLICK(m_object, m_local_pos) \
+ { \
+ _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, MouseButton::LEFT, MouseButton::LEFT); \
+ event->set_double_click(true); \
+ m_object->get_viewport()->push_input(event); \
+ MessageQueue::get_singleton()->flush(); \
+ }
+
+// Utility class / macros for testing signals
+//
+// Use SIGNAL_WATCH(*object, "signal_name") to start watching
+// Makes sure to call SIGNAL_UNWATCH(*object, "signal_name") to stop watching in cleanup, this is not done automatically.
+//
+// The SignalWatcher will capture all signals and their args sent between checks.
+//
+// Use SIGNAL_CHECK("signal_name"), Vector<Vector<Variant>>), to check the arguments of all fired signals.
+// The outer vector is each fired signal, the inner vector the list of arguments for that signal. Order does matter.
+//
+// Use SIGNAL_CHECK_FALSE("signal_name") to check if a signal was not fired.
+//
+// Use SIGNAL_DISCARD("signal_name") to discard records all of the given signal, use only in placed you don't need to check.
+//
+// All signals are automatically discarded between test/sub test cases.
+
+class SignalWatcher : public Object {
+private:
+ inline static SignalWatcher *singleton;
+
+ /* Equal to: Map<String, Vector<Vector<Variant>>> */
+ Map<String, Array> _signals;
+ void _add_signal_entry(const Array &p_args, const String &p_name) {
+ if (!_signals.has(p_name)) {
+ _signals[p_name] = Array();
+ }
+ _signals[p_name].push_back(p_args);
+ }
+
+ void _signal_callback_zero(const String &p_name) {
+ Array args;
+ _add_signal_entry(args, p_name);
+ }
+
+ void _signal_callback_one(Variant p_arg1, const String &p_name) {
+ Array args;
+ args.push_back(p_arg1);
+ _add_signal_entry(args, p_name);
+ }
+
+ void _signal_callback_two(Variant p_arg1, Variant p_arg2, const String &p_name) {
+ Array args;
+ args.push_back(p_arg1);
+ args.push_back(p_arg2);
+ _add_signal_entry(args, p_name);
+ }
+
+ void _signal_callback_three(Variant p_arg1, Variant p_arg2, Variant p_arg3, const String &p_name) {
+ Array args;
+ args.push_back(p_arg1);
+ args.push_back(p_arg2);
+ args.push_back(p_arg3);
+ _add_signal_entry(args, p_name);
+ }
+
+public:
+ static SignalWatcher *get_singleton() { return singleton; }
+
+ void watch_signal(Object *p_object, const String &p_signal) {
+ Vector<Variant> args;
+ args.push_back(p_signal);
+ MethodInfo method_info;
+ ClassDB::get_signal(p_object->get_class(), p_signal, &method_info);
+ switch (method_info.arguments.size()) {
+ case 0: {
+ p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_zero), args);
+ } break;
+ case 1: {
+ p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_one), args);
+ } break;
+ case 2: {
+ p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_two), args);
+ } break;
+ case 3: {
+ p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_three), args);
+ } break;
+ default: {
+ MESSAGE("Signal ", p_signal, " arg count not supported.");
+ } break;
+ }
+ }
+
+ void unwatch_signal(Object *p_object, const String &p_signal) {
+ MethodInfo method_info;
+ ClassDB::get_signal(p_object->get_class(), p_signal, &method_info);
+ switch (method_info.arguments.size()) {
+ case 0: {
+ p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_zero));
+ } break;
+ case 1: {
+ p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_one));
+ } break;
+ case 2: {
+ p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_two));
+ } break;
+ case 3: {
+ p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_three));
+ } break;
+ default: {
+ MESSAGE("Signal ", p_signal, " arg count not supported.");
+ } break;
+ }
+ }
+
+ bool check(const String &p_name, const Array &p_args) {
+ if (!_signals.has(p_name)) {
+ MESSAGE("Signal ", p_name, " not emitted");
+ return false;
+ }
+
+ if (p_args.size() != _signals[p_name].size()) {
+ MESSAGE("Signal has " << _signals[p_name] << " expected " << p_args);
+ discard_signal(p_name);
+ return false;
+ }
+
+ bool match = true;
+ for (int i = 0; i < p_args.size(); i++) {
+ if (((Array)p_args[i]).size() != ((Array)_signals[p_name][i]).size()) {
+ MESSAGE("Signal has " << _signals[p_name][i] << " expected " << p_args[i]);
+ match = false;
+ continue;
+ }
+
+ for (int j = 0; j < ((Array)p_args[i]).size(); j++) {
+ if (((Array)p_args[i])[j] != ((Array)_signals[p_name][i])[j]) {
+ MESSAGE("Signal has " << _signals[p_name][i] << " expected " << p_args[i]);
+ match = false;
+ break;
+ }
+ }
+ }
+
+ discard_signal(p_name);
+ return match;
+ }
+
+ bool check_false(const String &p_name) {
+ bool has = _signals.has(p_name);
+ discard_signal(p_name);
+ return !has;
+ }
+
+ void discard_signal(const String &p_name) {
+ if (_signals.has(p_name)) {
+ _signals.erase(p_name);
+ }
+ }
+
+ void _clear_signals() {
+ _signals.clear();
+ }
+
+ SignalWatcher() {
+ singleton = this;
+ }
+
+ ~SignalWatcher() {
+ singleton = nullptr;
+ }
+};
+
+#define SIGNAL_WATCH(m_object, m_signal) SignalWatcher::get_singleton()->watch_signal(m_object, m_signal);
+#define SIGNAL_UNWATCH(m_object, m_signal) SignalWatcher::get_singleton()->unwatch_signal(m_object, m_signal);
+
+#define SIGNAL_CHECK(m_signal, m_args) CHECK(SignalWatcher::get_singleton()->check(m_signal, m_args));
+#define SIGNAL_CHECK_FALSE(m_signal) CHECK(SignalWatcher::get_singleton()->check_false(m_signal));
+#define SIGNAL_DISCARD(m_signal) SignalWatcher::get_singleton()->discard_signal(m_signal);
+
+#endif // TEST_MACROS_H
diff --git a/tests/test_main.cpp b/tests/test_main.cpp
new file mode 100644
index 0000000000..2b2c89fbf1
--- /dev/null
+++ b/tests/test_main.cpp
@@ -0,0 +1,293 @@
+/*************************************************************************/
+/* test_main.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "test_main.h"
+
+#include "tests/core/io/test_config_file.h"
+#include "tests/core/io/test_file_access.h"
+#include "tests/core/io/test_image.h"
+#include "tests/core/io/test_json.h"
+#include "tests/core/io/test_marshalls.h"
+#include "tests/core/io/test_pck_packer.h"
+#include "tests/core/io/test_resource.h"
+#include "tests/core/io/test_xml_parser.h"
+#include "tests/core/math/test_aabb.h"
+#include "tests/core/math/test_astar.h"
+#include "tests/core/math/test_basis.h"
+#include "tests/core/math/test_color.h"
+#include "tests/core/math/test_expression.h"
+#include "tests/core/math/test_geometry_2d.h"
+#include "tests/core/math/test_geometry_3d.h"
+#include "tests/core/math/test_math.h"
+#include "tests/core/math/test_random_number_generator.h"
+#include "tests/core/math/test_rect2.h"
+#include "tests/core/object/test_class_db.h"
+#include "tests/core/object/test_method_bind.h"
+#include "tests/core/object/test_object.h"
+#include "tests/core/string/test_node_path.h"
+#include "tests/core/string/test_string.h"
+#include "tests/core/string/test_translation.h"
+#include "tests/core/templates/test_command_queue.h"
+#include "tests/core/templates/test_list.h"
+#include "tests/core/templates/test_local_vector.h"
+#include "tests/core/templates/test_lru.h"
+#include "tests/core/templates/test_oa_hash_map.h"
+#include "tests/core/templates/test_ordered_hash_map.h"
+#include "tests/core/templates/test_paged_array.h"
+#include "tests/core/templates/test_vector.h"
+#include "tests/core/test_crypto.h"
+#include "tests/core/test_hashing_context.h"
+#include "tests/core/test_time.h"
+#include "tests/core/variant/test_array.h"
+#include "tests/core/variant/test_dictionary.h"
+#include "tests/core/variant/test_variant.h"
+#include "tests/scene/test_code_edit.h"
+#include "tests/scene/test_curve.h"
+#include "tests/scene/test_gradient.h"
+#include "tests/scene/test_gui.h"
+#include "tests/scene/test_path_3d.h"
+#include "tests/servers/test_physics_2d.h"
+#include "tests/servers/test_physics_3d.h"
+#include "tests/servers/test_render.h"
+#include "tests/servers/test_shader_lang.h"
+#include "tests/servers/test_text_server.h"
+#include "tests/test_validate_testing.h"
+
+#include "modules/modules_tests.gen.h"
+
+#include "tests/test_macros.h"
+
+#include "scene/resources/default_theme/default_theme.h"
+
+int test_main(int argc, char *argv[]) {
+ bool run_tests = true;
+
+ // Convert arguments to Godot's command-line.
+ List<String> args;
+
+ for (int i = 0; i < argc; i++) {
+ args.push_back(String::utf8(argv[i]));
+ }
+ OS::get_singleton()->set_cmdline("", args);
+
+ // Run custom test tools.
+ if (test_commands) {
+ for (Map<String, TestFunc>::Element *E = test_commands->front(); E; E = E->next()) {
+ if (args.find(E->key())) {
+ const TestFunc &test_func = E->get();
+ test_func();
+ run_tests = false;
+ break;
+ }
+ }
+ if (!run_tests) {
+ delete test_commands;
+ return 0;
+ }
+ }
+ // Doctest runner.
+ doctest::Context test_context;
+ List<String> test_args;
+
+ // Clean arguments of "--test" from the args.
+ for (int x = 0; x < argc; x++) {
+ String arg = String(argv[x]);
+ if (arg != "--test") {
+ test_args.push_back(arg);
+ }
+ }
+
+ if (test_args.size() > 0) {
+ // Convert Godot command line arguments back to standard arguments.
+ char **doctest_args = new char *[test_args.size()];
+ for (int x = 0; x < test_args.size(); x++) {
+ // Operation to convert Godot string to non wchar string.
+ CharString cs = test_args[x].utf8();
+ const char *str = cs.get_data();
+ // Allocate the string copy.
+ doctest_args[x] = new char[strlen(str) + 1];
+ // Copy this into memory.
+ memcpy(doctest_args[x], str, strlen(str) + 1);
+ }
+
+ test_context.applyCommandLine(test_args.size(), doctest_args);
+
+ for (int x = 0; x < test_args.size(); x++) {
+ delete[] doctest_args[x];
+ }
+ delete[] doctest_args;
+ }
+
+ return test_context.run();
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "servers/navigation_server_2d.h"
+#include "servers/navigation_server_3d.h"
+#include "servers/rendering/rendering_server_default.h"
+
+struct GodotTestCaseListener : public doctest::IReporter {
+ GodotTestCaseListener(const doctest::ContextOptions &p_in) {}
+
+ SignalWatcher *signal_watcher = nullptr;
+
+ PhysicsServer3D *physics_3d_server = nullptr;
+ PhysicsServer2D *physics_2d_server = nullptr;
+ NavigationServer3D *navigation_3d_server = nullptr;
+ NavigationServer2D *navigation_2d_server = nullptr;
+
+ void test_case_start(const doctest::TestCaseData &p_in) override {
+ SignalWatcher::get_singleton()->_clear_signals();
+
+ String name = String(p_in.m_name);
+
+ if (name.find("[SceneTree]") != -1) {
+ GLOBAL_DEF("memory/limits/multithreaded_server/rid_pool_prealloc", 60);
+ memnew(MessageQueue);
+
+ GLOBAL_DEF("internationalization/rendering/force_right_to_left_layout_direction", false);
+
+ Error err = OK;
+ OS::get_singleton()->set_has_server_feature_callback(nullptr);
+ for (int i = 0; i < DisplayServer::get_create_function_count(); i++) {
+ if (String("headless") == DisplayServer::get_create_function_name(i)) {
+ DisplayServer::create(i, "", DisplayServer::WindowMode::WINDOW_MODE_MINIMIZED, DisplayServer::VSyncMode::VSYNC_ENABLED, 0, Vector2i(0, 0), err);
+ break;
+ }
+ }
+ memnew(RenderingServerDefault());
+ RenderingServerDefault::get_singleton()->init();
+ RenderingServerDefault::get_singleton()->set_render_loop_enabled(false);
+
+ physics_3d_server = PhysicsServer3DManager::new_default_server();
+ physics_3d_server->init();
+
+ physics_2d_server = PhysicsServer2DManager::new_default_server();
+ physics_2d_server->init();
+
+ navigation_3d_server = NavigationServer3DManager::new_default_server();
+ navigation_2d_server = memnew(NavigationServer2D);
+
+ memnew(InputMap);
+ InputMap::get_singleton()->load_default();
+
+ make_default_theme(false, Ref<Font>());
+
+ memnew(SceneTree);
+ SceneTree::get_singleton()->initialize();
+ return;
+ }
+ }
+
+ void test_case_end(const doctest::CurrentTestCaseStats &) override {
+ if (SceneTree::get_singleton()) {
+ SceneTree::get_singleton()->finalize();
+ }
+
+ if (MessageQueue::get_singleton()) {
+ MessageQueue::get_singleton()->flush();
+ }
+
+ if (SceneTree::get_singleton()) {
+ memdelete(SceneTree::get_singleton());
+ }
+
+ clear_default_theme();
+
+ if (navigation_3d_server) {
+ memdelete(navigation_3d_server);
+ navigation_3d_server = nullptr;
+ }
+
+ if (navigation_2d_server) {
+ memdelete(navigation_2d_server);
+ navigation_2d_server = nullptr;
+ }
+
+ if (physics_3d_server) {
+ physics_3d_server->finish();
+ memdelete(physics_3d_server);
+ physics_3d_server = nullptr;
+ }
+
+ if (physics_2d_server) {
+ physics_2d_server->finish();
+ memdelete(physics_2d_server);
+ physics_2d_server = nullptr;
+ }
+
+ if (RenderingServer::get_singleton()) {
+ RenderingServer::get_singleton()->sync();
+ RenderingServer::get_singleton()->global_variables_clear();
+ RenderingServer::get_singleton()->finish();
+ memdelete(RenderingServer::get_singleton());
+ }
+
+ if (DisplayServer::get_singleton()) {
+ memdelete(DisplayServer::get_singleton());
+ }
+
+ if (InputMap::get_singleton()) {
+ memdelete(InputMap::get_singleton());
+ }
+
+ if (MessageQueue::get_singleton()) {
+ MessageQueue::get_singleton()->flush();
+ memdelete(MessageQueue::get_singleton());
+ }
+ }
+
+ void test_run_start() override {
+ signal_watcher = memnew(SignalWatcher);
+ }
+
+ void test_run_end(const doctest::TestRunStats &) override {
+ memdelete(signal_watcher);
+ }
+
+ void test_case_reenter(const doctest::TestCaseData &) override {
+ SignalWatcher::get_singleton()->_clear_signals();
+ }
+
+ void subcase_start(const doctest::SubcaseSignature &) override {
+ SignalWatcher::get_singleton()->_clear_signals();
+ }
+
+ void report_query(const doctest::QueryData &) override {}
+ void test_case_exception(const doctest::TestCaseException &) override {}
+ void subcase_end() override {}
+
+ void log_assert(const doctest::AssertData &in) override {}
+ void log_message(const doctest::MessageData &) override {}
+ void test_case_skipped(const doctest::TestCaseData &) override {}
+};
+
+REGISTER_LISTENER("GodotTestCaseListener", 1, GodotTestCaseListener);
diff --git a/tests/test_main.h b/tests/test_main.h
new file mode 100644
index 0000000000..8e6a7361fc
--- /dev/null
+++ b/tests/test_main.h
@@ -0,0 +1,36 @@
+/*************************************************************************/
+/* test_main.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_MAIN_H
+#define TEST_MAIN_H
+
+int test_main(int argc, char *argv[]);
+
+#endif // TEST_MAIN_H
diff --git a/tests/test_tools.h b/tests/test_tools.h
new file mode 100644
index 0000000000..8ee7a4718f
--- /dev/null
+++ b/tests/test_tools.h
@@ -0,0 +1,59 @@
+/*************************************************************************/
+/* test_tools.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_TOOLS_H
+#define TEST_TOOLS_H
+
+struct ErrorDetector {
+ ErrorDetector() {
+ eh.errfunc = _detect_error;
+ eh.userdata = this;
+
+ add_error_handler(&eh);
+ }
+
+ ~ErrorDetector() {
+ remove_error_handler(&eh);
+ }
+
+ void clear() {
+ has_error = false;
+ }
+
+ static void _detect_error(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type) {
+ ErrorDetector *self = (ErrorDetector *)p_self;
+ self->has_error = true;
+ }
+
+ ErrorHandlerList eh;
+ bool has_error = false;
+};
+
+#endif // TEST_TOOLS_H
diff --git a/tests/test_utils.cpp b/tests/test_utils.cpp
new file mode 100644
index 0000000000..11cb6398aa
--- /dev/null
+++ b/tests/test_utils.cpp
@@ -0,0 +1,42 @@
+/*************************************************************************/
+/* test_utils.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "tests/test_utils.h"
+
+#include "core/os/os.h"
+
+String TestUtils::get_data_path(const String &p_file) {
+ String data_path = "../tests/data";
+ return get_executable_dir().plus_file(data_path.plus_file(p_file));
+}
+
+String TestUtils::get_executable_dir() {
+ return OS::get_singleton()->get_executable_path().get_base_dir();
+}
diff --git a/tests/test_utils.h b/tests/test_utils.h
new file mode 100644
index 0000000000..499ddb84b2
--- /dev/null
+++ b/tests/test_utils.h
@@ -0,0 +1,42 @@
+/*************************************************************************/
+/* test_utils.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_UTILS_H
+#define TEST_UTILS_H
+
+class String;
+
+namespace TestUtils {
+
+String get_data_path(const String &p_file);
+String get_executable_dir();
+} // namespace TestUtils
+
+#endif // TEST_UTILS_H
diff --git a/tests/test_validate_testing.h b/tests/test_validate_testing.h
new file mode 100644
index 0000000000..413a7e351d
--- /dev/null
+++ b/tests/test_validate_testing.h
@@ -0,0 +1,199 @@
+/*************************************************************************/
+/* test_validate_testing.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_VALIDATE_TESTING_H
+#define TEST_VALIDATE_TESTING_H
+
+#include "core/os/os.h"
+
+#include "tests/test_macros.h"
+#include "tests/test_tools.h"
+
+TEST_SUITE("Validate tests") {
+ TEST_CASE("Always pass") {
+ CHECK(true);
+ }
+ TEST_CASE_PENDING("Pending tests are skipped") {
+ if (!doctest::getContextOptions()->no_skip) { // Normal run.
+ FAIL("This should be skipped if `--no-skip` is NOT set (missing `doctest::skip()` decorator?)");
+ } else {
+ CHECK_MESSAGE(true, "Pending test is run with `--no-skip`");
+ }
+ }
+ TEST_CASE("Muting Godot error messages") {
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(!_print_error_enabled, "Error printing should be disabled.");
+ ERR_PRINT("Still waiting for Godot!"); // This should never get printed!
+ ERR_PRINT_ON;
+ CHECK_MESSAGE(_print_error_enabled, "Error printing should be re-enabled.");
+ }
+ TEST_CASE("Stringify Variant types") {
+ Variant var;
+ INFO(var);
+
+ String string("Godot is finally here!");
+ INFO(string);
+
+ Vector2 vec2(0.5, 1.0);
+ INFO(vec2);
+
+ Vector2i vec2i(1, 2);
+ INFO(vec2i);
+
+ Rect2 rect2(0.5, 0.5, 100.5, 100.5);
+ INFO(rect2);
+
+ Rect2i rect2i(0, 0, 100, 100);
+ INFO(rect2i);
+
+ Vector3 vec3(0.5, 1.0, 2.0);
+ INFO(vec3);
+
+ Vector3i vec3i(1, 2, 3);
+ INFO(vec3i);
+
+ Transform2D trans2d(0.5, Vector2(100, 100));
+ INFO(trans2d);
+
+ Plane plane(Vector3(1, 1, 1), 1.0);
+ INFO(plane);
+
+ Quaternion quat(Vector3(0.5, 1.0, 2.0));
+ INFO(quat);
+
+ AABB aabb(Vector3(), Vector3(100, 100, 100));
+ INFO(aabb);
+
+ Basis basis(quat);
+ INFO(basis);
+
+ Transform3D trans(basis);
+ INFO(trans);
+
+ Color color(1, 0.5, 0.2, 0.3);
+ INFO(color);
+
+ StringName string_name("has_method");
+ INFO(string_name);
+
+ NodePath node_path("godot/sprite");
+ INFO(node_path);
+
+ INFO(RID());
+
+ Object *obj = memnew(Object);
+ INFO(obj);
+
+ Callable callable(obj, "has_method");
+ INFO(callable);
+
+ Signal signal(obj, "script_changed");
+ INFO(signal);
+
+ memdelete(obj);
+
+ Dictionary dict;
+ dict["string"] = string;
+ dict["color"] = color;
+ INFO(dict);
+
+ Array arr;
+ arr.push_back(string);
+ arr.push_back(color);
+ INFO(arr);
+
+ PackedByteArray byte_arr;
+ byte_arr.push_back(0);
+ byte_arr.push_back(1);
+ byte_arr.push_back(2);
+ INFO(byte_arr);
+
+ PackedInt32Array int32_arr;
+ int32_arr.push_back(0);
+ int32_arr.push_back(1);
+ int32_arr.push_back(2);
+ INFO(int32_arr);
+
+ PackedInt64Array int64_arr;
+ int64_arr.push_back(0);
+ int64_arr.push_back(1);
+ int64_arr.push_back(2);
+ INFO(int64_arr);
+
+ PackedFloat32Array float32_arr;
+ float32_arr.push_back(0.5);
+ float32_arr.push_back(1.5);
+ float32_arr.push_back(2.5);
+ INFO(float32_arr);
+
+ PackedFloat64Array float64_arr;
+ float64_arr.push_back(0.5);
+ float64_arr.push_back(1.5);
+ float64_arr.push_back(2.5);
+ INFO(float64_arr);
+
+ PackedStringArray str_arr = string.split(" ");
+ INFO(str_arr);
+
+ PackedVector2Array vec2_arr;
+ vec2_arr.push_back(Vector2(0, 0));
+ vec2_arr.push_back(Vector2(1, 1));
+ vec2_arr.push_back(Vector2(2, 2));
+ INFO(vec2_arr);
+
+ PackedVector3Array vec3_arr;
+ vec3_arr.push_back(Vector3(0, 0, 0));
+ vec3_arr.push_back(Vector3(1, 1, 1));
+ vec3_arr.push_back(Vector3(2, 2, 2));
+ INFO(vec3_arr);
+
+ PackedColorArray color_arr;
+ color_arr.push_back(Color(0, 0, 0));
+ color_arr.push_back(Color(1, 1, 1));
+ color_arr.push_back(Color(2, 2, 2));
+ INFO(color_arr);
+
+ // doctest string concatenation.
+ CHECK_MESSAGE(true, var, " ", vec2, " ", rect2, " ", color);
+ }
+ TEST_CASE("Detect error messages") {
+ ErrorDetector ed;
+
+ REQUIRE_FALSE(ed.has_error);
+
+ ERR_PRINT_OFF;
+ ERR_PRINT("Still waiting for Godot!");
+ ERR_PRINT_ON;
+
+ REQUIRE(ed.has_error);
+ }
+}
+
+#endif // TEST_VALIDATE_TESTING_H