diff options
author | Rémi Verschelde <remi@verschelde.fr> | 2021-11-09 21:48:13 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-09 21:48:13 +0100 |
commit | c305de701c81d027ab6de2855cb3b4432a4e6e7b (patch) | |
tree | e2bc0dc3cc3c61a2511913f993876f59c5867de4 /tests/core | |
parent | e3f3fc517a610d308e92dbc98b38def0f6e81c94 (diff) | |
parent | 99a282f6319d0c7642f589163b955a636610719a (diff) |
Merge pull request #54697 from aaronfranke/move-tests
Move and organize tests into subfolders
Diffstat (limited to 'tests/core')
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<AB>\ +</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(¤t_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, ¤t_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, ¤t_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(¤t_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 = ""&'<>"; + CHECK(input.xml_unescape() == "\"&\'<>"); + + // Numeric entities + input = "AB"; + CHECK(input.xml_unescape() == "AB"); + + input = "�&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 = "�"; + result = input.xml_unescape(); + CHECK(result.length() == 1); + CHECK(result[0] == 0xFFFFFFFF); + input = "�"; + result = input.xml_unescape(); + CHECK(result.length() == 1); + CHECK(result[0] == 0xFFFFFFFF); + + // Check out of range of char32_t + input = "�"; + CHECK(input.xml_unescape() == input); + input = "�"; + CHECK(input.xml_unescape() == input); + + // Shouldn't consume without ending in a ';' + input = "B"; + CHECK(input.xml_unescape() == input); + input = "A"; + CHECK(input.xml_unescape() == input); + + // Invalid characters should make the entity ignored + input = "ASomeIrrelevantText;"; + CHECK(input.xml_unescape() == input); + input = "BSomeIrrelevantText;"; + 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 |