summaryrefslogtreecommitdiff
path: root/tests/core
diff options
context:
space:
mode:
authorRémi Verschelde <remi@verschelde.fr>2021-11-09 21:48:13 +0100
committerGitHub <noreply@github.com>2021-11-09 21:48:13 +0100
commitc305de701c81d027ab6de2855cb3b4432a4e6e7b (patch)
treee2bc0dc3cc3c61a2511913f993876f59c5867de4 /tests/core
parente3f3fc517a610d308e92dbc98b38def0f6e81c94 (diff)
parent99a282f6319d0c7642f589163b955a636610719a (diff)
Merge pull request #54697 from aaronfranke/move-tests
Move and organize tests into subfolders
Diffstat (limited to 'tests/core')
-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.h259
-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.h115
-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.h382
-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.h479
-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.h1450
-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.h229
-rw-r--r--tests/core/templates/test_lru.h99
-rw-r--r--tests/core/templates/test_oa_hash_map.cpp298
-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.h509
-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.h465
-rw-r--r--tests/core/variant/test_dictionary.h505
-rw-r--r--tests/core/variant/test_variant.h916
40 files changed, 13279 insertions, 0 deletions
diff --git a/tests/core/io/test_config_file.h b/tests/core/io/test_config_file.h
new file mode 100644
index 0000000000..f6fbaf9a88
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..4ffc57afe4
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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] == ""); // 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..c4c5f1e18b
--- /dev/null
+++ b/tests/core/io/test_image.h
@@ -0,0 +1,259 @@
+/*************************************************************************/
+/* test_image.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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());
+
+ 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.");
+
+ PackedByteArray 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 x = 0; x < image2->get_width(); x++) {
+ for (int y = 0; y < image2->get_height(); y++) {
+ CHECK_MESSAGE(
+ image2->get_pixel(x, y).r > 0.49,
+ "fill() should colorize all pixels of the image.");
+ }
+ }
+
+ // 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..3af58dfa1c
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..6bd916164e
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..ae51d58522
--- /dev/null
+++ b/tests/core/io/test_pck_packer.h
@@ -0,0 +1,115 @@
+/*************************************************************************/
+/* test_pck_packer.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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 {
+
+// Dummy 64-character encryption key (since it's required).
+constexpr const char *ENCRYPTION_KEY = "0000000000000000000000000000000000000000000000000000000000000000";
+
+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,
+ 32,
+ ENCRYPTION_KEY) == 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 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,
+ 32,
+ ENCRYPTION_KEY) == 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..cee3281995
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..2d00f29ddf
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..b838bed171
--- /dev/null
+++ b/tests/core/math/test_aabb.h
@@ -0,0 +1,382 @@
+/*************************************************************************/
+/* test_aabb.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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(-1.5, 3, 0)),
+ "has_point() with contained point on negative edge should return the expected value.");
+ CHECK_MESSAGE(
+ aabb.has_point(Vector3(2.5, 3, 0)),
+ "has_point() with contained point on positive edge 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.");
+}
+
+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..2c183374ac
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..500c069a33
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..82cf786f7a
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..cb1d29389f
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..8f6669b572
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..f42003ffcf
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..6ec9bc2473
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#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 != String()) {
+ namespace_stack[at_level] = name;
+ }
+
+ } else if (tk == TK_CURLY_BRACKET_OPEN) {
+ curly_stack++;
+ } else if (tk == TK_CURLY_BRACKET_CLOSE) {
+ curly_stack--;
+ if (namespace_stack.has(curly_stack)) {
+ namespace_stack.erase(curly_stack);
+ }
+ }
+
+ tk = get_token();
+ }
+
+ if (error) {
+ return ERR_PARSE_ERROR;
+ }
+
+ return OK;
+ }
+
+ String get_error() {
+ return error_str;
+ }
+
+ String get_class() {
+ return class_name;
+ }
+};
+
+void test_vec(Plane p_vec) {
+ CameraMatrix cm;
+ cm.set_perspective(45, 1, 0, 100);
+ Plane v0 = cm.xform4(p_vec);
+
+ print_line("out: " + v0);
+ v0.normal.z = (v0.d / 100.0 * 2.0 - 1.0) * v0.d;
+ print_line("out_F: " + v0);
+}
+
+uint32_t ihash(uint32_t a) {
+ a = (a + 0x7ed55d16) + (a << 12);
+ a = (a ^ 0xc761c23c) ^ (a >> 19);
+ a = (a + 0x165667b1) + (a << 5);
+ a = (a + 0xd3a2646c) ^ (a << 9);
+ a = (a + 0xfd7046c5) + (a << 3);
+ a = (a ^ 0xb55a4f09) ^ (a >> 16);
+ return a;
+}
+
+uint32_t ihash2(uint32_t a) {
+ a = (a ^ 61) ^ (a >> 16);
+ a = a + (a << 3);
+ a = a ^ (a >> 4);
+ a = a * 0x27d4eb2d;
+ a = a ^ (a >> 15);
+ return a;
+}
+
+uint32_t ihash3(uint32_t a) {
+ a = (a + 0x479ab41d) + (a << 8);
+ a = (a ^ 0xe4aa10ce) ^ (a >> 5);
+ a = (a + 0x9942f0a6) - (a << 14);
+ a = (a ^ 0x5aedd67d) ^ (a >> 3);
+ a = (a + 0x17bea992) + (a << 7);
+ return a;
+}
+
+MainLoop *test() {
+ {
+ Vector<Vector3> points;
+ points.push_back(Vector3(0, 0, 0));
+ points.push_back(Vector3(0, 0, 1));
+ points.push_back(Vector3(0, 1, 0));
+ points.push_back(Vector3(0, 1, 1));
+ points.push_back(Vector3(1, 1, 0));
+ points.push_back(Vector3(1, 0, 0));
+ points.push_back(Vector3(1, 0, 1));
+ points.push_back(Vector3(1, 1, 1));
+
+ for (int i = 0; i < 800; i++) {
+ points.push_back(Vector3(Math::randf() * 2.0 - 1.0, Math::randf() * 2.0 - 1.0, Math::randf() * 2.0 - 1.0) * Vector3(25, 30, 33));
+ }
+
+ Vector<Delaunay3D::OutputSimplex> os = Delaunay3D::tetrahedralize(points);
+ print_line("simplices in the end: " + itos(os.size()));
+ for (int i = 0; i < os.size(); i++) {
+ print_line("Simplex " + itos(i) + ": ");
+ print_line(points[os[i].points[0]]);
+ print_line(points[os[i].points[1]]);
+ print_line(points[os[i].points[2]]);
+ print_line(points[os[i].points[3]]);
+ }
+
+ {
+ FileAccessRef f = FileAccess::open("res://bsp.obj", FileAccess::WRITE);
+ for (int i = 0; i < os.size(); i++) {
+ f->store_line("o Simplex" + itos(i));
+ for (int j = 0; j < 4; j++) {
+ f->store_line(vformat("v %f %f %f", points[os[i].points[j]].x, points[os[i].points[j]].y, points[os[i].points[j]].z));
+ }
+ static const int face_order[4][3] = {
+ { 1, 2, 3 },
+ { 1, 3, 4 },
+ { 1, 2, 4 },
+ { 2, 3, 4 }
+ };
+
+ for (int j = 0; j < 4; j++) {
+ f->store_line(vformat("f %d %d %d", 4 * i + face_order[j][0], 4 * i + face_order[j][1], 4 * i + face_order[j][2]));
+ }
+ }
+ f->close();
+ }
+
+ return nullptr;
+ }
+
+ {
+ float r = 1;
+ float g = 0.5;
+ float b = 0.1;
+
+ const float pow2to9 = 512.0f;
+ const float B = 15.0f;
+ const float N = 9.0f;
+
+ float sharedexp = 65408.000f;
+
+ float cRed = MAX(0.0f, MIN(sharedexp, r));
+ float cGreen = MAX(0.0f, MIN(sharedexp, g));
+ float cBlue = MAX(0.0f, MIN(sharedexp, b));
+
+ float cMax = MAX(cRed, MAX(cGreen, cBlue));
+
+ float expp = MAX(-B - 1.0f, floor(Math::log(cMax) / Math_LN2)) + 1.0f + B;
+
+ float sMax = (float)floor((cMax / Math::pow(2.0f, expp - B - N)) + 0.5f);
+
+ float exps = expp + 1.0f;
+
+ if (0.0 <= sMax && sMax < pow2to9) {
+ exps = expp;
+ }
+
+ float sRed = Math::floor((cRed / pow(2.0f, exps - B - N)) + 0.5f);
+ float sGreen = Math::floor((cGreen / pow(2.0f, exps - B - N)) + 0.5f);
+ float sBlue = Math::floor((cBlue / pow(2.0f, exps - B - N)) + 0.5f);
+
+ print_line("R: " + rtos(sRed) + " G: " + rtos(sGreen) + " B: " + rtos(sBlue) + " EXP: " + rtos(exps));
+
+ uint32_t rgbe = (Math::fast_ftoi(sRed) & 0x1FF) | ((Math::fast_ftoi(sGreen) & 0x1FF) << 9) | ((Math::fast_ftoi(sBlue) & 0x1FF) << 18) | ((Math::fast_ftoi(exps) & 0x1F) << 27);
+
+ float rb = rgbe & 0x1ff;
+ float gb = (rgbe >> 9) & 0x1ff;
+ float bb = (rgbe >> 18) & 0x1ff;
+ float eb = (rgbe >> 27);
+ float mb = Math::pow(2.0, eb - 15.0 - 9.0);
+ float rd = rb * mb;
+ float gd = gb * mb;
+ float bd = bb * mb;
+
+ print_line("RGBE: " + Color(rd, gd, bd));
+ }
+
+ Vector<int> ints;
+ ints.resize(20);
+
+ {
+ int *w;
+ w = ints.ptrw();
+ for (int i = 0; i < ints.size(); i++) {
+ w[i] = i;
+ }
+ }
+
+ Vector<int> posho = ints;
+
+ {
+ const int *r = posho.ptr();
+ for (int i = 0; i < posho.size(); i++) {
+ print_line(itos(i) + " : " + itos(r[i]));
+ }
+ }
+
+ List<String> cmdlargs = OS::get_singleton()->get_cmdline_args();
+
+ if (cmdlargs.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..ab5fb6a050
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..39c4771c19
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..3d9fe5a32e
--- /dev/null
+++ b/tests/core/math/test_rect2.h
@@ -0,0 +1,479 @@
+/*************************************************************************/
+/* test_rect2.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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") {
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).has_point(Vector2(500, 600)),
+ "has_point() with contained Vector2 should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2(0, 100, 1280, 720).has_point(Vector2(0, 0)),
+ "has_point() with non-contained Vector2 should return the expected result.");
+
+ CHECK_MESSAGE(
+ Rect2(0, 100, 1280, 720).has_point(Vector2(0, 110)),
+ "has_point() with positive Vector2 on left edge should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2(0, 100, 1280, 720).has_point(Vector2(1280, 110)),
+ "has_point() with positive Vector2 on right edge should return the expected result.");
+
+ CHECK_MESSAGE(
+ Rect2(-4000, 100, 1280, 720).has_point(Vector2(-4000, 110)),
+ "has_point() with negative Vector2 on left edge should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2(-4000, 100, 1280, 720).has_point(Vector2(-2720, 110)),
+ "has_point() with negative Vector2 on right edge should return the expected result.");
+}
+
+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") {
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).has_point(Vector2i(500, 600)),
+ "has_point() with contained Vector2i should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2i(0, 100, 1280, 720).has_point(Vector2i(0, 0)),
+ "has_point() with non-contained Vector2i should return the expected result.");
+
+ CHECK_MESSAGE(
+ Rect2i(0, 100, 1280, 720).has_point(Vector2(0, 110)),
+ "has_point() with positive Vector2 on left edge should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2i(0, 100, 1280, 720).has_point(Vector2(1280, 110)),
+ "has_point() with positive Vector2 on right edge should return the expected result.");
+
+ CHECK_MESSAGE(
+ Rect2i(-4000, 100, 1280, 720).has_point(Vector2(-4000, 110)),
+ "has_point() with negative Vector2 on left edge should return the expected result.");
+ CHECK_MESSAGE(
+ !Rect2i(-4000, 100, 1280, 720).has_point(Vector2(-2720, 110)),
+ "has_point() with negative Vector2 on right edge should return the expected result.");
+}
+
+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..4b27905485
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..c3a869a8a4
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..4109ea521a
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..0216a30f8f
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..e5b26cf5a4
--- /dev/null
+++ b/tests/core/string/test_string.h
@@ -0,0 +1,1450 @@
+/*************************************************************************/
+/* test_string.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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 == String());
+
+ CharString cs = (const char *)u8str;
+ CHECK(String::utf8(cs) == String());
+ 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 == String());
+
+ Char16String cs = u16str;
+ CHECK(String::utf16(cs) == String());
+ 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]);
+ }
+}
+
+TEST_CASE("[String] Erasing") {
+ String s = "Josephine is such a cute girl!";
+ s.erase(s.find("cute "), String("cute ").length());
+ CHECK(s == "Josephine is such a girl!");
+}
+
+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] 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"));
+}
+} // 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..47e06add40
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..5d228f2bf6
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..52d5edff70
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..eff2a16abc
--- /dev/null
+++ b/tests/core/templates/test_local_vector.h
@@ -0,0 +1,229 @@
+/*************************************************************************/
+/* test_local_vector.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_LOCAL_VECTOR_H
+#define TEST_LOCAL_VECTOR_H
+
+#include "core/templates/local_vector.h"
+
+#include "tests/test_macros.h"
+
+namespace TestLocalVector {
+
+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(0);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 3);
+ CHECK(vector[3] == 4);
+
+ vector.remove(2);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 4);
+
+ vector.remove(1);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 4);
+
+ vector.remove(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_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_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_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_unordered(vector.find(4));
+
+ CHECK(vector.size() == 1);
+
+ CHECK(vector.find(4) == -1);
+ CHECK(vector.find(1) != -1);
+
+ // Remove the last one.
+ vector.remove_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(0);
+ vector.remove(0);
+ vector.remove(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..9359909c53
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..904c01642d
--- /dev/null
+++ b/tests/core/templates/test_oa_hash_map.cpp
@@ -0,0 +1,298 @@
+/*************************************************************************/
+/* test_oa_hash_map.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#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++;
+ }
+
+ CountedItem &operator=(const CountedItem &p_other) = default;
+
+ ~CountedItem() {
+ CRASH_COND(destroyed);
+ count--;
+ destroyed = true;
+ }
+};
+
+int CountedItem::count;
+
+MainLoop *test() {
+ OS::get_singleton()->print("\n\n\nHello from test\n");
+
+ // test element tracking.
+ {
+ OAHashMap<int, int> map;
+
+ map.set(42, 1337);
+ map.set(1337, 21);
+ map.set(42, 11880);
+
+ int value = 0;
+ map.lookup(42, value);
+
+ OS::get_singleton()->print("capacity %d\n", map.get_capacity());
+ OS::get_singleton()->print("elements %d\n", map.get_num_elements());
+
+ OS::get_singleton()->print("map[42] = %d\n", value);
+ }
+
+ // rehashing and deletion
+ {
+ OAHashMap<int, int> map;
+
+ for (int i = 0; i < 500; i++) {
+ map.set(i, i * 2);
+ }
+
+ for (int i = 0; i < 500; i += 2) {
+ map.remove(i);
+ }
+
+ uint32_t num_elems = 0;
+ for (int i = 0; i < 500; i++) {
+ int tmp;
+ if (map.lookup(i, tmp) && tmp == i * 2) {
+ num_elems++;
+ }
+ }
+
+ OS::get_singleton()->print("elements %d == %d.\n", map.get_num_elements(), num_elems);
+ }
+
+ // iteration
+ {
+ OAHashMap<String, int> map;
+
+ map.set("Hello", 1);
+ map.set("World", 2);
+ map.set("Godot rocks", 42);
+
+ for (OAHashMap<String, int>::Iterator it = map.iter(); it.valid; it = map.next_iter(it)) {
+ OS::get_singleton()->print("map[\"%s\"] = %d\n", it.key->utf8().get_data(), *it.value);
+ }
+ }
+
+ // stress test / test for issue #22928
+ {
+ OAHashMap<int, int> map;
+ int dummy = 0;
+ const int N = 1000;
+ uint32_t *keys = new uint32_t[N];
+
+ Math::seed(0);
+
+ // insert a couple of random keys (with a dummy value, which is ignored)
+ for (int i = 0; i < N; i++) {
+ keys[i] = Math::rand();
+ map.set(keys[i], dummy);
+
+ if (!map.lookup(keys[i], dummy)) {
+ OS::get_singleton()->print("could not find 0x%X despite it was just inserted!\n", unsigned(keys[i]));
+ }
+ }
+
+ // check whether the keys are still present
+ for (int i = 0; i < N; i++) {
+ if (!map.lookup(keys[i], dummy)) {
+ OS::get_singleton()->print("could not find 0x%X despite it has been inserted previously! (not checking the other keys, breaking...)\n", unsigned(keys[i]));
+ break;
+ }
+ }
+
+ delete[] keys;
+ }
+
+ // regression test / test for issue related to #31402
+ {
+ OS::get_singleton()->print("test for issue #31402 started...\n");
+
+ const int num_test_values = 12;
+ int test_values[num_test_values] = { 0, 24, 48, 72, 96, 120, 144, 168, 192, 216, 240, 264 };
+
+ int dummy = 0;
+ OAHashMap<int, int> map;
+ map.clear();
+
+ for (int i = 0; i < num_test_values; ++i) {
+ map.set(test_values[i], dummy);
+ }
+
+ OS::get_singleton()->print("test for issue #31402 passed.\n");
+ }
+
+ // test collision resolution, should not crash or run indefinitely
+ {
+ OAHashMap<int, int> map(4);
+ map.set(1, 1);
+ map.set(5, 1);
+ map.set(9, 1);
+ map.set(13, 1);
+ map.remove(5);
+ map.remove(9);
+ map.remove(13);
+ map.set(5, 1);
+ }
+
+ // test memory management of items, should not crash or leak items
+ {
+ // Exercise different patterns of removal
+ for (int i = 0; i < 4; ++i) {
+ {
+ OAHashMap<String, CountedItem> map;
+ int id = 0;
+ for (int j = 0; j < 100; ++j) {
+ map.insert(itos(j), CountedItem(id));
+ }
+ if (i <= 1) {
+ for (int j = 0; j < 100; ++j) {
+ map.remove(itos(j));
+ }
+ }
+ if (i % 2 == 0) {
+ map.clear();
+ }
+ }
+
+ if (CountedItem::count != 0) {
+ OS::get_singleton()->print("%d != 0 (not performing the other test sub-cases, breaking...)\n", CountedItem::count);
+ break;
+ }
+ }
+ }
+
+ // Test map with 0 capacity.
+ {
+ OAHashMap<int, String> original_map(0);
+ original_map.set(1, "1");
+ OS::get_singleton()->print("OAHashMap 0 capacity initialization passed.\n");
+ }
+
+ // Test copy constructor.
+ {
+ OAHashMap<int, String> original_map;
+ original_map.set(1, "1");
+ original_map.set(2, "2");
+ original_map.set(3, "3");
+ original_map.set(4, "4");
+ original_map.set(5, "5");
+
+ OAHashMap<int, String> map_copy(original_map);
+
+ bool pass = true;
+ for (
+ OAHashMap<int, String>::Iterator it = original_map.iter();
+ it.valid;
+ it = original_map.next_iter(it)) {
+ if (map_copy.lookup_ptr(*it.key) == nullptr) {
+ pass = false;
+ }
+ if (*it.value != *map_copy.lookup_ptr(*it.key)) {
+ pass = false;
+ }
+ }
+ if (pass) {
+ OS::get_singleton()->print("OAHashMap copy constructor test passed.\n");
+ } else {
+ OS::get_singleton()->print("OAHashMap copy constructor test FAILED.\n");
+ }
+
+ map_copy.set(1, "Random String");
+ if (*map_copy.lookup_ptr(1) == *original_map.lookup_ptr(1)) {
+ OS::get_singleton()->print("OAHashMap copy constructor, atomic copy test FAILED.\n");
+ } else {
+ OS::get_singleton()->print("OAHashMap copy constructor, atomic copy test passed.\n");
+ }
+ }
+
+ // Test assign operator.
+ {
+ OAHashMap<int, String> original_map;
+ original_map.set(1, "1");
+ original_map.set(2, "2");
+ original_map.set(3, "3");
+ original_map.set(4, "4");
+ original_map.set(5, "5");
+
+ OAHashMap<int, String> map_copy(100000);
+ map_copy.set(1, "Just a string.");
+ map_copy = original_map;
+
+ bool pass = true;
+ for (
+ OAHashMap<int, String>::Iterator it = map_copy.iter();
+ it.valid;
+ it = map_copy.next_iter(it)) {
+ if (original_map.lookup_ptr(*it.key) == nullptr) {
+ pass = false;
+ }
+ if (*it.value != *original_map.lookup_ptr(*it.key)) {
+ pass = false;
+ }
+ }
+ if (pass) {
+ OS::get_singleton()->print("OAHashMap assign operation test passed.\n");
+ } else {
+ OS::get_singleton()->print("OAHashMap assign operation test FAILED.\n");
+ }
+
+ map_copy.set(1, "Random String");
+ if (*map_copy.lookup_ptr(1) == *original_map.lookup_ptr(1)) {
+ OS::get_singleton()->print("OAHashMap assign operation atomic copy test FAILED.\n");
+ } else {
+ OS::get_singleton()->print("OAHashMap assign operation atomic copy test passed.\n");
+ }
+ }
+
+ return nullptr;
+}
+} // namespace TestOAHashMap
diff --git a/tests/core/templates/test_oa_hash_map.h b/tests/core/templates/test_oa_hash_map.h
new file mode 100644
index 0000000000..f229ac94ea
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..35ce0fc656
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..7efd3799f3
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..bfdf389aa7
--- /dev/null
+++ b/tests/core/templates/test_vector.h
@@ -0,0 +1,509 @@
+/*************************************************************************/
+/* test_vector.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_VECTOR_H
+#define TEST_VECTOR_H
+
+#include "core/templates/vector.h"
+
+#include "tests/test_macros.h"
+
+namespace TestVector {
+
+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(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(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(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] Subarray") {
+ 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> subarray1 = vector.subarray(1, 2);
+ CHECK(subarray1.size() == 2);
+ CHECK(subarray1[0] == 1);
+ CHECK(subarray1[1] == 2);
+
+ Vector<int> subarray2 = vector.subarray(1, -1);
+ CHECK(subarray2.size() == 4);
+ CHECK(subarray2[0] == 1);
+ CHECK(subarray2[1] == 2);
+ CHECK(subarray2[2] == 3);
+ CHECK(subarray2[3] == 4);
+
+ Vector<int> subarray3 = vector.subarray(-2, -1);
+ CHECK(subarray3.size() == 2);
+ CHECK(subarray3[0] == 3);
+ CHECK(subarray3[1] == 4);
+
+ Vector<int> subarray4 = vector.subarray(-3, 3);
+ CHECK(subarray4.size() == 2);
+ CHECK(subarray4[0] == 2);
+ CHECK(subarray4[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") {
+ 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(0);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 3);
+ CHECK(vector[3] == 4);
+
+ vector.remove(2);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 2);
+ CHECK(vector[2] == 4);
+
+ vector.remove(1);
+
+ CHECK(vector[0] == 1);
+ CHECK(vector[1] == 4);
+
+ vector.remove(0);
+
+ CHECK(vector[0] == 4);
+}
+
+TEST_CASE("[Vector] Remove 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(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(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(vector.find(2));
+
+ CHECK(vector.size() == 2);
+
+ CHECK(vector.find(2) == -1);
+ CHECK(vector.find(1) != -1);
+ CHECK(vector.find(4) != -1);
+
+ vector.remove(vector.find(4));
+
+ CHECK(vector.size() == 1);
+
+ CHECK(vector.find(4) == -1);
+ CHECK(vector.find(1) != -1);
+
+ vector.remove(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(0);
+ vector.remove(0);
+ vector.remove(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..3b909c7df8
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..728a5f2cfa
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..28f1cb2f20
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..298bd2ff63
--- /dev/null
+++ b/tests/core/variant/test_array.h
@@ -0,0 +1,465 @@
+/*************************************************************************/
+/* test_array.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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()") {
+ Array arr;
+ arr.push_back(1);
+ arr.push_back(2);
+ arr.remove(0);
+ CHECK(arr.size() == 1);
+ CHECK(int(arr[0]) == 2);
+ arr.remove(0);
+ CHECK(arr.size() == 0);
+
+ // The array is now empty; try to use `remove()` again.
+ // Normally, this prints an error message so we silence it.
+ ERR_PRINT_OFF;
+ arr.remove(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] 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..65079698a3
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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..0d16fa092c
--- /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-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_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