diff options
Diffstat (limited to 'tests')
40 files changed, 10023 insertions, 0 deletions
diff --git a/tests/SCsub b/tests/SCsub new file mode 100644 index 0000000000..7aab28531d --- /dev/null +++ b/tests/SCsub @@ -0,0 +1,23 @@ +#!/usr/bin/python + +Import("env") + +env.tests_sources = [] + +env_tests = env.Clone() + +# Include GDNative headers. +if env["module_gdnative_enabled"]: + env_tests.Append(CPPPATH=["#modules/gdnative/include"]) + +# We must disable the THREAD_LOCAL entirely in doctest to prevent crashes on debugging +# Since we link with /MT thread_local is always expired when the header is used +# So the debugger crashes the engine and it causes weird errors +# Explained in https://github.com/onqtam/doctest/issues/401 +if env_tests["platform"] == "windows": + env_tests.Append(CPPDEFINES=[("DOCTEST_THREAD_LOCAL", "")]) + +env_tests.add_source_files(env.tests_sources, "*.cpp") + +lib = env_tests.add_library("tests", env.tests_sources) +env.Prepend(LIBS=[lib]) diff --git a/tests/test_aabb.h b/tests/test_aabb.h new file mode 100644 index 0000000000..8acd2a9963 --- /dev/null +++ b/tests/test_aabb.h @@ -0,0 +1,375 @@ +/*************************************************************************/ +/* test_aabb.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 "core/string/print_string.h" +#include "tests/test_macros.h" + +#include "thirdparty/doctest/doctest.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))) == "-1.5, 2, -2.5 - 4, 5, 6", + "The string representation shouild 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."); +} + +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] Area getters") { + AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + CHECK_MESSAGE( + Math::is_equal_approx(aabb.get_area(), 120), + "get_area() should return the expected value with positive size."); + CHECK_MESSAGE( + !aabb.has_no_area(), + "Non-empty volumetric AABB should have an area."); + + aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(-4, 5, 6)); + CHECK_MESSAGE( + Math::is_equal_approx(aabb.get_area(), -120), + "get_area() 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_area(), 120), + "get_area() 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_area(), -120), + "get_area() 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_area(), + "Non-empty flat AABB should not have an area."); + + CHECK_MESSAGE( + AABB().has_no_area(), + "Empty AABB should not have an area."); +} + +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().is_equal_approx(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() should return the expected value."); + CHECK_MESSAGE( + Math::is_equal_approx(aabb.get_longest_axis_size(), 6), + "get_longest_axis() should return the expected value."); + + CHECK_MESSAGE( + aabb.get_shortest_axis().is_equal_approx(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() should return the expected value."); + CHECK_MESSAGE( + Math::is_equal_approx(aabb.get_shortest_axis_size(), 4), + "get_shortest_axis() should return the expected value."); +} + +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/test_astar.h b/tests/test_astar.h new file mode 100644 index 0000000000..cd1bd84c15 --- /dev/null +++ b/tests/test_astar.h @@ -0,0 +1,367 @@ +/*************************************************************************/ +/* test_astar.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 "core/math/math_funcs.h" +#include "core/os/os.h" + +#include <math.h> +#include <stdio.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. + float _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/test_basis.h b/tests/test_basis.h new file mode 100644 index 0000000000..00a00b4a5b --- /dev/null +++ b/tests/test_basis.h @@ -0,0 +1,287 @@ +/*************************************************************************/ +/* test_basis.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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/random_number_generator.h" +#include "core/os/os.h" +#include "core/string/ustring.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_xyz(p_rotation); + break; + + case EulerXZY: + ret.set_euler_xzy(p_rotation); + break; + + case EulerYZX: + ret.set_euler_yzx(p_rotation); + break; + + case EulerYXZ: + ret.set_euler_yxz(p_rotation); + break; + + case EulerZXY: + ret.set_euler_zxy(p_rotation); + break; + + case EulerZYX: + ret.set_euler_zyx(p_rotation); + 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_xyz(); + + case EulerXZY: + return p_rotation.get_euler_xzy(); + + case EulerYZX: + return p_rotation.get_euler_yzx(); + + case EulerYXZ: + return p_rotation.get_euler_yxz(); + + case EulerZXY: + return p_rotation.get_euler_zxy(); + + case EulerZYX: + return p_rotation.get_euler_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_xyz(); + Basis rotation_from_xyz_computed_euler; + rotation_from_xyz_computed_euler.set_euler_xyz(euler_xyz_from_rotation); + + 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/test_class_db.h b/tests/test_class_db.h new file mode 100644 index 0000000000..9a30891c16 --- /dev/null +++ b/tests/test_class_db.h @@ -0,0 +1,835 @@ +/*************************************************************************/ +/* test_class_db.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 GODOT_TEST_CLASS_DB_H +#define GODOT_TEST_CLASS_DB_H + +#include "core/register_core_types.h" + +#include "core/core_constants.h" +#include "core/os/os.h" +#include "core/string/string_name.h" +#include "core/string/ustring.h" +#include "core/templates/ordered_hash_map.h" +#include "core/variant/variant.h" + +#include "tests/test_macros.h" + +#define TEST_COND DOCTEST_CHECK_FALSE_MESSAGE +#define TEST_FAIL DOCTEST_FAIL +#define TEST_FAIL_COND DOCTEST_REQUIRE_FALSE_MESSAGE +#define TEST_FAIL_COND_WARN DOCTEST_WARN_FALSE_MESSAGE + +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_reference = 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 List<PropertyData>::Element *E = properties.front(); E; E = E->next()) { + if (E->get().name == p_name) { + return &E->get(); + } + } + + return nullptr; + } + + const MethodData *find_method_by_name(const StringName &p_name) const { + for (const List<MethodData>::Element *E = methods.front(); E; E = E->next()) { + if (E->get().name == p_name) { + return &E->get(); + } + } + + return nullptr; + } +}; + +struct NamesCache { + StringName variant_type = StaticCString::create("Variant"); + StringName object_class = StaticCString::create("Object"); + StringName reference_class = StaticCString::create("Reference"); + 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::TRANSFORM: + case Variant::TRANSFORM2D: + case Variant::BASIS: + case Variant::QUAT: + 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) + "'."); + } else { + 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) { + 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 + "'."); + } + + for (const List<ArgumentData>::Element *F = p_method.arguments.front(); F; F = F->next()) { + const ArgumentData &arg = F->get(); + + 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 + "'."); + } else { + 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.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 List<ArgumentData>::Element *F = p_signal.arguments.front(); F; F = F->next()) { + const ArgumentData &arg = F->get(); + + 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 + "'."); + } else { + 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 List<PropertyData>::Element *F = p_exposed_class.properties.front(); F; F = F->next()) { + validate_property(p_context, p_exposed_class, F->get()); + } + + for (const List<MethodData>::Element *F = p_exposed_class.methods.front(); F; F = F->next()) { + validate_method(p_context, p_exposed_class, F->get()); + } + + for (const List<SignalData>::Element *F = p_exposed_class.signals_.front(); F; F = F->next()) { + validate_signal(p_context, p_exposed_class, F->get()); + } +} + +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_reference = ClassDB::is_parent_class(class_name, "Reference"); + 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 List<PropertyInfo>::Element *E = property_list.front(); E; E = E->next()) { + const PropertyInfo &property = E->get(); + + if (property.usage & PROPERTY_USAGE_GROUP || property.usage & PROPERTY_USAGE_SUBGROUP || property.usage & PROPERTY_USAGE_CATEGORY) { + 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 (List<MethodInfo>::Element *E = method_list.front(); E; E = E->next()) { + const MethodInfo &method_info = E->get(); + + int argc = method_info.arguments.size(); + + if (method_info.name.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.reference_class); + TEST_COND(bad_reference_hint, String() + "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) + "'."); + + // Classes starting with an underscore are ignored unless they're used as a property setter or getter + if (!method.is_virtual && String(method.name)[0] == '_') { + for (const List<PropertyData>::Element *F = exposed_class.properties.front(); F; F = F->next()) { + const PropertyData &prop = F->get(); + + if (prop.setter == method.name || prop.getter == method.name) { + exposed_class.methods.push_back(method); + break; + } + } + } else { + exposed_class.methods.push_back(method); + } + } + + // 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 List<StringName>::Element *E = enum_constants.front(); E; E = E->next()) { + const StringName &constant_name = E->get(); + 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 List<String>::Element *E = constants.front(); E; E = E->next()) { + const String &constant_name = E->get(); + int *value = class_info->constant_map.getptr(StringName(E->get())); + TEST_FAIL_COND(!value, "Missing enum 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 (List<EnumData>::Element *E = r_context.global_enums.front(); E; E = E->next()) { + r_context.enum_types.push_back(E->get().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 (List<StringName>::Element *E = hardcoded_enums.front(); E; E = E->next()) { + // 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->get()); + } +} + +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 //GODOT_TEST_CLASS_DB_H diff --git a/tests/test_color.h b/tests/test_color.h new file mode 100644 index 0000000000..c2bb63b7d0 --- /dev/null +++ b/tests/test_color.h @@ -0,0 +1,207 @@ +/*************************************************************************/ +/* test_color.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 "thirdparty/doctest/doctest.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 / 360.0), + "The returned HSV hue should match the expected value."); + CHECK_MESSAGE( + Math::is_equal_approx(dark_blue.get_s(), 1), + "The returned HSV saturation should match the expected value."); + CHECK_MESSAGE( + Math::is_equal_approx(dark_blue.get_v(), 0.5), + "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/test_command_queue.h b/tests/test_command_queue.h new file mode 100644 index 0000000000..ce42d94475 --- /dev/null +++ b/tests/test_command_queue.h @@ -0,0 +1,481 @@ +/*************************************************************************/ +/* test_command_queue.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 "test_command_queue.h" + +#include "core/config/project_settings.h" +#include "core/os/mutex.h" +#include "core/os/os.h" +#include "core/os/semaphore.h" +#include "core/os/thread.h" +#include "core/templates/command_queue_mt.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 = nullptr; + Thread *writer_thread = nullptr; + + int func1_count = 0; + + void func1(Transform t) { + func1_count++; + } + void func2(Transform t, float f) { + func1_count++; + } + void func3(Transform t1, Transform t2, Transform t3, Transform t4, Transform t5, Transform t6) { + func1_count++; + } + Transform func1r(Transform t) { + func1_count++; + return t; + } + Transform func2r(Transform 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_one(); + } + 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) { + Transform tr; + Transform 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 = Thread::create(&SharedThreadState::static_reader_thread_loop, this); + writer_thread = Thread::create(&SharedThreadState::static_writer_thread_loop, this); + } + void destroy_threads() { + exit_threads = true; + reader_threadwork.main_start_work(); + writer_threadwork.main_start_work(); + + Thread::wait_to_finish(reader_thread); + memdelete(reader_thread); + reader_thread = nullptr; + Thread::wait_to_finish(writer_thread); + memdelete(writer_thread); + writer_thread = nullptr; + } +}; + +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 Waiting at Queue Full") { + 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(); + + int msgs_to_add = 24; // a queue of size 1kB fundamentally cannot fit 24 matrices. + for (int i = 0; i < msgs_to_add; i++) { + sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC1_TRANSFORM); + } + sts.writer_threadwork.main_start_work(); + // If we call main_wait_for_done, we will deadlock. So instead... + 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"); + CHECK_MESSAGE(sts.during_writing, + "Writer thread should still be blocked on writing."); + sts.message_count_to_read = msgs_to_add - 3; + sts.reader_threadwork.main_start_work(); + sts.reader_threadwork.main_wait_for_done(); + CHECK_MESSAGE(sts.func1_count >= msgs_to_add - 3, + "Reader should have read most messages"); + sts.writer_threadwork.main_wait_for_done(); + CHECK_MESSAGE(sts.during_writing == false, + "Writer thread should no longer be blocked on writing."); + sts.message_count_to_read = 2; + sts.reader_threadwork.main_start_work(); + sts.reader_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 == msgs_to_add, + "Reader should have read all messages"); + + 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)); +} + +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/test_config_file.h b/tests/test_config_file.h new file mode 100644 index 0000000000..f910ca4b1f --- /dev/null +++ b/tests/test_config_file.h @@ -0,0 +1,156 @@ +/*************************************************************************/ +/* test_config_file.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 "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); + +#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 = 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 +)"; + + 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/test_curve.h b/tests/test_curve.h new file mode 100644 index 0000000000..b123ef6325 --- /dev/null +++ b/tests/test_curve.h @@ -0,0 +1,221 @@ +/*************************************************************************/ +/* test_curve.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_CURVE_H +#define TEST_CURVE_H + +#include "scene/resources/curve.h" + +#include "thirdparty/doctest/doctest.h" + +namespace TestCurve { + +TEST_CASE("[Curve] Default curve") { + const Ref<Curve> curve = memnew(Curve); + + CHECK_MESSAGE( + curve->get_point_count() == 0, + "Default curve should contain the expected number of points."); + CHECK_MESSAGE( + Math::is_zero_approx(curve->interpolate(0)), + "Default curve should return the expected value at offset 0.0."); + CHECK_MESSAGE( + Math::is_zero_approx(curve->interpolate(0.5)), + "Default curve should return the expected value at offset 0.5."); + CHECK_MESSAGE( + Math::is_zero_approx(curve->interpolate(1)), + "Default curve should return the expected value at offset 1.0."); +} + +TEST_CASE("[Curve] Custom curve with free tangents") { + Ref<Curve> curve = memnew(Curve); + // "Sawtooth" curve with an open ending towards the 1.0 offset. + curve->add_point(Vector2(0, 0)); + curve->add_point(Vector2(0.25, 1)); + curve->add_point(Vector2(0.5, 0)); + curve->add_point(Vector2(0.75, 1)); + + CHECK_MESSAGE( + Math::is_zero_approx(curve->get_point_left_tangent(0)), + "get_point_left_tangent() should return the expected value for point index 0."); + CHECK_MESSAGE( + Math::is_zero_approx(curve->get_point_right_tangent(0)), + "get_point_right_tangent() should return the expected value for point index 0."); + CHECK_MESSAGE( + curve->get_point_left_mode(0) == Curve::TangentMode::TANGENT_FREE, + "get_point_left_mode() should return the expected value for point index 0."); + CHECK_MESSAGE( + curve->get_point_right_mode(0) == Curve::TangentMode::TANGENT_FREE, + "get_point_right_mode() should return the expected value for point index 0."); + + CHECK_MESSAGE( + curve->get_point_count() == 4, + "Custom free curve should contain the expected number of points."); + + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(-0.1), 0), + "Custom free curve should return the expected value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(0.1), 0.352), + "Custom free curve should return the expected value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(0.4), 0.352), + "Custom free curve should return the expected value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(0.7), 0.896), + "Custom free curve should return the expected value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(1), 1), + "Custom free curve should return the expected value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(2), 1), + "Custom free curve should return the expected value at offset 0.1."); + + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(-0.1), 0), + "Custom free curve should return the expected baked value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(0.1), 0.352), + "Custom free curve should return the expected baked value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(0.4), 0.352), + "Custom free curve should return the expected baked value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(0.7), 0.896), + "Custom free curve should return the expected baked value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(1), 1), + "Custom free curve should return the expected baked value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(2), 1), + "Custom free curve should return the expected baked value at offset 0.1."); + + curve->remove_point(1); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(0.1), 0), + "Custom free curve should return the expected value at offset 0.1 after removing point at index 1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(0.1), 0), + "Custom free curve should return the expected baked value at offset 0.1 after removing point at index 1."); + + curve->clear_points(); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(0.6), 0), + "Custom free curve should return the expected value at offset 0.6 after clearing all points."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(0.6), 0), + "Custom free curve should return the expected baked value at offset 0.6 after clearing all points."); +} + +TEST_CASE("[Curve] Custom curve with linear tangents") { + Ref<Curve> curve = memnew(Curve); + // "Sawtooth" curve with an open ending towards the 1.0 offset. + curve->add_point(Vector2(0, 0), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR); + curve->add_point(Vector2(0.25, 1), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR); + curve->add_point(Vector2(0.5, 0), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR); + curve->add_point(Vector2(0.75, 1), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR); + + CHECK_MESSAGE( + Math::is_equal_approx(curve->get_point_left_tangent(3), 4), + "get_point_left_tangent() should return the expected value for point index 3."); + CHECK_MESSAGE( + Math::is_zero_approx(curve->get_point_right_tangent(3)), + "get_point_right_tangent() should return the expected value for point index 3."); + CHECK_MESSAGE( + curve->get_point_left_mode(3) == Curve::TangentMode::TANGENT_LINEAR, + "get_point_left_mode() should return the expected value for point index 3."); + CHECK_MESSAGE( + curve->get_point_right_mode(3) == Curve::TangentMode::TANGENT_LINEAR, + "get_point_right_mode() should return the expected value for point index 3."); + + ERR_PRINT_OFF; + CHECK_MESSAGE( + Math::is_zero_approx(curve->get_point_right_tangent(300)), + "get_point_right_tangent() should return the expected value for invalid point index 300."); + CHECK_MESSAGE( + curve->get_point_left_mode(-12345) == Curve::TangentMode::TANGENT_FREE, + "get_point_left_mode() should return the expected value for invalid point index -12345."); + ERR_PRINT_ON; + + CHECK_MESSAGE( + curve->get_point_count() == 4, + "Custom linear curve should contain the expected number of points."); + + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(-0.1), 0), + "Custom linear curve should return the expected value at offset -0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(0.1), 0.4), + "Custom linear curve should return the expected value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(0.4), 0.4), + "Custom linear curve should return the expected value at offset 0.4."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(0.7), 0.8), + "Custom linear curve should return the expected value at offset 0.7."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(1), 1), + "Custom linear curve should return the expected value at offset 1.0."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(2), 1), + "Custom linear curve should return the expected value at offset 2.0."); + + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(-0.1), 0), + "Custom linear curve should return the expected baked value at offset -0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(0.1), 0.4), + "Custom linear curve should return the expected baked value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(0.4), 0.4), + "Custom linear curve should return the expected baked value at offset 0.4."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(0.7), 0.8), + "Custom linear curve should return the expected baked value at offset 0.7."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(1), 1), + "Custom linear curve should return the expected baked value at offset 1.0."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(2), 1), + "Custom linear curve should return the expected baked value at offset 2.0."); + + ERR_PRINT_OFF; + curve->remove_point(10); + ERR_PRINT_ON; + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate(0.7), 0.8), + "Custom free curve should return the expected value at offset 0.7 after removing point at invalid index 10."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->interpolate_baked(0.7), 0.8), + "Custom free curve should return the expected baked value at offset 0.7 after removing point at invalid index 10."); +} +} // namespace TestCurve + +#endif // TEST_CURVE_H diff --git a/tests/test_expression.h b/tests/test_expression.h new file mode 100644 index 0000000000..0d970ba87a --- /dev/null +++ b/tests/test_expression.h @@ -0,0 +1,444 @@ +/*************************************************************************/ +/* test_expression.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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(float(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(float(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(float(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(float(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(float(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(float(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(float(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(float(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(float(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("stepify(sin(0.5), 0.01)") == OK, + "The expression should parse successfully."); + CHECK_MESSAGE( + Math::is_equal_approx(float(expression.execute()), 0.48), + "`stepify(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(float(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(float(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/test_gradient.h b/tests/test_gradient.h new file mode 100644 index 0000000000..0c018c33e5 --- /dev/null +++ b/tests/test_gradient.h @@ -0,0 +1,151 @@ +/*************************************************************************/ +/* test_gradient.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_GRADIENT_H +#define TEST_GRADIENT_H + +#include "core/math/color.h" +#include "core/object/class_db.h" +#include "scene/resources/gradient.h" + +#include "thirdparty/doctest/doctest.h" + +namespace TestGradient { + +TEST_CASE("[Gradient] Default gradient") { + // Black-white gradient. + Ref<Gradient> gradient = memnew(Gradient); + + CHECK_MESSAGE( + gradient->get_points_count() == 2, + "Default gradient should contain the expected number of points."); + + CHECK_MESSAGE( + gradient->get_color_at_offset(0.0).is_equal_approx(Color(0, 0, 0)), + "Default gradient should return the expected interpolated value at offset 0.0."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.4).is_equal_approx(Color(0.4, 0.4, 0.4)), + "Default gradient should return the expected interpolated value at offset 0.4."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.8).is_equal_approx(Color(0.8, 0.8, 0.8)), + "Default gradient should return the expected interpolated value at offset 0.8."); + CHECK_MESSAGE( + gradient->get_color_at_offset(1.0).is_equal_approx(Color(1, 1, 1)), + "Default gradient should return the expected interpolated value at offset 1.0."); + + // Out of bounds checks. + CHECK_MESSAGE( + gradient->get_color_at_offset(-1.0).is_equal_approx(Color(0, 0, 0)), + "Default gradient should return the expected interpolated value at offset -1.0."); + CHECK_MESSAGE( + gradient->get_color_at_offset(1234.0).is_equal_approx(Color(1, 1, 1)), + "Default gradient should return the expected interpolated value at offset 1234.0."); +} + +TEST_CASE("[Gradient] Custom gradient (points specified in order)") { + // Red-yellow-green gradient (with overbright green). + Ref<Gradient> gradient = memnew(Gradient); + Vector<Gradient::Point> points; + points.push_back({ 0.0, Color(1, 0, 0) }); + points.push_back({ 0.5, Color(1, 1, 0) }); + points.push_back({ 1.0, Color(0, 2, 0) }); + gradient->set_points(points); + + CHECK_MESSAGE( + gradient->get_points_count() == 3, + "Custom gradient should contain the expected number of points."); + + CHECK_MESSAGE( + gradient->get_color_at_offset(0.0).is_equal_approx(Color(1, 0, 0)), + "Custom gradient should return the expected interpolated value at offset 0.0."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.25).is_equal_approx(Color(1, 0.5, 0)), + "Custom gradient should return the expected interpolated value at offset 0.25."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.5).is_equal_approx(Color(1, 1, 0)), + "Custom gradient should return the expected interpolated value at offset 0.5."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.75).is_equal_approx(Color(0.5, 1.5, 0)), + "Custom gradient should return the expected interpolated value at offset 0.75."); + CHECK_MESSAGE( + gradient->get_color_at_offset(1.0).is_equal_approx(Color(0, 2, 0)), + "Custom gradient should return the expected interpolated value at offset 1.0."); + + gradient->remove_point(1); + CHECK_MESSAGE( + gradient->get_points_count() == 2, + "Custom gradient should contain the expected number of points after removing one point."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.5).is_equal_approx(Color(0.5, 1, 0)), + "Custom gradient should return the expected interpolated value at offset 0.5 after removing point at index 1."); +} + +TEST_CASE("[Gradient] Custom gradient (points specified out-of-order)") { + // HSL rainbow with points specified out of order. + // These should be sorted automatically when adding points. + Ref<Gradient> gradient = memnew(Gradient); + Vector<Gradient::Point> points; + points.push_back({ 0.2, Color(1, 0, 0) }); + points.push_back({ 0.0, Color(1, 1, 0) }); + points.push_back({ 0.8, Color(0, 1, 0) }); + points.push_back({ 0.4, Color(0, 1, 1) }); + points.push_back({ 1.0, Color(0, 0, 1) }); + points.push_back({ 0.6, Color(1, 0, 1) }); + gradient->set_points(points); + + CHECK_MESSAGE( + gradient->get_points_count() == 6, + "Custom out-of-order gradient should contain the expected number of points."); + + CHECK_MESSAGE( + gradient->get_color_at_offset(0.0).is_equal_approx(Color(1, 1, 0)), + "Custom out-of-order gradient should return the expected interpolated value at offset 0.0."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.3).is_equal_approx(Color(0.5, 0.5, 0.5)), + "Custom out-of-order gradient should return the expected interpolated value at offset 0.3."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.6).is_equal_approx(Color(1, 0, 1)), + "Custom out-of-order gradient should return the expected interpolated value at offset 0.6."); + CHECK_MESSAGE( + gradient->get_color_at_offset(1.0).is_equal_approx(Color(0, 0, 1)), + "Custom out-of-order gradient should return the expected interpolated value at offset 1.0."); + + gradient->remove_point(0); + CHECK_MESSAGE( + gradient->get_points_count() == 5, + "Custom out-of-order gradient should contain the expected number of points after removing one point."); + // The color will be clamped to the nearest point (which is at offset 0.2). + CHECK_MESSAGE( + gradient->get_color_at_offset(0.1).is_equal_approx(Color(1, 0, 0)), + "Custom out-of-order gradient should return the expected interpolated value at offset 0.1 after removing point at index 0."); +} +} // namespace TestGradient + +#endif // TEST_GRADIENT_H diff --git a/tests/test_gui.cpp b/tests/test_gui.cpp new file mode 100644 index 0000000000..c2d81bda69 --- /dev/null +++ b/tests/test_gui.cpp @@ -0,0 +1,270 @@ +/*************************************************************************/ +/* test_gui.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef _3D_DISABLED + +#include "test_gui.h" + +#include "core/io/image_loader.h" +#include "core/os/os.h" +#include "core/string/print_string.h" +#include "scene/2d/sprite_2d.h" +#include "scene/gui/button.h" +#include "scene/gui/control.h" +#include "scene/gui/label.h" +#include "scene/gui/line_edit.h" +#include "scene/gui/menu_button.h" +#include "scene/gui/option_button.h" +#include "scene/gui/panel.h" +#include "scene/gui/popup_menu.h" +#include "scene/gui/progress_bar.h" +#include "scene/gui/rich_text_label.h" +#include "scene/gui/scroll_bar.h" +#include "scene/gui/spin_box.h" +#include "scene/gui/tab_container.h" +#include "scene/gui/texture_rect.h" +#include "scene/gui/tree.h" +#include "scene/main/scene_tree.h" + +#include "scene/3d/camera_3d.h" +#include "scene/main/window.h" + +namespace TestGUI { + +class TestMainLoop : public SceneTree { +public: + virtual void request_quit() { + quit(); + } + virtual void init() { + SceneTree::init(); + + Panel *frame = memnew(Panel); + frame->set_anchor(MARGIN_RIGHT, Control::ANCHOR_END); + frame->set_anchor(MARGIN_BOTTOM, Control::ANCHOR_END); + frame->set_end(Point2(0, 0)); + + Ref<Theme> t = memnew(Theme); + frame->set_theme(t); + + get_root()->add_child(frame); + + Label *label = memnew(Label); + + label->set_position(Point2(80, 90)); + label->set_size(Point2(170, 80)); + label->set_align(Label::ALIGN_FILL); + label->set_text("There was once upon a time a beautiful unicorn that loved to play with little girls..."); + + frame->add_child(label); + + Button *button = memnew(Button); + + button->set_position(Point2(20, 20)); + button->set_size(Point2(1, 1)); + button->set_text("This is a biggie button"); + + frame->add_child(button); + + Tree *tree = memnew(Tree); + tree->set_columns(2); + + tree->set_position(Point2(230, 210)); + tree->set_size(Point2(150, 250)); + + TreeItem *item = tree->create_item(); + item->set_editable(0, true); + item->set_text(0, "root"); + item = tree->create_item(tree->get_root()); + item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); + item->set_editable(0, true); + item->set_text(0, "check"); + item->set_cell_mode(1, TreeItem::CELL_MODE_CHECK); + item->set_editable(1, true); + item->set_text(1, "check2"); + item = tree->create_item(tree->get_root()); + item->set_cell_mode(0, TreeItem::CELL_MODE_RANGE); + item->set_editable(0, true); + item->set_range_config(0, 0, 20, 0.1); + item->set_range(0, 2); + item->add_button(0, Theme::get_default()->get_icon("folder", "FileDialog")); + item->set_cell_mode(1, TreeItem::CELL_MODE_RANGE); + item->set_editable(1, true); + item->set_range_config(1, 0, 20, 0.1); + item->set_range(1, 3); + + item = tree->create_item(tree->get_root()); + item->set_cell_mode(0, TreeItem::CELL_MODE_RANGE); + item->set_editable(0, true); + item->set_text(0, "Have,Many,Several,Options!"); + item->set_range(0, 2); + + item = tree->create_item(item); + item->set_editable(0, true); + item->set_text(0, "Gershwin!"); + + frame->add_child(tree); + + LineEdit *line_edit = memnew(LineEdit); + + line_edit->set_position(Point2(30, 190)); + line_edit->set_size(Point2(180, 1)); + + frame->add_child(line_edit); + + HScrollBar *hscroll = memnew(HScrollBar); + + hscroll->set_position(Point2(30, 290)); + hscroll->set_size(Point2(180, 1)); + hscroll->set_max(10); + hscroll->set_page(4); + + frame->add_child(hscroll); + + SpinBox *spin = memnew(SpinBox); + + spin->set_position(Point2(30, 260)); + spin->set_size(Point2(120, 1)); + + frame->add_child(spin); + hscroll->share(spin); + + ProgressBar *progress = memnew(ProgressBar); + + progress->set_position(Point2(30, 330)); + progress->set_size(Point2(120, 1)); + + frame->add_child(progress); + hscroll->share(progress); + + MenuButton *menu_button = memnew(MenuButton); + + menu_button->set_text("I'm a menu!"); + menu_button->set_position(Point2(30, 380)); + menu_button->set_size(Point2(1, 1)); + + frame->add_child(menu_button); + + PopupMenu *popup = menu_button->get_popup(); + + popup->add_item("Hello, testing"); + popup->add_item("My Dearest"); + popup->add_separator(); + popup->add_item("Popup"); + popup->add_check_item("Check Popup"); + popup->set_item_checked(4, true); + popup->add_separator(); + popup->add_radio_check_item("Option A"); + popup->set_item_checked(6, true); + popup->add_radio_check_item("Option B"); + + OptionButton *options = memnew(OptionButton); + + options->add_item("Hello, testing"); + options->add_item("My Dearest"); + + options->set_position(Point2(230, 180)); + options->set_size(Point2(1, 1)); + + frame->add_child(options); + + RichTextLabel *richtext = memnew(RichTextLabel); + + richtext->set_position(Point2(600, 210)); + richtext->set_size(Point2(180, 250)); + richtext->set_anchor_and_margin(MARGIN_RIGHT, Control::ANCHOR_END, -20); + + frame->add_child(richtext); + + richtext->add_text("Hello, My Friends!\n\nWelcome to the amazing world of "); + + richtext->add_newline(); + richtext->add_newline(); + + richtext->push_color(Color(1, 0.5, 0.5)); + richtext->add_text("leprechauns"); + richtext->pop(); + + richtext->add_text(" and "); + richtext->push_color(Color(0, 1.0, 0.5)); + richtext->add_text("faeries.\n"); + richtext->pop(); + richtext->add_text("In this new episode, we will attempt to "); + richtext->push_font(richtext->get_theme_font("mono_font", "Fonts")); + richtext->push_color(Color(0.7, 0.5, 1.0)); + richtext->add_text("deliver something nice"); + richtext->pop(); + richtext->pop(); + richtext->add_text(" to all the viewers! Unfortunately, I need to "); + richtext->push_underline(); + richtext->add_text("keep writing a lot of text"); + richtext->pop(); + richtext->add_text(" so the label control overflows and the scrollbar appears.\n"); + richtext->push_meta("http://www.scrollingcapabilities.xz"); + richtext->add_text("This allows to test for the scrolling capabilities "); + richtext->pop(); + richtext->add_text("of the rich text label for huge text (not like this text will really be huge but, you know).\nAs long as it is so long that it will work nicely for a test/demo, then it's welcomed in my book...\nChanging subject, the day is cloudy today and I'm wondering if I'll get che chance to travel somewhere nice. Sometimes, watching the clouds from satellite images may give a nice insight about how pressure zones in our planet work, although it also makes it pretty obvious to see why most weather forecasts get it wrong so often.\nClouds are so difficult to predict!\nBut it's pretty cool how our civilization has adapted to having water falling from the sky each time it rains..."); + + TabContainer *tabc = memnew(TabContainer); + + Control *ctl = memnew(Control); + ctl->set_name("tab 1"); + tabc->add_child(ctl); + + ctl = memnew(Control); + ctl->set_name("tab 2"); + tabc->add_child(ctl); + label = memnew(Label); + label->set_text("Some Label"); + label->set_position(Point2(20, 20)); + ctl->add_child(label); + + ctl = memnew(Control); + ctl->set_name("tab 3"); + button = memnew(Button); + button->set_text("Some Button"); + button->set_position(Point2(30, 50)); + ctl->add_child(button); + + tabc->add_child(ctl); + + frame->add_child(tabc); + + tabc->set_position(Point2(400, 210)); + tabc->set_size(Point2(180, 250)); + } +}; + +MainLoop *test() { + return memnew(TestMainLoop); +} +} // namespace TestGUI + +#endif diff --git a/tests/test_gui.h b/tests/test_gui.h new file mode 100644 index 0000000000..5a23179eee --- /dev/null +++ b/tests/test_gui.h @@ -0,0 +1,41 @@ +/*************************************************************************/ +/* test_gui.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_GUI_H +#define TEST_GUI_H + +#include "core/os/main_loop.h" + +namespace TestGUI { + +MainLoop *test(); +} + +#endif diff --git a/tests/test_json.h b/tests/test_json.h new file mode 100644 index 0000000000..fe29e89e06 --- /dev/null +++ b/tests/test_json.h @@ -0,0 +1,166 @@ +/*************************************************************************/ +/* test_json.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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; + Variant result; + String err_str; + int err_line; + + json.parse("null", result, err_str, err_line); + CHECK_MESSAGE( + err_line == 0, + "Parsing `null` as JSON should parse successfully."); + CHECK_MESSAGE( + result == Variant(), + "Parsing a double quoted string as JSON should return the expected value."); + + json.parse("true", result, err_str, err_line); + CHECK_MESSAGE( + err_line == 0, + "Parsing boolean `true` as JSON should parse successfully."); + CHECK_MESSAGE( + result, + "Parsing boolean `true` as JSON should return the expected value."); + + json.parse("false", result, err_str, err_line); + CHECK_MESSAGE( + err_line == 0, + "Parsing boolean `false` as JSON should parse successfully."); + CHECK_MESSAGE( + !result, + "Parsing boolean `false` as JSON should return the expected value."); + + // JSON only has a floating-point number type, no integer type. + // This is why we use `is_equal_approx()` for the comparison. + json.parse("123456", result, err_str, err_line); + CHECK_MESSAGE( + err_line == 0, + "Parsing an integer number as JSON should parse successfully."); + CHECK_MESSAGE( + Math::is_equal_approx(result, 123'456), + "Parsing an integer number as JSON should return the expected value."); + + json.parse("0.123456", result, err_str, err_line); + CHECK_MESSAGE( + err_line == 0, + "Parsing a floating-point number as JSON should parse successfully."); + CHECK_MESSAGE( + Math::is_equal_approx(result, 0.123456), + "Parsing a floating-point number as JSON should return the expected value."); + + json.parse("\"hello\"", result, err_str, err_line); + CHECK_MESSAGE( + err_line == 0, + "Parsing a double quoted string as JSON should parse successfully."); + CHECK_MESSAGE( + result == "hello", + "Parsing a double quoted string as JSON should return the expected value."); +} + +TEST_CASE("[JSON] Parsing arrays") { + JSON json; + Variant result; + String err_str; + int err_line; + + // 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!"]]]])", + result, err_str, err_line); + + const Array array = result; + CHECK_MESSAGE( + err_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; + Variant result; + String err_str; + int err_line; + + json.parse( + R"({"name": "Godot Engine", "is_free": true, "bugs": null, "apples": {"red": 500, "green": 0, "blue": -20}, "empty_object": {}})", + result, err_str, err_line); + + const Dictionary dictionary = result; + 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( + Math::is_equal_approx(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/test_list.h b/tests/test_list.h new file mode 100644 index 0000000000..1b23233838 --- /dev/null +++ b/tests/test_list.h @@ -0,0 +1,278 @@ +/*************************************************************************/ +/* test_list.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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] 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/test_macros.cpp b/tests/test_macros.cpp new file mode 100644 index 0000000000..2317223b23 --- /dev/null +++ b/tests/test_macros.cpp @@ -0,0 +1,42 @@ +/*************************************************************************/ +/* test_macros.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#define DOCTEST_CONFIG_IMPLEMENT +#include "test_macros.h" + +Map<String, TestFunc> *test_commands = nullptr; + +int register_test_command(String p_command, TestFunc p_function) { + if (!test_commands) { + test_commands = new Map<String, TestFunc>; + } + test_commands->insert(p_command, p_function); + return 0; +} diff --git a/tests/test_macros.h b/tests/test_macros.h new file mode 100644 index 0000000000..05fae128b3 --- /dev/null +++ b/tests/test_macros.h @@ -0,0 +1,123 @@ +/*************************************************************************/ +/* test_macros.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_MACROS_H +#define TEST_MACROS_H + +#include "core/templates/map.h" +#include "core/variant/variant.h" + +// See documentation for doctest at: +// https://github.com/onqtam/doctest/blob/master/doc/markdown/readme.md#reference +#include "thirdparty/doctest/doctest.h" + +// The test is skipped with this, run pending tests with `--test --no-skip`. +#define TEST_CASE_PENDING(name) TEST_CASE(name *doctest::skip()) + +// Temporarily disable error prints to test failure paths. +// This allows to avoid polluting the test summary with error messages. +// The `_print_error_enabled` boolean is defined in `core/print_string.cpp` and +// works at global scope. It's used by various loggers in `should_log()` method, +// which are used by error macros which call into `OS::print_error`, effectively +// disabling any error messages to be printed from the engine side (not tests). +#define ERR_PRINT_OFF _print_error_enabled = false; +#define ERR_PRINT_ON _print_error_enabled = true; + +// Stringify all `Variant` compatible types for doctest output by default. +// https://github.com/onqtam/doctest/blob/master/doc/markdown/stringification.md + +#define DOCTEST_STRINGIFY_VARIANT(m_type) \ + template <> \ + struct doctest::StringMaker<m_type> { \ + static doctest::String convert(const m_type &p_val) { \ + const Variant val = p_val; \ + return val.get_construct_string().utf8().get_data(); \ + } \ + }; + +#define DOCTEST_STRINGIFY_VARIANT_POINTER(m_type) \ + template <> \ + struct doctest::StringMaker<m_type> { \ + static doctest::String convert(const m_type *p_val) { \ + const Variant val = p_val; \ + return val.get_construct_string().utf8().get_data(); \ + } \ + }; + +DOCTEST_STRINGIFY_VARIANT(Variant); +DOCTEST_STRINGIFY_VARIANT(::String); // Disambiguate from `doctest::String`. + +DOCTEST_STRINGIFY_VARIANT(Vector2); +DOCTEST_STRINGIFY_VARIANT(Vector2i); +DOCTEST_STRINGIFY_VARIANT(Rect2); +DOCTEST_STRINGIFY_VARIANT(Rect2i); +DOCTEST_STRINGIFY_VARIANT(Vector3); +DOCTEST_STRINGIFY_VARIANT(Vector3i); +DOCTEST_STRINGIFY_VARIANT(Transform2D); +DOCTEST_STRINGIFY_VARIANT(Plane); +DOCTEST_STRINGIFY_VARIANT(Quat); +DOCTEST_STRINGIFY_VARIANT(AABB); +DOCTEST_STRINGIFY_VARIANT(Basis); +DOCTEST_STRINGIFY_VARIANT(Transform); + +DOCTEST_STRINGIFY_VARIANT(::Color); // Disambiguate from `doctest::Color`. +DOCTEST_STRINGIFY_VARIANT(StringName); +DOCTEST_STRINGIFY_VARIANT(NodePath); +DOCTEST_STRINGIFY_VARIANT(RID); +DOCTEST_STRINGIFY_VARIANT_POINTER(Object); +DOCTEST_STRINGIFY_VARIANT(Callable); +DOCTEST_STRINGIFY_VARIANT(Signal); +DOCTEST_STRINGIFY_VARIANT(Dictionary); +DOCTEST_STRINGIFY_VARIANT(Array); + +DOCTEST_STRINGIFY_VARIANT(PackedByteArray); +DOCTEST_STRINGIFY_VARIANT(PackedInt32Array); +DOCTEST_STRINGIFY_VARIANT(PackedInt64Array); +DOCTEST_STRINGIFY_VARIANT(PackedFloat32Array); +DOCTEST_STRINGIFY_VARIANT(PackedFloat64Array); +DOCTEST_STRINGIFY_VARIANT(PackedStringArray); +DOCTEST_STRINGIFY_VARIANT(PackedVector2Array); +DOCTEST_STRINGIFY_VARIANT(PackedVector3Array); +DOCTEST_STRINGIFY_VARIANT(PackedColorArray); + +// Register test commands to be launched from the command-line. +// For instance: REGISTER_TEST_COMMAND("gdscript-parser" &test_parser_func). +// Example usage: `godot --test gdscript-parser`. + +typedef void (*TestFunc)(); +extern Map<String, TestFunc> *test_commands; +int register_test_command(String p_command, TestFunc p_function); + +#define REGISTER_TEST_COMMAND(m_command, m_function) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_VAR_)) = \ + register_test_command(m_command, m_function); \ + DOCTEST_GLOBAL_NO_WARNINGS_END() + +#endif // TEST_MACROS_H diff --git a/tests/test_main.cpp b/tests/test_main.cpp new file mode 100644 index 0000000000..c9bf9ab5d5 --- /dev/null +++ b/tests/test_main.cpp @@ -0,0 +1,125 @@ +/*************************************************************************/ +/* test_main.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "test_main.h" + +#include "core/templates/list.h" + +#include "test_aabb.h" +#include "test_astar.h" +#include "test_basis.h" +#include "test_class_db.h" +#include "test_color.h" +#include "test_command_queue.h" +#include "test_config_file.h" +#include "test_curve.h" +#include "test_expression.h" +#include "test_gradient.h" +#include "test_gui.h" +#include "test_json.h" +#include "test_list.h" +#include "test_math.h" +#include "test_method_bind.h" +#include "test_node_path.h" +#include "test_oa_hash_map.h" +#include "test_object.h" +#include "test_ordered_hash_map.h" +#include "test_pck_packer.h" +#include "test_physics_2d.h" +#include "test_physics_3d.h" +#include "test_rect2.h" +#include "test_render.h" +#include "test_shader_lang.h" +#include "test_string.h" +#include "test_validate_testing.h" +#include "test_variant.h" + +#include "modules/modules_tests.gen.h" + +#include "tests/test_macros.h" + +int test_main(int argc, char *argv[]) { + bool run_tests = true; + + // Convert arguments to Godot's command-line. + List<String> args; + + for (int i = 0; i < argc; i++) { + args.push_back(String::utf8(argv[i])); + } + OS::get_singleton()->set_cmdline("", args); + + // Run custom test tools. + if (test_commands) { + for (Map<String, TestFunc>::Element *E = test_commands->front(); E; E = E->next()) { + if (args.find(E->key())) { + const TestFunc &test_func = E->get(); + test_func(); + run_tests = false; + break; + } + } + if (!run_tests) { + delete test_commands; + return 0; + } + } + // Doctest runner. + doctest::Context test_context; + List<String> test_args; + + // Clean arguments of "--test" from the args. + for (int x = 0; x < argc; x++) { + String arg = String(argv[x]); + if (arg != "--test") { + test_args.push_back(arg); + } + } + // Convert Godot command line arguments back to standard arguments. + char **doctest_args = new char *[test_args.size()]; + for (int x = 0; x < test_args.size(); x++) { + // Operation to convert Godot string to non wchar string. + CharString cs = test_args[x].utf8(); + const char *str = cs.get_data(); + // Allocate the string copy. + doctest_args[x] = new char[strlen(str) + 1]; + // Copy this into memory. + memcpy(doctest_args[x], str, strlen(str) + 1); + } + + test_context.applyCommandLine(test_args.size(), doctest_args); + + for (int x = 0; x < test_args.size(); x++) { + delete[] doctest_args[x]; + } + delete[] doctest_args; + + return test_context.run(); +} diff --git a/tests/test_main.h b/tests/test_main.h new file mode 100644 index 0000000000..983bfde402 --- /dev/null +++ b/tests/test_main.h @@ -0,0 +1,36 @@ +/*************************************************************************/ +/* test_main.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_MAIN_H +#define TEST_MAIN_H + +int test_main(int argc, char *argv[]); + +#endif // TEST_MAIN_H diff --git a/tests/test_math.cpp b/tests/test_math.cpp new file mode 100644 index 0000000000..a7f99e5401 --- /dev/null +++ b/tests/test_math.cpp @@ -0,0 +1,702 @@ +/*************************************************************************/ +/* test_math.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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/basis.h" +#include "core/math/camera_matrix.h" +#include "core/math/delaunay_3d.h" +#include "core/math/geometry_2d.h" +#include "core/math/math_funcs.h" +#include "core/math/transform.h" +#include "core/os/file_access.h" +#include "core/os/keyboard.h" +#include "core/os/os.h" +#include "core/string/print_string.h" +#include "core/string/ustring.h" +#include "core/templates/vmap.h" +#include "core/variant/method_ptrcall.h" +#include "core/variant/variant.h" +#include "scene/main/node.h" +#include "scene/resources/texture.h" +#include "servers/rendering/shader_language.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 (Map<int, String>::Element *E = namespace_stack.front(); E; E = E->next()) { + class_name += E->get() + "."; + } + 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.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; + int flen = fa->get_len(); + buf.resize(fa->get_len() + 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 (List<StringName>::Element *E = tl.front(); E; E = E->next()) { + Vector<uint8_t> m5b = E->get().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(); + float a = 0.3; + + Basis m(v, a); + + Vector3 v2(7, 3, 1); + v2.normalize(); + float a2 = 0.8; + + Basis m2(v2, a2); + + Quat q = m; + Quat q2 = m2; + + Basis m3 = m.inverse() * m2; + Quat q3 = (q.inverse() * q2); //.normalized(); + + print_line(Quat(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, true, nullptr, &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/test_math.h b/tests/test_math.h new file mode 100644 index 0000000000..77bce8dd66 --- /dev/null +++ b/tests/test_math.h @@ -0,0 +1,41 @@ +/*************************************************************************/ +/* test_math.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 + +#include "core/os/main_loop.h" + +namespace TestMath { + +MainLoop *test(); +} + +#endif diff --git a/tests/test_method_bind.h b/tests/test_method_bind.h new file mode 100644 index 0000000000..62d8bd132c --- /dev/null +++ b/tests/test_method_bind.h @@ -0,0 +1,164 @@ +/*************************************************************************/ +/* test_method_bind.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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" + +#include <inttypes.h> +#include <stdio.h> +#include <wchar.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/test_node_path.h b/tests/test_node_path.h new file mode 100644 index 0000000000..e9e06186f5 --- /dev/null +++ b/tests/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-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 "thirdparty/doctest/doctest.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/test_oa_hash_map.cpp b/tests/test_oa_hash_map.cpp new file mode 100644 index 0000000000..b0bb01bc71 --- /dev/null +++ b/tests/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-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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/test_oa_hash_map.h b/tests/test_oa_hash_map.h new file mode 100644 index 0000000000..eb2b3d1e99 --- /dev/null +++ b/tests/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-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 + +#include "core/os/main_loop.h" + +namespace TestOAHashMap { + +MainLoop *test(); +} + +#endif // TEST_OA_HASH_MAP_H diff --git a/tests/test_object.h b/tests/test_object.h new file mode 100644 index 0000000000..6fef2576e7 --- /dev/null +++ b/tests/test_object.h @@ -0,0 +1,92 @@ +/*************************************************************************/ +/* test_object.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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/object/object.h" + +#include "thirdparty/doctest/doctest.h" + +namespace TestObject { + +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."); +} + +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<String> 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<String> 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."); +} +} // namespace TestObject + +#endif // TEST_OBJECT_H diff --git a/tests/test_ordered_hash_map.h b/tests/test_ordered_hash_map.h new file mode 100644 index 0000000000..ef26d2531b --- /dev/null +++ b/tests/test_ordered_hash_map.h @@ -0,0 +1,138 @@ +/*************************************************************************/ +/* test_ordered_hash_map.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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/os/os.h" +#include "core/templates/ordered_hash_map.h" +#include "core/templates/pair.h" +#include "core/templates/vector.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/test_pck_packer.h b/tests/test_pck_packer.h new file mode 100644 index 0000000000..e086d65105 --- /dev/null +++ b/tests/test_pck_packer.h @@ -0,0 +1,114 @@ +/*************************************************************************/ +/* test_pck_packer.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 "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_len() >= 100, + "The generated empty PCK file shouldn't be too small (it should have the PCK header)."); + CHECK_MESSAGE( + f->get_len() <= 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_len() >= 25000, + "The generated non-empty PCK file should be large enough to actually hold the contents specified above."); + CHECK_MESSAGE( + f->get_len() <= 35000, + "The generated non-empty PCK file shouldn't be too large."); +} +} // namespace TestPCKPacker + +#endif // TEST_PCK_PACKER_H diff --git a/tests/test_physics_2d.cpp b/tests/test_physics_2d.cpp new file mode 100644 index 0000000000..d40df52f1b --- /dev/null +++ b/tests/test_physics_2d.cpp @@ -0,0 +1,406 @@ +/*************************************************************************/ +/* test_physics_2d.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "test_physics_2d.h" + +#include "core/os/main_loop.h" +#include "core/os/os.h" +#include "core/string/print_string.h" +#include "core/templates/map.h" +#include "scene/resources/texture.h" +#include "servers/display_server.h" +#include "servers/physics_server_2d.h" +#include "servers/rendering_server.h" + +static const unsigned char convex_png[] = { + 0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa, 0x0, 0x0, 0x0, 0xd, 0x49, 0x48, 0x44, 0x52, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x40, 0x8, 0x6, 0x0, 0x0, 0x0, 0xaa, 0x69, 0x71, 0xde, 0x0, 0x0, 0x0, 0x1, 0x73, 0x52, 0x47, 0x42, 0x0, 0xae, 0xce, 0x1c, 0xe9, 0x0, 0x0, 0x0, 0x6, 0x62, 0x4b, 0x47, 0x44, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf9, 0x43, 0xbb, 0x7f, 0x0, 0x0, 0x0, 0x9, 0x70, 0x48, 0x59, 0x73, 0x0, 0x0, 0xb, 0x13, 0x0, 0x0, 0xb, 0x13, 0x1, 0x0, 0x9a, 0x9c, 0x18, 0x0, 0x0, 0x0, 0x7, 0x74, 0x49, 0x4d, 0x45, 0x7, 0xdb, 0x6, 0xa, 0x3, 0x13, 0x31, 0x66, 0xa7, 0xac, 0x79, 0x0, 0x0, 0x4, 0xef, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0xed, 0x9b, 0xdd, 0x4e, 0x2a, 0x57, 0x14, 0xc7, 0xf7, 0x1e, 0xc0, 0x19, 0x38, 0x32, 0x80, 0xa, 0x6a, 0xda, 0x18, 0xa3, 0xc6, 0x47, 0x50, 0x7b, 0xa1, 0xd9, 0x36, 0x27, 0x7e, 0x44, 0xed, 0x45, 0x4d, 0x93, 0x3e, 0x40, 0x1f, 0x64, 0x90, 0xf4, 0x1, 0xbc, 0xf0, 0xc2, 0x9c, 0x57, 0x30, 0x4d, 0xbc, 0xa8, 0x6d, 0xc, 0x69, 0x26, 0xb5, 0x68, 0x8b, 0x35, 0x7e, 0x20, 0xb4, 0xf5, 0x14, 0xbf, 0x51, 0x3c, 0x52, 0xe, 0xc, 0xe, 0xc8, 0xf0, 0xb1, 0x7a, 0x51, 0x3d, 0xb1, 0x9e, 0x19, 0x1c, 0x54, 0x70, 0x1c, 0xdc, 0x9, 0x17, 0x64, 0x8, 0xc9, 0xff, 0xb7, 0xd6, 0x7f, 0xcd, 0x3f, 0x2b, 0xd9, 0x8, 0xbd, 0x9c, 0xda, 0x3e, 0xf8, 0x31, 0xff, 0xc, 0x0, 0x8, 0x42, 0x88, 0x9c, 0x9f, 0x9f, 0xbf, 0xa, 0x87, 0xc3, 0xad, 0x7d, 0x7d, 0x7d, 0x7f, 0x23, 0x84, 0x78, 0x8c, 0x31, 0xaf, 0x55, 0x0, 0xc6, 0xc7, 0x14, 0x1e, 0x8f, 0xc7, 0xbf, 0x38, 0x3c, 0x3c, 0x6c, 0x9b, 0x9f, 0x9f, 0x6f, 0xb8, 0x82, 0x9b, 0xee, 0xe8, 0xe8, 0xf8, 0x12, 0x0, 0xbe, 0xd3, 0x2a, 0x8, 0xfc, 0x50, 0xd1, 0xf9, 0x7c, 0x9e, 0x8a, 0x46, 0xa3, 0x5f, 0x9d, 0x9e, 0x9e, 0x7e, 0xb2, 0xb0, 0xb0, 0x60, 0xe5, 0x79, 0x1e, 0xf1, 0xfc, 0x7f, 0x3a, 0x9, 0x21, 0x88, 0x10, 0x82, 0x26, 0x26, 0x26, 0xde, 0x77, 0x75, 0x75, 0x85, 0x59, 0x96, 0xfd, 0x5e, 0x6b, 0x20, 0xf0, 0x7d, 0x85, 0x4b, 0x92, 0xf4, 0xfa, 0xe0, 0xe0, 0xe0, 0xd3, 0xb9, 0xb9, 0xb9, 0x46, 0x49, 0x92, 0xea, 0x6f, 0xa, 0xbf, 0x7d, 0x8, 0x21, 0x68, 0x70, 0x70, 0xb0, 0x38, 0x39, 0x39, 0x79, 0xd6, 0xd9, 0xd9, 0xb9, 0xcf, 0x30, 0xcc, 0xa2, 0xd6, 0xad, 0x21, 0x2b, 0x1c, 0x0, 0x38, 0x41, 0x10, 0xfc, 0xdb, 0xdb, 0xdb, 0x27, 0x1e, 0x8f, 0x27, 0x4b, 0x8, 0x1, 0x84, 0x90, 0xea, 0xf, 0x21, 0x4, 0x3c, 0x1e, 0x4f, 0x76, 0x67, 0x67, 0x67, 0x3f, 0x9f, 0xcf, 0xff, 0x7c, 0x5, 0xf3, 0xd9, 0x0, 0xe0, 0x2, 0x81, 0xc0, 0xa9, 0xdb, 0xed, 0x2e, 0x94, 0x2b, 0x5c, 0xe, 0xc4, 0xca, 0xca, 0x8a, 0x18, 0x8d, 0x46, 0x3, 0x0, 0xc0, 0x69, 0x1e, 0x4, 0x0, 0x90, 0x48, 0x24, 0x12, 0xe4, 0x38, 0xee, 0x41, 0xc2, 0x6f, 0x43, 0xe0, 0x38, 0xe, 0xfc, 0x7e, 0xbf, 0x10, 0x8b, 0xc5, 0xd6, 0x35, 0xd, 0x22, 0x9b, 0xcd, 0x7a, 0x96, 0x97, 0x97, 0x33, 0xf, 0xad, 0x7c, 0x29, 0x10, 0x9b, 0x9b, 0x9b, 0xef, 0x2e, 0x2e, 0x2e, 0x7e, 0xd5, 0x1c, 0x8, 0x0, 0x20, 0xe1, 0x70, 0x38, 0xfc, 0x98, 0xd5, 0x57, 0x2, 0xe1, 0x76, 0xbb, 0xf3, 0xa1, 0x50, 0xe8, 0x38, 0x9b, 0xcd, 0xfe, 0xa2, 0x9, 0x8, 0x0, 0x40, 0x2e, 0x2f, 0x2f, 0x7d, 0x4b, 0x4b, 0x4b, 0xb9, 0x4a, 0x54, 0x5f, 0x9, 0xc4, 0xd2, 0xd2, 0x92, 0xb4, 0xb7, 0xb7, 0xf7, 0x36, 0x97, 0xcb, 0x4d, 0x3d, 0x29, 0x8, 0x0, 0xe0, 0x42, 0xa1, 0xd0, 0x71, 0xb5, 0xc4, 0xdf, 0xb6, 0xc5, 0x93, 0xe, 0x4a, 0x0, 0x20, 0xa9, 0x54, 0xea, 0x37, 0xb7, 0xdb, 0x5d, 0xa8, 0xa6, 0x78, 0x39, 0x10, 0x6b, 0x6b, 0x6b, 0xf1, 0x64, 0x32, 0xb9, 0x5a, 0x55, 0x10, 0x0, 0xc0, 0x6d, 0x6c, 0x6c, 0x9c, 0x57, 0xbb, 0xfa, 0x25, 0x40, 0x14, 0x3, 0x81, 0x40, 0x34, 0x93, 0xc9, 0x2c, 0x57, 0x1c, 0x4, 0x0, 0x90, 0x58, 0x2c, 0xb6, 0x5e, 0xe9, 0xc1, 0x77, 0x1f, 0x10, 0x53, 0x53, 0x53, 0x52, 0xc5, 0x83, 0x14, 0x0, 0x70, 0x7e, 0xbf, 0x5f, 0xd0, 0x42, 0xf5, 0x95, 0x40, 0xf8, 0x7c, 0xbe, 0xcb, 0xa3, 0xa3, 0xa3, 0x3f, 0x1e, 0xbd, 0x1b, 0x0, 0x80, 0x1c, 0x1f, 0x1f, 0x87, 0xb4, 0x56, 0xfd, 0xaa, 0x5, 0x29, 0x51, 0x14, 0xbf, 0xf5, 0xf9, 0x7c, 0x97, 0x5a, 0xad, 0xbe, 0x12, 0x88, 0xf5, 0xf5, 0xf5, 0xd8, 0x83, 0x83, 0x54, 0xb5, 0x42, 0x8f, 0x66, 0x83, 0x94, 0xd6, 0xbd, 0x5f, 0xce, 0x7c, 0x38, 0x3c, 0x3c, 0xfc, 0xb3, 0x50, 0x28, 0xb8, 0xcb, 0x2, 0x1, 0x0, 0xdc, 0xf4, 0xf4, 0xf4, 0xfe, 0x73, 0x15, 0x2f, 0x17, 0xa4, 0x22, 0x91, 0x48, 0x50, 0xb5, 0x2d, 0x0, 0x80, 0x9b, 0x99, 0x99, 0x79, 0xfb, 0xdc, 0x1, 0xc8, 0x5, 0xa9, 0x44, 0x22, 0xf1, 0xfb, 0x9d, 0x10, 0x0, 0x80, 0x9b, 0x9d, 0x9d, 0xd, 0xea, 0x5, 0xc0, 0xad, 0xfd, 0x43, 0x1a, 0x0, 0xb8, 0xdb, 0x9a, 0xa9, 0x8f, 0xb6, 0xa4, 0x46, 0xa3, 0xa4, 0xb7, 0xd5, 0x37, 0xcf, 0xf3, 0x68, 0x75, 0x75, 0xf5, 0x4c, 0xee, 0x99, 0x1c, 0x80, 0x9c, 0x1e, 0xf7, 0xff, 0x16, 0x8b, 0x45, 0x50, 0x5, 0xa0, 0xb7, 0xb7, 0xb7, 0x85, 0x10, 0xa2, 0x2b, 0xf1, 0x84, 0x10, 0xd4, 0xdf, 0xdf, 0x6f, 0x57, 0x3, 0x80, 0x37, 0x18, 0xc, 0x5, 0x3d, 0x2, 0xa0, 0x69, 0x3a, 0x8b, 0x10, 0xe2, 0x4b, 0x2, 0xc0, 0x18, 0xf3, 0xc1, 0x60, 0x70, 0x47, 0x8f, 0x16, 0x38, 0x3a, 0x3a, 0x5a, 0x93, 0x5b, 0xc3, 0x7f, 0x64, 0x81, 0xba, 0xba, 0x3a, 0x49, 0x8f, 0x0, 0x1a, 0x1a, 0x1a, 0xd4, 0xcd, 0x0, 0x93, 0xc9, 0xa4, 0xcb, 0x21, 0xe8, 0x74, 0x3a, 0xd5, 0x1, 0xa0, 0x69, 0x5a, 0x77, 0x1d, 0x80, 0x31, 0x2e, 0x38, 0x9d, 0x4e, 0xb1, 0x66, 0x1, 0x30, 0xc, 0x23, 0x28, 0x3d, 0x93, 0x9b, 0x1, 0xb9, 0x9a, 0x6, 0x60, 0x36, 0x9b, 0x75, 0xd7, 0x1, 0x4a, 0x21, 0xa8, 0x26, 0x0, 0x94, 0xa, 0x41, 0xb2, 0x0, 0x18, 0x86, 0xc9, 0xe9, 0xd, 0x80, 0x52, 0x8, 0x92, 0x5, 0x60, 0xb1, 0x58, 0x74, 0x67, 0x1, 0xa5, 0x10, 0xa4, 0x4, 0x40, 0x77, 0x43, 0xd0, 0xe1, 0x70, 0xa8, 0x9f, 0x1, 0x14, 0x45, 0x1, 0x45, 0x51, 0x79, 0x3d, 0x1, 0x68, 0x6e, 0x6e, 0x4e, 0xaa, 0x6, 0x80, 0x10, 0x42, 0x6, 0x83, 0x41, 0x37, 0x36, 0x28, 0x15, 0x82, 0x6a, 0x2, 0x0, 0x4d, 0xd3, 0xa9, 0x52, 0xcf, 0x95, 0x0, 0xe8, 0x66, 0xe, 0x98, 0xcd, 0x66, 0xa1, 0x6c, 0x0, 0x7a, 0x5a, 0x8b, 0x59, 0x2c, 0x96, 0x64, 0xcd, 0x2, 0xb8, 0x2b, 0x4, 0xe9, 0xde, 0x2, 0x77, 0x85, 0xa0, 0x9a, 0xb0, 0x40, 0xa9, 0x10, 0xa4, 0x8, 0xc0, 0x64, 0x32, 0xe9, 0x6, 0x40, 0xa9, 0x10, 0x54, 0xaa, 0x3, 0x74, 0xf3, 0x16, 0x70, 0xb9, 0x5c, 0xe5, 0x3, 0xe8, 0xe9, 0xe9, 0x69, 0xd5, 0xc3, 0x66, 0x18, 0x63, 0x5c, 0x68, 0x6a, 0x6a, 0x12, 0xcb, 0x5, 0xa0, 0x9b, 0xd5, 0x38, 0x4d, 0xd3, 0x29, 0x8a, 0xa2, 0xa0, 0x2c, 0x0, 0x18, 0x63, 0x3e, 0x14, 0xa, 0xfd, 0x55, 0xb, 0x21, 0x48, 0xd1, 0x2, 0x7a, 0x59, 0x8d, 0xdf, 0x1b, 0x80, 0x1e, 0x56, 0xe3, 0x84, 0x10, 0x34, 0x30, 0x30, 0x60, 0xbb, 0xeb, 0x77, 0x46, 0x5, 0xef, 0x48, 0xcf, 0x4d, 0xec, 0x8d, 0x99, 0x5, 0xf5, 0xf5, 0xf5, 0xef, 0x46, 0x47, 0x47, 0xb, 0x2e, 0x97, 0xeb, 0xbc, 0x54, 0x8, 0x52, 0x4, 0xc0, 0x30, 0x8c, 0xf4, 0x5c, 0x4, 0x9b, 0x4c, 0xa6, 0xf4, 0xf8, 0xf8, 0xb8, 0xc8, 0xb2, 0x6c, 0x32, 0x9d, 0x4e, 0xff, 0xd4, 0xdd, 0xdd, 0x7d, 0x66, 0x34, 0x1a, 0x8b, 0xd7, 0x3, 0xfd, 0xae, 0x5b, 0x29, 0xb2, 0x57, 0x66, 0xb6, 0xb6, 0xb6, 0xde, 0xc4, 0xe3, 0xf1, 0x6f, 0xae, 0xaf, 0xc1, 0x28, 0x5d, 0x85, 0x79, 0x2, 0xc1, 0x60, 0xb5, 0x5a, 0xa3, 0xa3, 0xa3, 0xa3, 0x45, 0xab, 0xd5, 0x9a, 0x2a, 0x16, 0x8b, 0x8b, 0x6d, 0x6d, 0x6d, 0xef, 0xd5, 0x8a, 0x55, 0xd, 0x20, 0x91, 0x48, 0xbc, 0x3e, 0x38, 0x38, 0xf8, 0xda, 0x6e, 0xb7, 0xf7, 0x5f, 0x5c, 0x5c, 0xd4, 0x7b, 0xbd, 0xde, 0xbc, 0x20, 0x8, 0xcd, 0x85, 0x42, 0x81, 0xfe, 0xf0, 0xae, 0xac, 0x10, 0x98, 0x9b, 0xd5, 0xc5, 0x18, 0x17, 0x59, 0x96, 0x3d, 0x1d, 0x19, 0x19, 0x1, 0x96, 0x65, 0x5, 0x8a, 0xa2, 0x7e, 0x6c, 0x69, 0x69, 0x49, 0x3d, 0x44, 0xb0, 0x2a, 0x0, 0x1f, 0xcc, 0x74, 0x75, 0x41, 0xea, 0xfa, 0x7b, 0x32, 0x99, 0x64, 0x76, 0x77, 0x77, 0x5d, 0xe, 0x87, 0xa3, 0x5f, 0x14, 0xc5, 0x57, 0x57, 0x60, 0x5a, 0x8b, 0xc5, 0xa2, 0xf1, 0xbe, 0x50, 0x6e, 0xa, 0x66, 0x18, 0x26, 0x31, 0x36, 0x36, 0x96, 0x65, 0x59, 0x36, 0x29, 0x49, 0x92, 0xb7, 0xbd, 0xbd, 0xfd, 0x9f, 0x72, 0xda, 0xf9, 0xd1, 0x1, 0xa8, 0x1, 0x93, 0xcf, 0xe7, 0xa9, 0x93, 0x93, 0x13, 0x1b, 0x4d, 0xd3, 0x9f, 0xb, 0x82, 0x60, 0xf5, 0x7a, 0xbd, 0xd9, 0x54, 0x2a, 0xe5, 0xcc, 0x64, 0x32, 0xe, 0xb9, 0x6e, 0xb9, 0x16, 0x8c, 0x31, 0x2e, 0xda, 0x6c, 0xb6, 0xc8, 0xd0, 0xd0, 0x10, 0x65, 0xb3, 0xd9, 0x92, 0x95, 0xa8, 0x6e, 0xc5, 0x0, 0xa8, 0xe9, 0x96, 0x68, 0x34, 0x6a, 0xdd, 0xdf, 0xdf, 0x6f, 0x76, 0xb9, 0x5c, 0x9f, 0x89, 0xa2, 0x58, 0xbf, 0xb8, 0xb8, 0x8, 0x26, 0x93, 0x29, 0x3b, 0x3c, 0x3c, 0x8c, 0xed, 0x76, 0x7b, 0xd2, 0x68, 0x34, 0xfe, 0xd0, 0xd8, 0xd8, 0x98, 0xae, 0xb6, 0xe0, 0x8a, 0x1, 0x50, 0xb, 0xe6, 0xa9, 0x5, 0xbf, 0x9c, 0x97, 0xf3, 0xff, 0xf3, 0x2f, 0x6a, 0x82, 0x7f, 0xf6, 0x4e, 0xca, 0x1b, 0xf5, 0x0, 0x0, 0x0, 0x0, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 +}; + +class TestPhysics2DMainLoop : public MainLoop { + GDCLASS(TestPhysics2DMainLoop, MainLoop); + + RID circle_img; + RID circle_shape; + RID space; + RID canvas; + RID ray; + RID ray_query; + Transform2D view_xform; + + Vector2 ray_from, ray_to; + + struct BodyShapeData { + RID image; + RID shape; + }; + + BodyShapeData body_shape_data[8]; + + void _create_body_shape_data() { + RenderingServer *vs = RenderingServer::get_singleton(); + PhysicsServer2D *ps = PhysicsServer2D::get_singleton(); + + // SEGMENT + + { + Vector<uint8_t> pixels; + pixels.resize(32 * 2 * 2); + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 32; j++) { + pixels.set(i * 32 * 2 + j * 2 + 0, (j == 0) ? 255 : 0); + pixels.set(i * 32 * 2 + j * 2 + 1, 255); + } + } + + Ref<Image> image = memnew(Image(32, 2, 0, Image::FORMAT_LA8, pixels)); + + body_shape_data[PhysicsServer2D::SHAPE_SEGMENT].image = vs->texture_2d_create(image); + + RID segment_shape = ps->segment_shape_create(); + Rect2 sg(Point2(-16, 0), Point2(16, 0)); + ps->shape_set_data(segment_shape, sg); + + body_shape_data[PhysicsServer2D::SHAPE_SEGMENT].shape = segment_shape; + } + // CIRCLE + + { + Vector<uint8_t> pixels; + pixels.resize(32 * 32 * 2); + for (int i = 0; i < 32; i++) { + for (int j = 0; j < 32; j++) { + bool black = Vector2(i - 16, j - 16).length_squared() < 16 * 16; + + pixels.set(i * 32 * 2 + j * 2 + 0, (i == 16 || j == 16) ? 255 : 0); + pixels.set(i * 32 * 2 + j * 2 + 1, black ? 255 : 0); + } + } + + Ref<Image> image = memnew(Image(32, 32, 0, Image::FORMAT_LA8, pixels)); + + body_shape_data[PhysicsServer2D::SHAPE_CIRCLE].image = vs->texture_2d_create(image); + + RID circle_shape = ps->circle_shape_create(); + ps->shape_set_data(circle_shape, 16); + + body_shape_data[PhysicsServer2D::SHAPE_CIRCLE].shape = circle_shape; + } + + // BOX + + { + Vector<uint8_t> pixels; + pixels.resize(32 * 32 * 2); + for (int i = 0; i < 32; i++) { + for (int j = 0; j < 32; j++) { + bool black = i > 0 && i < 31 && j > 0 && j < 31; + + pixels.set(i * 32 * 2 + j * 2 + 0, black ? 0 : 255); + pixels.set(i * 32 * 2 + j * 2 + 1, 255); + } + } + + Ref<Image> image = memnew(Image(32, 32, 0, Image::FORMAT_LA8, pixels)); + + body_shape_data[PhysicsServer2D::SHAPE_RECTANGLE].image = vs->texture_2d_create(image); + + RID rectangle_shape = ps->rectangle_shape_create(); + ps->shape_set_data(rectangle_shape, Vector2(16, 16)); + + body_shape_data[PhysicsServer2D::SHAPE_RECTANGLE].shape = rectangle_shape; + } + + // CAPSULE + + { + Vector<uint8_t> pixels; + pixels.resize(32 * 64 * 2); + for (int i = 0; i < 64; i++) { + for (int j = 0; j < 32; j++) { + int si = i > 48 ? i - 32 : (i < 16 ? i : 16); + bool black = Vector2(si - 16, j - 16).length_squared() < 16 * 16; + + pixels.set(i * 32 * 2 + j * 2 + 0, (i == 16 || j == 16 || i == 48) ? 255 : 0); + pixels.set(i * 32 * 2 + j * 2 + 1, black ? 255 : 0); + } + } + + Ref<Image> image = memnew(Image(32, 64, 0, Image::FORMAT_LA8, pixels)); + + body_shape_data[PhysicsServer2D::SHAPE_CAPSULE].image = vs->texture_2d_create(image); + + RID capsule_shape = ps->capsule_shape_create(); + ps->shape_set_data(capsule_shape, Vector2(16, 32)); + + body_shape_data[PhysicsServer2D::SHAPE_CAPSULE].shape = capsule_shape; + } + + // CONVEX + + { + Ref<Image> image = memnew(Image(convex_png)); + + body_shape_data[PhysicsServer2D::SHAPE_CONVEX_POLYGON].image = vs->texture_2d_create(image); + + RID convex_polygon_shape = ps->convex_polygon_shape_create(); + + Vector<Vector2> arr; + Point2 sb(32, 32); + arr.push_back(Point2(20, 3) - sb); + arr.push_back(Point2(58, 23) - sb); + arr.push_back(Point2(55, 54) - sb); + arr.push_back(Point2(27, 60) - sb); + arr.push_back(Point2(5, 56) - sb); + arr.push_back(Point2(4, 20) - sb); + arr.push_back(Point2(11, 7) - sb); + ps->shape_set_data(convex_polygon_shape, arr); + + body_shape_data[PhysicsServer2D::SHAPE_CONVEX_POLYGON].shape = convex_polygon_shape; + } + } + + void _do_ray_query() { + /* + PhysicsServer2D *ps = PhysicsServer2D::get_singleton(); + ps->query_intersection_segment(ray_query,ray_from,ray_to); + */ + } + +protected: + void input_event(const Ref<InputEvent> &p_event) { + Ref<InputEventMouseButton> mb = p_event; + + if (mb.is_valid()) { + if (mb->is_pressed()) { + Point2 p(mb->get_position().x, mb->get_position().y); + + if (mb->get_button_index() == 1) { + ray_to = p; + _do_ray_query(); + } else if (mb->get_button_index() == 2) { + ray_from = p; + _do_ray_query(); + } + } + } + + Ref<InputEventMouseMotion> mm = p_event; + + if (mm.is_valid()) { + Point2 p = mm->get_position(); + + if (mm->get_button_mask() & BUTTON_MASK_LEFT) { + ray_to = p; + _do_ray_query(); + } else if (mm->get_button_mask() & BUTTON_MASK_RIGHT) { + ray_from = p; + _do_ray_query(); + } + } + } + + RID _add_body(PhysicsServer2D::ShapeType p_shape, const Transform2D &p_xform) { + RenderingServer *vs = RenderingServer::get_singleton(); + PhysicsServer2D *ps = PhysicsServer2D::get_singleton(); + + RID body = ps->body_create(); + ps->body_add_shape(body, body_shape_data[p_shape].shape); + ps->body_set_space(body, space); + ps->body_set_continuous_collision_detection_mode(body, PhysicsServer2D::CCD_MODE_CAST_SHAPE); + ps->body_set_state(body, PhysicsServer2D::BODY_STATE_TRANSFORM, p_xform); + + //print_line("add body with xform: "+p_xform); + RID sprite = vs->canvas_item_create(); + vs->canvas_item_set_parent(sprite, canvas); + vs->canvas_item_set_transform(sprite, p_xform); + Size2 imgsize(5, 5); //vs->texture_get_width(body_shape_data[p_shape].image), vs->texture_get_height(body_shape_data[p_shape].image)); + vs->canvas_item_add_texture_rect(sprite, Rect2(-imgsize / 2.0, imgsize), body_shape_data[p_shape].image); + + ps->body_set_force_integration_callback(body, this, "_body_moved", sprite); + //RID q = ps->query_create(this,"_body_moved",sprite); + //ps->query_body_state(q,body); + + return body; + } + + void _add_plane(const Vector2 &p_normal, real_t p_d) { + PhysicsServer2D *ps = PhysicsServer2D::get_singleton(); + + Array arr; + arr.push_back(p_normal); + arr.push_back(p_d); + + RID plane = ps->line_shape_create(); + ps->shape_set_data(plane, arr); + + RID plane_body = ps->body_create(); + ps->body_set_mode(plane_body, PhysicsServer2D::BODY_MODE_STATIC); + ps->body_set_space(plane_body, space); + ps->body_add_shape(plane_body, plane); + } + + void _add_concave(const Vector<Vector2> &p_points, const Transform2D &p_xform = Transform2D()) { + PhysicsServer2D *ps = PhysicsServer2D::get_singleton(); + RenderingServer *vs = RenderingServer::get_singleton(); + + RID concave = ps->concave_polygon_shape_create(); + ps->shape_set_data(concave, p_points); + RID body = ps->body_create(); + ps->body_set_mode(body, PhysicsServer2D::BODY_MODE_STATIC); + ps->body_set_space(body, space); + ps->body_add_shape(body, concave); + ps->body_set_state(body, PhysicsServer2D::BODY_STATE_TRANSFORM, p_xform); + + RID sprite = vs->canvas_item_create(); + vs->canvas_item_set_parent(sprite, canvas); + vs->canvas_item_set_transform(sprite, p_xform); + for (int i = 0; i < p_points.size(); i += 2) { + vs->canvas_item_add_line(sprite, p_points[i], p_points[i + 1], Color(0, 0, 0), 2); + } + } + + void _body_moved(Object *p_state, RID p_sprite) { + PhysicsDirectBodyState2D *state = (PhysicsDirectBodyState2D *)p_state; + RenderingServer::get_singleton()->canvas_item_set_transform(p_sprite, state->get_transform()); + } + + void _ray_query_callback(const RID &p_rid, ObjectID p_id, int p_shape, const Vector2 &p_point, const Vector2 &p_normal) { + Vector2 ray_end; + + if (p_rid.is_valid()) { + ray_end = p_point; + } else { + ray_end = ray_to; + } + + RenderingServer *vs = RenderingServer::get_singleton(); + + vs->canvas_item_clear(ray); + vs->canvas_item_add_line(ray, ray_from, ray_end, p_rid.is_valid() ? Color(0, 1, 0.4) : Color(1, 0.4, 0), 2); + if (p_rid.is_valid()) { + vs->canvas_item_add_line(ray, ray_end, ray_end + p_normal * 20, p_rid.is_valid() ? Color(0, 1, 0.4) : Color(1, 0.4, 0), 2); + } + } + + static void _bind_methods() { + ClassDB::bind_method(D_METHOD("_body_moved"), &TestPhysics2DMainLoop::_body_moved); + ClassDB::bind_method(D_METHOD("_ray_query_callback"), &TestPhysics2DMainLoop::_ray_query_callback); + } + +public: + virtual void init() override { + RenderingServer *vs = RenderingServer::get_singleton(); + PhysicsServer2D *ps = PhysicsServer2D::get_singleton(); + + space = ps->space_create(); + ps->space_set_active(space, true); + ps->set_active(true); + ps->area_set_param(space, PhysicsServer2D::AREA_PARAM_GRAVITY_VECTOR, Vector2(0, 1)); + ps->area_set_param(space, PhysicsServer2D::AREA_PARAM_GRAVITY, 98); + + { + RID vp = vs->viewport_create(); + canvas = vs->canvas_create(); + + Size2i screen_size = DisplayServer::get_singleton()->window_get_size(); + vs->viewport_attach_canvas(vp, canvas); + vs->viewport_set_size(vp, screen_size.x, screen_size.y); + vs->viewport_attach_to_screen(vp, Rect2(Vector2(), screen_size)); + vs->viewport_set_active(vp, true); + + Transform2D smaller; + //smaller.scale(Vector2(0.6,0.6)); + //smaller.elements[2]=Vector2(100,0); + + //view_xform = smaller; + vs->viewport_set_canvas_transform(vp, canvas, view_xform); + } + + ray = vs->canvas_item_create(); + vs->canvas_item_set_parent(ray, canvas); + //ray_query = ps->query_create(this,"_ray_query_callback",Variant()); + //ps->query_intersection(ray_query,space); + + _create_body_shape_data(); + + for (int i = 0; i < 32; i++) { + PhysicsServer2D::ShapeType types[4] = { + PhysicsServer2D::SHAPE_CIRCLE, + PhysicsServer2D::SHAPE_CAPSULE, + PhysicsServer2D::SHAPE_RECTANGLE, + PhysicsServer2D::SHAPE_CONVEX_POLYGON, + + }; + + PhysicsServer2D::ShapeType type = types[i % 4]; + //type=PhysicsServer2D::SHAPE_SEGMENT; + _add_body(type, Transform2D(i * 0.8, Point2(152 + i * 40, 100 - 40 * i))); + /* + if (i==0) + ps->body_set_mode(b,PhysicsServer2D::BODY_MODE_STATIC); + */ + } + + //RID b= _add_body(PhysicsServer2D::SHAPE_CIRCLE,Transform2D(0,Point2(101,140))); + //ps->body_set_mode(b,PhysicsServer2D::BODY_MODE_STATIC); + + Point2 prev; + + Vector<Point2> parr; + for (int i = 0; i < 30; i++) { + Point2 p(i * 60, Math::randf() * 70 + 340); + if (i > 0) { + parr.push_back(prev); + parr.push_back(p); + } + prev = p; + } + + _add_concave(parr); + //_add_plane(Vector2(0.0,-1).normalized(),-300); + //_add_plane(Vector2(1,0).normalized(),50); + //_add_plane(Vector2(-1,0).normalized(),-600); + } + + virtual bool idle(float p_time) override { + return false; + } + virtual void finish() override { + } + + TestPhysics2DMainLoop() {} +}; + +namespace TestPhysics2D { + +MainLoop *test() { + return memnew(TestPhysics2DMainLoop); +} +} // namespace TestPhysics2D diff --git a/tests/test_physics_2d.h b/tests/test_physics_2d.h new file mode 100644 index 0000000000..517d324f3b --- /dev/null +++ b/tests/test_physics_2d.h @@ -0,0 +1,41 @@ +/*************************************************************************/ +/* test_physics_2d.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_PHYSICS_2D_H +#define TEST_PHYSICS_2D_H + +#include "core/os/main_loop.h" + +namespace TestPhysics2D { + +MainLoop *test(); +} + +#endif // TEST_PHYSICS_2D_H diff --git a/tests/test_physics_3d.cpp b/tests/test_physics_3d.cpp new file mode 100644 index 0000000000..5f84b2eb50 --- /dev/null +++ b/tests/test_physics_3d.cpp @@ -0,0 +1,412 @@ +/*************************************************************************/ +/* test_physics_3d.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "test_physics_3d.h" + +#include "core/math/math_funcs.h" +#include "core/math/quick_hull.h" +#include "core/os/main_loop.h" +#include "core/os/os.h" +#include "core/string/print_string.h" +#include "core/templates/map.h" +#include "servers/display_server.h" +#include "servers/physics_server_3d.h" +#include "servers/rendering_server.h" + +class TestPhysics3DMainLoop : public MainLoop { + GDCLASS(TestPhysics3DMainLoop, MainLoop); + + enum { + LINK_COUNT = 20, + }; + + RID test_cube; + + RID plane; + RID sphere; + RID light; + RID camera; + RID mover; + RID scenario; + RID space; + + RID character; + + float ofs_x, ofs_y; + + Point2 joy_direction; + + List<RID> bodies; + Map<PhysicsServer3D::ShapeType, RID> type_shape_map; + Map<PhysicsServer3D::ShapeType, RID> type_mesh_map; + + void body_changed_transform(Object *p_state, RID p_visual_instance) { + PhysicsDirectBodyState3D *state = (PhysicsDirectBodyState3D *)p_state; + RenderingServer *vs = RenderingServer::get_singleton(); + Transform t = state->get_transform(); + vs->instance_set_transform(p_visual_instance, t); + } + + bool quit; + +protected: + static void _bind_methods() { + ClassDB::bind_method("body_changed_transform", &TestPhysics3DMainLoop::body_changed_transform); + } + + RID create_body(PhysicsServer3D::ShapeType p_shape, PhysicsServer3D::BodyMode p_body, const Transform p_location, bool p_active_default = true, const Transform &p_shape_xform = Transform()) { + RenderingServer *vs = RenderingServer::get_singleton(); + PhysicsServer3D *ps = PhysicsServer3D::get_singleton(); + + RID mesh_instance = vs->instance_create2(type_mesh_map[p_shape], scenario); + RID body = ps->body_create(p_body, !p_active_default); + ps->body_set_space(body, space); + ps->body_set_param(body, PhysicsServer3D::BODY_PARAM_BOUNCE, 0.0); + //todo set space + ps->body_add_shape(body, type_shape_map[p_shape]); + ps->body_set_force_integration_callback(body, this, "body_changed_transform", mesh_instance); + + ps->body_set_state(body, PhysicsServer3D::BODY_STATE_TRANSFORM, p_location); + bodies.push_back(body); + + if (p_body == PhysicsServer3D::BODY_MODE_STATIC) { + vs->instance_set_transform(mesh_instance, p_location); + } + return body; + } + + RID create_static_plane(const Plane &p_plane) { + PhysicsServer3D *ps = PhysicsServer3D::get_singleton(); + + RID world_margin_shape = ps->shape_create(PhysicsServer3D::SHAPE_PLANE); + ps->shape_set_data(world_margin_shape, p_plane); + + RID b = ps->body_create(PhysicsServer3D::BODY_MODE_STATIC); + ps->body_set_space(b, space); + //todo set space + ps->body_add_shape(b, world_margin_shape); + return b; + } + + void configure_body(RID p_body, float p_mass, float p_friction, float p_bounce) { + PhysicsServer3D *ps = PhysicsServer3D::get_singleton(); + ps->body_set_param(p_body, PhysicsServer3D::BODY_PARAM_MASS, p_mass); + ps->body_set_param(p_body, PhysicsServer3D::BODY_PARAM_FRICTION, p_friction); + ps->body_set_param(p_body, PhysicsServer3D::BODY_PARAM_BOUNCE, p_bounce); + } + + void init_shapes() { + RenderingServer *vs = RenderingServer::get_singleton(); + PhysicsServer3D *ps = PhysicsServer3D::get_singleton(); + + /* SPHERE SHAPE */ + RID sphere_mesh = vs->make_sphere_mesh(10, 20, 0.5); + type_mesh_map[PhysicsServer3D::SHAPE_SPHERE] = sphere_mesh; + + RID sphere_shape = ps->shape_create(PhysicsServer3D::SHAPE_SPHERE); + ps->shape_set_data(sphere_shape, 0.5); + type_shape_map[PhysicsServer3D::SHAPE_SPHERE] = sphere_shape; + + /* BOX SHAPE */ + + Vector<Plane> box_planes = Geometry3D::build_box_planes(Vector3(0.5, 0.5, 0.5)); + RID box_mesh = vs->mesh_create(); + Geometry3D::MeshData box_data = Geometry3D::build_convex_mesh(box_planes); + vs->mesh_add_surface_from_mesh_data(box_mesh, box_data); + type_mesh_map[PhysicsServer3D::SHAPE_BOX] = box_mesh; + + RID box_shape = ps->shape_create(PhysicsServer3D::SHAPE_BOX); + ps->shape_set_data(box_shape, Vector3(0.5, 0.5, 0.5)); + type_shape_map[PhysicsServer3D::SHAPE_BOX] = box_shape; + + /* CAPSULE SHAPE */ + + Vector<Plane> capsule_planes = Geometry3D::build_capsule_planes(0.5, 0.7, 12, Vector3::AXIS_Z); + + RID capsule_mesh = vs->mesh_create(); + Geometry3D::MeshData capsule_data = Geometry3D::build_convex_mesh(capsule_planes); + vs->mesh_add_surface_from_mesh_data(capsule_mesh, capsule_data); + + type_mesh_map[PhysicsServer3D::SHAPE_CAPSULE] = capsule_mesh; + + RID capsule_shape = ps->shape_create(PhysicsServer3D::SHAPE_CAPSULE); + Dictionary capsule_params; + capsule_params["radius"] = 0.5; + capsule_params["height"] = 1.4; + ps->shape_set_data(capsule_shape, capsule_params); + type_shape_map[PhysicsServer3D::SHAPE_CAPSULE] = capsule_shape; + + /* CONVEX SHAPE */ + + Vector<Plane> convex_planes = Geometry3D::build_cylinder_planes(0.5, 0.7, 5, Vector3::AXIS_Z); + + RID convex_mesh = vs->mesh_create(); + Geometry3D::MeshData convex_data = Geometry3D::build_convex_mesh(convex_planes); + QuickHull::build(convex_data.vertices, convex_data); + vs->mesh_add_surface_from_mesh_data(convex_mesh, convex_data); + + type_mesh_map[PhysicsServer3D::SHAPE_CONVEX_POLYGON] = convex_mesh; + + RID convex_shape = ps->shape_create(PhysicsServer3D::SHAPE_CONVEX_POLYGON); + ps->shape_set_data(convex_shape, convex_data.vertices); + type_shape_map[PhysicsServer3D::SHAPE_CONVEX_POLYGON] = convex_shape; + } + + void make_trimesh(Vector<Vector3> p_faces, const Transform &p_xform = Transform()) { + RenderingServer *vs = RenderingServer::get_singleton(); + PhysicsServer3D *ps = PhysicsServer3D::get_singleton(); + RID trimesh_shape = ps->shape_create(PhysicsServer3D::SHAPE_CONCAVE_POLYGON); + ps->shape_set_data(trimesh_shape, p_faces); + p_faces = ps->shape_get_data(trimesh_shape); // optimized one + Vector<Vector3> normals; // for drawing + for (int i = 0; i < p_faces.size() / 3; i++) { + Plane p(p_faces[i * 3 + 0], p_faces[i * 3 + 1], p_faces[i * 3 + 2]); + normals.push_back(p.normal); + normals.push_back(p.normal); + normals.push_back(p.normal); + } + + RID trimesh_mesh = vs->mesh_create(); + Array d; + d.resize(RS::ARRAY_MAX); + d[RS::ARRAY_VERTEX] = p_faces; + d[RS::ARRAY_NORMAL] = normals; + vs->mesh_add_surface_from_arrays(trimesh_mesh, RS::PRIMITIVE_TRIANGLES, d); + + RID triins = vs->instance_create2(trimesh_mesh, scenario); + + RID tribody = ps->body_create(PhysicsServer3D::BODY_MODE_STATIC); + ps->body_set_space(tribody, space); + //todo set space + ps->body_add_shape(tribody, trimesh_shape); + Transform tritrans = p_xform; + ps->body_set_state(tribody, PhysicsServer3D::BODY_STATE_TRANSFORM, tritrans); + vs->instance_set_transform(triins, tritrans); + } + + void make_grid(int p_width, int p_height, float p_cellsize, float p_cellheight, const Transform &p_xform = Transform()) { + Vector<Vector<float>> grid; + + grid.resize(p_width); + + for (int i = 0; i < p_width; i++) { + grid.write[i].resize(p_height); + + for (int j = 0; j < p_height; j++) { + grid.write[i].write[j] = 1.0 + Math::random(-p_cellheight, p_cellheight); + } + } + + Vector<Vector3> faces; + + for (int i = 1; i < p_width; i++) { + for (int j = 1; j < p_height; j++) { +#define MAKE_VERTEX(m_x, m_z) \ + faces.push_back(Vector3((m_x - p_width / 2) * p_cellsize, grid[m_x][m_z], (m_z - p_height / 2) * p_cellsize)) + + MAKE_VERTEX(i, j - 1); + MAKE_VERTEX(i, j); + MAKE_VERTEX(i - 1, j); + + MAKE_VERTEX(i - 1, j - 1); + MAKE_VERTEX(i, j - 1); + MAKE_VERTEX(i - 1, j); + } + } + + make_trimesh(faces, p_xform); + } + +public: + virtual void input_event(const Ref<InputEvent> &p_event) { + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid() && mm->get_button_mask() & 4) { + ofs_y -= mm->get_relative().y / 200.0; + ofs_x += mm->get_relative().x / 200.0; + } + + if (mm.is_valid() && mm->get_button_mask() & 1) { + float y = -mm->get_relative().y / 20.0; + float x = mm->get_relative().x / 20.0; + + if (mover.is_valid()) { + PhysicsServer3D *ps = PhysicsServer3D::get_singleton(); + Transform t = ps->body_get_state(mover, PhysicsServer3D::BODY_STATE_TRANSFORM); + t.origin += Vector3(x, y, 0); + + ps->body_set_state(mover, PhysicsServer3D::BODY_STATE_TRANSFORM, t); + } + } + } + + virtual void request_quit() { + quit = true; + } + virtual void init() override { + ofs_x = ofs_y = 0; + init_shapes(); + + PhysicsServer3D *ps = PhysicsServer3D::get_singleton(); + space = ps->space_create(); + ps->space_set_active(space, true); + + RenderingServer *vs = RenderingServer::get_singleton(); + + /* LIGHT */ + RID lightaux = vs->directional_light_create(); + scenario = vs->scenario_create(); + vs->light_set_shadow(lightaux, true); + light = vs->instance_create2(lightaux, scenario); + Transform t; + t.rotate(Vector3(1.0, 0, 0), 0.6); + vs->instance_set_transform(light, t); + + /* CAMERA */ + + camera = vs->camera_create(); + + RID viewport = vs->viewport_create(); + Size2i screen_size = DisplayServer::get_singleton()->window_get_size(); + vs->viewport_set_size(viewport, screen_size.x, screen_size.y); + vs->viewport_attach_to_screen(viewport, Rect2(Vector2(), screen_size)); + vs->viewport_set_active(viewport, true); + vs->viewport_attach_camera(viewport, camera); + vs->viewport_set_scenario(viewport, scenario); + + vs->camera_set_perspective(camera, 60, 0.1, 40.0); + vs->camera_set_transform(camera, Transform(Basis(), Vector3(0, 9, 12))); + + Transform gxf; + gxf.basis.scale(Vector3(1.4, 0.4, 1.4)); + gxf.origin = Vector3(-2, 1, -2); + make_grid(5, 5, 2.5, 1, gxf); + test_fall(); + quit = false; + } + virtual bool iteration(float p_time) override { + if (mover.is_valid()) { + static float joy_speed = 10; + PhysicsServer3D *ps = PhysicsServer3D::get_singleton(); + Transform t = ps->body_get_state(mover, PhysicsServer3D::BODY_STATE_TRANSFORM); + t.origin += Vector3(joy_speed * joy_direction.x * p_time, -joy_speed * joy_direction.y * p_time, 0); + ps->body_set_state(mover, PhysicsServer3D::BODY_STATE_TRANSFORM, t); + }; + + Transform cameratr; + cameratr.rotate(Vector3(0, 1, 0), ofs_x); + cameratr.rotate(Vector3(1, 0, 0), -ofs_y); + cameratr.translate(Vector3(0, 2, 8)); + RenderingServer *vs = RenderingServer::get_singleton(); + vs->camera_set_transform(camera, cameratr); + + return quit; + } + virtual void finish() override { + } + + void test_joint() { + } + + void test_hinge() { + } + + void test_character() { + RenderingServer *vs = RenderingServer::get_singleton(); + PhysicsServer3D *ps = PhysicsServer3D::get_singleton(); + + Vector<Plane> capsule_planes = Geometry3D::build_capsule_planes(0.5, 1, 12, 5, Vector3::AXIS_Y); + + RID capsule_mesh = vs->mesh_create(); + Geometry3D::MeshData capsule_data = Geometry3D::build_convex_mesh(capsule_planes); + vs->mesh_add_surface_from_mesh_data(capsule_mesh, capsule_data); + type_mesh_map[PhysicsServer3D::SHAPE_CAPSULE] = capsule_mesh; + + RID capsule_shape = ps->shape_create(PhysicsServer3D::SHAPE_CAPSULE); + Dictionary capsule_params; + capsule_params["radius"] = 0.5; + capsule_params["height"] = 1; + Transform shape_xform; + shape_xform.rotate(Vector3(1, 0, 0), Math_PI / 2.0); + //shape_xform.origin=Vector3(1,1,1); + ps->shape_set_data(capsule_shape, capsule_params); + + RID mesh_instance = vs->instance_create2(capsule_mesh, scenario); + character = ps->body_create(PhysicsServer3D::BODY_MODE_CHARACTER); + ps->body_set_space(character, space); + //todo add space + ps->body_add_shape(character, capsule_shape); + + ps->body_set_force_integration_callback(character, this, "body_changed_transform", mesh_instance); + + ps->body_set_state(character, PhysicsServer3D::BODY_STATE_TRANSFORM, Transform(Basis(), Vector3(-2, 5, -2))); + bodies.push_back(character); + } + + void test_fall() { + for (int i = 0; i < 35; i++) { + static const PhysicsServer3D::ShapeType shape_idx[] = { + PhysicsServer3D::SHAPE_CAPSULE, + PhysicsServer3D::SHAPE_BOX, + PhysicsServer3D::SHAPE_SPHERE, + PhysicsServer3D::SHAPE_CONVEX_POLYGON + }; + + PhysicsServer3D::ShapeType type = shape_idx[i % 4]; + + Transform t; + + t.origin = Vector3(0.0 * i, 3.5 + 1.1 * i, 0.7 + 0.0 * i); + t.basis.rotate(Vector3(0.2, -1, 0), Math_PI / 2 * 0.6); + + create_body(type, PhysicsServer3D::BODY_MODE_RIGID, t); + } + + create_static_plane(Plane(Vector3(0, 1, 0), -1)); + } + + void test_activate() { + create_body(PhysicsServer3D::SHAPE_BOX, PhysicsServer3D::BODY_MODE_RIGID, Transform(Basis(), Vector3(0, 2, 0)), true); + create_static_plane(Plane(Vector3(0, 1, 0), -1)); + } + + virtual bool idle(float p_time) override { + return false; + } + + TestPhysics3DMainLoop() { + } +}; + +namespace TestPhysics3D { + +MainLoop *test() { + return memnew(TestPhysics3DMainLoop); +} +} // namespace TestPhysics3D diff --git a/tests/test_physics_3d.h b/tests/test_physics_3d.h new file mode 100644 index 0000000000..d03f2c6573 --- /dev/null +++ b/tests/test_physics_3d.h @@ -0,0 +1,41 @@ +/*************************************************************************/ +/* test_physics_3d.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_PHYSICS_H +#define TEST_PHYSICS_H + +#include "core/os/main_loop.h" + +namespace TestPhysics3D { + +MainLoop *test(); +} + +#endif diff --git a/tests/test_rect2.h b/tests/test_rect2.h new file mode 100644 index 0000000000..aefceb1128 --- /dev/null +++ b/tests/test_rect2.h @@ -0,0 +1,467 @@ +/*************************************************************************/ +/* test_rect2.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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)) == "0, 100, 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."); +} + +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] Clipping") { + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).clip(Rect2(0, 300, 100, 100)).is_equal_approx(Rect2(0, 300, 100, 100)), + "clip() 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).clip(Rect2(1200, 700, 100, 100)).is_equal_approx(Rect2(1200, 700, 80, 100)), + "clip() with partially enclosed Rect2 should return the expected result."); + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).clip(Rect2(-4000, -4000, 100, 100)).is_equal_approx(Rect2()), + "clip() 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)), + "clip() with fully contained Rect2 should return the expected result."); + CHECK_MESSAGE( + !Rect2(0, 100, 1280, 720).encloses(Rect2(1200, 700, 100, 100)), + "clip() with partially contained Rect2 should return the expected result."); + CHECK_MESSAGE( + !Rect2(0, 100, 1280, 720).encloses(Rect2(-4000, -4000, 100, 100)), + "clip() 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_margin(MARGIN_TOP, 500).is_equal_approx(Rect2(0, -400, 1280, 1220)), + "grow_margin() with positive value should return the expected Rect2."); + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).grow_margin(MARGIN_TOP, -500).is_equal_approx(Rect2(0, 600, 1280, 220)), + "grow_margin() 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)) == "0, 100, 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."); +} + +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( + Math::is_equal_approx(Rect2i(0, 100, 1280, 720).get_area(), 921'600), + "get_area() should return the expected value."); + CHECK_MESSAGE( + Math::is_equal_approx(Rect2i(0, 100, -1280, -720).get_area(), 921'600), + "get_area() should return the expected value."); + CHECK_MESSAGE( + Math::is_equal_approx(Rect2i(0, 100, 1280, -720).get_area(), -921'600), + "get_area() should return the expected value."); + CHECK_MESSAGE( + Math::is_equal_approx(Rect2i(0, 100, -1280, 720).get_area(), -921'600), + "get_area() should return the expected value."); + CHECK_MESSAGE( + Math::is_zero_approx(Rect2i(0, 100, 0, 720).get_area()), + "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] Clipping") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).clip(Rect2i(0, 300, 100, 100)) == Rect2i(0, 300, 100, 100), + "clip() 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).clip(Rect2i(1200, 700, 100, 100)) == Rect2i(1200, 700, 80, 100), + "clip() with partially enclosed Rect2i should return the expected result."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).clip(Rect2i(-4000, -4000, 100, 100)) == Rect2i(), + "clip() 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)), + "clip() with fully contained Rect2i should return the expected result."); + CHECK_MESSAGE( + !Rect2i(0, 100, 1280, 720).encloses(Rect2i(1200, 700, 100, 100)), + "clip() with partially contained Rect2i should return the expected result."); + CHECK_MESSAGE( + !Rect2i(0, 100, 1280, 720).encloses(Rect2i(-4000, -4000, 100, 100)), + "clip() 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_margin(MARGIN_TOP, 500) == Rect2i(0, -400, 1280, 1220), + "grow_margin() with positive value should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow_margin(MARGIN_TOP, -500) == Rect2i(0, 600, 1280, 220), + "grow_margin() 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/test_render.cpp b/tests/test_render.cpp new file mode 100644 index 0000000000..d14251bc6a --- /dev/null +++ b/tests/test_render.cpp @@ -0,0 +1,237 @@ +/*************************************************************************/ +/* test_render.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "test_render.h" + +#include "core/math/math_funcs.h" +#include "core/math/quick_hull.h" +#include "core/os/keyboard.h" +#include "core/os/main_loop.h" +#include "core/os/os.h" +#include "core/string/print_string.h" +#include "servers/display_server.h" +#include "servers/rendering_server.h" + +#define OBJECT_COUNT 50 + +namespace TestRender { + +class TestMainLoop : public MainLoop { + RID test_cube; + RID instance; + RID camera; + RID viewport; + RID light; + RID scenario; + + struct InstanceInfo { + RID instance; + Transform base; + Vector3 rot_axis; + }; + + List<InstanceInfo> instances; + + float ofs; + bool quit; + +protected: +public: + virtual void input_event(const Ref<InputEvent> &p_event) { + if (p_event->is_pressed()) { + quit = true; + } + } + + virtual void init() { + print_line("INITIALIZING TEST RENDER"); + RenderingServer *vs = RenderingServer::get_singleton(); + test_cube = vs->get_test_cube(); + scenario = vs->scenario_create(); + + Vector<Vector3> vts; + + /* + Vector<Plane> sp = Geometry3D::build_sphere_planes(2,5,5); + Geometry3D::MeshData md2 = Geometry3D::build_convex_mesh(sp); + vts=md2.vertices; +*/ + /* + + static const int s = 20; + for(int i=0;i<s;i++) { + Basis rot(Vector3(0,1,0),i*Math_PI/s); + + for(int j=0;j<s;j++) { + Vector3 v; + v.x=Math::sin(j*Math_PI*2/s); + v.y=Math::cos(j*Math_PI*2/s); + + vts.push_back( rot.xform(v*2 ) ); + } + }*/ + /*for(int i=0;i<100;i++) { + vts.push_back( Vector3(Math::randf()*2-1.0,Math::randf()*2-1.0,Math::randf()*2-1.0).normalized()*2); + }*/ + /* + vts.push_back(Vector3(0,0,1)); + vts.push_back(Vector3(0,0,-1)); + vts.push_back(Vector3(0,1,0)); + vts.push_back(Vector3(0,-1,0)); + vts.push_back(Vector3(1,0,0)); + vts.push_back(Vector3(-1,0,0));*/ + + vts.push_back(Vector3(1, 1, 1)); + vts.push_back(Vector3(1, -1, 1)); + vts.push_back(Vector3(-1, 1, 1)); + vts.push_back(Vector3(-1, -1, 1)); + vts.push_back(Vector3(1, 1, -1)); + vts.push_back(Vector3(1, -1, -1)); + vts.push_back(Vector3(-1, 1, -1)); + vts.push_back(Vector3(-1, -1, -1)); + + Geometry3D::MeshData md; + Error err = QuickHull::build(vts, md); + print_line("ERR: " + itos(err)); + test_cube = vs->mesh_create(); + vs->mesh_add_surface_from_mesh_data(test_cube, md); + //vs->scenario_set_debug(scenario,RS::SCENARIO_DEBUG_WIREFRAME); + + /* + RID sm = vs->shader_create(); + //vs->shader_set_fragment_code(sm,"OUT_ALPHA=mod(TIME,1);"); + //vs->shader_set_vertex_code(sm,"OUT_VERTEX=IN_VERTEX*mod(TIME,1);"); + vs->shader_set_fragment_code(sm,"OUT_DIFFUSE=vec3(1,0,1);OUT_GLOW=abs(sin(TIME));"); + RID tcmat = vs->mesh_surface_get_material(test_cube,0); + vs->material_set_shader(tcmat,sm); + */ + + List<String> cmdline = OS::get_singleton()->get_cmdline_args(); + int object_count = OBJECT_COUNT; + if (cmdline.size() > 0 && cmdline[cmdline.size() - 1].to_int()) { + object_count = cmdline[cmdline.size() - 1].to_int(); + }; + + for (int i = 0; i < object_count; i++) { + InstanceInfo ii; + + ii.instance = vs->instance_create2(test_cube, scenario); + + ii.base.translate(Math::random(-20, 20), Math::random(-20, 20), Math::random(-20, 18)); + ii.base.rotate(Vector3(0, 1, 0), Math::randf() * Math_PI); + ii.base.rotate(Vector3(1, 0, 0), Math::randf() * Math_PI); + vs->instance_set_transform(ii.instance, ii.base); + + ii.rot_axis = Vector3(Math::random(-1, 1), Math::random(-1, 1), Math::random(-1, 1)).normalized(); + + instances.push_back(ii); + } + + camera = vs->camera_create(); + + // vs->camera_set_perspective( camera, 60.0,0.1, 100.0 ); + + viewport = vs->viewport_create(); + Size2i screen_size = DisplayServer::get_singleton()->window_get_size(); + vs->viewport_set_size(viewport, screen_size.x, screen_size.y); + vs->viewport_attach_to_screen(viewport, Rect2(Vector2(), screen_size)); + vs->viewport_set_active(viewport, true); + vs->viewport_attach_camera(viewport, camera); + vs->viewport_set_scenario(viewport, scenario); + vs->camera_set_transform(camera, Transform(Basis(), Vector3(0, 3, 30))); + vs->camera_set_perspective(camera, 60, 0.1, 1000); + + /* + RID lightaux = vs->light_create( RenderingServer::LIGHT_OMNI ); + vs->light_set_var( lightaux, RenderingServer::LIGHT_VAR_RADIUS, 80 ); + vs->light_set_var( lightaux, RenderingServer::LIGHT_VAR_ATTENUATION, 1 ); + vs->light_set_var( lightaux, RenderingServer::LIGHT_VAR_ENERGY, 1.5 ); + light = vs->instance_create( lightaux ); + */ + RID lightaux; + + lightaux = vs->directional_light_create(); + //vs->light_set_color( lightaux, RenderingServer::LIGHT_COLOR_AMBIENT, Color(0.0,0.0,0.0) ); + vs->light_set_color(lightaux, Color(1.0, 1.0, 1.0)); + //vs->light_set_shadow( lightaux, true ); + light = vs->instance_create2(lightaux, scenario); + Transform lla; + //lla.set_look_at(Vector3(),Vector3(1,-1,1),Vector3(0,1,0)); + lla.set_look_at(Vector3(), Vector3(-0.000000, -0.836026, -0.548690), Vector3(0, 1, 0)); + + vs->instance_set_transform(light, lla); + + lightaux = vs->omni_light_create(); + //vs->light_set_color( lightaux, RenderingServer::LIGHT_COLOR_AMBIENT, Color(0.0,0.0,1.0) ); + vs->light_set_color(lightaux, Color(1.0, 1.0, 0.0)); + vs->light_set_param(lightaux, RenderingServer::LIGHT_PARAM_RANGE, 4); + vs->light_set_param(lightaux, RenderingServer::LIGHT_PARAM_ENERGY, 8); + //vs->light_set_shadow( lightaux, true ); + //light = vs->instance_create( lightaux ); + + ofs = 0; + quit = false; + } + virtual bool iteration(float p_time) { + RenderingServer *vs = RenderingServer::get_singleton(); + //Transform t; + //t.rotate(Vector3(0, 1, 0), ofs); + //t.translate(Vector3(0,0,20 )); + //vs->camera_set_transform(camera, t); + + ofs += p_time * 0.05; + + //return quit; + + for (List<InstanceInfo>::Element *E = instances.front(); E; E = E->next()) { + Transform pre(Basis(E->get().rot_axis, ofs), Vector3()); + vs->instance_set_transform(E->get().instance, pre * E->get().base); + /* + if( !E->next() ) { + vs->free( E->get().instance ); + instances.erase(E ); + }*/ + } + + return quit; + } + + virtual bool idle(float p_time) { + return quit; + } + + virtual void finish() { + } +}; + +MainLoop *test() { + return memnew(TestMainLoop); +} +} // namespace TestRender diff --git a/tests/test_render.h b/tests/test_render.h new file mode 100644 index 0000000000..4a6340c443 --- /dev/null +++ b/tests/test_render.h @@ -0,0 +1,41 @@ +/*************************************************************************/ +/* test_render.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_RENDER_H +#define TEST_RENDER_H + +#include "core/os/main_loop.h" + +namespace TestRender { + +MainLoop *test(); +} + +#endif diff --git a/tests/test_shader_lang.cpp b/tests/test_shader_lang.cpp new file mode 100644 index 0000000000..e79c83b001 --- /dev/null +++ b/tests/test_shader_lang.cpp @@ -0,0 +1,360 @@ +/*************************************************************************/ +/* test_shader_lang.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "test_shader_lang.h" + +#include "core/os/file_access.h" +#include "core/os/main_loop.h" +#include "core/os/os.h" + +#include "core/string/print_string.h" +#include "scene/gui/control.h" +#include "scene/gui/text_edit.h" +#include "servers/rendering/shader_language.h" + +typedef ShaderLanguage SL; + +namespace TestShaderLang { + +static String _mktab(int p_level) { + String tb; + for (int i = 0; i < p_level; i++) { + tb += "\t"; + } + + return tb; +} + +static String _typestr(SL::DataType p_type) { + return ShaderLanguage::get_datatype_name(p_type); +} + +static String _prestr(SL::DataPrecision p_pres) { + switch (p_pres) { + case SL::PRECISION_LOWP: + return "lowp "; + case SL::PRECISION_MEDIUMP: + return "mediump "; + case SL::PRECISION_HIGHP: + return "highp "; + case SL::PRECISION_DEFAULT: + return ""; + } + return ""; +} + +static String _opstr(SL::Operator p_op) { + return ShaderLanguage::get_operator_text(p_op); +} + +static String get_constant_text(SL::DataType p_type, const Vector<SL::ConstantNode::Value> &p_values) { + switch (p_type) { + case SL::TYPE_BOOL: + return p_values[0].boolean ? "true" : "false"; + case SL::TYPE_BVEC2: + return String() + "bvec2(" + (p_values[0].boolean ? "true" : "false") + (p_values[1].boolean ? "true" : "false") + ")"; + case SL::TYPE_BVEC3: + return String() + "bvec3(" + (p_values[0].boolean ? "true" : "false") + "," + (p_values[1].boolean ? "true" : "false") + "," + (p_values[2].boolean ? "true" : "false") + ")"; + case SL::TYPE_BVEC4: + return String() + "bvec4(" + (p_values[0].boolean ? "true" : "false") + "," + (p_values[1].boolean ? "true" : "false") + "," + (p_values[2].boolean ? "true" : "false") + "," + (p_values[3].boolean ? "true" : "false") + ")"; + case SL::TYPE_INT: + return rtos(p_values[0].sint); + case SL::TYPE_IVEC2: + return String() + "ivec2(" + rtos(p_values[0].sint) + "," + rtos(p_values[1].sint) + ")"; + case SL::TYPE_IVEC3: + return String() + "ivec3(" + rtos(p_values[0].sint) + "," + rtos(p_values[1].sint) + "," + rtos(p_values[2].sint) + ")"; + case SL::TYPE_IVEC4: + return String() + "ivec4(" + rtos(p_values[0].sint) + "," + rtos(p_values[1].sint) + "," + rtos(p_values[2].sint) + "," + rtos(p_values[3].sint) + ")"; + case SL::TYPE_UINT: + return rtos(p_values[0].real); + case SL::TYPE_UVEC2: + return String() + "uvec2(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + ")"; + case SL::TYPE_UVEC3: + return String() + "uvec3(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + "," + rtos(p_values[2].real) + ")"; + case SL::TYPE_UVEC4: + return String() + "uvec4(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + "," + rtos(p_values[2].real) + "," + rtos(p_values[3].real) + ")"; + case SL::TYPE_FLOAT: + return rtos(p_values[0].real); + case SL::TYPE_VEC2: + return String() + "vec2(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + ")"; + case SL::TYPE_VEC3: + return String() + "vec3(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + "," + rtos(p_values[2].real) + ")"; + case SL::TYPE_VEC4: + return String() + "vec4(" + rtos(p_values[0].real) + "," + rtos(p_values[1].real) + "," + rtos(p_values[2].real) + "," + rtos(p_values[3].real) + ")"; + default: + ERR_FAIL_V(String()); + } +} + +static String dump_node_code(SL::Node *p_node, int p_level) { + String code; + + switch (p_node->type) { + case SL::Node::TYPE_SHADER: { + SL::ShaderNode *pnode = (SL::ShaderNode *)p_node; + + for (Map<StringName, SL::ShaderNode::Uniform>::Element *E = pnode->uniforms.front(); E; E = E->next()) { + String ucode = "uniform "; + ucode += _prestr(E->get().precision); + ucode += _typestr(E->get().type); + ucode += " " + String(E->key()); + + if (E->get().default_value.size()) { + ucode += " = " + get_constant_text(E->get().type, E->get().default_value); + } + + static const char *hint_name[SL::ShaderNode::Uniform::HINT_MAX] = { + "", + "color", + "range", + "albedo", + "normal", + "black", + "white" + }; + + if (E->get().hint) { + ucode += " : " + String(hint_name[E->get().hint]); + } + + code += ucode + "\n"; + } + + for (Map<StringName, SL::ShaderNode::Varying>::Element *E = pnode->varyings.front(); E; E = E->next()) { + String vcode = "varying "; + vcode += _prestr(E->get().precision); + vcode += _typestr(E->get().type); + vcode += " " + String(E->key()); + + code += vcode + "\n"; + } + for (int i = 0; i < pnode->functions.size(); i++) { + SL::FunctionNode *fnode = pnode->functions[i].function; + + String header; + header = _typestr(fnode->return_type) + " " + fnode->name + "("; + for (int j = 0; j < fnode->arguments.size(); j++) { + if (j > 0) { + header += ", "; + } + header += _prestr(fnode->arguments[j].precision) + _typestr(fnode->arguments[j].type) + " " + fnode->arguments[j].name; + } + + header += ")\n"; + code += header; + code += dump_node_code(fnode->body, p_level + 1); + } + + //code+=dump_node_code(pnode->body,p_level); + } break; + case SL::Node::TYPE_STRUCT: { + } break; + case SL::Node::TYPE_FUNCTION: { + } break; + case SL::Node::TYPE_BLOCK: { + SL::BlockNode *bnode = (SL::BlockNode *)p_node; + + //variables + code += _mktab(p_level - 1) + "{\n"; + for (Map<StringName, SL::BlockNode::Variable>::Element *E = bnode->variables.front(); E; E = E->next()) { + code += _mktab(p_level) + _prestr(E->get().precision) + _typestr(E->get().type) + " " + E->key() + ";\n"; + } + + for (int i = 0; i < bnode->statements.size(); i++) { + String scode = dump_node_code(bnode->statements[i], p_level); + + if (bnode->statements[i]->type == SL::Node::TYPE_CONTROL_FLOW) { + code += scode; //use directly + } else { + code += _mktab(p_level) + scode + ";\n"; + } + } + code += _mktab(p_level - 1) + "}\n"; + + } break; + case SL::Node::TYPE_VARIABLE: { + SL::VariableNode *vnode = (SL::VariableNode *)p_node; + code = vnode->name; + + } break; + case SL::Node::TYPE_VARIABLE_DECLARATION: { + // FIXME: Implement + } break; + case SL::Node::TYPE_ARRAY: { + SL::ArrayNode *vnode = (SL::ArrayNode *)p_node; + code = vnode->name; + } break; + case SL::Node::TYPE_ARRAY_DECLARATION: { + // FIXME: Implement + } break; + case SL::Node::TYPE_ARRAY_CONSTRUCT: { + // FIXME: Implement + } break; + case SL::Node::TYPE_CONSTANT: { + SL::ConstantNode *cnode = (SL::ConstantNode *)p_node; + return get_constant_text(cnode->datatype, cnode->values); + + } break; + case SL::Node::TYPE_OPERATOR: { + SL::OperatorNode *onode = (SL::OperatorNode *)p_node; + + switch (onode->op) { + case SL::OP_ASSIGN: + case SL::OP_ASSIGN_ADD: + case SL::OP_ASSIGN_SUB: + case SL::OP_ASSIGN_MUL: + case SL::OP_ASSIGN_DIV: + case SL::OP_ASSIGN_SHIFT_LEFT: + case SL::OP_ASSIGN_SHIFT_RIGHT: + case SL::OP_ASSIGN_MOD: + case SL::OP_ASSIGN_BIT_AND: + case SL::OP_ASSIGN_BIT_OR: + case SL::OP_ASSIGN_BIT_XOR: + code = dump_node_code(onode->arguments[0], p_level) + _opstr(onode->op) + dump_node_code(onode->arguments[1], p_level); + break; + case SL::OP_BIT_INVERT: + case SL::OP_NEGATE: + case SL::OP_NOT: + case SL::OP_DECREMENT: + case SL::OP_INCREMENT: + code = _opstr(onode->op) + dump_node_code(onode->arguments[0], p_level); + break; + case SL::OP_POST_DECREMENT: + case SL::OP_POST_INCREMENT: + code = dump_node_code(onode->arguments[0], p_level) + _opstr(onode->op); + break; + case SL::OP_CALL: + case SL::OP_CONSTRUCT: + code = dump_node_code(onode->arguments[0], p_level) + "("; + for (int i = 1; i < onode->arguments.size(); i++) { + if (i > 1) { + code += ", "; + } + code += dump_node_code(onode->arguments[i], p_level); + } + code += ")"; + break; + default: { + code = "(" + dump_node_code(onode->arguments[0], p_level) + _opstr(onode->op) + dump_node_code(onode->arguments[1], p_level) + ")"; + break; + } + } + + } break; + case SL::Node::TYPE_CONTROL_FLOW: { + SL::ControlFlowNode *cfnode = (SL::ControlFlowNode *)p_node; + if (cfnode->flow_op == SL::FLOW_OP_IF) { + code += _mktab(p_level) + "if (" + dump_node_code(cfnode->expressions[0], p_level) + ")\n"; + code += dump_node_code(cfnode->blocks[0], p_level + 1); + if (cfnode->blocks.size() == 2) { + code += _mktab(p_level) + "else\n"; + code += dump_node_code(cfnode->blocks[1], p_level + 1); + } + + } else if (cfnode->flow_op == SL::FLOW_OP_RETURN) { + if (cfnode->blocks.size()) { + code = "return " + dump_node_code(cfnode->blocks[0], p_level); + } else { + code = "return"; + } + } + + } break; + case SL::Node::TYPE_MEMBER: { + SL::MemberNode *mnode = (SL::MemberNode *)p_node; + code = dump_node_code(mnode->owner, p_level) + "." + mnode->name; + + } break; + } + + return code; +} + +static Error recreate_code(void *p_str, SL::ShaderNode *p_program) { + String *str = (String *)p_str; + + *str = dump_node_code(p_program, 0); + + return OK; +} + +MainLoop *test() { + List<String> cmdlargs = OS::get_singleton()->get_cmdline_args(); + + if (cmdlargs.empty()) { + //try editor! + print_line("usage: godot -test shader_lang <shader>"); + return nullptr; + } + + String test = cmdlargs.back()->get(); + + FileAccess *fa = FileAccess::open(test, FileAccess::READ); + + if (!fa) { + ERR_FAIL_V(nullptr); + } + + String code; + + while (true) { + char32_t c = fa->get_8(); + if (fa->eof_reached()) { + break; + } + code += c; + } + + SL sl; + print_line("tokens:\n\n" + sl.token_debug(code)); + + Map<StringName, SL::FunctionInfo> dt; + dt["fragment"].built_ins["ALBEDO"] = SL::TYPE_VEC3; + dt["fragment"].can_discard = true; + + Vector<StringName> rm; + rm.push_back("popo"); + Set<String> types; + types.insert("spatial"); + + Error err = sl.compile(code, dt, rm, types, nullptr); + + if (err) { + print_line("Error at line: " + rtos(sl.get_error_line()) + ": " + sl.get_error_text()); + return nullptr; + } else { + String code2; + recreate_code(&code2, sl.get_shader()); + print_line("code:\n\n" + code2); + } + + return nullptr; +} +} // namespace TestShaderLang diff --git a/tests/test_shader_lang.h b/tests/test_shader_lang.h new file mode 100644 index 0000000000..2811c5f46e --- /dev/null +++ b/tests/test_shader_lang.h @@ -0,0 +1,41 @@ +/*************************************************************************/ +/* test_shader_lang.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_SHADER_LANG_H +#define TEST_SHADER_LANG_H + +#include "core/os/main_loop.h" + +namespace TestShaderLang { + +MainLoop *test(); +} + +#endif // TEST_SHADER_LANG_H diff --git a/tests/test_string.h b/tests/test_string.h new file mode 100644 index 0000000000..3c5d4a2f01 --- /dev/null +++ b/tests/test_string.h @@ -0,0 +1,1288 @@ +/*************************************************************************/ +/* test_string.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 <inttypes.h> +#include <stdio.h> +#include <wchar.h> + +#include "core/io/ip_address.h" +#include "core/os/main_loop.h" +#include "core/os/os.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").empty()); + // do this more than once, to check for string corruption + CHECK(String("").empty()); + CHECK(String("").empty()); + CHECK(String("").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.ord_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 "); +} + +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(3.141593) == "3.141593"); + CHECK(String::num(3.141593, 3) == "3.142"); + CHECK(String::num_real(3.141593) == "3.141593"); + 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"); +} + +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 }; + static const bool wo_prefix[4] = { false, true, true, true }; + static const bool w_prefix[4] = { true, false, true, false }; + + for (int i = 0; i < 4; i++) { + CHECK((String(nums[i]).hex_to_int(true) == num[i]) == w_prefix[i]); + CHECK((String(nums[i]).hex_to_int(false) == num[i]) == wo_prefix[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") { + IP_Address ip0("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); + IP_Address ip(0x0123, 0x4567, 0x89ab, 0xcdef, true); + IP_Address ip2("fe80::52e5:49ff:fe93:1baf"); + IP_Address 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(NULL, -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[4] = { "C:\\Godot\\project\\test.tscn", "/Godot/project/test.xscn", "../Godot/project/test.scn", "Godot\\test.doc" }; + static const char *base_dir[4] = { "C:\\Godot\\project", "/Godot/project", "../Godot/project", "Godot" }; + static const char *base_name[4] = { "C:\\Godot\\project\\test", "/Godot/project/test", "../Godot/project/test", "Godot\\test" }; + static const char *ext[4] = { "tscn", "xscn", "scn", "doc" }; + static const char *file[4] = { "test.tscn", "test.xscn", "test.scn", "test.doc" }; + static const bool abs[4] = { true, true, false, false }; + + for (int i = 0; i < 4; 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_abs_path() == abs[i]); + CHECK(String(path[i]).is_rel_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] http_escape/unescape") { + String s = "Godot Engine:'docs'"; + String t = "Godot%20Engine%3A%27docs%27"; + + CHECK(s.http_escape() == t); + CHECK(t.http_unescape() == s); +} + +TEST_CASE("[String] percent_encode/decode") { // Note: is it redundant? Seems to be same as http_escape/unescape but in lower case. + String s = "Godot Engine:'docs'"; + String t = "Godot%20Engine%3a%27docs%27"; + + CHECK(s.percent_encode() == t); + CHECK(t.percent_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] 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) == "tbbb"); + CHECK(s.left(6) == "aaaTes"); +} + +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_integer() == 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"); +} +} // namespace TestString + +#endif // TEST_STRING_H diff --git a/tests/test_validate_testing.h b/tests/test_validate_testing.h new file mode 100644 index 0000000000..b4ea6eb576 --- /dev/null +++ b/tests/test_validate_testing.h @@ -0,0 +1,189 @@ +/*************************************************************************/ +/* test_validate_testing.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_VALIDATE_TESTING_H +#define TEST_VALIDATE_TESTING_H + +#include "core/os/os.h" + +#include "tests/test_macros.h" + +TEST_SUITE("Validate tests") { + TEST_CASE("Always pass") { + CHECK(true); + } + TEST_CASE_PENDING("Pending tests are skipped") { + if (!doctest::getContextOptions()->no_skip) { // Normal run. + FAIL("This should be skipped if `--no-skip` is NOT set (missing `doctest::skip()` decorator?)"); + } else { + CHECK_MESSAGE(true, "Pending test is run with `--no-skip`"); + } + } + TEST_CASE("Muting Godot error messages") { + ERR_PRINT_OFF; + CHECK_MESSAGE(!_print_error_enabled, "Error printing should be disabled."); + ERR_PRINT("Still waiting for Godot!"); // This should never get printed! + ERR_PRINT_ON; + CHECK_MESSAGE(_print_error_enabled, "Error printing should be re-enabled."); + } + TEST_CASE("Stringify Variant types") { + Variant var; + INFO(var); + + String string("Godot is finally here!"); + INFO(string); + + Vector2 vec2(0.5, 1.0); + INFO(vec2); + + Vector2i vec2i(1, 2); + INFO(vec2i); + + Rect2 rect2(0.5, 0.5, 100.5, 100.5); + INFO(rect2); + + Rect2i rect2i(0, 0, 100, 100); + INFO(rect2i); + + Vector3 vec3(0.5, 1.0, 2.0); + INFO(vec3); + + Vector3i vec3i(1, 2, 3); + INFO(vec3i); + + Transform2D trans2d(0.5, Vector2(100, 100)); + INFO(trans2d); + + Plane plane(Vector3(1, 1, 1), 1.0); + INFO(plane); + + Quat quat(Vector3(0.5, 1.0, 2.0)); + INFO(quat); + + AABB aabb(Vector3(), Vector3(100, 100, 100)); + INFO(aabb); + + Basis basis(quat); + INFO(basis); + + Transform trans(basis); + INFO(trans); + + Color color(1, 0.5, 0.2, 0.3); + INFO(color); + + StringName string_name("has_method"); + INFO(string_name); + + NodePath node_path("godot/sprite"); + INFO(node_path); + + INFO(RID()); + + Object *obj = memnew(Object); + INFO(obj); + + Callable callable(obj, "has_method"); + INFO(callable); + + Signal signal(obj, "script_changed"); + INFO(signal); + + memdelete(obj); + + Dictionary dict; + dict["string"] = string; + dict["color"] = color; + INFO(dict); + + Array arr; + arr.push_back(string); + arr.push_back(color); + INFO(arr); + + PackedByteArray byte_arr; + byte_arr.push_back(0); + byte_arr.push_back(1); + byte_arr.push_back(2); + INFO(byte_arr); + + PackedInt32Array int32_arr; + int32_arr.push_back(0); + int32_arr.push_back(1); + int32_arr.push_back(2); + INFO(int32_arr); + + PackedInt64Array int64_arr; + int64_arr.push_back(0); + int64_arr.push_back(1); + int64_arr.push_back(2); + INFO(int64_arr); + + PackedFloat32Array float32_arr; + float32_arr.push_back(0.5); + float32_arr.push_back(1.5); + float32_arr.push_back(2.5); + INFO(float32_arr); + + PackedFloat64Array float64_arr; + float64_arr.push_back(0.5); + float64_arr.push_back(1.5); + float64_arr.push_back(2.5); + INFO(float64_arr); + + PackedStringArray str_arr = string.split(" "); + INFO(str_arr); + + PackedVector2Array vec2_arr; + vec2_arr.push_back(Vector2(0, 0)); + vec2_arr.push_back(Vector2(1, 1)); + vec2_arr.push_back(Vector2(2, 2)); + INFO(vec2_arr); + + PackedVector3Array vec3_arr; + vec3_arr.push_back(Vector3(0, 0, 0)); + vec3_arr.push_back(Vector3(1, 1, 1)); + vec3_arr.push_back(Vector3(2, 2, 2)); + INFO(vec3_arr); + + PackedColorArray color_arr; + color_arr.push_back(Color(0, 0, 0)); + color_arr.push_back(Color(1, 1, 1)); + color_arr.push_back(Color(2, 2, 2)); + INFO(color_arr); + + INFO("doctest insertion operator << " + << var << " " << vec2 << " " << rect2 << " " << color); + + CHECK(true); // So all above prints. + } +} + +#endif // TEST_VALIDATE_TESTING_H diff --git a/tests/test_variant.h b/tests/test_variant.h new file mode 100644 index 0000000000..b575f6744d --- /dev/null +++ b/tests/test_variant.h @@ -0,0 +1,110 @@ +/*************************************************************************/ +/* test_variant.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 { + +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 float") { + // Assuming real_t is double. + real_t a64 = 340282346638528859811704183484516925440.0; // std::numeric_limits<real_t>::max() + String a64_str; + VariantWriter::write_to_string(a64, a64_str); + + CHECK_MESSAGE(a64_str == "3.40282e+38", "Writes in scientific notation."); + CHECK_MESSAGE(a64_str != "inf", "Should not overflow."); + CHECK_MESSAGE(a64_str != "nan", "The result should be defined."); + + VariantParser::StreamString ss; + String errs; + int line; + Variant b64_parsed; + real_t b64_float_parsed; + + ss.s = a64_str; + VariantParser::parse(&ss, b64_parsed, errs, line); + b64_float_parsed = b64_parsed; + + CHECK_MESSAGE(b64_float_parsed == 340282001837565597733306976381245063168.0, "Should parse back."); + // Loses precision, but that's alright. + + ss.s = "1.0e+100"; // Float version of Googol! + VariantParser::parse(&ss, b64_parsed, errs, line); + b64_float_parsed = b64_parsed; + + CHECK_MESSAGE(b64_float_parsed == 340282001837565597733306976381245063168.0, "Should not overflow."); +} +} // namespace TestVariant + +#endif // TEST_VARIANT_H |