initial commit, 4.5 stable
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled

This commit is contained in:
2025-09-16 20:46:46 -04:00
commit 9d30169a8d
13378 changed files with 7050105 additions and 0 deletions

23
tests/SCsub Normal file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python
from misc.utility.scons_hints import *
Import("env")
env.tests_sources = []
env_tests = env.Clone()
# 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", "")])
if env["disable_exceptions"]:
env_tests.Append(CPPDEFINES=["DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS"])
env_tests.add_source_files(env.tests_sources, "*.cpp")
lib = env_tests.add_library("tests", env.tests_sources)
env.Prepend(LIBS=[lib])

View File

@@ -0,0 +1,158 @@
/**************************************************************************/
/* test_project_settings.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/config/project_settings.h"
#include "core/io/dir_access.h"
#include "core/variant/variant.h"
#include "tests/test_macros.h"
class TestProjectSettingsInternalsAccessor {
public:
static String &resource_path() {
return ProjectSettings::get_singleton()->resource_path;
}
};
namespace TestProjectSettings {
TEST_CASE("[ProjectSettings] Get existing setting") {
CHECK(ProjectSettings::get_singleton()->has_setting("application/run/main_scene"));
Variant variant = ProjectSettings::get_singleton()->get_setting("application/run/main_scene");
CHECK_EQ(variant.get_type(), Variant::STRING);
String name = variant;
CHECK_EQ(name, String());
}
TEST_CASE("[ProjectSettings] Default value is ignored if setting exists") {
CHECK(ProjectSettings::get_singleton()->has_setting("application/run/main_scene"));
Variant variant = ProjectSettings::get_singleton()->get_setting("application/run/main_scene", "SomeDefaultValue");
CHECK_EQ(variant.get_type(), Variant::STRING);
String name = variant;
CHECK_EQ(name, String());
}
TEST_CASE("[ProjectSettings] Non existing setting is null") {
CHECK_FALSE(ProjectSettings::get_singleton()->has_setting("not_existing_setting"));
Variant variant = ProjectSettings::get_singleton()->get_setting("not_existing_setting");
CHECK_EQ(variant.get_type(), Variant::NIL);
}
TEST_CASE("[ProjectSettings] Non existing setting should return default value") {
CHECK_FALSE(ProjectSettings::get_singleton()->has_setting("not_existing_setting"));
Variant variant = ProjectSettings::get_singleton()->get_setting("not_existing_setting");
CHECK_EQ(variant.get_type(), Variant::NIL);
variant = ProjectSettings::get_singleton()->get_setting("not_existing_setting", "my_nice_default_value");
CHECK_EQ(variant.get_type(), Variant::STRING);
String name = variant;
CHECK_EQ(name, "my_nice_default_value");
CHECK_FALSE(ProjectSettings::get_singleton()->has_setting("not_existing_setting"));
}
TEST_CASE("[ProjectSettings] Set value should be returned when retrieved") {
CHECK_FALSE(ProjectSettings::get_singleton()->has_setting("my_custom_setting"));
Variant variant = ProjectSettings::get_singleton()->get_setting("my_custom_setting");
CHECK_EQ(variant.get_type(), Variant::NIL);
ProjectSettings::get_singleton()->set_setting("my_custom_setting", true);
CHECK(ProjectSettings::get_singleton()->has_setting("my_custom_setting"));
variant = ProjectSettings::get_singleton()->get_setting("my_custom_setting");
CHECK_EQ(variant.get_type(), Variant::BOOL);
bool value = variant;
CHECK_EQ(true, value);
CHECK(ProjectSettings::get_singleton()->has_setting("my_custom_setting"));
}
TEST_CASE("[ProjectSettings] localize_path") {
String old_resource_path = TestProjectSettingsInternalsAccessor::resource_path();
TestProjectSettingsInternalsAccessor::resource_path() = DirAccess::create(DirAccess::ACCESS_FILESYSTEM)->get_current_dir();
String root_path = ProjectSettings::get_singleton()->get_resource_path();
#ifdef WINDOWS_ENABLED
String root_path_win = ProjectSettings::get_singleton()->get_resource_path().replace_char('/', '\\');
#endif
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("filename"), "res://filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("path/filename"), "res://path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("path/something/../filename"), "res://path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("path/./filename"), "res://path/filename");
#ifdef WINDOWS_ENABLED
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("path\\filename"), "res://path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("path\\something\\..\\filename"), "res://path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("path\\.\\filename"), "res://path/filename");
#endif
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("../filename"), "../filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("../path/filename"), "../path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("..\\path\\filename"), "../path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("/testroot/filename"), "/testroot/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("/testroot/path/filename"), "/testroot/path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("/testroot/path/something/../filename"), "/testroot/path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("/testroot/path/./filename"), "/testroot/path/filename");
#ifdef WINDOWS_ENABLED
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:/testroot/filename"), "C:/testroot/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:/testroot/path/filename"), "C:/testroot/path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:/testroot/path/something/../filename"), "C:/testroot/path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:/testroot/path/./filename"), "C:/testroot/path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:\\testroot\\filename"), "C:/testroot/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:\\testroot\\path\\filename"), "C:/testroot/path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:\\testroot\\path\\something\\..\\filename"), "C:/testroot/path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path("C:\\testroot\\path\\.\\filename"), "C:/testroot/path/filename");
#endif
CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path + "/filename"), "res://filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path + "/path/filename"), "res://path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path + "/path/something/../filename"), "res://path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path + "/path/./filename"), "res://path/filename");
#ifdef WINDOWS_ENABLED
CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path_win + "\\filename"), "res://filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path_win + "\\path\\filename"), "res://path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path_win + "\\path\\something\\..\\filename"), "res://path/filename");
CHECK_EQ(ProjectSettings::get_singleton()->localize_path(root_path_win + "\\path\\.\\filename"), "res://path/filename");
#endif
TestProjectSettingsInternalsAccessor::resource_path() = old_resource_path;
}
} // namespace TestProjectSettings

View File

@@ -0,0 +1,110 @@
/**************************************************************************/
/* test_input_event.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/input/input_event.h"
#include "core/math/rect2.h"
#include "core/os/memory.h"
#include "core/variant/array.h"
#include "tests/test_macros.h"
namespace TestInputEvent {
TEST_CASE("[InputEvent] Signal is emitted when device is changed") {
Ref<InputEventKey> input_event;
input_event.instantiate();
SIGNAL_WATCH(*input_event, CoreStringName(changed));
Array empty_args = { {} };
input_event->set_device(1);
SIGNAL_CHECK("changed", empty_args);
CHECK(input_event->get_device() == 1);
SIGNAL_UNWATCH(*input_event, CoreStringName(changed));
}
TEST_CASE("[InputEvent] Test accumulate") {
Ref<InputEventMouseMotion> iemm1, iemm2;
Ref<InputEventKey> iek;
iemm1.instantiate(), iemm2.instantiate();
iek.instantiate();
iemm1->set_button_mask(MouseButtonMask::LEFT);
CHECK_FALSE(iemm1->accumulate(iemm2));
iemm2->set_button_mask(MouseButtonMask::LEFT);
CHECK(iemm1->accumulate(iemm2));
CHECK_FALSE(iemm1->accumulate(iek));
CHECK_FALSE(iemm2->accumulate(iek));
}
TEST_CASE("[InputEvent][SceneTree] Test methods that interact with the InputMap") {
const String mock_action = "mock_action";
Ref<InputEventJoypadMotion> iejm;
iejm.instantiate();
InputMap::get_singleton()->add_action(mock_action, 0.5);
InputMap::get_singleton()->action_add_event(mock_action, iejm);
CHECK(iejm->is_action_type());
CHECK(iejm->is_action(mock_action));
CHECK(iejm->is_action_released(mock_action));
CHECK(Math::is_equal_approx(iejm->get_action_strength(mock_action), 0.0f));
iejm->set_axis_value(0.8f);
// Since deadzone is 0.5, action_strength grows linearly from 0.5 to 1.0.
CHECK(Math::is_equal_approx(iejm->get_action_strength(mock_action), 0.6f));
CHECK(Math::is_equal_approx(iejm->get_action_raw_strength(mock_action), 0.8f));
CHECK(iejm->is_action_pressed(mock_action));
InputMap::get_singleton()->erase_action(mock_action);
}
TEST_CASE("[InputEvent] Test xformed_by") {
Ref<InputEventMouseMotion> iemm1;
iemm1.instantiate();
iemm1->set_position(Vector2(0.0f, 0.0f));
Transform2D transform;
transform = transform.translated(Vector2(2.0f, 3.0f));
Ref<InputEventMouseMotion> iemm2 = iemm1->xformed_by(transform);
CHECK(iemm2->get_position().is_equal_approx(Vector2(2.0f, 3.0f)));
}
} // namespace TestInputEvent

View File

@@ -0,0 +1,336 @@
/**************************************************************************/
/* test_input_event_key.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/input/input_event.h"
#include "core/os/keyboard.h"
#include "tests/test_macros.h"
namespace TestInputEventKey {
TEST_CASE("[InputEventKey] Key correctly registers being pressed") {
InputEventKey key;
key.set_pressed(true);
CHECK(key.is_pressed() == true);
key.set_pressed(false);
CHECK(key.is_pressed() == false);
}
TEST_CASE("[InputEventKey] Key correctly stores and retrieves keycode") {
InputEventKey key;
key.set_keycode(Key::ENTER);
CHECK(key.get_keycode() == Key::ENTER);
CHECK(key.get_keycode() != Key::PAUSE);
key.set_physical_keycode(Key::BACKSPACE);
CHECK(key.get_physical_keycode() == Key::BACKSPACE);
CHECK(key.get_physical_keycode() != Key::PAUSE);
}
TEST_CASE("[InputEventKey] Key correctly stores and retrieves keycode with modifiers") {
InputEventKey key;
key.set_keycode(Key::ENTER);
key.set_ctrl_pressed(true);
CHECK(key.get_keycode_with_modifiers() == (Key::ENTER | KeyModifierMask::CTRL));
CHECK(key.get_keycode_with_modifiers() != (Key::ENTER | KeyModifierMask::SHIFT));
CHECK(key.get_keycode_with_modifiers() != Key::ENTER);
key.set_physical_keycode(Key::SPACE);
key.set_ctrl_pressed(true);
CHECK(key.get_physical_keycode_with_modifiers() == (Key::SPACE | KeyModifierMask::CTRL));
CHECK(key.get_physical_keycode_with_modifiers() != (Key::SPACE | KeyModifierMask::SHIFT));
CHECK(key.get_physical_keycode_with_modifiers() != Key::SPACE);
}
TEST_CASE("[InputEventKey] Key correctly stores and retrieves unicode") {
InputEventKey key;
key.set_unicode('x');
CHECK(key.get_unicode() == 'x');
CHECK(key.get_unicode() != 'y');
}
TEST_CASE("[InputEventKey] Key correctly stores and retrieves location") {
InputEventKey key;
CHECK(key.get_location() == KeyLocation::UNSPECIFIED);
key.set_location(KeyLocation::LEFT);
CHECK(key.get_location() == KeyLocation::LEFT);
CHECK(key.get_location() != KeyLocation::RIGHT);
}
TEST_CASE("[InputEventKey] Key correctly stores and checks echo") {
InputEventKey key;
key.set_echo(true);
CHECK(key.is_echo() == true);
key.set_echo(false);
CHECK(key.is_echo() == false);
}
TEST_CASE("[InputEventKey] Key correctly converts itself to text") {
InputEventKey none_key;
// These next three tests test the functionality of getting a key that is set to None
// as text. These cases are a bit weird, since None has no textual representation
// (find_keycode_name(Key::NONE) results in a nullptr). Thus, these tests look weird
// with only (Physical) or a lonely modifier with (Physical) but (as far as I
// understand the code, that is intended behavior.
// Key is None without a physical key.
none_key.set_keycode(Key::NONE);
CHECK(none_key.as_text() == "(Unset)");
// Key is none and has modifiers.
none_key.set_ctrl_pressed(true);
CHECK(none_key.as_text() == "Ctrl+(Unset)");
// Key is None WITH a physical key AND modifiers.
none_key.set_physical_keycode(Key::ENTER);
CHECK(none_key.as_text() == "Ctrl+Enter (Physical)");
InputEventKey none_key2;
// Key is None without modifiers with a physical key.
none_key2.set_keycode(Key::NONE);
none_key2.set_physical_keycode(Key::ENTER);
CHECK(none_key2.as_text() == "Enter (Physical)");
InputEventKey key;
// Key has keycode.
key.set_keycode(Key::SPACE);
CHECK(key.as_text() != "");
CHECK(key.as_text() == "Space");
// Key has keycode and modifiers.
key.set_ctrl_pressed(true);
CHECK(key.as_text() != "Space");
CHECK(key.as_text() == "Ctrl+Space");
// Since the keycode is set to Key::NONE upon initialization of the
// InputEventKey and you can only update it with another Key, the keycode
// cannot be empty, so the kc.is_empty() case cannot be tested.
}
TEST_CASE("[InputEventKey] Key correctly converts its state to a string representation") {
InputEventKey none_key;
CHECK(none_key.to_string() == "InputEventKey: keycode=(Unset), mods=none, physical=false, location=unspecified, pressed=false, echo=false");
// Set physical key to Escape.
none_key.set_physical_keycode(Key::ESCAPE);
CHECK(none_key.to_string() == "InputEventKey: keycode=4194305 (Escape), mods=none, physical=true, location=unspecified, pressed=false, echo=false");
InputEventKey key;
// Set physical to None, set keycode to Space.
key.set_keycode(Key::SPACE);
CHECK(key.to_string() == "InputEventKey: keycode=32 (Space), mods=none, physical=false, location=unspecified, pressed=false, echo=false");
// Set location
key.set_location(KeyLocation::RIGHT);
CHECK(key.to_string() == "InputEventKey: keycode=32 (Space), mods=none, physical=false, location=right, pressed=false, echo=false");
// Set pressed to true.
key.set_pressed(true);
CHECK(key.to_string() == "InputEventKey: keycode=32 (Space), mods=none, physical=false, location=right, pressed=true, echo=false");
// set echo to true.
key.set_echo(true);
CHECK(key.to_string() == "InputEventKey: keycode=32 (Space), mods=none, physical=false, location=right, pressed=true, echo=true");
// Press Ctrl and Alt.
key.set_ctrl_pressed(true);
key.set_alt_pressed(true);
#ifdef MACOS_ENABLED
CHECK(key.to_string() == "InputEventKey: keycode=32 (Space), mods=Ctrl+Option, physical=false, location=right, pressed=true, echo=true");
#else
CHECK(key.to_string() == "InputEventKey: keycode=32 (Space), mods=Ctrl+Alt, physical=false, location=right, pressed=true, echo=true");
#endif
}
TEST_CASE("[InputEventKey] Key is correctly converted to reference") {
InputEventKey base_key;
Ref<InputEventKey> key_ref = base_key.create_reference(Key::ENTER);
CHECK(key_ref->get_keycode() == Key::ENTER);
}
TEST_CASE("[InputEventKey] Keys are correctly matched based on action") {
bool pressed = false;
float strength, raw_strength = 0.0;
InputEventKey key;
// Nullptr.
CHECK_MESSAGE(key.action_match(nullptr, false, 0.0f, &pressed, &strength, &raw_strength) == false, "nullptr as key reference should result in false");
// Match on keycode.
key.set_keycode(Key::SPACE);
Ref<InputEventKey> match = key.create_reference(Key::SPACE);
Ref<InputEventKey> no_match = key.create_reference(Key::ENTER);
CHECK(key.action_match(match, false, 0.0f, &pressed, &strength, &raw_strength) == true);
CHECK(key.action_match(no_match, false, 0.0f, &pressed, &strength, &raw_strength) == false);
// Check that values are correctly transferred to the pointers.
CHECK(pressed == false);
CHECK(strength < 0.5);
CHECK(raw_strength < 0.5);
match->set_pressed(true);
key.action_match(match, false, 0.0f, &pressed, &strength, &raw_strength);
CHECK(pressed == true);
CHECK(strength > 0.5);
CHECK(raw_strength > 0.5);
// Tests when keycode is None: Then you rely on physical keycode.
InputEventKey none_key;
none_key.set_physical_keycode(Key::SPACE);
Ref<InputEventKey> match_none = none_key.create_reference(Key::NONE);
match_none->set_physical_keycode(Key::SPACE);
Ref<InputEventKey> no_match_none = none_key.create_reference(Key::NONE);
no_match_none->set_physical_keycode(Key::ENTER);
CHECK(none_key.action_match(match_none, false, 0.0f, &pressed, &strength, &raw_strength) == true);
CHECK(none_key.action_match(no_match_none, false, 0.0f, &pressed, &strength, &raw_strength) == false);
// Test exact match.
InputEventKey key2, ref_key;
key2.set_keycode(Key::SPACE);
Ref<InputEventKey> match2 = ref_key.create_reference(Key::SPACE);
// Now both press Ctrl and Shift.
key2.set_ctrl_pressed(true);
key2.set_shift_pressed(true);
match2->set_ctrl_pressed(true);
match2->set_shift_pressed(true);
// Now they should match.
bool exact_match = true;
CHECK(key2.action_match(match2, exact_match, 0.0f, &pressed, &strength, &raw_strength) == true);
// Modify matching key such that it does no longer match in terms of modifiers: Shift
// is no longer pressed.
match2->set_shift_pressed(false);
CHECK(match2->is_shift_pressed() == false);
CHECK(key2.action_match(match2, exact_match, 0.0f, &pressed, &strength, &raw_strength) == false);
}
TEST_CASE("[IsMatch] Keys are correctly matched") {
// Key with NONE as keycode.
InputEventKey key;
key.set_keycode(Key::NONE);
key.set_physical_keycode(Key::SPACE);
// Nullptr.
CHECK(key.is_match(nullptr, false) == false);
Ref<InputEventKey> none_ref = key.create_reference(Key::NONE);
none_ref->set_physical_keycode(Key::SPACE);
CHECK(key.is_match(none_ref, false) == true);
none_ref->set_physical_keycode(Key::ENTER);
CHECK(key.is_match(none_ref, false) == false);
none_ref->set_physical_keycode(Key::SPACE);
key.set_ctrl_pressed(true);
none_ref->set_ctrl_pressed(false);
CHECK(key.is_match(none_ref, true) == false);
none_ref->set_ctrl_pressed(true);
CHECK(key.is_match(none_ref, true) == true);
// Ref with actual keycode.
InputEventKey key2;
key2.set_keycode(Key::SPACE);
Ref<InputEventKey> match = key2.create_reference(Key::SPACE);
Ref<InputEventKey> no_match = key2.create_reference(Key::ENTER);
CHECK(key2.is_match(match, false) == true);
CHECK(key2.is_match(no_match, false) == false);
// Now the keycode is the same, but the modifiers differ.
no_match->set_keycode(Key::SPACE);
key2.set_ctrl_pressed(true);
match->set_ctrl_pressed(true);
no_match->set_shift_pressed(true);
CHECK(key2.is_match(match, true) == true);
CHECK(key2.is_match(no_match, true) == false);
// Physical key with location.
InputEventKey key3;
key3.set_keycode(Key::NONE);
key3.set_physical_keycode(Key::SHIFT);
Ref<InputEventKey> loc_ref = key.create_reference(Key::NONE);
loc_ref->set_keycode(Key::SHIFT);
loc_ref->set_physical_keycode(Key::SHIFT);
CHECK(key3.is_match(loc_ref, false) == true);
key3.set_location(KeyLocation::UNSPECIFIED);
CHECK(key3.is_match(loc_ref, false) == true);
loc_ref->set_location(KeyLocation::LEFT);
CHECK(key3.is_match(loc_ref, false) == true);
key3.set_location(KeyLocation::LEFT);
CHECK(key3.is_match(loc_ref, false) == true);
key3.set_location(KeyLocation::RIGHT);
CHECK(key3.is_match(loc_ref, false) == false);
// Keycode key with location.
key3.set_physical_keycode(Key::NONE);
key3.set_keycode(Key::SHIFT);
CHECK(key3.is_match(loc_ref, false) == true);
}
} // namespace TestInputEventKey

View File

@@ -0,0 +1,78 @@
/**************************************************************************/
/* test_input_event_mouse.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/input/input_event.h"
#include "tests/test_macros.h"
namespace TestInputEventMouse {
TEST_CASE("[InputEventMouse] Mouse button mask is set correctly") {
InputEventMouse mousekey;
mousekey.set_button_mask(MouseButtonMask::LEFT);
CHECK(mousekey.get_button_mask().has_flag(MouseButtonMask::LEFT));
mousekey.set_button_mask(MouseButtonMask::MB_XBUTTON1);
CHECK(mousekey.get_button_mask().has_flag(MouseButtonMask::MB_XBUTTON1));
mousekey.set_button_mask(MouseButtonMask::MB_XBUTTON2);
CHECK(mousekey.get_button_mask().has_flag(MouseButtonMask::MB_XBUTTON2));
mousekey.set_button_mask(MouseButtonMask::MIDDLE);
CHECK(mousekey.get_button_mask().has_flag(MouseButtonMask::MIDDLE));
mousekey.set_button_mask(MouseButtonMask::RIGHT);
CHECK(mousekey.get_button_mask().has_flag(MouseButtonMask::RIGHT));
}
TEST_CASE("[InputEventMouse] Setting the mouse position works correctly") {
InputEventMouse mousekey;
mousekey.set_position(Vector2{ 10, 10 });
CHECK(mousekey.get_position() == Vector2{ 10, 10 });
mousekey.set_position(Vector2{ -1, -1 });
CHECK(mousekey.get_position() == Vector2{ -1, -1 });
}
TEST_CASE("[InputEventMouse] Setting the global mouse position works correctly") {
InputEventMouse mousekey;
mousekey.set_global_position(Vector2{ 10, 10 });
CHECK(mousekey.get_global_position() == Vector2{ 10, 10 });
CHECK(mousekey.get_global_position() != Vector2{ 1, 1 });
mousekey.set_global_position(Vector2{ -1, -1 });
CHECK(mousekey.get_global_position() == Vector2{ -1, -1 });
CHECK(mousekey.get_global_position() != Vector2{ 1, 1 });
}
} // namespace TestInputEventMouse

View File

@@ -0,0 +1,218 @@
/**************************************************************************/
/* test_shortcut.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/input/input_event.h"
#include "core/input/shortcut.h"
#include "core/io/config_file.h"
#include "core/object/ref_counted.h"
#include "core/os/keyboard.h"
#include "core/os/os.h"
#include "tests/test_macros.h"
namespace TestShortcut {
TEST_CASE("[Shortcut] Empty shortcut should have no valid events and text equal to None") {
Shortcut s;
CHECK(s.get_as_text() == "None");
CHECK(s.has_valid_event() == false);
}
TEST_CASE("[Shortcut] Setting and getting an event should result in the same event as the input") {
Ref<InputEventKey> k1;
Ref<InputEventKey> k2;
k1.instantiate();
k2.instantiate();
k1->set_keycode(Key::ENTER);
k2->set_keycode(Key::BACKSPACE);
// Cast to InputEvent so the internal code recognizes the objects.
Ref<InputEvent> e1 = k1;
Ref<InputEvent> e2 = k2;
Array input_array = { e1, e2 };
Shortcut s;
s.set_events(input_array);
// Get result, read it out, check whether it equals the input.
Array result_array = s.get_events();
Ref<InputEventKey> result1 = result_array.front();
Ref<InputEventKey> result2 = result_array.back();
CHECK(result1->get_keycode() == k1->get_keycode());
CHECK(result2->get_keycode() == k2->get_keycode());
}
TEST_CASE("[Shortcut] 'set_events_list' should result in the same events as the input") {
Ref<InputEventKey> k1;
Ref<InputEventKey> k2;
k1.instantiate();
k2.instantiate();
k1->set_keycode(Key::ENTER);
k2->set_keycode(Key::BACKSPACE);
// Cast to InputEvent so the set_events_list() method recognizes the objects.
Ref<InputEvent> e1 = k1;
Ref<InputEvent> e2 = k2;
List<Ref<InputEvent>> list;
list.push_back(e1);
list.push_back(e2);
Shortcut s;
s.set_events_list(&list);
// Get result, read it out, check whether it equals the input.
Array result_array = s.get_events();
Ref<InputEventKey> result1 = result_array.front();
Ref<InputEventKey> result2 = result_array.back();
CHECK(result1->get_keycode() == k1->get_keycode());
CHECK(result2->get_keycode() == k2->get_keycode());
}
TEST_CASE("[Shortcut] 'matches_event' should correctly match the same event") {
Ref<InputEventKey> original; // The one we compare with.
Ref<InputEventKey> similar_but_not_equal; // Same keycode, different event.
Ref<InputEventKey> different; // Different event, different keycode.
Ref<InputEventKey> copy; // Copy of original event.
original.instantiate();
similar_but_not_equal.instantiate();
different.instantiate();
copy.instantiate();
original->set_keycode(Key::ENTER);
similar_but_not_equal->set_keycode(Key::ENTER);
similar_but_not_equal->set_keycode(Key::ESCAPE);
copy = original;
// Only the copy is really the same, so only that one should match.
// The rest should not match.
Ref<InputEvent> e_original = original;
Ref<InputEvent> e_similar_but_not_equal = similar_but_not_equal;
Ref<InputEvent> e_different = different;
Ref<InputEvent> e_copy = copy;
Array a = { e_original };
Shortcut s;
s.set_events(a);
CHECK(s.matches_event(e_similar_but_not_equal) == false);
CHECK(s.matches_event(e_different) == false);
CHECK(s.matches_event(e_copy) == true);
}
TEST_CASE("[Shortcut] 'get_as_text' text representation should be correct") {
Ref<InputEventKey> same;
// k2 will not go into the shortcut but only be used to compare.
Ref<InputEventKey> different;
same.instantiate();
different.instantiate();
same->set_keycode(Key::ENTER);
different->set_keycode(Key::ESCAPE);
Ref<InputEvent> key_event1 = same;
Array a = { key_event1 };
Shortcut s;
s.set_events(a);
CHECK(s.get_as_text() == same->as_text());
CHECK(s.get_as_text() != different->as_text());
}
TEST_CASE("[Shortcut] Event validity should be correctly checked.") {
Ref<InputEventKey> valid;
// k2 will not go into the shortcut but only be used to compare.
Ref<InputEventKey> invalid = nullptr;
valid.instantiate();
valid->set_keycode(Key::ENTER);
Ref<InputEvent> valid_event = valid;
Ref<InputEvent> invalid_event = invalid;
Array a = { invalid_event, valid_event };
Shortcut s;
s.set_events(a);
CHECK(s.has_valid_event() == true);
Array b = { invalid_event };
Shortcut shortcut_with_invalid_event;
shortcut_with_invalid_event.set_events(b);
CHECK(shortcut_with_invalid_event.has_valid_event() == false);
}
TEST_CASE("[Shortcut] Equal arrays should be recognized as such.") {
Ref<InputEventKey> k1;
// k2 will not go into the shortcut but only be used to compare.
Ref<InputEventKey> k2;
k1.instantiate();
k2.instantiate();
k1->set_keycode(Key::ENTER);
k2->set_keycode(Key::ESCAPE);
Ref<InputEvent> key_event1 = k1;
Ref<InputEvent> key_event2 = k2;
Array same;
same.append(key_event1);
Array same_as_same;
same_as_same.append(key_event1);
Array different1 = { key_event2 };
Array different2 = { key_event1, key_event2 };
Array different3;
Shortcut s;
CHECK(s.is_event_array_equal(same, same_as_same) == true);
CHECK(s.is_event_array_equal(same, different1) == false);
CHECK(s.is_event_array_equal(same, different2) == false);
CHECK(s.is_event_array_equal(same, different3) == false);
}
} // namespace TestShortcut

View File

@@ -0,0 +1,161 @@
/**************************************************************************/
/* test_config_file.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/config_file.h"
#include "core/os/os.h"
#include "tests/test_macros.h"
namespace TestConfigFile {
TEST_CASE("[ConfigFile] Parsing well-formatted files") {
ConfigFile config_file;
// Formatting is intentionally hand-edited to see how human-friendly the parser is.
const Error error = config_file.parse(R"(
[player]
name = "Unnamed Player"
tagline="Waiting
for
Godot"
color =Color( 0, 0.5,1, 1) ; Inline comment
position= Vector2(
3,
4
)
[graphics]
antialiasing = true
; Testing comments and case-sensitivity...
antiAliasing = false
)");
CHECK_MESSAGE(error == OK, "The configuration file should parse successfully.");
CHECK_MESSAGE(
String(config_file.get_value("player", "name")) == "Unnamed Player",
"Reading `player/name` should return the expected value.");
CHECK_MESSAGE(
String(config_file.get_value("player", "tagline")) == "Waiting\nfor\nGodot",
"Reading `player/tagline` should return the expected value.");
CHECK_MESSAGE(
Color(config_file.get_value("player", "color")).is_equal_approx(Color(0, 0.5, 1)),
"Reading `player/color` should return the expected value.");
CHECK_MESSAGE(
Vector2(config_file.get_value("player", "position")).is_equal_approx(Vector2(3, 4)),
"Reading `player/position` should return the expected value.");
CHECK_MESSAGE(
bool(config_file.get_value("graphics", "antialiasing")),
"Reading `graphics/antialiasing` should return `true`.");
CHECK_MESSAGE(
bool(config_file.get_value("graphics", "antiAliasing")) == false,
"Reading `graphics/antiAliasing` should return `false`.");
// An empty ConfigFile is valid.
const Error error_empty = config_file.parse("");
CHECK_MESSAGE(error_empty == OK,
"An empty configuration file should parse successfully.");
}
TEST_CASE("[ConfigFile] Parsing malformatted file") {
ConfigFile config_file;
ERR_PRINT_OFF;
const Error error = config_file.parse(R"(
[player]
name = "Unnamed Player"" ; Extraneous closing quote.
tagline = "Waiting\nfor\nGodot"
color = Color(0, 0.5, 1) ; Missing 4th parameter.
position = Vector2(
3,,
4
) ; Extraneous comma.
[graphics]
antialiasing = true
antialiasing = false ; Duplicate key.
)");
ERR_PRINT_ON;
CHECK_MESSAGE(error == ERR_PARSE_ERROR,
"The configuration file shouldn't parse successfully.");
}
TEST_CASE("[ConfigFile] Saving file") {
ConfigFile config_file;
config_file.set_value("player", "name", "Unnamed Player");
config_file.set_value("player", "tagline", "Waiting\nfor\nGodot");
config_file.set_value("player", "color", Color(0, 0.5, 1));
config_file.set_value("player", "position", Vector2(3, 4));
config_file.set_value("graphics", "antialiasing", true);
config_file.set_value("graphics", "antiAliasing", false);
config_file.set_value("quoted", String::utf8("静音"), 42);
config_file.set_value("quoted", "a=b", 7);
#ifdef WINDOWS_ENABLED
const String config_path = OS::get_singleton()->get_environment("TEMP").path_join("config.ini");
#else
const String config_path = "/tmp/config.ini";
#endif
config_file.save(config_path);
// Expected contents of the saved ConfigFile.
const String contents = String::utf8(R"([player]
name="Unnamed Player"
tagline="Waiting
for
Godot"
color=Color(0, 0.5, 1, 1)
position=Vector2(3, 4)
[graphics]
antialiasing=true
antiAliasing=false
[quoted]
"静音"=42
"a=b"=7
)");
Ref<FileAccess> 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

View File

@@ -0,0 +1,223 @@
/**************************************************************************/
/* test_file_access.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/file_access.h"
#include "tests/test_macros.h"
#include "tests/test_utils.h"
namespace TestFileAccess {
TEST_CASE("[FileAccess] CSV read") {
Ref<FileAccess> f = FileAccess::open(TestUtils::get_data_path("testdata.csv"), FileAccess::READ);
REQUIRE(f.is_valid());
Vector<String> header = f->get_csv_line(); // Default delimiter: ",".
REQUIRE(header.size() == 4);
Vector<String> row1 = f->get_csv_line(","); // Explicit delimiter, should be the same.
REQUIRE(row1.size() == 4);
CHECK(row1[0] == "GOOD_MORNING");
CHECK(row1[1] == "Good Morning");
CHECK(row1[2] == "Guten Morgen");
CHECK(row1[3] == "Bonjour");
Vector<String> row2 = f->get_csv_line();
REQUIRE(row2.size() == 4);
CHECK(row2[0] == "GOOD_EVENING");
CHECK(row2[1] == "Good Evening");
CHECK(row2[2].is_empty()); // Use case: not yet translated!
// https://github.com/godotengine/godot/issues/44269
CHECK_MESSAGE(row2[2] != "\"", "Should not parse empty string as a single double quote.");
CHECK(row2[3] == "\"\""); // Intentionally testing only escaped double quotes.
Vector<String> row3 = f->get_csv_line();
REQUIRE(row3.size() == 6);
CHECK(row3[0] == "Without quotes");
CHECK(row3[1] == "With, comma");
CHECK(row3[2] == "With \"inner\" quotes");
CHECK(row3[3] == "With \"inner\", quotes\",\" and comma");
CHECK(row3[4] == "With \"inner\nsplit\" quotes and\nline breaks");
CHECK(row3[5] == "With \\nnewline chars"); // Escaped, not an actual newline.
Vector<String> row4 = f->get_csv_line("~"); // Custom delimiter, makes inline commas easier.
REQUIRE(row4.size() == 3);
CHECK(row4[0] == "Some other");
CHECK(row4[1] == "delimiter");
CHECK(row4[2] == "should still work, shouldn't it?");
Vector<String> row5 = f->get_csv_line("\t"); // Tab separated variables.
REQUIRE(row5.size() == 3);
CHECK(row5[0] == "What about");
CHECK(row5[1] == "tab separated");
CHECK(row5[2] == "lines, good?");
}
TEST_CASE("[FileAccess] Get as UTF-8 String") {
Ref<FileAccess> f_lf = FileAccess::open(TestUtils::get_data_path("line_endings_lf.test.txt"), FileAccess::READ);
REQUIRE(f_lf.is_valid());
String s_lf = f_lf->get_as_utf8_string();
f_lf->seek(0);
String s_lf_nocr = f_lf->get_as_utf8_string(true);
CHECK(s_lf == "Hello darkness\nMy old friend\nI've come to talk\nWith you again\n");
CHECK(s_lf_nocr == "Hello darkness\nMy old friend\nI've come to talk\nWith you again\n");
Ref<FileAccess> f_crlf = FileAccess::open(TestUtils::get_data_path("line_endings_crlf.test.txt"), FileAccess::READ);
REQUIRE(f_crlf.is_valid());
String s_crlf = f_crlf->get_as_utf8_string();
f_crlf->seek(0);
String s_crlf_nocr = f_crlf->get_as_utf8_string(true);
CHECK(s_crlf == "Hello darkness\r\nMy old friend\r\nI've come to talk\r\nWith you again\r\n");
CHECK(s_crlf_nocr == "Hello darkness\nMy old friend\nI've come to talk\nWith you again\n");
Ref<FileAccess> f_cr = FileAccess::open(TestUtils::get_data_path("line_endings_cr.test.txt"), FileAccess::READ);
REQUIRE(f_cr.is_valid());
String s_cr = f_cr->get_as_utf8_string();
f_cr->seek(0);
String s_cr_nocr = f_cr->get_as_utf8_string(true);
CHECK(s_cr == "Hello darkness\rMy old friend\rI've come to talk\rWith you again\r");
CHECK(s_cr_nocr == "Hello darknessMy old friendI've come to talkWith you again");
}
TEST_CASE("[FileAccess] Get/Store floating point values") {
// BigEndian Hex: 0x40490E56
// LittleEndian Hex: 0x560E4940
float value = 3.1415f;
SUBCASE("Little Endian") {
const String file_path = TestUtils::get_data_path("floating_point_little_endian.bin");
const String file_path_new = TestUtils::get_data_path("floating_point_little_endian_new.bin");
Ref<FileAccess> f = FileAccess::open(file_path, FileAccess::READ);
REQUIRE(f.is_valid());
CHECK_EQ(f->get_float(), value);
Ref<FileAccess> fw = FileAccess::open(file_path_new, FileAccess::WRITE);
REQUIRE(fw.is_valid());
fw->store_float(value);
fw->close();
CHECK_EQ(FileAccess::get_sha256(file_path_new), FileAccess::get_sha256(file_path));
DirAccess::remove_file_or_error(file_path_new);
}
SUBCASE("Big Endian") {
const String file_path = TestUtils::get_data_path("floating_point_big_endian.bin");
const String file_path_new = TestUtils::get_data_path("floating_point_big_endian_new.bin");
Ref<FileAccess> f = FileAccess::open(file_path, FileAccess::READ);
REQUIRE(f.is_valid());
f->set_big_endian(true);
CHECK_EQ(f->get_float(), value);
Ref<FileAccess> fw = FileAccess::open(file_path_new, FileAccess::WRITE);
REQUIRE(fw.is_valid());
fw->set_big_endian(true);
fw->store_float(value);
fw->close();
CHECK_EQ(FileAccess::get_sha256(file_path_new), FileAccess::get_sha256(file_path));
DirAccess::remove_file_or_error(file_path_new);
}
}
TEST_CASE("[FileAccess] Get/Store floating point half precision values") {
// IEEE 754 half-precision binary floating-point format:
// sign exponent (5 bits) fraction (10 bits)
// 0 01101 0101010101
// BigEndian Hex: 0x3555
// LittleEndian Hex: 0x5535
float value = 0.33325195f;
SUBCASE("Little Endian") {
const String file_path = TestUtils::get_data_path("half_precision_floating_point_little_endian.bin");
const String file_path_new = TestUtils::get_data_path("half_precision_floating_point_little_endian_new.bin");
Ref<FileAccess> f = FileAccess::open(file_path, FileAccess::READ);
REQUIRE(f.is_valid());
CHECK_EQ(f->get_half(), value);
Ref<FileAccess> fw = FileAccess::open(file_path_new, FileAccess::WRITE);
REQUIRE(fw.is_valid());
fw->store_half(value);
fw->close();
CHECK_EQ(FileAccess::get_sha256(file_path_new), FileAccess::get_sha256(file_path));
DirAccess::remove_file_or_error(file_path_new);
}
SUBCASE("Big Endian") {
const String file_path = TestUtils::get_data_path("half_precision_floating_point_big_endian.bin");
const String file_path_new = TestUtils::get_data_path("half_precision_floating_point_big_endian_new.bin");
Ref<FileAccess> f = FileAccess::open(file_path, FileAccess::READ);
REQUIRE(f.is_valid());
f->set_big_endian(true);
CHECK_EQ(f->get_half(), value);
Ref<FileAccess> fw = FileAccess::open(file_path_new, FileAccess::WRITE);
REQUIRE(fw.is_valid());
fw->set_big_endian(true);
fw->store_half(value);
fw->close();
CHECK_EQ(FileAccess::get_sha256(file_path_new), FileAccess::get_sha256(file_path));
DirAccess::remove_file_or_error(file_path_new);
}
SUBCASE("4096 bytes fastlz compressed") {
const String file_path = TestUtils::get_data_path("exactly_4096_bytes_fastlz.bin");
Ref<FileAccess> f = FileAccess::open_compressed(file_path, FileAccess::READ, FileAccess::COMPRESSION_FASTLZ);
const Vector<uint8_t> full_data = f->get_buffer(4096 * 2);
CHECK(full_data.size() == 4096);
CHECK(f->eof_reached());
// Data should be empty.
PackedByteArray reference;
reference.resize_initialized(4096);
CHECK(reference == full_data);
f->seek(0);
const Vector<uint8_t> partial_data = f->get_buffer(4095);
CHECK(partial_data.size() == 4095);
CHECK(!f->eof_reached());
reference.resize_initialized(4095);
CHECK(reference == partial_data);
}
}
} // namespace TestFileAccess

View File

@@ -0,0 +1,104 @@
/**************************************************************************/
/* test_http_client.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/http_client.h"
#include "tests/test_macros.h"
#include "modules/modules_enabled.gen.h"
namespace TestHTTPClient {
TEST_CASE("[HTTPClient] Instantiation") {
Ref<HTTPClient> client = HTTPClient::create();
CHECK_MESSAGE(client.is_valid(), "A HTTP Client created should not be a null pointer");
}
TEST_CASE("[HTTPClient] query_string_from_dict") {
Ref<HTTPClient> client = HTTPClient::create();
Dictionary empty_dict;
String empty_query = client->query_string_from_dict(empty_dict);
CHECK_MESSAGE(empty_query.is_empty(), "A empty dictionary should return a empty string");
Dictionary dict1;
dict1["key"] = "value";
String single_key = client->query_string_from_dict(dict1);
CHECK_MESSAGE(single_key == "key=value", "The query should return key=value for every string in the dictionary");
// Check Dictionary with multiple values of different types.
Dictionary dict2;
dict2["key1"] = "value";
dict2["key2"] = 123;
Array values = { 1, 2, 3 };
dict2["key3"] = values;
dict2["key4"] = Variant();
String multiple_keys = client->query_string_from_dict(dict2);
CHECK_MESSAGE(multiple_keys == "key1=value&key2=123&key3=1&key3=2&key3=3&key4",
"The query should return key=value for every string in the dictionary. Pairs should be separated by &, arrays should be have a query for every element, and variants should have empty values");
}
TEST_CASE("[HTTPClient] verify_headers") {
Ref<HTTPClient> client = HTTPClient::create();
Vector<String> headers = { "Accept: text/html", "Content-Type: application/json", "Authorization: Bearer abc123" };
Error err = client->verify_headers(headers);
CHECK_MESSAGE(err == OK, "Expected OK for valid headers");
ERR_PRINT_OFF;
Vector<String> empty_header = { "" };
err = client->verify_headers(empty_header);
CHECK_MESSAGE(err == ERR_INVALID_PARAMETER, "Expected ERR_INVALID_PARAMETER for empty header");
Vector<String> invalid_header = { "InvalidHeader", "Header: " };
err = client->verify_headers(invalid_header);
CHECK_MESSAGE(err == ERR_INVALID_PARAMETER, "Expected ERR_INVALID_PARAMETER for header with no colon");
Vector<String> invalid_header_b = { ":", "Header: " };
err = client->verify_headers(invalid_header_b);
CHECK_MESSAGE(err == ERR_INVALID_PARAMETER, "Expected ERR_INVALID_PARAMETER for header with colon in first position");
ERR_PRINT_ON;
}
#if defined(MODULE_MBEDTLS_ENABLED) || defined(WEB_ENABLED)
TEST_CASE("[HTTPClient] connect_to_host") {
Ref<HTTPClient> client = HTTPClient::create();
String host = "https://www.example.com";
int port = 443;
Ref<TLSOptions> tls_options;
// Connect to host.
Error err = client->connect_to_host(host, port, tls_options);
CHECK_MESSAGE(err == OK, "Expected OK for successful connection");
}
#endif // MODULE_MBEDTLS_ENABLED || WEB_ENABLED
} // namespace TestHTTPClient

473
tests/core/io/test_image.h Normal file
View File

@@ -0,0 +1,473 @@
/**************************************************************************/
/* test_image.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/image.h"
#include "core/os/os.h"
#include "tests/test_utils.h"
#include "modules/modules_enabled.gen.h"
#include "thirdparty/doctest/doctest.h"
namespace TestImage {
TEST_CASE("[Image] Instantiation") {
Ref<Image> image = memnew(Image(8, 4, false, Image::FORMAT_RGBA8));
CHECK_MESSAGE(
!image->is_empty(),
"An image created with specified size and format should not be empty at first.");
CHECK_MESSAGE(
image->is_invisible(),
"A newly created image should be invisible.");
CHECK_MESSAGE(
!image->is_compressed(),
"A newly created image should not be compressed.");
CHECK(!image->has_mipmaps());
PackedByteArray image_data = image->get_data();
for (int i = 0; i < image_data.size(); i++) {
CHECK_MESSAGE(
image_data[i] == 0,
"An image created without data specified should have its data zeroed out.");
}
Ref<Image> image_copy = memnew(Image());
CHECK_MESSAGE(
image_copy->is_empty(),
"An image created without any specified size and format be empty at first.");
image_copy->copy_internals_from(image);
CHECK_MESSAGE(
image->get_data() == image_copy->get_data(),
"Duplicated images should have the same data.");
image_data = image->get_data();
Ref<Image> image_from_data = memnew(Image(8, 4, false, Image::FORMAT_RGBA8, image_data));
CHECK_MESSAGE(
image->get_data() == image_from_data->get_data(),
"An image created from data of another image should have the same data of the original image.");
}
TEST_CASE("[Image] Saving and loading") {
Ref<Image> image = memnew(Image(4, 4, false, Image::FORMAT_RGBA8));
const String save_path_png = TestUtils::get_temp_path("image.png");
const String save_path_exr = TestUtils::get_temp_path("image.exr");
// Save PNG
Error err;
err = image->save_png(save_path_png);
CHECK_MESSAGE(
err == OK,
"The image should be saved successfully as a .png file.");
// Only available on editor builds.
#ifdef TOOLS_ENABLED
// Save EXR
err = image->save_exr(save_path_exr, false);
CHECK_MESSAGE(
err == OK,
"The image should be saved successfully as an .exr file.");
#endif // TOOLS_ENABLED
// Load using load()
Ref<Image> image_load = memnew(Image());
err = image_load->load(save_path_png);
CHECK_MESSAGE(
err == OK,
"The image should load successfully using load().");
CHECK_MESSAGE(
image->get_data() == image_load->get_data(),
"The loaded image should have the same data as the one that got saved.");
#ifdef MODULE_BMP_ENABLED
// Load BMP
Ref<Image> image_bmp = memnew(Image());
Ref<FileAccess> f_bmp = FileAccess::open(TestUtils::get_data_path("images/icon.bmp"), FileAccess::READ, &err);
REQUIRE(f_bmp.is_valid());
PackedByteArray data_bmp;
data_bmp.resize(f_bmp->get_length() + 1);
f_bmp->get_buffer(data_bmp.ptrw(), f_bmp->get_length());
CHECK_MESSAGE(
image_bmp->load_bmp_from_buffer(data_bmp) == OK,
"The BMP image should load successfully.");
#endif // MODULE_BMP_ENABLED
#ifdef MODULE_JPG_ENABLED
// Load JPG
Ref<Image> image_jpg = memnew(Image());
Ref<FileAccess> f_jpg = FileAccess::open(TestUtils::get_data_path("images/icon.jpg"), FileAccess::READ, &err);
REQUIRE(f_jpg.is_valid());
PackedByteArray data_jpg;
data_jpg.resize(f_jpg->get_length() + 1);
f_jpg->get_buffer(data_jpg.ptrw(), f_jpg->get_length());
CHECK_MESSAGE(
image_jpg->load_jpg_from_buffer(data_jpg) == OK,
"The JPG image should load successfully.");
Ref<Image> image_grayscale_jpg = memnew(Image());
Ref<FileAccess> f_grayscale_jpg = FileAccess::open(TestUtils::get_data_path("images/grayscale.jpg"), FileAccess::READ, &err);
REQUIRE(f_grayscale_jpg.is_valid());
PackedByteArray data_grayscale_jpg;
data_grayscale_jpg.resize(f_grayscale_jpg->get_length() + 1);
f_grayscale_jpg->get_buffer(data_grayscale_jpg.ptrw(), f_grayscale_jpg->get_length());
CHECK_MESSAGE(
image_jpg->load_jpg_from_buffer(data_grayscale_jpg) == OK,
"The grayscale JPG image should load successfully.");
// Save JPG
const String save_path_jpg = TestUtils::get_temp_path("image.jpg");
CHECK_MESSAGE(image->save_jpg(save_path_jpg) == OK,
"The image should be saved successfully as a .jpg file.");
#ifdef MODULE_SVG_ENABLED
// Load SVG with embedded jpg image
Ref<Image> image_svg = memnew(Image());
Ref<FileAccess> f_svg = FileAccess::open(TestUtils::get_data_path("images/embedded_jpg.svg"), FileAccess::READ, &err);
REQUIRE(f_svg.is_valid());
PackedByteArray data_svg;
data_svg.resize(f_svg->get_length() + 1);
f_svg->get_buffer(data_svg.ptrw(), f_svg->get_length());
CHECK_MESSAGE(
image_svg->load_svg_from_buffer(data_svg) == OK,
"The SVG image should load successfully.");
#endif // MODULE_SVG_ENABLED
#endif // MODULE_JPG_ENABLED
#ifdef MODULE_WEBP_ENABLED
// Load WebP
Ref<Image> image_webp = memnew(Image());
Ref<FileAccess> f_webp = FileAccess::open(TestUtils::get_data_path("images/icon.webp"), FileAccess::READ, &err);
REQUIRE(f_webp.is_valid());
PackedByteArray data_webp;
data_webp.resize(f_webp->get_length() + 1);
f_webp->get_buffer(data_webp.ptrw(), f_webp->get_length());
CHECK_MESSAGE(
image_webp->load_webp_from_buffer(data_webp) == OK,
"The WebP image should load successfully.");
#endif // MODULE_WEBP_ENABLED
// Load PNG
Ref<Image> image_png = memnew(Image());
Ref<FileAccess> f_png = FileAccess::open(TestUtils::get_data_path("images/icon.png"), FileAccess::READ, &err);
REQUIRE(f_png.is_valid());
PackedByteArray data_png;
data_png.resize(f_png->get_length() + 1);
f_png->get_buffer(data_png.ptrw(), f_png->get_length());
CHECK_MESSAGE(
image_png->load_png_from_buffer(data_png) == OK,
"The PNG image should load successfully.");
#ifdef MODULE_TGA_ENABLED
// Load TGA
Ref<Image> image_tga = memnew(Image());
Ref<FileAccess> f_tga = FileAccess::open(TestUtils::get_data_path("images/icon.tga"), FileAccess::READ, &err);
REQUIRE(f_tga.is_valid());
PackedByteArray data_tga;
data_tga.resize(f_tga->get_length() + 1);
f_tga->get_buffer(data_tga.ptrw(), f_tga->get_length());
CHECK_MESSAGE(
image_tga->load_tga_from_buffer(data_tga) == OK,
"The TGA image should load successfully.");
#endif // MODULE_TGA_ENABLED
}
TEST_CASE("[Image] Basic getters") {
Ref<Image> image = memnew(Image(8, 4, false, Image::FORMAT_LA8));
CHECK(image->get_width() == 8);
CHECK(image->get_height() == 4);
CHECK(image->get_size() == Vector2(8, 4));
CHECK(image->get_format() == Image::FORMAT_LA8);
CHECK(image->get_used_rect() == Rect2i(0, 0, 0, 0));
Ref<Image> image_get_rect = image->get_region(Rect2i(0, 0, 2, 1));
CHECK(image_get_rect->get_size() == Vector2(2, 1));
}
TEST_CASE("[Image] Resizing") {
Ref<Image> image = memnew(Image(8, 8, false, Image::FORMAT_RGBA8));
// Crop
image->crop(4, 4);
CHECK_MESSAGE(
image->get_size() == Vector2(4, 4),
"get_size() should return the correct size after cropping.");
image->set_pixel(0, 0, Color(1, 1, 1, 1));
// Resize
for (int i = 0; i < 5; i++) {
Ref<Image> image_resized = memnew(Image());
image_resized->copy_internals_from(image);
Image::Interpolation interpolation = static_cast<Image::Interpolation>(i);
image_resized->resize(8, 8, interpolation);
CHECK_MESSAGE(
image_resized->get_size() == Vector2(8, 8),
"get_size() should return the correct size after resizing.");
CHECK_MESSAGE(
image_resized->get_pixel(1, 1).a > 0,
"Resizing an image should also affect its content.");
}
// shrink_x2()
image->shrink_x2();
CHECK_MESSAGE(
image->get_size() == Vector2(2, 2),
"get_size() should return the correct size after shrink_x2().");
// resize_to_po2()
Ref<Image> image_po_2 = memnew(Image(14, 28, false, Image::FORMAT_RGBA8));
image_po_2->resize_to_po2();
CHECK_MESSAGE(
image_po_2->get_size() == Vector2(16, 32),
"get_size() should return the correct size after resize_to_po2().");
}
TEST_CASE("[Image] Modifying pixels of an image") {
Ref<Image> image = memnew(Image(3, 3, false, Image::FORMAT_RGBA8));
image->set_pixel(0, 0, Color(1, 1, 1, 1));
CHECK_MESSAGE(
!image->is_invisible(),
"Image should not be invisible after drawing on it.");
CHECK_MESSAGE(
image->get_pixelv(Vector2(0, 0)).is_equal_approx(Color(1, 1, 1, 1)),
"Image's get_pixel() should return the same color value as the one being set with set_pixel() in the same position.");
CHECK_MESSAGE(
image->get_used_rect() == Rect2i(0, 0, 1, 1),
"Image's get_used_rect should return the expected value, larger than Rect2i(0, 0, 0, 0) if it's visible.");
image->set_pixelv(Vector2(0, 0), Color(0.5, 0.5, 0.5, 0.5));
Ref<Image> image2 = memnew(Image(3, 3, false, Image::FORMAT_RGBA8));
// Fill image with color
image2->fill(Color(0.5, 0.5, 0.5, 0.5));
for (int y = 0; y < image2->get_height(); y++) {
for (int x = 0; x < image2->get_width(); x++) {
CHECK_MESSAGE(
image2->get_pixel(x, y).r > 0.49,
"fill() should colorize all pixels of the image.");
}
}
// Fill rect with color
{
const int img_width = 3;
const int img_height = 3;
Vector<Rect2i> rects;
rects.push_back(Rect2i());
rects.push_back(Rect2i(-5, -5, 3, 3));
rects.push_back(Rect2i(img_width, 0, 12, 12));
rects.push_back(Rect2i(0, img_height, 12, 12));
rects.push_back(Rect2i(img_width + 1, img_height + 2, 12, 12));
rects.push_back(Rect2i(1, 1, 1, 1));
rects.push_back(Rect2i(0, 1, 2, 3));
rects.push_back(Rect2i(-5, 0, img_width + 10, 2));
rects.push_back(Rect2i(0, -5, 2, img_height + 10));
rects.push_back(Rect2i(-1, -1, img_width + 1, img_height + 1));
for (const Rect2i &rect : rects) {
Ref<Image> img = memnew(Image(img_width, img_height, false, Image::FORMAT_RGBA8));
img->fill_rect(rect, Color(1, 1, 1, 1));
for (int y = 0; y < img->get_height(); y++) {
for (int x = 0; x < img->get_width(); x++) {
if (rect.abs().has_point(Point2(x, y))) {
CHECK_MESSAGE(
img->get_pixel(x, y).is_equal_approx(Color(1, 1, 1, 1)),
"fill_rect() should colorize all image pixels within rect bounds.");
} else {
CHECK_MESSAGE(
!img->get_pixel(x, y).is_equal_approx(Color(1, 1, 1, 1)),
"fill_rect() shouldn't colorize any image pixel out of rect bounds.");
}
}
}
}
}
// Blend two images together
image->blend_rect(image2, Rect2i(Vector2i(0, 0), image2->get_size()), Vector2i(0, 0));
CHECK_MESSAGE(
image->get_pixel(0, 0).a > 0.7,
"blend_rect() should blend the alpha values of the two images.");
CHECK_MESSAGE(
image->get_used_rect().size == image->get_size(),
"get_used_rect() should return the expected value, its Rect size should be the same as get_size() if there are no transparent pixels.");
Ref<Image> image3 = memnew(Image(2, 2, false, Image::FORMAT_RGBA8));
image3->set_pixel(0, 0, Color(0, 1, 0, 1));
//blit_rect() two images together
image->blit_rect(image3, Rect2i(Vector2i(0, 0), image3->get_size()), Vector2i(0, 0));
CHECK_MESSAGE(
image->get_pixel(0, 0).is_equal_approx(Color(0, 1, 0, 1)),
"blit_rect() should replace old colors and not blend them.");
CHECK_MESSAGE(
!image->get_pixel(2, 2).is_equal_approx(Color(0, 1, 0, 1)),
"blit_rect() should not affect the area of the image that is outside src_rect.");
// Flip image
image3->flip_x();
CHECK(image3->get_pixel(1, 0).is_equal_approx(Color(0, 1, 0, 1)));
CHECK_MESSAGE(
image3->get_pixel(0, 0).is_equal_approx(Color(0, 0, 0, 0)),
"flip_x() should not leave old pixels behind.");
image3->flip_y();
CHECK(image3->get_pixel(1, 1).is_equal_approx(Color(0, 1, 0, 1)));
CHECK_MESSAGE(
image3->get_pixel(1, 0).is_equal_approx(Color(0, 0, 0, 0)),
"flip_y() should not leave old pixels behind.");
// Pre-multiply Alpha then Convert from RGBA to L8, checking alpha
{
Ref<Image> gray_image = memnew(Image(3, 3, false, Image::FORMAT_RGBA8));
gray_image->fill_rect(Rect2i(0, 0, 3, 3), Color(1, 1, 1, 0));
gray_image->set_pixel(1, 1, Color(1, 1, 1, 1));
gray_image->set_pixel(1, 2, Color(0.5, 0.5, 0.5, 0.5));
gray_image->set_pixel(2, 1, Color(0.25, 0.05, 0.5, 1.0));
gray_image->set_pixel(2, 2, Color(0.5, 0.25, 0.95, 0.75));
gray_image->premultiply_alpha();
gray_image->convert(Image::FORMAT_L8);
CHECK_MESSAGE(gray_image->get_pixel(0, 0).is_equal_approx(Color(0, 0, 0, 1)), "convert() RGBA to L8 should be black.");
CHECK_MESSAGE(gray_image->get_pixel(0, 1).is_equal_approx(Color(0, 0, 0, 1)), "convert() RGBA to L8 should be black.");
CHECK_MESSAGE(gray_image->get_pixel(0, 2).is_equal_approx(Color(0, 0, 0, 1)), "convert() RGBA to L8 should be black.");
CHECK_MESSAGE(gray_image->get_pixel(1, 0).is_equal_approx(Color(0, 0, 0, 1)), "convert() RGBA to L8 should be black.");
CHECK_MESSAGE(gray_image->get_pixel(1, 1).is_equal_approx(Color(1, 1, 1, 1)), "convert() RGBA to L8 should be white.");
CHECK_MESSAGE(gray_image->get_pixel(1, 2).is_equal_approx(Color(0.250980407, 0.250980407, 0.250980407, 1)), "convert() RGBA to L8 should be around 0.250980407 (64).");
CHECK_MESSAGE(gray_image->get_pixel(2, 0).is_equal_approx(Color(0, 0, 0, 1)), "convert() RGBA to L8 should be black.");
CHECK_MESSAGE(gray_image->get_pixel(2, 1).is_equal_approx(Color(0.121568628, 0.121568628, 0.121568628, 1)), "convert() RGBA to L8 should be around 0.121568628 (31).");
CHECK_MESSAGE(gray_image->get_pixel(2, 2).is_equal_approx(Color(0.266666681, 0.266666681, 0.266666681, 1)), "convert() RGBA to L8 should be around 0.266666681 (68).");
}
}
TEST_CASE("[Image] Custom mipmaps") {
Ref<Image> image = memnew(Image(100, 100, false, Image::FORMAT_RGBA8));
REQUIRE(!image->has_mipmaps());
image->generate_mipmaps();
REQUIRE(image->has_mipmaps());
const int mipmaps = image->get_mipmap_count() + 1;
REQUIRE(mipmaps == 7);
// Initialize reference mipmap data.
// Each byte is given value "mipmap_index * 5".
{
PackedByteArray data = image->get_data();
uint8_t *data_ptr = data.ptrw();
for (int mip = 0; mip < mipmaps; mip++) {
int64_t mip_offset = 0;
int64_t mip_size = 0;
image->get_mipmap_offset_and_size(mip, mip_offset, mip_size);
for (int i = 0; i < mip_size; i++) {
data_ptr[mip_offset + i] = mip * 5;
}
}
image->set_data(image->get_width(), image->get_height(), image->has_mipmaps(), image->get_format(), data);
}
// Byte format conversion.
for (int format = Image::FORMAT_L8; format <= Image::FORMAT_RGBA8; format++) {
Ref<Image> image_bytes = memnew(Image());
image_bytes->copy_internals_from(image);
image_bytes->convert((Image::Format)format);
REQUIRE(image_bytes->has_mipmaps());
PackedByteArray data = image_bytes->get_data();
const uint8_t *data_ptr = data.ptr();
for (int mip = 0; mip < mipmaps; mip++) {
int64_t mip_offset = 0;
int64_t mip_size = 0;
image_bytes->get_mipmap_offset_and_size(mip, mip_offset, mip_size);
for (int i = 0; i < mip_size; i++) {
if (data_ptr[mip_offset + i] != mip * 5) {
REQUIRE_MESSAGE(false, "Byte format conversion error.");
}
}
}
}
// Floating point format conversion.
for (int format = Image::FORMAT_RF; format <= Image::FORMAT_RGBAF; format++) {
Ref<Image> image_rgbaf = memnew(Image());
image_rgbaf->copy_internals_from(image);
image_rgbaf->convert((Image::Format)format);
REQUIRE(image_rgbaf->has_mipmaps());
PackedByteArray data = image_rgbaf->get_data();
const uint8_t *data_ptr = data.ptr();
for (int mip = 0; mip < mipmaps; mip++) {
int64_t mip_offset = 0;
int64_t mip_size = 0;
image_rgbaf->get_mipmap_offset_and_size(mip, mip_offset, mip_size);
for (int i = 0; i < mip_size; i += 4) {
float value = *(float *)(data_ptr + mip_offset + i);
if (!Math::is_equal_approx(value * 255.0f, mip * 5)) {
REQUIRE_MESSAGE(false, "Floating point conversion error.");
}
}
}
}
}
TEST_CASE("[Image] Convert image") {
for (int format = Image::FORMAT_RF; format < Image::FORMAT_RGBE9995; format++) {
for (int new_format = Image::FORMAT_RF; new_format < Image::FORMAT_RGBE9995; new_format++) {
Ref<Image> image = memnew(Image(4, 4, false, (Image::Format)format));
image->convert((Image::Format)new_format);
String format_string = Image::format_names[(Image::Format)format];
String new_format_string = Image::format_names[(Image::Format)new_format];
format_string = "Error converting from " + format_string + " to " + new_format_string + ".";
CHECK_MESSAGE(image->get_format() == new_format, format_string);
}
}
Ref<Image> image = memnew(Image(4, 4, false, Image::FORMAT_RGBA8));
PackedByteArray image_data = image->get_data();
ERR_PRINT_OFF;
image->convert((Image::Format)-1);
ERR_PRINT_ON;
CHECK_MESSAGE(image->get_data() == image_data, "Image conversion to invalid type (-1) should not alter image.");
Ref<Image> image2 = memnew(Image(4, 4, false, Image::FORMAT_RGBA8));
image_data = image2->get_data();
ERR_PRINT_OFF;
image2->convert((Image::Format)(Image::FORMAT_MAX + 1));
ERR_PRINT_ON;
CHECK_MESSAGE(image2->get_data() == image_data, "Image conversion to invalid type (Image::FORMAT_MAX + 1) should not alter image.");
}
} // namespace TestImage

48
tests/core/io/test_ip.h Normal file
View File

@@ -0,0 +1,48 @@
/**************************************************************************/
/* test_ip.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/ip.h"
#include "tests/test_macros.h"
namespace TestIP {
TEST_CASE("[IP] resolve_hostname") {
for (int x = 0; x < 1000; x++) {
IPAddress IPV4 = IP::get_singleton()->resolve_hostname("localhost", IP::TYPE_IPV4);
CHECK("127.0.0.1" == String(IPV4));
IPAddress IPV6 = IP::get_singleton()->resolve_hostname("localhost", IP::TYPE_IPV6);
CHECK("0:0:0:0:0:0:0:1" == String(IPV6));
}
}
} // namespace TestIP

404
tests/core/io/test_json.h Normal file
View File

@@ -0,0 +1,404 @@
/**************************************************************************/
/* test_json.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/json.h"
#include "thirdparty/doctest/doctest.h"
namespace TestJSON {
TEST_CASE("[JSON] Stringify single data types") {
CHECK(JSON::stringify(Variant()) == "null");
CHECK(JSON::stringify(false) == "false");
CHECK(JSON::stringify(true) == "true");
CHECK(JSON::stringify(0) == "0");
CHECK(JSON::stringify(12345) == "12345");
CHECK(JSON::stringify(0.75) == "0.75");
CHECK(JSON::stringify("test") == "\"test\"");
CHECK(JSON::stringify("\\\b\f\n\r\t\v\"") == "\"\\\\\\b\\f\\n\\r\\t\\v\\\"\"");
}
TEST_CASE("[JSON] Stringify arrays") {
CHECK(JSON::stringify(Array()) == "[]");
Array int_array;
for (int i = 0; i < 10; i++) {
int_array.push_back(i);
}
CHECK(JSON::stringify(int_array) == "[0,1,2,3,4,5,6,7,8,9]");
Array str_array;
str_array.push_back("Hello");
str_array.push_back("World");
str_array.push_back("!");
CHECK(JSON::stringify(str_array) == "[\"Hello\",\"World\",\"!\"]");
Array indented_array;
Array nested_array;
for (int i = 0; i < 5; i++) {
indented_array.push_back(i);
nested_array.push_back(i);
}
indented_array.push_back(nested_array);
CHECK(JSON::stringify(indented_array, "\t") == "[\n\t0,\n\t1,\n\t2,\n\t3,\n\t4,\n\t[\n\t\t0,\n\t\t1,\n\t\t2,\n\t\t3,\n\t\t4\n\t]\n]");
Array full_precision_array;
full_precision_array.push_back(0.123456789012345677);
CHECK(JSON::stringify(full_precision_array, "", true, true) == "[0.123456789012345677]");
ERR_PRINT_OFF
Array self_array;
self_array.push_back(self_array);
CHECK(JSON::stringify(self_array) == "[\"[...]\"]");
self_array.clear();
Array max_recursion_array;
for (int i = 0; i < Variant::MAX_RECURSION_DEPTH + 1; i++) {
Array next;
next.push_back(max_recursion_array);
max_recursion_array = next;
}
CHECK(JSON::stringify(max_recursion_array).contains("[...]"));
ERR_PRINT_ON
}
TEST_CASE("[JSON] Stringify dictionaries") {
CHECK(JSON::stringify(Dictionary()) == "{}");
Dictionary single_entry;
single_entry["key"] = "value";
CHECK(JSON::stringify(single_entry) == "{\"key\":\"value\"}");
Dictionary indented;
indented["key1"] = "value1";
indented["key2"] = 2;
CHECK(JSON::stringify(indented, "\t") == "{\n\t\"key1\": \"value1\",\n\t\"key2\": 2\n}");
Dictionary outer;
Dictionary inner;
inner["key"] = "value";
outer["inner"] = inner;
CHECK(JSON::stringify(outer) == "{\"inner\":{\"key\":\"value\"}}");
Dictionary full_precision_dictionary;
full_precision_dictionary["key"] = 0.123456789012345677;
CHECK(JSON::stringify(full_precision_dictionary, "", true, true) == "{\"key\":0.123456789012345677}");
ERR_PRINT_OFF
Dictionary self_dictionary;
self_dictionary["key"] = self_dictionary;
CHECK(JSON::stringify(self_dictionary) == "{\"key\":\"{...}\"}");
self_dictionary.clear();
Dictionary max_recursion_dictionary;
for (int i = 0; i < Variant::MAX_RECURSION_DEPTH + 1; i++) {
Dictionary next;
next["key"] = max_recursion_dictionary;
max_recursion_dictionary = next;
}
CHECK(JSON::stringify(max_recursion_dictionary).contains("{...:...}"));
ERR_PRINT_ON
}
// NOTE: The current JSON parser accepts many non-conformant strings such as
// single-quoted strings, duplicate commas and trailing commas.
// This is intentionally not tested as users shouldn't rely on this behavior.
TEST_CASE("[JSON] Parsing single data types") {
// Parsing a single data type as JSON is valid per the JSON specification.
JSON json;
json.parse("null");
CHECK_MESSAGE(
json.get_error_line() == 0,
"Parsing `null` as JSON should parse successfully.");
CHECK_MESSAGE(
json.get_data() == Variant(),
"Parsing a double quoted string as JSON should return the expected value.");
json.parse("true");
CHECK_MESSAGE(
json.get_error_line() == 0,
"Parsing boolean `true` as JSON should parse successfully.");
CHECK_MESSAGE(
json.get_data(),
"Parsing boolean `true` as JSON should return the expected value.");
json.parse("false");
CHECK_MESSAGE(
json.get_error_line() == 0,
"Parsing boolean `false` as JSON should parse successfully.");
CHECK_MESSAGE(
!json.get_data(),
"Parsing boolean `false` as JSON should return the expected value.");
json.parse("123456");
CHECK_MESSAGE(
json.get_error_line() == 0,
"Parsing an integer number as JSON should parse successfully.");
CHECK_MESSAGE(
(int)(json.get_data()) == 123456,
"Parsing an integer number as JSON should return the expected value.");
json.parse("0.123456");
CHECK_MESSAGE(
json.get_error_line() == 0,
"Parsing a floating-point number as JSON should parse successfully.");
CHECK_MESSAGE(
double(json.get_data()) == doctest::Approx(0.123456),
"Parsing a floating-point number as JSON should return the expected value.");
json.parse("\"hello\"");
CHECK_MESSAGE(
json.get_error_line() == 0,
"Parsing a double quoted string as JSON should parse successfully.");
CHECK_MESSAGE(
json.get_data() == "hello",
"Parsing a double quoted string as JSON should return the expected value.");
}
TEST_CASE("[JSON] Parsing arrays") {
JSON json;
// JSON parsing fails if it's split over several lines (even if leading indentation is removed).
json.parse(R"(["Hello", "world.", "This is",["a","json","array.",[]], "Empty arrays ahoy:", [[["Gotcha!"]]]])");
const Array array = json.get_data();
CHECK_MESSAGE(
json.get_error_line() == 0,
"Parsing a JSON array should parse successfully.");
CHECK_MESSAGE(
array[0] == "Hello",
"The parsed JSON should contain the expected values.");
const Array sub_array = array[3];
CHECK_MESSAGE(
sub_array.size() == 4,
"The parsed JSON should contain the expected values.");
CHECK_MESSAGE(
sub_array[1] == "json",
"The parsed JSON should contain the expected values.");
CHECK_MESSAGE(
sub_array[3].hash() == Array().hash(),
"The parsed JSON should contain the expected values.");
const Array deep_array = Array(Array(array[5])[0])[0];
CHECK_MESSAGE(
deep_array[0] == "Gotcha!",
"The parsed JSON should contain the expected values.");
}
TEST_CASE("[JSON] Parsing objects (dictionaries)") {
JSON json;
json.parse(R"({"name": "Godot Engine", "is_free": true, "bugs": null, "apples": {"red": 500, "green": 0, "blue": -20}, "empty_object": {}})");
const Dictionary dictionary = json.get_data();
CHECK_MESSAGE(
dictionary["name"] == "Godot Engine",
"The parsed JSON should contain the expected values.");
CHECK_MESSAGE(
dictionary["is_free"],
"The parsed JSON should contain the expected values.");
CHECK_MESSAGE(
dictionary["bugs"] == Variant(),
"The parsed JSON should contain the expected values.");
CHECK_MESSAGE(
(int)Dictionary(dictionary["apples"])["blue"] == -20,
"The parsed JSON should contain the expected values.");
CHECK_MESSAGE(
dictionary["empty_object"].hash() == Dictionary().hash(),
"The parsed JSON should contain the expected values.");
}
TEST_CASE("[JSON] Parsing escape sequences") {
// Only certain escape sequences are valid according to the JSON specification.
// Others must result in a parsing error instead.
JSON json;
TypedArray<String> valid_escapes = { "\";\"", "\\;\\", "/;/", "b;\b", "f;\f", "n;\n", "r;\r", "t;\t" };
SUBCASE("Basic valid escape sequences") {
for (int i = 0; i < valid_escapes.size(); i++) {
String valid_escape = valid_escapes[i];
String valid_escape_string = valid_escape.get_slicec(';', 0);
String valid_escape_value = valid_escape.get_slicec(';', 1);
String json_string = "\"\\";
json_string += valid_escape_string;
json_string += "\"";
json.parse(json_string);
CHECK_MESSAGE(
json.get_error_line() == 0,
vformat("Parsing valid escape sequence `%s` as JSON should parse successfully.", valid_escape_string));
String json_value = json.get_data();
CHECK_MESSAGE(
json_value == valid_escape_value,
vformat("Parsing valid escape sequence `%s` as JSON should return the expected value.", valid_escape_string));
}
}
SUBCASE("Valid unicode escape sequences") {
String json_string = "\"\\u0020\"";
json.parse(json_string);
CHECK_MESSAGE(
json.get_error_line() == 0,
vformat("Parsing valid unicode escape sequence with value `0020` as JSON should parse successfully."));
String json_value = json.get_data();
CHECK_MESSAGE(
json_value == " ",
vformat("Parsing valid unicode escape sequence with value `0020` as JSON should return the expected value."));
}
SUBCASE("Invalid escape sequences") {
ERR_PRINT_OFF
for (char32_t i = 0; i < 128; i++) {
bool skip = false;
for (int j = 0; j < valid_escapes.size(); j++) {
String valid_escape = valid_escapes[j];
String valid_escape_string = valid_escape.get_slicec(';', 0);
if (valid_escape_string[0] == i) {
skip = true;
break;
}
}
if (skip) {
continue;
}
String json_string = "\"\\";
json_string += i;
json_string += "\"";
Error err = json.parse(json_string);
// TODO: Line number is currently kept on 0, despite an error occurring. This should be fixed in the JSON parser.
// CHECK_MESSAGE(
// json.get_error_line() != 0,
// vformat("Parsing invalid escape sequence with ASCII value `%d` as JSON should fail to parse.", i));
CHECK_MESSAGE(
err == ERR_PARSE_ERROR,
vformat("Parsing invalid escape sequence with ASCII value `%d` as JSON should fail to parse with ERR_PARSE_ERROR.", i));
}
ERR_PRINT_ON
}
}
TEST_CASE("[JSON] Serialization") {
JSON json;
struct FpTestCase {
double number;
String json;
};
struct IntTestCase {
int64_t number;
String json;
};
struct UIntTestCase {
uint64_t number;
String json;
};
static FpTestCase fp_tests_default_precision[] = {
{ 0.0, "0.0" },
{ 1000.1234567890123456789, "1000.12345678901" },
{ -1000.1234567890123456789, "-1000.12345678901" },
{ DBL_MAX, "179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0" },
{ DBL_MAX - 1, "179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0" },
{ std::pow(2, 53), "9007199254740992.0" },
{ -std::pow(2, 53), "-9007199254740992.0" },
{ 0.00000000000000011, "0.00000000000000011" },
{ -0.00000000000000011, "-0.00000000000000011" },
{ 1.0 / 3.0, "0.333333333333333" },
{ 0.9999999999999999, "1.0" },
{ 1.0000000000000001, "1.0" },
};
static FpTestCase fp_tests_full_precision[] = {
{ 0.0, "0.0" },
{ 1000.1234567890123456789, "1000.12345678901238" },
{ -1000.1234567890123456789, "-1000.12345678901238" },
{ DBL_MAX, "179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0" },
{ DBL_MAX - 1, "179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0" },
{ std::pow(2, 53), "9007199254740992.0" },
{ -std::pow(2, 53), "-9007199254740992.0" },
{ 0.00000000000000011, "0.00000000000000011" },
{ -0.00000000000000011, "-0.00000000000000011" },
{ 1.0 / 3.0, "0.333333333333333315" },
{ 0.9999999999999999, "0.999999999999999889" },
{ 1.0000000000000001, "1.0" },
};
static IntTestCase int_tests[] = {
{ 0, "0" },
{ INT64_MAX, "9223372036854775807" },
{ INT64_MIN, "-9223372036854775808" },
};
SUBCASE("Floating point default precision") {
for (FpTestCase &test : fp_tests_default_precision) {
String json_value = json.stringify(test.number, "", true, false);
CHECK_MESSAGE(
json_value == test.json,
vformat("Serializing `%.20d` to JSON should return the expected value.", test.number));
}
}
SUBCASE("Floating point full precision") {
for (FpTestCase &test : fp_tests_full_precision) {
String json_value = json.stringify(test.number, "", true, true);
CHECK_MESSAGE(
json_value == test.json,
vformat("Serializing `%20f` to JSON should return the expected value.", test.number));
}
}
SUBCASE("Signed integer") {
for (IntTestCase &test : int_tests) {
String json_value = json.stringify(test.number, "", true, true);
CHECK_MESSAGE(
json_value == test.json,
vformat("Serializing `%d` to JSON should return the expected value.", test.number));
}
}
}
} // namespace TestJSON

View File

@@ -0,0 +1,210 @@
/**************************************************************************/
/* test_json_native.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/json.h"
#include "core/variant/typed_array.h"
#include "core/variant/typed_dictionary.h"
#include "tests/test_macros.h"
namespace TestJSONNative {
String encode(const Variant &p_variant, bool p_full_objects = false) {
return JSON::stringify(JSON::from_native(p_variant, p_full_objects), "", false);
}
Variant decode(const String &p_string, bool p_allow_objects = false) {
return JSON::to_native(JSON::parse_string(p_string), p_allow_objects);
}
void test(const Variant &p_variant, const String &p_string, bool p_with_objects = false) {
CHECK(encode(p_variant, p_with_objects) == p_string);
CHECK(decode(p_string, p_with_objects).get_construct_string() == p_variant.get_construct_string());
}
TEST_CASE("[JSON][Native] Conversion between native and JSON formats") {
// `Nil` and `bool` (represented as JSON keyword literals).
test(Variant(), "null");
test(false, "false");
test(true, "true");
// Numbers and strings (represented as JSON strings).
test(1, R"("i:1")");
test(1.0, R"("f:1.0")");
test(String("abc"), R"("s:abc")");
test(StringName("abc"), R"("sn:abc")");
test(NodePath("abc"), R"("np:abc")");
// Non-serializable types (always empty after deserialization).
test(RID(), R"({"type":"RID"})");
test(Callable(), R"({"type":"Callable"})");
test(Signal(), R"({"type":"Signal"})");
// Math types.
test(Vector2(1, 2), R"({"type":"Vector2","args":[1.0,2.0]})");
test(Vector2i(1, 2), R"({"type":"Vector2i","args":[1,2]})");
test(Rect2(1, 2, 3, 4), R"({"type":"Rect2","args":[1.0,2.0,3.0,4.0]})");
test(Rect2i(1, 2, 3, 4), R"({"type":"Rect2i","args":[1,2,3,4]})");
test(Vector3(1, 2, 3), R"({"type":"Vector3","args":[1.0,2.0,3.0]})");
test(Vector3i(1, 2, 3), R"({"type":"Vector3i","args":[1,2,3]})");
test(Transform2D(1, 2, 3, 4, 5, 6), R"({"type":"Transform2D","args":[1.0,2.0,3.0,4.0,5.0,6.0]})");
test(Vector4(1, 2, 3, 4), R"({"type":"Vector4","args":[1.0,2.0,3.0,4.0]})");
test(Vector4i(1, 2, 3, 4), R"({"type":"Vector4i","args":[1,2,3,4]})");
test(Plane(1, 2, 3, 4), R"({"type":"Plane","args":[1.0,2.0,3.0,4.0]})");
test(Quaternion(1, 2, 3, 4), R"({"type":"Quaternion","args":[1.0,2.0,3.0,4.0]})");
test(AABB(Vector3(1, 2, 3), Vector3(4, 5, 6)), R"({"type":"AABB","args":[1.0,2.0,3.0,4.0,5.0,6.0]})");
const Basis b(Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9));
test(b, R"({"type":"Basis","args":[1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0]})");
const Transform3D tr3d(Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9), Vector3(10, 11, 12));
test(tr3d, R"({"type":"Transform3D","args":[1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0]})");
const Projection p(Vector4(1, 2, 3, 4), Vector4(5, 6, 7, 8), Vector4(9, 10, 11, 12), Vector4(13, 14, 15, 16));
test(p, R"({"type":"Projection","args":[1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,16.0]})");
test(Color(1, 2, 3, 4), R"({"type":"Color","args":[1.0,2.0,3.0,4.0]})");
// `Object`.
Ref<Resource> res;
res.instantiate();
// The properties are stored in an array because the order in which they are assigned may be important during initialization.
const String res_repr = R"({"type":"Resource","props":["resource_local_to_scene",false,"resource_name","s:","script",null]})";
test(res, res_repr, true);
ERR_PRINT_OFF;
CHECK(encode(res) == "null");
CHECK(decode(res_repr).get_type() == Variant::NIL);
ERR_PRINT_ON;
// `Dictionary`.
Dictionary dict;
dict[false] = true;
dict[0] = 1;
dict[0.0] = 1.0;
// Godot dictionaries preserve insertion order, so an array is used for keys/values.
test(dict, R"({"type":"Dictionary","args":[false,true,"i:0","i:1","f:0.0","f:1.0"]})");
TypedDictionary<int64_t, int64_t> int_int_dict;
int_int_dict[1] = 2;
int_int_dict[3] = 4;
test(int_int_dict, R"({"type":"Dictionary","key_type":"int","value_type":"int","args":["i:1","i:2","i:3","i:4"]})");
TypedDictionary<int64_t, Variant> int_var_dict;
int_var_dict[1] = "2";
int_var_dict[3] = "4";
test(int_var_dict, R"({"type":"Dictionary","key_type":"int","args":["i:1","s:2","i:3","s:4"]})");
TypedDictionary<Variant, int64_t> var_int_dict;
var_int_dict["1"] = 2;
var_int_dict["3"] = 4;
test(var_int_dict, R"({"type":"Dictionary","value_type":"int","args":["s:1","i:2","s:3","i:4"]})");
Dictionary dict2;
dict2["x"] = res;
const String dict2_repr = vformat(R"({"type":"Dictionary","args":["s:x",%s]})", res_repr);
test(dict2, dict2_repr, true);
ERR_PRINT_OFF;
CHECK(encode(dict2) == R"({"type":"Dictionary","args":["s:x",null]})");
CHECK(decode(dict2_repr).get_construct_string() == "{\n\"x\": null\n}");
ERR_PRINT_ON;
TypedDictionary<String, Resource> res_dict;
res_dict["x"] = res;
const String res_dict_repr = vformat(R"({"type":"Dictionary","key_type":"String","value_type":"Resource","args":["s:x",%s]})", res_repr);
test(res_dict, res_dict_repr, true);
ERR_PRINT_OFF;
CHECK(encode(res_dict) == "null");
CHECK(decode(res_dict_repr).get_type() == Variant::NIL);
ERR_PRINT_ON;
// `Array`.
Array arr = { true, 1, "abc" };
test(arr, R"([true,"i:1","s:abc"])");
TypedArray<int64_t> int_arr = { 1, 2, 3 };
test(int_arr, R"({"type":"Array","elem_type":"int","args":["i:1","i:2","i:3"]})");
Array arr2 = { 1, res, 9 };
const String arr2_repr = vformat(R"(["i:1",%s,"i:9"])", res_repr);
test(arr2, arr2_repr, true);
ERR_PRINT_OFF;
CHECK(encode(arr2) == R"(["i:1",null,"i:9"])");
CHECK(decode(arr2_repr).get_construct_string() == "[1, null, 9]");
ERR_PRINT_ON;
TypedArray<Resource> res_arr = { res };
const String res_arr_repr = vformat(R"({"type":"Array","elem_type":"Resource","args":[%s]})", res_repr);
test(res_arr, res_arr_repr, true);
ERR_PRINT_OFF;
CHECK(encode(res_arr) == "null");
CHECK(decode(res_arr_repr).get_type() == Variant::NIL);
ERR_PRINT_ON;
// Packed arrays.
test(PackedByteArray({ 1, 2, 3 }), R"({"type":"PackedByteArray","args":[1,2,3]})");
test(PackedInt32Array({ 1, 2, 3 }), R"({"type":"PackedInt32Array","args":[1,2,3]})");
test(PackedInt64Array({ 1, 2, 3 }), R"({"type":"PackedInt64Array","args":[1,2,3]})");
test(PackedFloat32Array({ 1, 2, 3 }), R"({"type":"PackedFloat32Array","args":[1.0,2.0,3.0]})");
test(PackedFloat64Array({ 1, 2, 3 }), R"({"type":"PackedFloat64Array","args":[1.0,2.0,3.0]})");
test(PackedStringArray({ "a", "b", "c" }), R"({"type":"PackedStringArray","args":["a","b","c"]})");
const PackedVector2Array pv2arr({ Vector2(1, 2), Vector2(3, 4), Vector2(5, 6) });
test(pv2arr, R"({"type":"PackedVector2Array","args":[1.0,2.0,3.0,4.0,5.0,6.0]})");
const PackedVector3Array pv3arr({ Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9) });
test(pv3arr, R"({"type":"PackedVector3Array","args":[1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0]})");
const PackedColorArray pcolarr({ Color(1, 2, 3, 4), Color(5, 6, 7, 8), Color(9, 10, 11, 12) });
test(pcolarr, R"({"type":"PackedColorArray","args":[1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0]})");
const PackedVector4Array pv4arr({ Vector4(1, 2, 3, 4), Vector4(5, 6, 7, 8), Vector4(9, 10, 11, 12) });
test(pv4arr, R"({"type":"PackedVector4Array","args":[1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0]})");
}
} // namespace TestJSONNative

167
tests/core/io/test_logger.h Normal file
View File

@@ -0,0 +1,167 @@
/**************************************************************************/
/* test_logger.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/dir_access.h"
#include "core/io/logger.h"
#include "modules/regex/regex.h"
#include "tests/test_macros.h"
namespace TestLogger {
constexpr int sleep_duration = 1200000;
void initialize_logs() {
ProjectSettings::get_singleton()->set_setting("application/config/name", "godot_tests");
DirAccess::make_dir_recursive_absolute(OS::get_singleton()->get_user_data_dir().path_join("logs"));
}
void cleanup_logs() {
ProjectSettings::get_singleton()->set_setting("application/config/name", "godot_tests");
Ref<DirAccess> dir = DirAccess::open("user://logs");
dir->list_dir_begin();
String file = dir->get_next();
while (file != "") {
if (file.match("*.log")) {
dir->remove(file);
}
file = dir->get_next();
}
DirAccess::remove_absolute(OS::get_singleton()->get_user_data_dir().path_join("logs"));
DirAccess::remove_absolute(OS::get_singleton()->get_user_data_dir());
}
TEST_CASE("[Logger][RotatedFileLogger] Creates the first log file and logs on it") {
initialize_logs();
String waiting_for_godot = "Waiting for Godot";
RotatedFileLogger logger("user://logs/godot.log");
logger.logf("%s", "Waiting for Godot");
Error err = Error::OK;
Ref<FileAccess> log = FileAccess::open("user://logs/godot.log", FileAccess::READ, &err);
CHECK_EQ(err, Error::OK);
CHECK_EQ(log->get_as_text(), waiting_for_godot);
cleanup_logs();
}
void get_log_files(Vector<String> &log_files) {
Ref<DirAccess> dir = DirAccess::open("user://logs");
dir->list_dir_begin();
String file = dir->get_next();
while (file != "") {
// Filtering godot.log because ordered_insert will put it first and should be the last.
if (file.match("*.log") && file != "godot.log") {
log_files.ordered_insert(file);
}
file = dir->get_next();
}
if (FileAccess::exists("user://logs/godot.log")) {
log_files.push_back("godot.log");
}
}
// All things related to log file rotation are in the same test because testing it require some sleeps.
TEST_CASE("[Logger][RotatedFileLogger] Rotates logs files") {
initialize_logs();
Vector<String> all_waiting_for_godot;
const int number_of_files = 3;
for (int i = 0; i < number_of_files; i++) {
String waiting_for_godot = "Waiting for Godot " + itos(i);
RotatedFileLogger logger("user://logs/godot.log", number_of_files);
logger.logf("%s", waiting_for_godot.ascii().get_data());
all_waiting_for_godot.push_back(waiting_for_godot);
// Required to ensure the rotation of the log file.
OS::get_singleton()->delay_usec(sleep_duration);
}
Vector<String> log_files;
get_log_files(log_files);
CHECK_MESSAGE(log_files.size() == number_of_files, "Did not rotate all files");
for (int i = 0; i < log_files.size(); i++) {
Error err = Error::OK;
Ref<FileAccess> log_file = FileAccess::open("user://logs/" + log_files[i], FileAccess::READ, &err);
REQUIRE_EQ(err, Error::OK);
CHECK_EQ(log_file->get_as_text(), all_waiting_for_godot[i]);
}
// Required to ensure the rotation of the log file.
OS::get_singleton()->delay_usec(sleep_duration);
// This time the oldest log must be removed and godot.log updated.
String new_waiting_for_godot = "Waiting for Godot " + itos(number_of_files);
all_waiting_for_godot = all_waiting_for_godot.slice(1, all_waiting_for_godot.size());
all_waiting_for_godot.push_back(new_waiting_for_godot);
RotatedFileLogger logger("user://logs/godot.log", number_of_files);
logger.logf("%s", new_waiting_for_godot.ascii().get_data());
log_files.clear();
get_log_files(log_files);
CHECK_MESSAGE(log_files.size() == number_of_files, "Did not remove old log file");
for (int i = 0; i < log_files.size(); i++) {
Error err = Error::OK;
Ref<FileAccess> log_file = FileAccess::open("user://logs/" + log_files[i], FileAccess::READ, &err);
REQUIRE_EQ(err, Error::OK);
CHECK_EQ(log_file->get_as_text(), all_waiting_for_godot[i]);
}
cleanup_logs();
}
TEST_CASE("[Logger][CompositeLogger] Logs the same into multiple loggers") {
initialize_logs();
Vector<Logger *> all_loggers;
all_loggers.push_back(memnew(RotatedFileLogger("user://logs/godot_logger_1.log", 1)));
all_loggers.push_back(memnew(RotatedFileLogger("user://logs/godot_logger_2.log", 1)));
String waiting_for_godot = "Waiting for Godot";
CompositeLogger logger(all_loggers);
logger.logf("%s", "Waiting for Godot");
Error err = Error::OK;
Ref<FileAccess> log = FileAccess::open("user://logs/godot_logger_1.log", FileAccess::READ, &err);
CHECK_EQ(err, Error::OK);
CHECK_EQ(log->get_as_text(), waiting_for_godot);
log = FileAccess::open("user://logs/godot_logger_2.log", FileAccess::READ, &err);
CHECK_EQ(err, Error::OK);
CHECK_EQ(log->get_as_text(), waiting_for_godot);
cleanup_logs();
}
} // namespace TestLogger

View File

@@ -0,0 +1,492 @@
/**************************************************************************/
/* test_marshalls.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/marshalls.h"
#include "tests/test_macros.h"
namespace TestMarshalls {
TEST_CASE("[Marshalls] Unsigned 16 bit integer encoding") {
uint8_t arr[2];
unsigned int actual_size = encode_uint16(0x1234, arr);
CHECK(actual_size == sizeof(uint16_t));
CHECK_MESSAGE(arr[0] == 0x34, "First encoded byte value should be equal to low order byte value.");
CHECK_MESSAGE(arr[1] == 0x12, "Last encoded byte value should be equal to high order byte value.");
}
TEST_CASE("[Marshalls] Unsigned 32 bit integer encoding") {
uint8_t arr[4];
unsigned int actual_size = encode_uint32(0x12345678, arr);
CHECK(actual_size == sizeof(uint32_t));
CHECK_MESSAGE(arr[0] == 0x78, "First encoded byte value should be equal to low order byte value.");
CHECK(arr[1] == 0x56);
CHECK(arr[2] == 0x34);
CHECK_MESSAGE(arr[3] == 0x12, "Last encoded byte value should be equal to high order byte value.");
}
TEST_CASE("[Marshalls] Unsigned 64 bit integer encoding") {
uint8_t arr[8];
unsigned int actual_size = encode_uint64(0x0f123456789abcdef, arr);
CHECK(actual_size == sizeof(uint64_t));
CHECK_MESSAGE(arr[0] == 0xef, "First encoded byte value should be equal to low order byte value.");
CHECK(arr[1] == 0xcd);
CHECK(arr[2] == 0xab);
CHECK(arr[3] == 0x89);
CHECK(arr[4] == 0x67);
CHECK(arr[5] == 0x45);
CHECK(arr[6] == 0x23);
CHECK_MESSAGE(arr[7] == 0xf1, "Last encoded byte value should be equal to high order byte value.");
}
TEST_CASE("[Marshalls] Unsigned 16 bit integer decoding") {
uint8_t arr[] = { 0x34, 0x12 };
CHECK(decode_uint16(arr) == 0x1234);
}
TEST_CASE("[Marshalls] Unsigned 32 bit integer decoding") {
uint8_t arr[] = { 0x78, 0x56, 0x34, 0x12 };
CHECK(decode_uint32(arr) == 0x12345678);
}
TEST_CASE("[Marshalls] Unsigned 64 bit integer decoding") {
uint8_t arr[] = { 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0xf1 };
CHECK(decode_uint64(arr) == 0x0f123456789abcdef);
}
TEST_CASE("[Marshalls] Floating point half precision encoding") {
uint8_t arr[2];
// Decimal: 0.33325195
// IEEE 754 half-precision binary floating-point format:
// sign exponent (5 bits) fraction (10 bits)
// 0 01101 0101010101
// Hexadecimal: 0x3555
unsigned int actual_size = encode_half(0.33325195f, arr);
CHECK(actual_size == sizeof(uint16_t));
CHECK(arr[0] == 0x55);
CHECK(arr[1] == 0x35);
}
TEST_CASE("[Marshalls] Floating point single precision encoding") {
uint8_t arr[4];
// Decimal: 0.15625
// IEEE 754 single-precision binary floating-point format:
// sign exponent (8 bits) fraction (23 bits)
// 0 01111100 01000000000000000000000
// Hexadecimal: 0x3E200000
unsigned int actual_size = encode_float(0.15625f, arr);
CHECK(actual_size == sizeof(uint32_t));
CHECK(arr[0] == 0x00);
CHECK(arr[1] == 0x00);
CHECK(arr[2] == 0x20);
CHECK(arr[3] == 0x3e);
}
TEST_CASE("[Marshalls] Floating point double precision encoding") {
uint8_t arr[8];
// Decimal: 0.333333333333333314829616256247390992939472198486328125
// IEEE 754 double-precision binary floating-point format:
// sign exponent (11 bits) fraction (52 bits)
// 0 01111111101 0101010101010101010101010101010101010101010101010101
// Hexadecimal: 0x3FD5555555555555
unsigned int actual_size = encode_double(0.33333333333333333, arr);
CHECK(actual_size == sizeof(uint64_t));
CHECK(arr[0] == 0x55);
CHECK(arr[1] == 0x55);
CHECK(arr[2] == 0x55);
CHECK(arr[3] == 0x55);
CHECK(arr[4] == 0x55);
CHECK(arr[5] == 0x55);
CHECK(arr[6] == 0xd5);
CHECK(arr[7] == 0x3f);
}
TEST_CASE("[Marshalls] Floating point half precision decoding") {
uint8_t arr[] = { 0x55, 0x35 };
// See floating point half precision encoding test case for details behind expected values.
CHECK(decode_half(arr) == 0.33325195f);
}
TEST_CASE("[Marshalls] Floating point single precision decoding") {
uint8_t arr[] = { 0x00, 0x00, 0x20, 0x3e };
// See floating point encoding test case for details behind expected values
CHECK(decode_float(arr) == 0.15625f);
}
TEST_CASE("[Marshalls] Floating point double precision decoding") {
uint8_t arr[] = { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0xd5, 0x3f };
// See floating point encoding test case for details behind expected values
CHECK(decode_double(arr) == 0.33333333333333333);
}
TEST_CASE("[Marshalls] C string encoding") {
char cstring[] = "Godot"; // 5 characters
uint8_t data[6];
int actual_size = encode_cstring(cstring, data);
CHECK(actual_size == 6);
CHECK(data[0] == 'G');
CHECK(data[1] == 'o');
CHECK(data[2] == 'd');
CHECK(data[3] == 'o');
CHECK(data[4] == 't');
CHECK(data[5] == '\0');
}
TEST_CASE("[Marshalls] NIL Variant encoding") {
int r_len;
Variant variant;
uint8_t buffer[4];
CHECK(encode_variant(variant, buffer, r_len) == OK);
CHECK_MESSAGE(r_len == 4, "Length == 4 bytes for header.");
CHECK_MESSAGE(buffer[0] == 0x00, "Variant::NIL");
CHECK(buffer[1] == 0x00);
CHECK(buffer[2] == 0x00);
CHECK(buffer[3] == 0x00);
// No value
}
TEST_CASE("[Marshalls] INT 32 bit Variant encoding") {
int r_len;
Variant variant(0x12345678);
uint8_t buffer[8];
CHECK(encode_variant(variant, buffer, r_len) == OK);
CHECK_MESSAGE(r_len == 8, "Length == 4 bytes for header + 4 bytes for `int32_t`.");
CHECK_MESSAGE(buffer[0] == 0x02, "Variant::INT");
CHECK(buffer[1] == 0x00);
CHECK(buffer[2] == 0x00);
CHECK(buffer[3] == 0x00);
// Check value
CHECK(buffer[4] == 0x78);
CHECK(buffer[5] == 0x56);
CHECK(buffer[6] == 0x34);
CHECK(buffer[7] == 0x12);
}
TEST_CASE("[Marshalls] INT 64 bit Variant encoding") {
int r_len;
Variant variant(uint64_t(0x0f123456789abcdef));
uint8_t buffer[12];
CHECK(encode_variant(variant, buffer, r_len) == OK);
CHECK_MESSAGE(r_len == 12, "Length == 4 bytes for header + 8 bytes for `int64_t`.");
CHECK_MESSAGE(buffer[0] == 0x02, "Variant::INT");
CHECK(buffer[1] == 0x00);
CHECK_MESSAGE(buffer[2] == 0x01, "HEADER_DATA_FLAG_64");
CHECK(buffer[3] == 0x00);
// Check value
CHECK(buffer[4] == 0xef);
CHECK(buffer[5] == 0xcd);
CHECK(buffer[6] == 0xab);
CHECK(buffer[7] == 0x89);
CHECK(buffer[8] == 0x67);
CHECK(buffer[9] == 0x45);
CHECK(buffer[10] == 0x23);
CHECK(buffer[11] == 0xf1);
}
TEST_CASE("[Marshalls] FLOAT single precision Variant encoding") {
int r_len;
Variant variant(0.15625f);
uint8_t buffer[8];
CHECK(encode_variant(variant, buffer, r_len) == OK);
CHECK_MESSAGE(r_len == 8, "Length == 4 bytes for header + 4 bytes for `float`.");
CHECK_MESSAGE(buffer[0] == 0x03, "Variant::FLOAT");
CHECK(buffer[1] == 0x00);
CHECK(buffer[2] == 0x00);
CHECK(buffer[3] == 0x00);
// Check value
CHECK(buffer[4] == 0x00);
CHECK(buffer[5] == 0x00);
CHECK(buffer[6] == 0x20);
CHECK(buffer[7] == 0x3e);
}
TEST_CASE("[Marshalls] FLOAT double precision Variant encoding") {
int r_len;
Variant variant(0.33333333333333333);
uint8_t buffer[12];
CHECK(encode_variant(variant, buffer, r_len) == OK);
CHECK_MESSAGE(r_len == 12, "Length == 4 bytes for header + 8 bytes for `double`.");
CHECK_MESSAGE(buffer[0] == 0x03, "Variant::FLOAT");
CHECK(buffer[1] == 0x00);
CHECK_MESSAGE(buffer[2] == 0x01, "HEADER_DATA_FLAG_64");
CHECK(buffer[3] == 0x00);
// Check value
CHECK(buffer[4] == 0x55);
CHECK(buffer[5] == 0x55);
CHECK(buffer[6] == 0x55);
CHECK(buffer[7] == 0x55);
CHECK(buffer[8] == 0x55);
CHECK(buffer[9] == 0x55);
CHECK(buffer[10] == 0xd5);
CHECK(buffer[11] == 0x3f);
}
TEST_CASE("[Marshalls] Invalid data Variant decoding") {
Variant variant;
int r_len = 0;
uint8_t some_buffer[1] = { 0x00 };
uint8_t out_of_range_type_buffer[4] = { 0xff }; // Greater than Variant::VARIANT_MAX
ERR_PRINT_OFF;
CHECK(decode_variant(variant, some_buffer, /* less than 4 */ 1, &r_len) == ERR_INVALID_DATA);
CHECK(r_len == 0);
CHECK(decode_variant(variant, out_of_range_type_buffer, 4, &r_len) == ERR_INVALID_DATA);
CHECK(r_len == 0);
ERR_PRINT_ON;
}
TEST_CASE("[Marshalls] NIL Variant decoding") {
Variant variant;
int r_len;
uint8_t buffer[] = {
0x00, 0x00, 0x00, 0x00 // Variant::NIL
};
CHECK(decode_variant(variant, buffer, 4, &r_len) == OK);
CHECK(r_len == 4);
CHECK(variant == Variant());
}
TEST_CASE("[Marshalls] INT 32 bit Variant decoding") {
Variant variant;
int r_len;
uint8_t buffer[] = {
0x02, 0x00, 0x00, 0x00, // Variant::INT
0x78, 0x56, 0x34, 0x12 // value
};
CHECK(decode_variant(variant, buffer, 8, &r_len) == OK);
CHECK(r_len == 8);
CHECK(variant == Variant(0x12345678));
}
TEST_CASE("[Marshalls] INT 64 bit Variant decoding") {
Variant variant;
int r_len;
uint8_t buffer[] = {
0x02, 0x00, 0x01, 0x00, // Variant::INT, HEADER_DATA_FLAG_64
0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0xf1 // value
};
CHECK(decode_variant(variant, buffer, 12, &r_len) == OK);
CHECK(r_len == 12);
CHECK(variant == Variant(uint64_t(0x0f123456789abcdef)));
}
TEST_CASE("[Marshalls] FLOAT single precision Variant decoding") {
Variant variant;
int r_len;
uint8_t buffer[] = {
0x03, 0x00, 0x00, 0x00, // Variant::FLOAT
0x00, 0x00, 0x20, 0x3e // value
};
CHECK(decode_variant(variant, buffer, 8, &r_len) == OK);
CHECK(r_len == 8);
CHECK(variant == Variant(0.15625f));
}
TEST_CASE("[Marshalls] FLOAT double precision Variant decoding") {
Variant variant;
int r_len;
uint8_t buffer[] = {
0x03, 0x00, 0x01, 0x00, // Variant::FLOAT, HEADER_DATA_FLAG_64
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0xd5, 0x3f // value
};
CHECK(decode_variant(variant, buffer, 12, &r_len) == OK);
CHECK(r_len == 12);
CHECK(variant == Variant(0.33333333333333333));
}
TEST_CASE("[Marshalls] Typed array encoding") {
int r_len;
Array array;
array.set_typed(Variant::INT, StringName(), Ref<Script>());
array.push_back(Variant(uint64_t(0x0f123456789abcdef)));
uint8_t buffer[24];
CHECK(encode_variant(array, buffer, r_len) == OK);
CHECK_MESSAGE(r_len == 24, "Length == 4 bytes for header + 4 bytes for array type + 4 bytes for array size + 12 bytes for element.");
CHECK_MESSAGE(buffer[0] == 0x1c, "Variant::ARRAY");
CHECK(buffer[1] == 0x00);
CHECK_MESSAGE(buffer[2] == 0x01, "CONTAINER_TYPE_KIND_BUILTIN");
CHECK(buffer[3] == 0x00);
// Check array type.
CHECK_MESSAGE(buffer[4] == 0x02, "Variant::INT");
CHECK(buffer[5] == 0x00);
CHECK(buffer[6] == 0x00);
CHECK(buffer[7] == 0x00);
// Check array size.
CHECK(buffer[8] == 0x01);
CHECK(buffer[9] == 0x00);
CHECK(buffer[10] == 0x00);
CHECK(buffer[11] == 0x00);
// Check element type.
CHECK_MESSAGE(buffer[12] == 0x02, "Variant::INT");
CHECK(buffer[13] == 0x00);
CHECK_MESSAGE(buffer[14] == 0x01, "HEADER_DATA_FLAG_64");
CHECK(buffer[15] == 0x00);
// Check element value.
CHECK(buffer[16] == 0xef);
CHECK(buffer[17] == 0xcd);
CHECK(buffer[18] == 0xab);
CHECK(buffer[19] == 0x89);
CHECK(buffer[20] == 0x67);
CHECK(buffer[21] == 0x45);
CHECK(buffer[22] == 0x23);
CHECK(buffer[23] == 0xf1);
}
TEST_CASE("[Marshalls] Typed array decoding") {
Variant variant;
int r_len;
uint8_t buffer[] = {
0x1c, 0x00, 0x01, 0x00, // Variant::ARRAY, CONTAINER_TYPE_KIND_BUILTIN
0x02, 0x00, 0x00, 0x00, // Array type (Variant::INT).
0x01, 0x00, 0x00, 0x00, // Array size.
0x02, 0x00, 0x01, 0x00, // Element type (Variant::INT, HEADER_DATA_FLAG_64).
0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0xf1, // Element value.
};
CHECK(decode_variant(variant, buffer, 24, &r_len) == OK);
CHECK(r_len == 24);
CHECK(variant.get_type() == Variant::ARRAY);
Array array = variant;
CHECK(array.get_typed_builtin() == Variant::INT);
CHECK(array.size() == 1);
CHECK(array[0] == Variant(uint64_t(0x0f123456789abcdef)));
}
TEST_CASE("[Marshalls] Typed dicttionary encoding") {
int r_len;
Dictionary dictionary;
dictionary.set_typed(Variant::INT, StringName(), Ref<Script>(), Variant::INT, StringName(), Ref<Script>());
dictionary[Variant(uint64_t(0x0f123456789abcdef))] = Variant(uint64_t(0x0f123456789abcdef));
uint8_t buffer[40];
CHECK(encode_variant(dictionary, buffer, r_len) == OK);
CHECK_MESSAGE(r_len == 40, "Length == 4 bytes for header + 8 bytes for dictionary type + 4 bytes for dictionary size + 24 bytes for key-value pair.");
CHECK_MESSAGE(buffer[0] == 0x1b, "Variant::DICTIONARY");
CHECK(buffer[1] == 0x00);
CHECK_MESSAGE(buffer[2] == 0x05, "key: CONTAINER_TYPE_KIND_BUILTIN | value: CONTAINER_TYPE_KIND_BUILTIN");
CHECK(buffer[3] == 0x00);
// Check dictionary key type.
CHECK_MESSAGE(buffer[4] == 0x02, "Variant::INT");
CHECK(buffer[5] == 0x00);
CHECK(buffer[6] == 0x00);
CHECK(buffer[7] == 0x00);
// Check dictionary value type.
CHECK_MESSAGE(buffer[8] == 0x02, "Variant::INT");
CHECK(buffer[9] == 0x00);
CHECK(buffer[10] == 0x00);
CHECK(buffer[11] == 0x00);
// Check dictionary size.
CHECK(buffer[12] == 0x01);
CHECK(buffer[13] == 0x00);
CHECK(buffer[14] == 0x00);
CHECK(buffer[15] == 0x00);
// Check key type.
CHECK_MESSAGE(buffer[16] == 0x02, "Variant::INT");
CHECK(buffer[17] == 0x00);
CHECK_MESSAGE(buffer[18] == 0x01, "HEADER_DATA_FLAG_64");
CHECK(buffer[19] == 0x00);
// Check key value.
CHECK(buffer[20] == 0xef);
CHECK(buffer[21] == 0xcd);
CHECK(buffer[22] == 0xab);
CHECK(buffer[23] == 0x89);
CHECK(buffer[24] == 0x67);
CHECK(buffer[25] == 0x45);
CHECK(buffer[26] == 0x23);
CHECK(buffer[27] == 0xf1);
// Check value type.
CHECK_MESSAGE(buffer[28] == 0x02, "Variant::INT");
CHECK(buffer[29] == 0x00);
CHECK_MESSAGE(buffer[30] == 0x01, "HEADER_DATA_FLAG_64");
CHECK(buffer[31] == 0x00);
// Check value value.
CHECK(buffer[32] == 0xef);
CHECK(buffer[33] == 0xcd);
CHECK(buffer[34] == 0xab);
CHECK(buffer[35] == 0x89);
CHECK(buffer[36] == 0x67);
CHECK(buffer[37] == 0x45);
CHECK(buffer[38] == 0x23);
CHECK(buffer[39] == 0xf1);
}
TEST_CASE("[Marshalls] Typed dictionary decoding") {
Variant variant;
int r_len;
uint8_t buffer[] = {
0x1b, 0x00, 0x05, 0x00, // Variant::DICTIONARY, key: CONTAINER_TYPE_KIND_BUILTIN | value: CONTAINER_TYPE_KIND_BUILTIN
0x02, 0x00, 0x00, 0x00, // Dictionary key type (Variant::INT).
0x02, 0x00, 0x00, 0x00, // Dictionary value type (Variant::INT).
0x01, 0x00, 0x00, 0x00, // Dictionary size.
0x02, 0x00, 0x01, 0x00, // Key type (Variant::INT, HEADER_DATA_FLAG_64).
0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0xf1, // Key value.
0x02, 0x00, 0x01, 0x00, // Value type (Variant::INT, HEADER_DATA_FLAG_64).
0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0xf1, // Value value.
};
CHECK(decode_variant(variant, buffer, 40, &r_len) == OK);
CHECK(r_len == 40);
CHECK(variant.get_type() == Variant::DICTIONARY);
Dictionary dictionary = variant;
CHECK(dictionary.get_typed_key_builtin() == Variant::INT);
CHECK(dictionary.get_typed_value_builtin() == Variant::INT);
CHECK(dictionary.size() == 1);
CHECK(dictionary.has(Variant(uint64_t(0x0f123456789abcdef))));
CHECK(dictionary[Variant(uint64_t(0x0f123456789abcdef))] == Variant(uint64_t(0x0f123456789abcdef)));
}
} // namespace TestMarshalls

View File

@@ -0,0 +1,201 @@
/**************************************************************************/
/* test_packet_peer.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/packet_peer.h"
#include "tests/test_macros.h"
namespace TestPacketPeer {
TEST_CASE("[PacketPeer][PacketPeerStream] Encode buffer max size") {
Ref<PacketPeerStream> pps;
pps.instantiate();
SUBCASE("Default value") {
CHECK_EQ(pps->get_encode_buffer_max_size(), 8 * 1024 * 1024);
}
SUBCASE("Max encode buffer must be at least 1024 bytes") {
ERR_PRINT_OFF;
pps->set_encode_buffer_max_size(42);
ERR_PRINT_ON;
CHECK_EQ(pps->get_encode_buffer_max_size(), 8 * 1024 * 1024);
}
SUBCASE("Max encode buffer cannot exceed 256 MiB") {
ERR_PRINT_OFF;
pps->set_encode_buffer_max_size((256 * 1024 * 1024) + 42);
ERR_PRINT_ON;
CHECK_EQ(pps->get_encode_buffer_max_size(), 8 * 1024 * 1024);
}
SUBCASE("Should be next power of two") {
pps->set_encode_buffer_max_size(2000);
CHECK_EQ(pps->get_encode_buffer_max_size(), 2048);
}
}
TEST_CASE("[PacketPeer][PacketPeerStream] Read a variant from peer") {
String godot_rules = "Godot Rules!!!";
Ref<StreamPeerBuffer> spb;
spb.instantiate();
spb->put_var(godot_rules);
spb->seek(0);
Ref<PacketPeerStream> pps;
pps.instantiate();
pps->set_stream_peer(spb);
Variant value;
CHECK_EQ(pps->get_var(value), Error::OK);
CHECK_EQ(String(value), godot_rules);
}
TEST_CASE("[PacketPeer][PacketPeerStream] Read a variant from peer fails") {
Ref<PacketPeerStream> pps;
pps.instantiate();
Variant value;
ERR_PRINT_OFF;
CHECK_EQ(pps->get_var(value), Error::ERR_UNCONFIGURED);
ERR_PRINT_ON;
}
TEST_CASE("[PacketPeer][PacketPeerStream] Put a variant to peer") {
String godot_rules = "Godot Rules!!!";
Ref<StreamPeerBuffer> spb;
spb.instantiate();
Ref<PacketPeerStream> pps;
pps.instantiate();
pps->set_stream_peer(spb);
CHECK_EQ(pps->put_var(godot_rules), Error::OK);
spb->seek(0);
CHECK_EQ(String(spb->get_var()), godot_rules);
}
TEST_CASE("[PacketPeer][PacketPeerStream] Put a variant to peer out of memory failure") {
String more_than_1mb = String("*").repeat(1024 + 1);
Ref<StreamPeerBuffer> spb;
spb.instantiate();
Ref<PacketPeerStream> pps;
pps.instantiate();
pps->set_stream_peer(spb);
pps->set_encode_buffer_max_size(1024);
ERR_PRINT_OFF;
CHECK_EQ(pps->put_var(more_than_1mb), Error::ERR_OUT_OF_MEMORY);
ERR_PRINT_ON;
}
TEST_CASE("[PacketPeer][PacketPeerStream] Get packet buffer") {
String godot_rules = "Godot Rules!!!";
Ref<StreamPeerBuffer> spb;
spb.instantiate();
// First 4 bytes are the length of the string.
CharString cs = godot_rules.ascii();
Vector<uint8_t> buffer = { (uint8_t)(cs.length() + 1), 0, 0, 0 };
buffer.resize_initialized(4 + cs.length() + 1);
memcpy(buffer.ptrw() + 4, cs.get_data(), cs.length());
spb->set_data_array(buffer);
Ref<PacketPeerStream> pps;
pps.instantiate();
pps->set_stream_peer(spb);
buffer.clear();
CHECK_EQ(pps->get_packet_buffer(buffer), Error::OK);
CHECK_EQ(String(reinterpret_cast<const char *>(buffer.ptr())), godot_rules);
}
TEST_CASE("[PacketPeer][PacketPeerStream] Get packet buffer from an empty peer") {
Ref<StreamPeerBuffer> spb;
spb.instantiate();
Ref<PacketPeerStream> pps;
pps.instantiate();
pps->set_stream_peer(spb);
Vector<uint8_t> buffer;
ERR_PRINT_OFF;
CHECK_EQ(pps->get_packet_buffer(buffer), Error::ERR_UNAVAILABLE);
ERR_PRINT_ON;
CHECK_EQ(buffer.size(), 0);
}
TEST_CASE("[PacketPeer][PacketPeerStream] Put packet buffer") {
String godot_rules = "Godot Rules!!!";
Ref<StreamPeerBuffer> spb;
spb.instantiate();
Ref<PacketPeerStream> pps;
pps.instantiate();
pps->set_stream_peer(spb);
CHECK_EQ(pps->put_packet_buffer(godot_rules.to_ascii_buffer()), Error::OK);
spb->seek(0);
CHECK_EQ(spb->get_string(), godot_rules);
// First 4 bytes are the length of the string.
CharString cs = godot_rules.ascii();
Vector<uint8_t> buffer = { (uint8_t)cs.length(), 0, 0, 0 };
buffer.resize(4 + cs.length());
memcpy(buffer.ptrw() + 4, cs.get_data(), cs.length());
CHECK_EQ(spb->get_data_array(), buffer);
}
TEST_CASE("[PacketPeer][PacketPeerStream] Put packet buffer when is empty") {
Ref<StreamPeerBuffer> spb;
spb.instantiate();
Ref<PacketPeerStream> pps;
pps.instantiate();
pps->set_stream_peer(spb);
Vector<uint8_t> buffer;
CHECK_EQ(pps->put_packet_buffer(buffer), Error::OK);
CHECK_EQ(spb->get_size(), 0);
}
} // namespace TestPacketPeer

View File

@@ -0,0 +1,119 @@
/**************************************************************************/
/* test_pck_packer.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/file_access_pack.h"
#include "core/io/pck_packer.h"
#include "core/os/os.h"
#include "tests/test_utils.h"
#include "thirdparty/doctest/doctest.h"
namespace TestPCKPacker {
TEST_CASE("[PCKPacker] Pack an empty PCK file") {
PCKPacker pck_packer;
const String output_pck_path = TestUtils::get_temp_path("output_empty.pck");
CHECK_MESSAGE(
pck_packer.pck_start(output_pck_path) == OK,
"Starting a PCK file should return an OK error code.");
CHECK_MESSAGE(
pck_packer.flush() == OK,
"Flushing the PCK should return an OK error code.");
Error err;
Ref<FileAccess> f = FileAccess::open(output_pck_path, FileAccess::READ, &err);
CHECK_MESSAGE(
err == OK,
"The generated empty PCK file should be opened successfully.");
CHECK_MESSAGE(
f->get_length() >= 100,
"The generated empty PCK file shouldn't be too small (it should have the PCK header).");
CHECK_MESSAGE(
f->get_length() <= 500,
"The generated empty PCK file shouldn't be too large.");
}
TEST_CASE("[PCKPacker] Pack empty with zero alignment invalid") {
PCKPacker pck_packer;
const String output_pck_path = TestUtils::get_temp_path("output_empty.pck");
ERR_PRINT_OFF;
CHECK_MESSAGE(pck_packer.pck_start(output_pck_path, 0) != OK, "PCK with zero alignment should fail.");
ERR_PRINT_ON;
}
TEST_CASE("[PCKPacker] Pack empty with invalid key") {
PCKPacker pck_packer;
const String output_pck_path = TestUtils::get_temp_path("output_empty.pck");
ERR_PRINT_OFF;
CHECK_MESSAGE(pck_packer.pck_start(output_pck_path, 32, "") != OK, "PCK with invalid key should fail.");
ERR_PRINT_ON;
}
TEST_CASE("[PCKPacker] Pack a PCK file with some files and directories") {
PCKPacker pck_packer;
const String output_pck_path = TestUtils::get_temp_path("output_with_files.pck");
CHECK_MESSAGE(
pck_packer.pck_start(output_pck_path) == OK,
"Starting a PCK file should return an OK error code.");
const String base_dir = OS::get_singleton()->get_executable_path().get_base_dir();
CHECK_MESSAGE(
pck_packer.add_file("version.py", base_dir.path_join("../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.path_join("../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.path_join("../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.path_join("../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;
Ref<FileAccess> f = FileAccess::open(output_pck_path, FileAccess::READ, &err);
CHECK_MESSAGE(
err == OK,
"The generated non-empty PCK file should be opened successfully.");
CHECK_MESSAGE(
f->get_length() >= 18000,
"The generated non-empty PCK file should be large enough to actually hold the contents specified above.");
CHECK_MESSAGE(
f->get_length() <= 27000,
"The generated non-empty PCK file shouldn't be too large.");
}
} // namespace TestPCKPacker

View File

@@ -0,0 +1,631 @@
/**************************************************************************/
/* test_resource.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/resource.h"
#include "core/io/resource_loader.h"
#include "core/io/resource_saver.h"
#include "core/os/os.h"
#include "scene/main/node.h"
#include "thirdparty/doctest/doctest.h"
#include "tests/test_macros.h"
#include <functional>
namespace TestResource {
enum TestDuplicateMode {
TEST_MODE_RESOURCE_DUPLICATE_SHALLOW,
TEST_MODE_RESOURCE_DUPLICATE_DEEP,
TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE,
TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE,
TEST_MODE_VARIANT_DUPLICATE_SHALLOW,
TEST_MODE_VARIANT_DUPLICATE_DEEP,
TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE,
};
class DuplicateGuineaPigData : public Object {
GDSOFTCLASS(DuplicateGuineaPigData, Object)
public:
const Variant SENTINEL_1 = "A";
const Variant SENTINEL_2 = 645;
const Variant SENTINEL_3 = StringName("X");
const Variant SENTINEL_4 = true;
Ref<Resource> SUBRES_1 = memnew(Resource);
Ref<Resource> SUBRES_2 = memnew(Resource);
Ref<Resource> SUBRES_3 = memnew(Resource);
Ref<Resource> SUBRES_SL_1 = memnew(Resource);
Ref<Resource> SUBRES_SL_2 = memnew(Resource);
Ref<Resource> SUBRES_SL_3 = memnew(Resource);
Variant obj; // Variant helps with lifetime so duplicates pointing to the same don't try to double-free it.
Array arr;
Dictionary dict;
Variant packed; // A PackedByteArray, but using Variant to be able to tell if the array is shared or not.
Ref<Resource> subres;
Ref<Resource> subres_sl;
void set_defaults() {
SUBRES_1->set_name("juan");
SUBRES_2->set_name("you");
SUBRES_3->set_name("tree");
SUBRES_SL_1->set_name("maybe_scene_local");
SUBRES_SL_2->set_name("perhaps_local_to_scene");
SUBRES_SL_3->set_name("sometimes_locality_scenial");
// To try some cases of internal and external.
SUBRES_1->set_path_cache("");
SUBRES_2->set_path_cache("local://hehe");
SUBRES_3->set_path_cache("res://some.tscn::1");
DEV_ASSERT(SUBRES_1->is_built_in());
DEV_ASSERT(SUBRES_2->is_built_in());
DEV_ASSERT(SUBRES_3->is_built_in());
SUBRES_SL_1->set_path_cache("res://thing.scn");
SUBRES_SL_2->set_path_cache("C:/not/really/possible/but/still/external");
SUBRES_SL_3->set_path_cache("/this/neither");
DEV_ASSERT(!SUBRES_SL_1->is_built_in());
DEV_ASSERT(!SUBRES_SL_2->is_built_in());
DEV_ASSERT(!SUBRES_SL_3->is_built_in());
obj = memnew(Object);
// Construct enough cases to test deep recursion involving resources;
// we mix some primitive values with recurses nested in different ways,
// acting as array values and dictionary keys and values, some of those
// being marked as scene-local when for subcases where scene-local is relevant.
arr.push_back(SENTINEL_1);
arr.push_back(SUBRES_1);
arr.push_back(SUBRES_SL_1);
{
Dictionary d;
d[SENTINEL_2] = SENTINEL_3;
d[SENTINEL_4] = SUBRES_2;
d[SUBRES_3] = SUBRES_SL_2;
d[SUBRES_SL_3] = SUBRES_1;
arr.push_back(d);
}
dict[SENTINEL_4] = SENTINEL_1;
dict[SENTINEL_2] = SUBRES_2;
dict[SUBRES_3] = SUBRES_SL_1;
dict[SUBRES_SL_2] = SUBRES_1;
{
Array a;
a.push_back(SENTINEL_3);
a.push_back(SUBRES_2);
a.push_back(SUBRES_SL_3);
dict[SENTINEL_4] = a;
}
packed = PackedByteArray{ 0xaa, 0xbb, 0xcc };
subres = SUBRES_1;
subres_sl = SUBRES_SL_1;
}
void verify_empty() const {
CHECK(obj.get_type() == Variant::NIL);
CHECK(arr.size() == 0);
CHECK(dict.size() == 0);
CHECK(packed.get_type() == Variant::NIL);
CHECK(subres.is_null());
}
void verify_duplication(const DuplicateGuineaPigData *p_orig, uint32_t p_property_usage, TestDuplicateMode p_test_mode, ResourceDeepDuplicateMode p_deep_mode) const {
if (!(p_property_usage & PROPERTY_USAGE_STORAGE)) {
verify_empty();
return;
}
// To see if each resource involved is copied once at most,
// and then the reference to the duplicate reused.
HashMap<Resource *, Resource *> duplicates;
auto _verify_resource = [&](const Ref<Resource> &p_dupe_res, const Ref<Resource> &p_orig_res, bool p_is_property = false) {
bool expect_true_copy = (p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP && p_orig_res->is_built_in()) ||
(p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_INTERNAL && p_orig_res->is_built_in()) ||
(p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_ALL) ||
(p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE && p_orig_res->is_local_to_scene()) ||
(p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_INTERNAL && p_orig_res->is_built_in()) ||
(p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_ALL);
if (expect_true_copy) {
if (p_deep_mode == RESOURCE_DEEP_DUPLICATE_NONE) {
expect_true_copy = false;
} else if (p_deep_mode == RESOURCE_DEEP_DUPLICATE_INTERNAL) {
expect_true_copy = p_orig_res->is_built_in();
}
}
if (p_is_property) {
if ((p_property_usage & PROPERTY_USAGE_ALWAYS_DUPLICATE)) {
expect_true_copy = true;
} else if ((p_property_usage & PROPERTY_USAGE_NEVER_DUPLICATE)) {
expect_true_copy = false;
}
}
if (expect_true_copy) {
CHECK(p_dupe_res != p_orig_res);
CHECK(p_dupe_res->get_name() == p_orig_res->get_name());
if (duplicates.has(p_orig_res.ptr())) {
CHECK(duplicates[p_orig_res.ptr()] == p_dupe_res.ptr());
} else {
duplicates[p_orig_res.ptr()] = p_dupe_res.ptr();
}
} else {
CHECK(p_dupe_res == p_orig_res);
}
};
std::function<void(const Variant &p_a, const Variant &p_b)> _verify_deep_copied_variants = [&](const Variant &p_a, const Variant &p_b) {
CHECK(p_a.get_type() == p_b.get_type());
const Ref<Resource> &res_a = p_a;
const Ref<Resource> &res_b = p_b;
if (res_a.is_valid()) {
_verify_resource(res_a, res_b);
} else if (p_a.get_type() == Variant::ARRAY) {
const Array &arr_a = p_a;
const Array &arr_b = p_b;
CHECK(!arr_a.is_same_instance(arr_b));
CHECK(arr_a.size() == arr_b.size());
for (int i = 0; i < arr_a.size(); i++) {
_verify_deep_copied_variants(arr_a[i], arr_b[i]);
}
} else if (p_a.get_type() == Variant::DICTIONARY) {
const Dictionary &dict_a = p_a;
const Dictionary &dict_b = p_b;
CHECK(!dict_a.is_same_instance(dict_b));
CHECK(dict_a.size() == dict_b.size());
for (int i = 0; i < dict_a.size(); i++) {
_verify_deep_copied_variants(dict_a.get_key_at_index(i), dict_b.get_key_at_index(i));
_verify_deep_copied_variants(dict_a.get_value_at_index(i), dict_b.get_value_at_index(i));
}
} else {
CHECK(p_a == p_b);
}
};
CHECK(this != p_orig);
CHECK((Object *)obj == (Object *)p_orig->obj);
bool expect_true_copy = p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP ||
p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE ||
p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE ||
p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP ||
p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE;
if (expect_true_copy) {
_verify_deep_copied_variants(arr, p_orig->arr);
_verify_deep_copied_variants(dict, p_orig->dict);
CHECK(!packed.identity_compare(p_orig->packed));
} else {
CHECK(arr.is_same_instance(p_orig->arr));
CHECK(dict.is_same_instance(p_orig->dict));
CHECK(packed.identity_compare(p_orig->packed));
}
_verify_resource(subres, p_orig->subres, true);
_verify_resource(subres_sl, p_orig->subres_sl, true);
}
void enable_scene_local_subresources() {
SUBRES_SL_1->set_local_to_scene(true);
SUBRES_SL_2->set_local_to_scene(true);
SUBRES_SL_3->set_local_to_scene(true);
}
virtual ~DuplicateGuineaPigData() {
Object *obj_ptr = obj.get_validated_object();
if (obj_ptr) {
memdelete(obj_ptr);
}
}
};
#define DEFINE_DUPLICATE_GUINEA_PIG(m_class_name, m_property_usage) \
class m_class_name : public Resource { \
GDCLASS(m_class_name, Resource) \
\
DuplicateGuineaPigData data; \
\
public: \
void set_obj(Object *p_obj) { \
data.obj = p_obj; \
} \
Object *get_obj() const { \
return data.obj; \
} \
\
void set_arr(const Array &p_arr) { \
data.arr = p_arr; \
} \
Array get_arr() const { \
return data.arr; \
} \
\
void set_dict(const Dictionary &p_dict) { \
data.dict = p_dict; \
} \
Dictionary get_dict() const { \
return data.dict; \
} \
\
void set_packed(const Variant &p_packed) { \
data.packed = p_packed; \
} \
Variant get_packed() const { \
return data.packed; \
} \
\
void set_subres(const Ref<Resource> &p_subres) { \
data.subres = p_subres; \
} \
Ref<Resource> get_subres() const { \
return data.subres; \
} \
\
void set_subres_sl(const Ref<Resource> &p_subres) { \
data.subres_sl = p_subres; \
} \
Ref<Resource> get_subres_sl() const { \
return data.subres_sl; \
} \
\
void set_defaults() { \
data.set_defaults(); \
} \
\
Object *get_data() { \
return &data; \
} \
\
void verify_duplication(const Ref<Resource> &p_orig, int p_test_mode, int p_deep_mode) const { \
const DuplicateGuineaPigData *orig_data = Object::cast_to<DuplicateGuineaPigData>(p_orig->call("get_data")); \
data.verify_duplication(orig_data, m_property_usage, (TestDuplicateMode)p_test_mode, (ResourceDeepDuplicateMode)p_deep_mode); \
} \
\
protected: \
static void _bind_methods() { \
ClassDB::bind_method(D_METHOD("set_obj", "obj"), &m_class_name::set_obj); \
ClassDB::bind_method(D_METHOD("get_obj"), &m_class_name::get_obj); \
\
ClassDB::bind_method(D_METHOD("set_arr", "arr"), &m_class_name::set_arr); \
ClassDB::bind_method(D_METHOD("get_arr"), &m_class_name::get_arr); \
\
ClassDB::bind_method(D_METHOD("set_dict", "dict"), &m_class_name::set_dict); \
ClassDB::bind_method(D_METHOD("get_dict"), &m_class_name::get_dict); \
\
ClassDB::bind_method(D_METHOD("set_packed", "packed"), &m_class_name::set_packed); \
ClassDB::bind_method(D_METHOD("get_packed"), &m_class_name::get_packed); \
\
ClassDB::bind_method(D_METHOD("set_subres", "subres"), &m_class_name::set_subres); \
ClassDB::bind_method(D_METHOD("get_subres"), &m_class_name::get_subres); \
\
ClassDB::bind_method(D_METHOD("set_subres_sl", "subres"), &m_class_name::set_subres_sl); \
ClassDB::bind_method(D_METHOD("get_subres_sl"), &m_class_name::get_subres_sl); \
\
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "obj", PROPERTY_HINT_NONE, "", m_property_usage), "set_obj", "get_obj"); \
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "arr", PROPERTY_HINT_NONE, "", m_property_usage), "set_arr", "get_arr"); \
ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "dict", PROPERTY_HINT_NONE, "", m_property_usage), "set_dict", "get_dict"); \
ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "packed", PROPERTY_HINT_NONE, "", m_property_usage), "set_packed", "get_packed"); \
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "subres", PROPERTY_HINT_NONE, "", m_property_usage), "set_subres", "get_subres"); \
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "subres_sl", PROPERTY_HINT_NONE, "", m_property_usage), "set_subres_sl", "get_subres_sl"); \
\
ClassDB::bind_method(D_METHOD("set_defaults"), &m_class_name::set_defaults); \
ClassDB::bind_method(D_METHOD("get_data"), &m_class_name::get_data); \
ClassDB::bind_method(D_METHOD("verify_duplication", "orig", "test_mode", "deep_mode"), &m_class_name::verify_duplication); \
} \
\
public: \
static m_class_name *register_and_instantiate() { \
static bool registered = false; \
if (!registered) { \
GDREGISTER_CLASS(m_class_name); \
registered = true; \
} \
return memnew(m_class_name); \
} \
};
DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_None, PROPERTY_USAGE_NONE)
DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Always, PROPERTY_USAGE_ALWAYS_DUPLICATE)
DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Storage, PROPERTY_USAGE_STORAGE)
DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Storage_Always, (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_ALWAYS_DUPLICATE))
DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Storage_Never, (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_NEVER_DUPLICATE))
TEST_CASE("[Resource] Duplication") {
auto _run_test = [](
TestDuplicateMode p_test_mode,
ResourceDeepDuplicateMode p_deep_mode,
Ref<Resource> (*p_duplicate_fn)(const Ref<Resource> &)) -> void {
LocalVector<Ref<Resource>> resources = {
DuplicateGuineaPig_None::register_and_instantiate(),
DuplicateGuineaPig_Always::register_and_instantiate(),
DuplicateGuineaPig_Storage::register_and_instantiate(),
DuplicateGuineaPig_Storage_Always::register_and_instantiate(),
DuplicateGuineaPig_Storage_Never::register_and_instantiate(),
};
for (const Ref<Resource> &orig : resources) {
INFO(std::string(String(orig->get_class_name()).utf8().get_data()));
orig->call("set_defaults");
const Ref<Resource> &dupe = p_duplicate_fn(orig);
dupe->call("verify_duplication", orig, p_test_mode, p_deep_mode);
}
};
SUBCASE("Resource::duplicate(), shallow") {
_run_test(
TEST_MODE_RESOURCE_DUPLICATE_SHALLOW,
RESOURCE_DEEP_DUPLICATE_MAX,
[](const Ref<Resource> &p_res) -> Ref<Resource> {
return p_res->duplicate(false);
});
}
SUBCASE("Resource::duplicate(), deep") {
_run_test(
TEST_MODE_RESOURCE_DUPLICATE_DEEP,
RESOURCE_DEEP_DUPLICATE_MAX,
[](const Ref<Resource> &p_res) -> Ref<Resource> {
return p_res->duplicate(true);
});
}
SUBCASE("Resource::duplicate_deep()") {
static int deep_mode = 0;
for (deep_mode = 0; deep_mode < RESOURCE_DEEP_DUPLICATE_MAX; deep_mode++) {
_run_test(
TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE,
(ResourceDeepDuplicateMode)deep_mode,
[](const Ref<Resource> &p_res) -> Ref<Resource> {
return p_res->duplicate_deep((ResourceDeepDuplicateMode)deep_mode);
});
}
}
SUBCASE("Resource::duplicate_for_local_scene()") {
static int mark_main_as_local = 0;
static int mark_some_subs_as_local = 0;
for (mark_main_as_local = 0; mark_main_as_local < 2; ++mark_main_as_local) { // Whether main is local-to-scene shouldn't matter.
for (mark_some_subs_as_local = 0; mark_some_subs_as_local < 2; ++mark_some_subs_as_local) {
_run_test(
TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE,
RESOURCE_DEEP_DUPLICATE_MAX,
[](const Ref<Resource> &p_res) -> Ref<Resource> {
if (mark_main_as_local) {
p_res->set_local_to_scene(true);
}
if (mark_some_subs_as_local) {
Object::cast_to<DuplicateGuineaPigData>(p_res->call("get_data"))->enable_scene_local_subresources();
}
HashMap<Ref<Resource>, Ref<Resource>> remap_cache;
Node fake_scene;
return p_res->duplicate_for_local_scene(&fake_scene, remap_cache);
});
}
}
}
SUBCASE("Variant::duplicate(), shallow") {
_run_test(
TEST_MODE_VARIANT_DUPLICATE_SHALLOW,
RESOURCE_DEEP_DUPLICATE_MAX,
[](const Ref<Resource> &p_res) -> Ref<Resource> {
return Variant(p_res).duplicate(false);
});
}
SUBCASE("Variant::duplicate(), deep") {
_run_test(
TEST_MODE_VARIANT_DUPLICATE_DEEP,
RESOURCE_DEEP_DUPLICATE_MAX,
[](const Ref<Resource> &p_res) -> Ref<Resource> {
return Variant(p_res).duplicate(true);
});
}
SUBCASE("Variant::duplicate_deep()") {
static int deep_mode = 0;
for (deep_mode = 0; deep_mode < RESOURCE_DEEP_DUPLICATE_MAX; deep_mode++) {
_run_test(
TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE,
(ResourceDeepDuplicateMode)deep_mode,
[](const Ref<Resource> &p_res) -> Ref<Resource> {
return Variant(p_res).duplicate_deep((ResourceDeepDuplicateMode)deep_mode);
});
}
}
SUBCASE("Via Variant, resource not being the root") {
// Variant controls the deep copy, recursing until resources are found, and then
// it's Resource who controls the deep copy from it onwards.
// Therefore, we have to test if Variant is able to track unique duplicates across
// multiple times Resource takes over.
// Since the other test cases already prove Resource's mechanism to have at most
// one duplicate per resource involved, the test for Variant is simple.
Ref<Resource> res;
res.instantiate();
res->set_name("risi");
Array a;
a.push_back(res);
{
Dictionary d;
d[res] = res;
a.push_back(d);
}
Array dupe_a;
Ref<Resource> dupe_res;
SUBCASE("Variant::duplicate(), shallow") {
dupe_a = Variant(a).duplicate(false);
// Ensure it's referencing the original.
dupe_res = dupe_a[0];
CHECK(dupe_res == res);
}
SUBCASE("Variant::duplicate(), deep") {
dupe_a = Variant(a).duplicate(true);
// Ensure it's referencing the original.
dupe_res = dupe_a[0];
CHECK(dupe_res == res);
}
SUBCASE("Variant::duplicate_deep(), no resources") {
dupe_a = Variant(a).duplicate_deep(RESOURCE_DEEP_DUPLICATE_NONE);
// Ensure it's referencing the original.
dupe_res = dupe_a[0];
CHECK(dupe_res == res);
}
SUBCASE("Variant::duplicate_deep(), with resources") {
dupe_a = Variant(a).duplicate_deep(RESOURCE_DEEP_DUPLICATE_ALL);
// Ensure it's a copy.
dupe_res = dupe_a[0];
CHECK(dupe_res != res);
CHECK(dupe_res->get_name() == "risi");
// Ensure the map is already gone so we get new instances.
Array dupe_a_2 = Variant(a).duplicate_deep(RESOURCE_DEEP_DUPLICATE_ALL);
CHECK(dupe_a_2[0] != dupe_a[0]);
}
// Ensure all the usages are of the same resource.
CHECK(((Dictionary)dupe_a[1]).get_key_at_index(0) == dupe_res);
CHECK(((Dictionary)dupe_a[1]).get_value_at_index(0) == dupe_res);
}
}
TEST_CASE("[Resource] Saving and loading") {
Ref<Resource> resource = memnew(Resource);
resource->set_name("Hello world");
resource->set_meta("ExampleMetadata", Vector2i(40, 80));
resource->set_meta("string", "The\nstring\nwith\nunnecessary\nline\n\t\\\nbreaks");
Ref<Resource> child_resource = memnew(Resource);
child_resource->set_name("I'm a child resource");
resource->set_meta("other_resource", child_resource);
const String save_path_binary = TestUtils::get_temp_path("resource.res");
const String save_path_text = TestUtils::get_temp_path("resource.tres");
ResourceSaver::save(resource, save_path_binary);
ResourceSaver::save(resource, save_path_text);
const Ref<Resource> &loaded_resource_binary = ResourceLoader::load(save_path_binary);
CHECK_MESSAGE(
loaded_resource_binary->get_name() == "Hello world",
"The loaded resource name should be equal to the expected value.");
CHECK_MESSAGE(
loaded_resource_binary->get_meta("ExampleMetadata") == Vector2i(40, 80),
"The loaded resource metadata should be equal to the expected value.");
CHECK_MESSAGE(
loaded_resource_binary->get_meta("string") == "The\nstring\nwith\nunnecessary\nline\n\t\\\nbreaks",
"The loaded resource metadata should be equal to the expected value.");
const Ref<Resource> &loaded_child_resource_binary = loaded_resource_binary->get_meta("other_resource");
CHECK_MESSAGE(
loaded_child_resource_binary->get_name() == "I'm a child resource",
"The loaded child resource name should be equal to the expected value.");
const Ref<Resource> &loaded_resource_text = ResourceLoader::load(save_path_text);
CHECK_MESSAGE(
loaded_resource_text->get_name() == "Hello world",
"The loaded resource name should be equal to the expected value.");
CHECK_MESSAGE(
loaded_resource_text->get_meta("ExampleMetadata") == Vector2i(40, 80),
"The loaded resource metadata should be equal to the expected value.");
CHECK_MESSAGE(
loaded_resource_text->get_meta("string") == "The\nstring\nwith\nunnecessary\nline\n\t\\\nbreaks",
"The loaded resource metadata should be equal to the expected value.");
const Ref<Resource> &loaded_child_resource_text = loaded_resource_text->get_meta("other_resource");
CHECK_MESSAGE(
loaded_child_resource_text->get_name() == "I'm a child resource",
"The loaded child resource name should be equal to the expected value.");
}
TEST_CASE("[Resource] Breaking circular references on save") {
Ref<Resource> resource_a = memnew(Resource);
resource_a->set_name("A");
Ref<Resource> resource_b = memnew(Resource);
resource_b->set_name("B");
Ref<Resource> resource_c = memnew(Resource);
resource_c->set_name("C");
resource_a->set_meta("next", resource_b);
resource_b->set_meta("next", resource_c);
resource_c->set_meta("next", resource_b);
const String save_path_binary = TestUtils::get_temp_path("resource.res");
const String save_path_text = TestUtils::get_temp_path("resource.tres");
ResourceSaver::save(resource_a, save_path_binary);
// Suppress expected errors caused by the resources above being uncached.
ERR_PRINT_OFF;
ResourceSaver::save(resource_a, save_path_text);
const Ref<Resource> &loaded_resource_a_binary = ResourceLoader::load(save_path_binary);
ERR_PRINT_ON;
CHECK_MESSAGE(
loaded_resource_a_binary->get_name() == "A",
"The loaded resource name should be equal to the expected value.");
const Ref<Resource> &loaded_resource_b_binary = loaded_resource_a_binary->get_meta("next");
CHECK_MESSAGE(
loaded_resource_b_binary->get_name() == "B",
"The loaded child resource name should be equal to the expected value.");
const Ref<Resource> &loaded_resource_c_binary = loaded_resource_b_binary->get_meta("next");
CHECK_MESSAGE(
loaded_resource_c_binary->get_name() == "C",
"The loaded child resource name should be equal to the expected value.");
CHECK_MESSAGE(
!loaded_resource_c_binary->has_meta("next"),
"The loaded child resource circular reference should be NULL.");
const Ref<Resource> &loaded_resource_a_text = ResourceLoader::load(save_path_text);
CHECK_MESSAGE(
loaded_resource_a_text->get_name() == "A",
"The loaded resource name should be equal to the expected value.");
const Ref<Resource> &loaded_resource_b_text = loaded_resource_a_text->get_meta("next");
CHECK_MESSAGE(
loaded_resource_b_text->get_name() == "B",
"The loaded child resource name should be equal to the expected value.");
const Ref<Resource> &loaded_resource_c_text = loaded_resource_b_text->get_meta("next");
CHECK_MESSAGE(
loaded_resource_c_text->get_name() == "C",
"The loaded child resource name should be equal to the expected value.");
CHECK_MESSAGE(
!loaded_resource_c_text->has_meta("next"),
"The loaded child resource circular reference should be NULL.");
// Break circular reference to avoid memory leak
resource_c->remove_meta("next");
}
} // namespace TestResource

View File

@@ -0,0 +1,68 @@
/**************************************************************************/
/* test_resource_uid.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/resource_uid.h"
#include "thirdparty/doctest/doctest.h"
#include "tests/test_macros.h"
namespace TestResourceUID {
TEST_CASE("[ResourceUID] Must encode/decode maximum/minimum UID correctly") {
CHECK_MESSAGE(ResourceUID::get_singleton()->id_to_text(0x7fffffffffffffff) == "uid://d4n4ub6itg400", "Maximum UID must encode correctly.");
CHECK_MESSAGE(ResourceUID::get_singleton()->text_to_id("uid://d4n4ub6itg400") == 0x7fffffffffffffff, "Maximum UID must decode correctly.");
CHECK_MESSAGE(ResourceUID::get_singleton()->id_to_text(0) == "uid://a", "Minimum UID must encode correctly.");
CHECK_MESSAGE(ResourceUID::get_singleton()->text_to_id("uid://a") == 0, "Minimum UID must decode correctly.");
}
TEST_CASE("[ResourceUID] Must encode and decode invalid UIDs correctly") {
ResourceUID *rid = ResourceUID::get_singleton();
CHECK_MESSAGE(rid->id_to_text(-1) == "uid://<invalid>", "Invalid UID -1 must encode correctly.");
CHECK_MESSAGE(rid->text_to_id("uid://<invalid>") == -1, "Invalid UID -1 must decode correctly.");
CHECK_MESSAGE(rid->id_to_text(-2) == rid->id_to_text(-1), "Invalid UID -2 must encode to the same as -1.");
CHECK_MESSAGE(rid->text_to_id("dm3rdgs30kfci") == -1, "UID without scheme must decode correctly.");
}
TEST_CASE("[ResourceUID] Must encode and decode various UIDs correctly") {
ResourceUID *rid = ResourceUID::get_singleton();
CHECK_MESSAGE(rid->id_to_text(1) == "uid://b", "UID 1 must encode correctly.");
CHECK_MESSAGE(rid->text_to_id("uid://b") == 1, "UID 1 must decode correctly.");
CHECK_MESSAGE(rid->id_to_text(8060368642360689600) == "uid://dm3rdgs30kfci", "A normal UID must encode correctly.");
CHECK_MESSAGE(rid->text_to_id("uid://dm3rdgs30kfci") == 8060368642360689600, "A normal UID must decode correctly.");
}
} // namespace TestResourceUID

View File

@@ -0,0 +1,308 @@
/**************************************************************************/
/* test_stream_peer.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/stream_peer.h"
#include "tests/test_macros.h"
namespace TestStreamPeer {
TEST_CASE("[StreamPeer] Initialization through StreamPeerBuffer") {
Ref<StreamPeerBuffer> spb;
spb.instantiate();
CHECK_EQ(spb->is_big_endian_enabled(), false);
}
TEST_CASE("[StreamPeer] Get and sets through StreamPeerBuffer") {
Ref<StreamPeerBuffer> spb;
spb.instantiate();
SUBCASE("A int8_t value") {
int8_t value = 42;
spb->clear();
spb->put_8(value);
spb->seek(0);
CHECK_EQ(spb->get_8(), value);
}
SUBCASE("A uint8_t value") {
uint8_t value = 42;
spb->clear();
spb->put_u8(value);
spb->seek(0);
CHECK_EQ(spb->get_u8(), value);
}
SUBCASE("A int16_t value") {
int16_t value = 42;
spb->clear();
spb->put_16(value);
spb->seek(0);
CHECK_EQ(spb->get_16(), value);
}
SUBCASE("A uint16_t value") {
uint16_t value = 42;
spb->clear();
spb->put_u16(value);
spb->seek(0);
CHECK_EQ(spb->get_u16(), value);
}
SUBCASE("A int32_t value") {
int32_t value = 42;
spb->clear();
spb->put_32(value);
spb->seek(0);
CHECK_EQ(spb->get_32(), value);
}
SUBCASE("A uint32_t value") {
uint32_t value = 42;
spb->clear();
spb->put_u32(value);
spb->seek(0);
CHECK_EQ(spb->get_u32(), value);
}
SUBCASE("A int64_t value") {
int64_t value = 42;
spb->clear();
spb->put_64(value);
spb->seek(0);
CHECK_EQ(spb->get_64(), value);
}
SUBCASE("A int64_t value") {
uint64_t value = 42;
spb->clear();
spb->put_u64(value);
spb->seek(0);
CHECK_EQ(spb->get_u64(), value);
}
SUBCASE("A half-precision float value") {
float value = 3.1415927f;
float expected = 3.14062f;
spb->clear();
spb->put_half(value);
spb->seek(0);
CHECK(spb->get_half() == doctest::Approx(expected));
}
SUBCASE("A float value") {
float value = 42.0f;
spb->clear();
spb->put_float(value);
spb->seek(0);
CHECK_EQ(spb->get_float(), value);
}
SUBCASE("A double value") {
double value = 42.0;
spb->clear();
spb->put_double(value);
spb->seek(0);
CHECK_EQ(spb->get_double(), value);
}
SUBCASE("A string value") {
String value = "Hello, World!";
spb->clear();
spb->put_string(value);
spb->seek(0);
CHECK_EQ(spb->get_string(), value);
}
SUBCASE("A utf8 string value") {
String value = String::utf8("Hello✩, World✩!");
spb->clear();
spb->put_utf8_string(value);
spb->seek(0);
CHECK_EQ(spb->get_utf8_string(), value);
}
SUBCASE("A variant value") {
Array value;
value.push_front(42);
value.push_front("Hello, World!");
spb->clear();
spb->put_var(value);
spb->seek(0);
CHECK_EQ(spb->get_var(), value);
}
}
TEST_CASE("[StreamPeer] Get and sets big endian through StreamPeerBuffer") {
Ref<StreamPeerBuffer> spb;
spb.instantiate();
spb->set_big_endian(true);
SUBCASE("A int16_t value") {
int16_t value = 42;
spb->clear();
spb->put_16(value);
spb->seek(0);
CHECK_EQ(spb->get_16(), value);
}
SUBCASE("A uint16_t value") {
uint16_t value = 42;
spb->clear();
spb->put_u16(value);
spb->seek(0);
CHECK_EQ(spb->get_u16(), value);
}
SUBCASE("A int32_t value") {
int32_t value = 42;
spb->clear();
spb->put_32(value);
spb->seek(0);
CHECK_EQ(spb->get_32(), value);
}
SUBCASE("A uint32_t value") {
uint32_t value = 42;
spb->clear();
spb->put_u32(value);
spb->seek(0);
CHECK_EQ(spb->get_u32(), value);
}
SUBCASE("A int64_t value") {
int64_t value = 42;
spb->clear();
spb->put_64(value);
spb->seek(0);
CHECK_EQ(spb->get_64(), value);
}
SUBCASE("A int64_t value") {
uint64_t value = 42;
spb->clear();
spb->put_u64(value);
spb->seek(0);
CHECK_EQ(spb->get_u64(), value);
}
SUBCASE("A float value") {
float value = 42.0f;
spb->clear();
spb->put_float(value);
spb->seek(0);
CHECK_EQ(spb->get_float(), value);
}
SUBCASE("A half-precision float value") {
float value = 3.1415927f;
float expected = 3.14062f;
spb->clear();
spb->put_half(value);
spb->seek(0);
CHECK(spb->get_half() == doctest::Approx(expected));
}
SUBCASE("A double value") {
double value = 42.0;
spb->clear();
spb->put_double(value);
spb->seek(0);
CHECK_EQ(spb->get_double(), value);
}
}
TEST_CASE("[StreamPeer] Get string when there is no string") {
Ref<StreamPeerBuffer> spb;
spb.instantiate();
ERR_PRINT_OFF;
CHECK_EQ(spb->get_string(), "");
ERR_PRINT_ON;
}
TEST_CASE("[StreamPeer] Get UTF8 string when there is no string") {
Ref<StreamPeerBuffer> spb;
spb.instantiate();
ERR_PRINT_OFF;
CHECK_EQ(spb->get_utf8_string(), "");
ERR_PRINT_ON;
}
} // namespace TestStreamPeer

View File

@@ -0,0 +1,182 @@
/**************************************************************************/
/* test_stream_peer_buffer.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/stream_peer.h"
#include "tests/test_macros.h"
namespace TestStreamPeerBuffer {
TEST_CASE("[StreamPeerBuffer] Initialization") {
Ref<StreamPeerBuffer> spb;
spb.instantiate();
CHECK_EQ(spb->get_size(), 0);
CHECK_EQ(spb->get_position(), 0);
CHECK_EQ(spb->get_available_bytes(), 0);
}
TEST_CASE("[StreamPeerBuffer] Seek") {
Ref<StreamPeerBuffer> spb;
spb.instantiate();
uint8_t first = 5;
uint8_t second = 7;
uint8_t third = 11;
spb->put_u8(first);
spb->put_u8(second);
spb->put_u8(third);
spb->seek(0);
CHECK_EQ(spb->get_u8(), first);
CHECK_EQ(spb->get_u8(), second);
CHECK_EQ(spb->get_u8(), third);
spb->seek(1);
CHECK_EQ(spb->get_position(), 1);
CHECK_EQ(spb->get_u8(), second);
spb->seek(1);
ERR_PRINT_OFF;
spb->seek(-1);
ERR_PRINT_ON;
CHECK_EQ(spb->get_position(), 1);
ERR_PRINT_OFF;
spb->seek(5);
ERR_PRINT_ON;
CHECK_EQ(spb->get_position(), 1);
}
TEST_CASE("[StreamPeerBuffer] Resize") {
Ref<StreamPeerBuffer> spb;
spb.instantiate();
CHECK_EQ(spb->get_size(), 0);
CHECK_EQ(spb->get_position(), 0);
CHECK_EQ(spb->get_available_bytes(), 0);
spb->resize(42);
CHECK_EQ(spb->get_size(), 42);
CHECK_EQ(spb->get_position(), 0);
CHECK_EQ(spb->get_available_bytes(), 42);
spb->seek(21);
CHECK_EQ(spb->get_size(), 42);
CHECK_EQ(spb->get_position(), 21);
CHECK_EQ(spb->get_available_bytes(), 21);
}
TEST_CASE("[StreamPeerBuffer] Get underlying data array") {
uint8_t first = 5;
uint8_t second = 7;
uint8_t third = 11;
Ref<StreamPeerBuffer> spb;
spb.instantiate();
spb->put_u8(first);
spb->put_u8(second);
spb->put_u8(third);
Vector<uint8_t> data_array = spb->get_data_array();
CHECK_EQ(data_array[0], first);
CHECK_EQ(data_array[1], second);
CHECK_EQ(data_array[2], third);
}
TEST_CASE("[StreamPeerBuffer] Set underlying data array") {
uint8_t first = 5;
uint8_t second = 7;
uint8_t third = 11;
Ref<StreamPeerBuffer> spb;
spb.instantiate();
spb->put_u8(1);
spb->put_u8(2);
spb->put_u8(3);
Vector<uint8_t> new_data_array;
new_data_array.push_back(first);
new_data_array.push_back(second);
new_data_array.push_back(third);
spb->set_data_array(new_data_array);
CHECK_EQ(spb->get_u8(), first);
CHECK_EQ(spb->get_u8(), second);
CHECK_EQ(spb->get_u8(), third);
}
TEST_CASE("[StreamPeerBuffer] Duplicate") {
uint8_t first = 5;
uint8_t second = 7;
uint8_t third = 11;
Ref<StreamPeerBuffer> spb;
spb.instantiate();
spb->put_u8(first);
spb->put_u8(second);
spb->put_u8(third);
Ref<StreamPeerBuffer> spb2 = spb->duplicate();
CHECK_EQ(spb2->get_u8(), first);
CHECK_EQ(spb2->get_u8(), second);
CHECK_EQ(spb2->get_u8(), third);
}
TEST_CASE("[StreamPeerBuffer] Put data with size equal to zero does nothing") {
Ref<StreamPeerBuffer> spb;
spb.instantiate();
uint8_t data = 42;
Error error = spb->put_data((const uint8_t *)&data, 0);
CHECK_EQ(error, OK);
CHECK_EQ(spb->get_size(), 0);
CHECK_EQ(spb->get_position(), 0);
CHECK_EQ(spb->get_available_bytes(), 0);
}
TEST_CASE("[StreamPeerBuffer] Get data with invalid size returns an error") {
Ref<StreamPeerBuffer> spb;
spb.instantiate();
uint8_t data = 42;
spb->put_u8(data);
spb->seek(0);
uint8_t data_out = 0;
Error error = spb->get_data(&data_out, 3);
CHECK_EQ(error, ERR_INVALID_PARAMETER);
CHECK_EQ(spb->get_size(), 1);
CHECK_EQ(spb->get_position(), 1);
}
} // namespace TestStreamPeerBuffer

View File

@@ -0,0 +1,195 @@
/**************************************************************************/
/* test_stream_peer_gzip.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/stream_peer_gzip.h"
#include "tests/test_macros.h"
namespace TestStreamPeerGZIP {
const String hello = "Hello World!!!";
TEST_CASE("[StreamPeerGZIP] Initialization") {
Ref<StreamPeerGZIP> spgz;
spgz.instantiate();
CHECK_EQ(spgz->get_available_bytes(), 0);
}
TEST_CASE("[StreamPeerGZIP] Compress/Decompress") {
Ref<StreamPeerGZIP> spgz;
spgz.instantiate();
bool is_deflate = false;
SUBCASE("GZIP") {
is_deflate = false;
}
SUBCASE("DEFLATE") {
is_deflate = true;
}
CHECK_EQ(spgz->start_compression(is_deflate), Error::OK);
CHECK_EQ(spgz->put_data(hello.to_ascii_buffer().ptr(), hello.to_ascii_buffer().size()), Error::OK);
CHECK_EQ(spgz->finish(), Error::OK);
Vector<uint8_t> hello_compressed;
int hello_compressed_size = spgz->get_available_bytes();
hello_compressed.resize(hello_compressed_size);
CHECK_EQ(spgz->get_data(hello_compressed.ptrw(), hello_compressed_size), Error::OK);
spgz->clear();
CHECK_EQ(spgz->start_decompression(is_deflate), Error::OK);
CHECK_EQ(spgz->put_data(hello_compressed.ptr(), hello_compressed.size()), Error::OK);
Vector<uint8_t> hello_decompressed;
int hello_decompressed_size = spgz->get_available_bytes();
hello_decompressed.resize(hello_decompressed_size);
CHECK_EQ(spgz->get_data(hello_decompressed.ptrw(), hello_decompressed_size), Error::OK);
CHECK_EQ(hello_decompressed, hello.to_ascii_buffer());
}
TEST_CASE("[StreamPeerGZIP] Compress/Decompress big chunks of data") { // GH-97201
Ref<StreamPeerGZIP> spgz;
spgz.instantiate();
CHECK_EQ(spgz->start_compression(false), Error::OK);
Vector<uint8_t> big_data;
big_data.resize(2500);
// Filling it with random data because the issue is related to the size of the data when it's compress.
// Random data results in bigger compressed data size.
for (int i = 0; i < big_data.size(); i++) {
big_data.write[i] = Math::random(48, 122);
}
CHECK_EQ(spgz->put_data(big_data.ptr(), big_data.size()), Error::OK);
CHECK_EQ(spgz->finish(), Error::OK);
Vector<uint8_t> big_data_compressed;
int big_data_compressed_size = spgz->get_available_bytes();
big_data_compressed.resize(big_data_compressed_size);
CHECK_EQ(spgz->get_data(big_data_compressed.ptrw(), big_data_compressed_size), Error::OK);
spgz->clear();
CHECK_EQ(spgz->start_decompression(false), Error::OK);
CHECK_EQ(spgz->put_data(big_data_compressed.ptr(), big_data_compressed.size()), Error::OK);
Vector<uint8_t> big_data_decompressed;
int big_data_decompressed_size = spgz->get_available_bytes();
big_data_decompressed.resize(big_data_decompressed_size);
CHECK_EQ(spgz->get_data(big_data_decompressed.ptrw(), big_data_decompressed_size), Error::OK);
CHECK_EQ(big_data_decompressed, big_data);
}
TEST_CASE("[StreamPeerGZIP] Can't start twice") {
Ref<StreamPeerGZIP> spgz;
spgz.instantiate();
CHECK_EQ(spgz->start_compression(false), Error::OK);
ERR_PRINT_OFF;
CHECK_EQ(spgz->start_compression(false), Error::ERR_ALREADY_IN_USE);
CHECK_EQ(spgz->start_decompression(false), Error::ERR_ALREADY_IN_USE);
ERR_PRINT_ON;
}
TEST_CASE("[StreamPeerGZIP] Can't start with a buffer size equal or less than zero") {
Ref<StreamPeerGZIP> spgz;
spgz.instantiate();
ERR_PRINT_OFF;
CHECK_EQ(spgz->start_compression(false, 0), Error::ERR_INVALID_PARAMETER);
CHECK_EQ(spgz->start_compression(false, -1), Error::ERR_INVALID_PARAMETER);
CHECK_EQ(spgz->start_decompression(false, 0), Error::ERR_INVALID_PARAMETER);
CHECK_EQ(spgz->start_decompression(false, -1), Error::ERR_INVALID_PARAMETER);
ERR_PRINT_ON;
}
TEST_CASE("[StreamPeerGZIP] Can't put/get data with a buffer size less than zero") {
Ref<StreamPeerGZIP> spgz;
spgz.instantiate();
CHECK_EQ(spgz->start_compression(false), Error::OK);
ERR_PRINT_OFF;
CHECK_EQ(spgz->put_data(hello.to_ascii_buffer().ptr(), -1), Error::ERR_INVALID_PARAMETER);
Vector<uint8_t> hello_compressed;
hello_compressed.resize(5);
CHECK_EQ(spgz->get_data(hello_compressed.ptrw(), -1), Error::ERR_INVALID_PARAMETER);
ERR_PRINT_ON;
}
TEST_CASE("[StreamPeerGZIP] Needs to be started before use") {
Ref<StreamPeerGZIP> spgz;
spgz.instantiate();
ERR_PRINT_OFF;
CHECK_EQ(spgz->put_data(hello.to_ascii_buffer().ptr(), hello.to_ascii_buffer().size()), Error::ERR_UNCONFIGURED);
ERR_PRINT_ON;
}
TEST_CASE("[StreamPeerGZIP] Can't be finished after clear or if it's decompressing") {
Ref<StreamPeerGZIP> spgz;
spgz.instantiate();
CHECK_EQ(spgz->start_compression(false), Error::OK);
spgz->clear();
ERR_PRINT_OFF;
CHECK_EQ(spgz->finish(), Error::ERR_UNAVAILABLE);
ERR_PRINT_ON;
spgz->clear();
CHECK_EQ(spgz->start_decompression(false), Error::OK);
ERR_PRINT_OFF;
CHECK_EQ(spgz->finish(), Error::ERR_UNAVAILABLE);
ERR_PRINT_ON;
}
TEST_CASE("[StreamPeerGZIP] Fails to get if nothing was compress/decompress") {
Ref<StreamPeerGZIP> spgz;
spgz.instantiate();
SUBCASE("Compression") {
CHECK_EQ(spgz->start_compression(false), Error::OK);
}
SUBCASE("Decompression") {
CHECK_EQ(spgz->start_decompression(false), Error::OK);
}
ERR_PRINT_OFF;
Vector<uint8_t> hello_compressed;
hello_compressed.resize(5);
CHECK_EQ(spgz->get_data(hello_compressed.ptrw(), hello_compressed.size()), Error::ERR_UNAVAILABLE);
ERR_PRINT_ON;
}
} // namespace TestStreamPeerGZIP

View File

@@ -0,0 +1,252 @@
/**************************************************************************/
/* test_tcp_server.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/stream_peer_tcp.h"
#include "core/io/tcp_server.h"
#include "tests/test_macros.h"
#include <functional>
namespace TestTCPServer {
const int PORT = 12345;
const IPAddress LOCALHOST("127.0.0.1");
const uint32_t SLEEP_DURATION = 1000;
const uint64_t MAX_WAIT_USEC = 2000000;
void wait_for_condition(std::function<bool()> f_test) {
const uint64_t time = OS::get_singleton()->get_ticks_usec();
while (!f_test() && (OS::get_singleton()->get_ticks_usec() - time) < MAX_WAIT_USEC) {
OS::get_singleton()->delay_usec(SLEEP_DURATION);
}
}
Ref<TCPServer> create_server(const IPAddress &p_address, int p_port) {
Ref<TCPServer> server;
server.instantiate();
REQUIRE_EQ(server->listen(PORT, LOCALHOST), Error::OK);
REQUIRE(server->is_listening());
CHECK_FALSE(server->is_connection_available());
return server;
}
Ref<StreamPeerTCP> create_client(const IPAddress &p_address, int p_port) {
Ref<StreamPeerTCP> client;
client.instantiate();
REQUIRE_EQ(client->connect_to_host(LOCALHOST, PORT), Error::OK);
CHECK_EQ(client->get_connected_host(), LOCALHOST);
CHECK_EQ(client->get_connected_port(), PORT);
CHECK_EQ(client->get_status(), StreamPeerTCP::STATUS_CONNECTING);
return client;
}
Ref<StreamPeerTCP> accept_connection(Ref<TCPServer> &p_server) {
wait_for_condition([&]() {
return p_server->is_connection_available();
});
REQUIRE(p_server->is_connection_available());
Ref<StreamPeerTCP> client_from_server = p_server->take_connection();
REQUIRE(client_from_server.is_valid());
CHECK_EQ(client_from_server->get_connected_host(), LOCALHOST);
CHECK_EQ(client_from_server->get_status(), StreamPeerTCP::STATUS_CONNECTED);
return client_from_server;
}
TEST_CASE("[TCPServer] Instantiation") {
Ref<TCPServer> server;
server.instantiate();
REQUIRE(server.is_valid());
CHECK_EQ(false, server->is_listening());
}
TEST_CASE("[TCPServer] Accept a connection and receive/send data") {
Ref<TCPServer> server = create_server(LOCALHOST, PORT);
Ref<StreamPeerTCP> client = create_client(LOCALHOST, PORT);
Ref<StreamPeerTCP> client_from_server = accept_connection(server);
wait_for_condition([&]() {
return client->poll() != Error::OK || client->get_status() == StreamPeerTCP::STATUS_CONNECTED;
});
CHECK_EQ(client->get_status(), StreamPeerTCP::STATUS_CONNECTED);
// Sending data from client to server.
const String hello_world = "Hello World!";
client->put_string(hello_world);
CHECK_EQ(client_from_server->get_string(), hello_world);
// Sending data from server to client.
const float pi = 3.1415;
client_from_server->put_float(pi);
CHECK_EQ(client->get_float(), pi);
client->disconnect_from_host();
server->stop();
CHECK_FALSE(server->is_listening());
}
TEST_CASE("[TCPServer] Handle multiple clients at the same time") {
Ref<TCPServer> server = create_server(LOCALHOST, PORT);
Vector<Ref<StreamPeerTCP>> clients;
for (int i = 0; i < 5; i++) {
clients.push_back(create_client(LOCALHOST, PORT));
}
Vector<Ref<StreamPeerTCP>> clients_from_server;
for (int i = 0; i < clients.size(); i++) {
clients_from_server.push_back(accept_connection(server));
}
wait_for_condition([&]() {
bool should_exit = true;
for (Ref<StreamPeerTCP> &c : clients) {
if (c->poll() != Error::OK) {
return true;
}
StreamPeerTCP::Status status = c->get_status();
if (status != StreamPeerTCP::STATUS_CONNECTED && status != StreamPeerTCP::STATUS_CONNECTING) {
return true;
}
if (status != StreamPeerTCP::STATUS_CONNECTED) {
should_exit = false;
}
}
return should_exit;
});
for (Ref<StreamPeerTCP> &c : clients) {
REQUIRE_EQ(c->get_status(), StreamPeerTCP::STATUS_CONNECTED);
}
// Sending data from each client to server.
for (int i = 0; i < clients.size(); i++) {
String hello_client = "Hello " + itos(i);
clients[i]->put_string(hello_client);
CHECK_EQ(clients_from_server[i]->get_string(), hello_client);
}
for (Ref<StreamPeerTCP> &c : clients) {
c->disconnect_from_host();
}
server->stop();
}
TEST_CASE("[TCPServer] When stopped shouldn't accept new connections") {
Ref<TCPServer> server = create_server(LOCALHOST, PORT);
Ref<StreamPeerTCP> client = create_client(LOCALHOST, PORT);
Ref<StreamPeerTCP> client_from_server = accept_connection(server);
wait_for_condition([&]() {
return client->poll() != Error::OK || client->get_status() == StreamPeerTCP::STATUS_CONNECTED;
});
CHECK_EQ(client->get_status(), StreamPeerTCP::STATUS_CONNECTED);
// Sending data from client to server.
const String hello_world = "Hello World!";
client->put_string(hello_world);
CHECK_EQ(client_from_server->get_string(), hello_world);
client->disconnect_from_host();
server->stop();
CHECK_FALSE(server->is_listening());
// Make sure the client times out in less than the wait time.
int timeout = ProjectSettings::get_singleton()->get_setting("network/limits/tcp/connect_timeout_seconds");
ProjectSettings::get_singleton()->set_setting("network/limits/tcp/connect_timeout_seconds", 1);
Ref<StreamPeerTCP> new_client = create_client(LOCALHOST, PORT);
// Reset the timeout setting.
ProjectSettings::get_singleton()->set_setting("network/limits/tcp/connect_timeout_seconds", timeout);
CHECK_FALSE(server->is_connection_available());
wait_for_condition([&]() {
return new_client->poll() != Error::OK || new_client->get_status() == StreamPeerTCP::STATUS_ERROR;
});
CHECK_FALSE(server->is_connection_available());
CHECK_EQ(new_client->get_status(), StreamPeerTCP::STATUS_ERROR);
new_client->disconnect_from_host();
CHECK_EQ(new_client->get_status(), StreamPeerTCP::STATUS_NONE);
}
TEST_CASE("[TCPServer] Should disconnect client") {
Ref<TCPServer> server = create_server(LOCALHOST, PORT);
Ref<StreamPeerTCP> client = create_client(LOCALHOST, PORT);
Ref<StreamPeerTCP> client_from_server = accept_connection(server);
wait_for_condition([&]() {
return client->poll() != Error::OK || client->get_status() == StreamPeerTCP::STATUS_CONNECTED;
});
CHECK_EQ(client->get_status(), StreamPeerTCP::STATUS_CONNECTED);
// Sending data from client to server.
const String hello_world = "Hello World!";
client->put_string(hello_world);
CHECK_EQ(client_from_server->get_string(), hello_world);
client_from_server->disconnect_from_host();
server->stop();
CHECK_FALSE(server->is_listening());
// Wait for disconnection
wait_for_condition([&]() {
return client->poll() != Error::OK || client->get_status() == StreamPeerTCP::STATUS_NONE;
});
// Wait for disconnection
wait_for_condition([&]() {
return client_from_server->poll() != Error::OK || client_from_server->get_status() == StreamPeerTCP::STATUS_NONE;
});
CHECK_EQ(client->get_status(), StreamPeerTCP::STATUS_NONE);
CHECK_EQ(client_from_server->get_status(), StreamPeerTCP::STATUS_NONE);
ERR_PRINT_OFF;
CHECK_EQ(client->get_string(), String());
CHECK_EQ(client_from_server->get_string(), String());
ERR_PRINT_ON;
}
} // namespace TestTCPServer

View File

@@ -0,0 +1,216 @@
/**************************************************************************/
/* test_udp_server.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/packet_peer_udp.h"
#include "core/io/udp_server.h"
#include "tests/test_macros.h"
namespace TestUDPServer {
const int PORT = 12345;
const IPAddress LOCALHOST("127.0.0.1");
const uint32_t SLEEP_DURATION = 1000;
const uint64_t MAX_WAIT_USEC = 100000;
void wait_for_condition(std::function<bool()> f_test) {
const uint64_t time = OS::get_singleton()->get_ticks_usec();
while (!f_test() && (OS::get_singleton()->get_ticks_usec() - time) < MAX_WAIT_USEC) {
OS::get_singleton()->delay_usec(SLEEP_DURATION);
}
}
Ref<UDPServer> create_server(const IPAddress &p_address, int p_port) {
Ref<UDPServer> server;
server.instantiate();
Error err = server->listen(PORT, LOCALHOST);
REQUIRE_EQ(Error::OK, err);
REQUIRE(server->is_listening());
CHECK_FALSE(server->is_connection_available());
CHECK_EQ(server->get_max_pending_connections(), 16);
return server;
}
Ref<PacketPeerUDP> create_client(const IPAddress &p_address, int p_port) {
Ref<PacketPeerUDP> client;
client.instantiate();
Error err = client->connect_to_host(LOCALHOST, PORT);
REQUIRE_EQ(Error::OK, err);
CHECK(client->is_bound());
CHECK(client->is_socket_connected());
return client;
}
Ref<PacketPeerUDP> accept_connection(Ref<UDPServer> &p_server) {
wait_for_condition([&]() {
return p_server->poll() != Error::OK || p_server->is_connection_available();
});
CHECK_EQ(p_server->poll(), Error::OK);
REQUIRE(p_server->is_connection_available());
Ref<PacketPeerUDP> client_from_server = p_server->take_connection();
REQUIRE(client_from_server.is_valid());
CHECK(client_from_server->is_bound());
CHECK(client_from_server->is_socket_connected());
return client_from_server;
}
TEST_CASE("[UDPServer] Instantiation") {
Ref<UDPServer> server;
server.instantiate();
REQUIRE(server.is_valid());
CHECK_EQ(false, server->is_listening());
}
TEST_CASE("[UDPServer] Accept a connection and receive/send data") {
Ref<UDPServer> server = create_server(LOCALHOST, PORT);
Ref<PacketPeerUDP> client = create_client(LOCALHOST, PORT);
// Sending data from client to server.
const String hello_world = "Hello World!";
CHECK_EQ(client->put_var(hello_world), Error::OK);
Variant hello_world_received;
Ref<PacketPeerUDP> client_from_server = accept_connection(server);
CHECK_EQ(client_from_server->get_var(hello_world_received), Error::OK);
CHECK_EQ(String(hello_world_received), hello_world);
// Sending data from server to client.
const Variant pi = 3.1415;
CHECK_EQ(client_from_server->put_var(pi), Error::OK);
wait_for_condition([&]() {
return client->get_available_packet_count() > 0;
});
CHECK_GT(client->get_available_packet_count(), 0);
Variant pi_received;
CHECK_EQ(client->get_var(pi_received), Error::OK);
CHECK_EQ(pi_received, pi);
client->close();
server->stop();
CHECK_FALSE(server->is_listening());
}
TEST_CASE("[UDPServer] Handle multiple clients at the same time") {
Ref<UDPServer> server = create_server(LOCALHOST, PORT);
Vector<Ref<PacketPeerUDP>> clients;
for (int i = 0; i < 5; i++) {
Ref<PacketPeerUDP> c = create_client(LOCALHOST, PORT);
// Sending data from client to server.
const String hello_client = itos(i);
CHECK_EQ(c->put_var(hello_client), Error::OK);
clients.push_back(c);
}
Array packets;
for (int i = 0; i < clients.size(); i++) {
Ref<PacketPeerUDP> cfs = accept_connection(server);
Variant received_var;
CHECK_EQ(cfs->get_var(received_var), Error::OK);
CHECK_EQ(received_var.get_type(), Variant::STRING);
packets.push_back(received_var);
// Sending data from server to client.
const float sent_float = 3.1415 + received_var.operator String().to_float();
CHECK_EQ(cfs->put_var(sent_float), Error::OK);
}
CHECK_EQ(packets.size(), clients.size());
packets.sort();
for (int i = 0; i < clients.size(); i++) {
CHECK_EQ(packets[i].operator String(), itos(i));
}
wait_for_condition([&]() {
bool should_exit = true;
for (Ref<PacketPeerUDP> &c : clients) {
int count = c->get_available_packet_count();
if (count < 0) {
return true;
}
if (count == 0) {
should_exit = false;
}
}
return should_exit;
});
for (int i = 0; i < clients.size(); i++) {
CHECK_GT(clients[i]->get_available_packet_count(), 0);
Variant received_var;
const float expected = 3.1415 + i;
CHECK_EQ(clients[i]->get_var(received_var), Error::OK);
CHECK_EQ(received_var.get_type(), Variant::FLOAT);
CHECK_EQ(received_var.operator float(), expected);
}
for (Ref<PacketPeerUDP> &c : clients) {
c->close();
}
server->stop();
}
TEST_CASE("[UDPServer] Should not accept new connections after stop") {
Ref<UDPServer> server = create_server(LOCALHOST, PORT);
Ref<PacketPeerUDP> client = create_client(LOCALHOST, PORT);
// Sending data from client to server.
const String hello_world = "Hello World!";
CHECK_EQ(client->put_var(hello_world), Error::OK);
wait_for_condition([&]() {
return server->poll() != Error::OK || server->is_connection_available();
});
REQUIRE(server->is_connection_available());
server->stop();
CHECK_FALSE(server->is_listening());
CHECK_FALSE(server->is_connection_available());
}
} // namespace TestUDPServer

View File

@@ -0,0 +1,232 @@
/**************************************************************************/
/* test_xml_parser.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/xml_parser.h"
#include "tests/test_macros.h"
namespace TestXMLParser {
TEST_CASE("[XMLParser] End-to-end") {
String source = "<?xml version = \"1.0\" encoding=\"UTF-8\" ?>\
<top attr=\"attr value\">\
Text&lt;&#65;&#x42;&gt;\
</top>";
Vector<uint8_t> buff = source.to_utf8_buffer();
XMLParser parser;
parser.open_buffer(buff);
// <?xml ...?> gets parsed as NODE_UNKNOWN
CHECK(parser.read() == OK);
CHECK(parser.get_node_type() == XMLParser::NodeType::NODE_UNKNOWN);
CHECK(parser.read() == OK);
CHECK(parser.get_node_type() == XMLParser::NodeType::NODE_ELEMENT);
CHECK(parser.get_node_name() == "top");
CHECK(parser.has_attribute("attr"));
CHECK(parser.get_named_attribute_value("attr") == "attr value");
CHECK(parser.read() == OK);
CHECK(parser.get_node_type() == XMLParser::NodeType::NODE_TEXT);
CHECK(parser.get_node_data().lstrip(" \t") == "Text<AB>");
CHECK(parser.read() == OK);
CHECK(parser.get_node_type() == XMLParser::NodeType::NODE_ELEMENT_END);
CHECK(parser.get_node_name() == "top");
parser.close();
}
TEST_CASE("[XMLParser] Comments") {
XMLParser parser;
SUBCASE("Missing end of comment") {
const String input = "<first></first><!-- foo";
REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT_END);
REQUIRE_EQ(parser.read(), OK);
CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_COMMENT);
CHECK_EQ(parser.get_node_name(), " foo");
}
SUBCASE("Bad start of comment") {
const String input = "<first></first><!-";
REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT_END);
REQUIRE_EQ(parser.read(), OK);
CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_COMMENT);
CHECK_EQ(parser.get_node_name(), "-");
}
SUBCASE("Unblanced angle brackets in comment") {
const String input = "<!-- example << --><next-tag></next-tag>";
REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
REQUIRE_EQ(parser.read(), OK);
CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_COMMENT);
CHECK_EQ(parser.get_node_name(), " example << ");
}
SUBCASE("Doctype") {
const String input = "<!DOCTYPE greeting [<!ELEMENT greeting (#PCDATA)>]>";
REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
REQUIRE_EQ(parser.read(), OK);
CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_COMMENT);
CHECK_EQ(parser.get_node_name(), "DOCTYPE greeting [<!ELEMENT greeting (#PCDATA)>]");
}
}
TEST_CASE("[XMLParser] Premature endings") {
SUBCASE("Simple cases") {
String input;
String expected_name;
XMLParser::NodeType expected_type;
SUBCASE("Incomplete Unknown") {
input = "<first></first><?xml";
expected_type = XMLParser::NodeType::NODE_UNKNOWN;
expected_name = "?xml";
}
SUBCASE("Incomplete CDStart") {
input = "<first></first><![CD";
expected_type = XMLParser::NodeType::NODE_CDATA;
expected_name = "";
}
SUBCASE("Incomplete CData") {
input = "<first></first><![CDATA[example";
expected_type = XMLParser::NodeType::NODE_CDATA;
expected_name = "example";
}
SUBCASE("Incomplete CDEnd") {
input = "<first></first><![CDATA[example]]";
expected_type = XMLParser::NodeType::NODE_CDATA;
expected_name = "example]]";
}
SUBCASE("Incomplete start-tag name") {
input = "<first></first><second";
expected_type = XMLParser::NodeType::NODE_ELEMENT;
expected_name = "second";
}
XMLParser parser;
REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT_END);
REQUIRE_EQ(parser.read(), OK);
CHECK_EQ(parser.get_node_type(), expected_type);
CHECK_EQ(parser.get_node_name(), expected_name);
}
SUBCASE("With attributes and texts") {
XMLParser parser;
SUBCASE("Incomplete start-tag attribute name") {
const String input = "<first></first><second attr1=\"foo\" attr2";
REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.read(), OK);
CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
CHECK_EQ(parser.get_node_name(), "second");
CHECK_EQ(parser.get_attribute_count(), 1);
CHECK_EQ(parser.get_attribute_name(0), "attr1");
CHECK_EQ(parser.get_attribute_value(0), "foo");
}
SUBCASE("Incomplete start-tag attribute unquoted value") {
const String input = "<first></first><second attr1=\"foo\" attr2=bar";
REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.read(), OK);
CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
CHECK_EQ(parser.get_node_name(), "second");
CHECK_EQ(parser.get_attribute_count(), 1);
CHECK_EQ(parser.get_attribute_name(0), "attr1");
CHECK_EQ(parser.get_attribute_value(0), "foo");
}
SUBCASE("Incomplete start-tag attribute quoted value") {
const String input = "<first></first><second attr1=\"foo\" attr2=\"bar";
REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.read(), OK);
CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
CHECK_EQ(parser.get_node_name(), "second");
CHECK_EQ(parser.get_attribute_count(), 2);
CHECK_EQ(parser.get_attribute_name(0), "attr1");
CHECK_EQ(parser.get_attribute_value(0), "foo");
CHECK_EQ(parser.get_attribute_name(1), "attr2");
CHECK_EQ(parser.get_attribute_value(1), "bar");
}
SUBCASE("Incomplete end-tag name") {
const String input = "<first></fir";
REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.read(), OK);
CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT_END);
CHECK_EQ(parser.get_node_name(), "fir");
}
SUBCASE("Trailing text") {
const String input = "<first></first>example";
REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.read(), OK);
REQUIRE_EQ(parser.read(), OK);
CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_TEXT);
CHECK_EQ(parser.get_node_data(), "example");
}
}
}
TEST_CASE("[XMLParser] CDATA") {
const String input = "<a><![CDATA[my cdata content goes here]]></a>";
XMLParser parser;
REQUIRE_EQ(parser.open_buffer(input.to_utf8_buffer()), OK);
REQUIRE_EQ(parser.read(), OK);
CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT);
CHECK_EQ(parser.get_node_name(), "a");
REQUIRE_EQ(parser.read(), OK);
CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_CDATA);
CHECK_EQ(parser.get_node_name(), "my cdata content goes here");
REQUIRE_EQ(parser.read(), OK);
CHECK_EQ(parser.get_node_type(), XMLParser::NodeType::NODE_ELEMENT_END);
CHECK_EQ(parser.get_node_name(), "a");
}
} // namespace TestXMLParser

482
tests/core/math/test_aabb.h Normal file
View File

@@ -0,0 +1,482 @@
/**************************************************************************/
/* test_aabb.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/aabb.h"
#include "tests/test_macros.h"
namespace TestAABB {
TEST_CASE("[AABB] Constructor methods") {
constexpr AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
constexpr AABB aabb_copy = AABB(aabb);
CHECK_MESSAGE(
aabb == aabb_copy,
"AABBs created with the same dimensions but by different methods should be equal.");
}
TEST_CASE("[AABB] String conversion") {
CHECK_MESSAGE(
String(AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6))) == "[P: (-1.5, 2.0, -2.5), S: (4.0, 5.0, 6.0)]",
"The string representation should match the expected value.");
}
TEST_CASE("[AABB] Basic getters") {
constexpr AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
CHECK_MESSAGE(
aabb.get_position().is_equal_approx(Vector3(-1.5, 2, -2.5)),
"get_position() should return the expected value.");
CHECK_MESSAGE(
aabb.get_size().is_equal_approx(Vector3(4, 5, 6)),
"get_size() should return the expected value.");
CHECK_MESSAGE(
aabb.get_end().is_equal_approx(Vector3(2.5, 7, 3.5)),
"get_end() should return the expected value.");
CHECK_MESSAGE(
aabb.get_center().is_equal_approx(Vector3(0.5, 4.5, 0.5)),
"get_center() should return the expected value.");
}
TEST_CASE("[AABB] Basic setters") {
AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
aabb.set_end(Vector3(100, 0, 100));
CHECK_MESSAGE(
aabb.is_equal_approx(AABB(Vector3(-1.5, 2, -2.5), Vector3(101.5, -2, 102.5))),
"set_end() should result in the expected AABB.");
aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
aabb.set_position(Vector3(-1000, -2000, -3000));
CHECK_MESSAGE(
aabb.is_equal_approx(AABB(Vector3(-1000, -2000, -3000), Vector3(4, 5, 6))),
"set_position() should result in the expected AABB.");
aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
aabb.set_size(Vector3(0, 0, -50));
CHECK_MESSAGE(
aabb.is_equal_approx(AABB(Vector3(-1.5, 2, -2.5), Vector3(0, 0, -50))),
"set_size() should result in the expected AABB.");
}
TEST_CASE("[AABB] Volume getters") {
AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
CHECK_MESSAGE(
aabb.get_volume() == doctest::Approx(120),
"get_volume() should return the expected value with positive size.");
CHECK_MESSAGE(
aabb.has_volume(),
"Non-empty volumetric AABB should have a volume.");
aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(-4, 5, 6));
CHECK_MESSAGE(
aabb.get_volume() == doctest::Approx(-120),
"get_volume() should return the expected value with negative size (1 component).");
aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(-4, -5, 6));
CHECK_MESSAGE(
aabb.get_volume() == doctest::Approx(120),
"get_volume() should return the expected value with negative size (2 components).");
aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(-4, -5, -6));
CHECK_MESSAGE(
aabb.get_volume() == doctest::Approx(-120),
"get_volume() should return the expected value with negative size (3 components).");
aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 0, 6));
CHECK_MESSAGE(
!aabb.has_volume(),
"Non-empty flat AABB should not have a volume.");
CHECK_MESSAGE(
!AABB().has_volume(),
"Empty AABB should not have a volume.");
}
TEST_CASE("[AABB] Surface getters") {
AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
CHECK_MESSAGE(
aabb.has_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_surface(),
"Non-empty flat AABB should have a surface.");
aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 0, 0));
CHECK_MESSAGE(
aabb.has_surface(),
"Non-empty linear AABB should have a surface.");
CHECK_MESSAGE(
!AABB().has_surface(),
"Empty AABB should not have an surface.");
}
TEST_CASE("[AABB] Intersection") {
constexpr 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.");
CHECK_MESSAGE( // Simple ray intersection test.
aabb_big.intersects_ray(Vector3(-100, 3, 0), Vector3(1, 0, 0)),
"intersects_ray() should return true when ray points directly to AABB from outside.");
CHECK_MESSAGE( // Ray parallel to an edge.
!aabb_big.intersects_ray(Vector3(10, 10, 0), Vector3(0, 1, 0)),
"intersects_ray() should return false for ray parallel and outside of AABB.");
CHECK_MESSAGE( // Ray origin inside aabb.
aabb_big.intersects_ray(Vector3(1, 1, 1), Vector3(0, 1, 0)),
"intersects_ray() should return true for rays originating inside the AABB.");
CHECK_MESSAGE( // Ray pointing away from aabb.
!aabb_big.intersects_ray(Vector3(-10, 0, 0), Vector3(-1, 0, 0)),
"intersects_ray() should return false when ray points away from AABB.");
CHECK_MESSAGE( // Ray along a diagonal of aabb.
aabb_big.intersects_ray(Vector3(0, 0, 0), Vector3(1, 1, 1)),
"intersects_ray() should return true for rays along the AABB diagonal.");
CHECK_MESSAGE( // Ray originating at aabb edge.
aabb_big.intersects_ray(aabb_big.position, Vector3(-1, 0, 0)),
"intersects_ray() should return true for rays starting on AABB's edge.");
CHECK_MESSAGE( // Ray with zero direction inside.
aabb_big.intersects_ray(Vector3(-1, 3, -2), Vector3(0, 0, 0)),
"intersects_ray() should return true because its inside.");
CHECK_MESSAGE( // Ray with zero direction outside.
!aabb_big.intersects_ray(Vector3(-1000, 3, -2), Vector3(0, 0, 0)),
"intersects_ray() should return false for being outside.");
// Finding ray intersections.
constexpr AABB aabb_simple = AABB(Vector3(), Vector3(1, 1, 1));
bool inside = false;
Vector3 intersection_point;
Vector3 intersection_normal;
// Borders.
aabb_simple.find_intersects_ray(Vector3(0.5, 0, 0.5), Vector3(0, 1, 0), inside, &intersection_point, &intersection_normal);
CHECK_MESSAGE(inside == false, "find_intersects_ray() should return outside on borders.");
CHECK_MESSAGE(intersection_point.is_equal_approx(Vector3(0.5, 0, 0.5)), "find_intersects_ray() border intersection point incorrect.");
CHECK_MESSAGE(intersection_normal.is_equal_approx(Vector3(0, -1, 0)), "find_intersects_ray() border intersection normal incorrect.");
aabb_simple.find_intersects_ray(Vector3(0.5, 1, 0.5), Vector3(0, -1, 0), inside, &intersection_point, &intersection_normal);
CHECK_MESSAGE(inside == false, "find_intersects_ray() should return outside on borders.");
CHECK_MESSAGE(intersection_point.is_equal_approx(Vector3(0.5, 1, 0.5)), "find_intersects_ray() border intersection point incorrect.");
CHECK_MESSAGE(intersection_normal.is_equal_approx(Vector3(0, 1, 0)), "find_intersects_ray() border intersection normal incorrect.");
// Inside.
aabb_simple.find_intersects_ray(Vector3(0.5, 0.1, 0.5), Vector3(0, 1, 0), inside, &intersection_point, &intersection_normal);
CHECK_MESSAGE(inside == true, "find_intersects_ray() should return inside when inside.");
CHECK_MESSAGE(intersection_point.is_equal_approx(Vector3(0.5, 0, 0.5)), "find_intersects_ray() inside backtracking intersection point incorrect.");
CHECK_MESSAGE(intersection_normal.is_equal_approx(Vector3(0, -1, 0)), "find_intersects_ray() inside intersection normal incorrect.");
// Zero sized AABB.
constexpr AABB aabb_zero = AABB(Vector3(), Vector3(1, 0, 1));
aabb_zero.find_intersects_ray(Vector3(0.5, 0, 0.5), Vector3(0, 1, 0), inside, &intersection_point, &intersection_normal);
CHECK_MESSAGE(inside == false, "find_intersects_ray() should return outside on borders of zero sized AABB.");
CHECK_MESSAGE(intersection_point.is_equal_approx(Vector3(0.5, 0, 0.5)), "find_intersects_ray() border intersection point incorrect for zero sized AABB.");
CHECK_MESSAGE(intersection_normal.is_equal_approx(Vector3(0, -1, 0)), "find_intersects_ray() border intersection normal incorrect for zero sized AABB.");
aabb_zero.find_intersects_ray(Vector3(0.5, 0, 0.5), Vector3(0, -1, 0), inside, &intersection_point, &intersection_normal);
CHECK_MESSAGE(inside == false, "find_intersects_ray() should return outside on borders of zero sized AABB.");
CHECK_MESSAGE(intersection_point.is_equal_approx(Vector3(0.5, 0, 0.5)), "find_intersects_ray() border intersection point incorrect for zero sized AABB.");
CHECK_MESSAGE(intersection_normal.is_equal_approx(Vector3(0, 1, 0)), "find_intersects_ray() border intersection normal incorrect for zero sized AABB.");
aabb_zero.find_intersects_ray(Vector3(0.5, -1, 0.5), Vector3(0, 1, 0), inside, &intersection_point, &intersection_normal);
CHECK_MESSAGE(inside == false, "find_intersects_ray() should return outside on borders of zero sized AABB.");
CHECK_MESSAGE(intersection_point.is_equal_approx(Vector3(0.5, 0, 0.5)), "find_intersects_ray() border intersection point incorrect for zero sized AABB.");
CHECK_MESSAGE(intersection_normal.is_equal_approx(Vector3(0, -1, 0)), "find_intersects_ray() border intersection normal incorrect for zero sized AABB.");
}
TEST_CASE("[AABB] Merging") {
constexpr 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") {
constexpr AABB aabb_big = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
CHECK_MESSAGE(
aabb_big.encloses(aabb_big),
"encloses() with itself should return the expected result.");
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(1.5, 6, 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") {
constexpr 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") {
constexpr AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
CHECK_MESSAGE(
aabb.get_longest_axis() == Vector3(0, 0, 1),
"get_longest_axis() should return the expected value.");
CHECK_MESSAGE(
aabb.get_longest_axis_index() == Vector3::AXIS_Z,
"get_longest_axis_index() should return the expected value.");
CHECK_MESSAGE(
aabb.get_longest_axis_size() == 6,
"get_longest_axis_size() should return the expected value.");
CHECK_MESSAGE(
aabb.get_shortest_axis() == Vector3(1, 0, 0),
"get_shortest_axis() should return the expected value.");
CHECK_MESSAGE(
aabb.get_shortest_axis_index() == Vector3::AXIS_X,
"get_shortest_axis_index() should return the expected value.");
CHECK_MESSAGE(
aabb.get_shortest_axis_size() == 4,
"get_shortest_axis_size() should return the expected value.");
}
TEST_CASE("[AABB] Get support") {
constexpr AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
CHECK_MESSAGE(
aabb.get_support(Vector3(1, 0, 0)) == Vector3(2.5, 2, -2.5),
"get_support() should return the expected value.");
CHECK_MESSAGE(
aabb.get_support(Vector3(0.5, 1, 1)) == Vector3(2.5, 7, 3.5),
"get_support() should return the expected value.");
CHECK_MESSAGE(
aabb.get_support(Vector3(0.5, 1, -400)) == Vector3(2.5, 7, -2.5),
"get_support() should return the expected value.");
CHECK_MESSAGE(
aabb.get_support(Vector3(0, -1, 0)) == Vector3(-1.5, 2, -2.5),
"get_support() should return the expected value.");
CHECK_MESSAGE(
aabb.get_support(Vector3(0, -0.1, 0)) == Vector3(-1.5, 2, -2.5),
"get_support() should return the expected value.");
CHECK_MESSAGE(
aabb.get_support(Vector3()) == Vector3(-1.5, 2, -2.5),
"get_support() should return the AABB position when given a zero vector.");
}
TEST_CASE("[AABB] Grow") {
constexpr 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") {
constexpr AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6));
CHECK_MESSAGE(
aabb.has_point(Vector3(-1, 3, 0)),
"has_point() with contained point should return the expected value.");
CHECK_MESSAGE(
aabb.has_point(Vector3(2, 3, 0)),
"has_point() with contained point should return the expected value.");
CHECK_MESSAGE(
!aabb.has_point(Vector3(-20, 0, 0)),
"has_point() with non-contained point should return the expected value.");
CHECK_MESSAGE(
aabb.has_point(Vector3(-1.5, 3, 0)),
"has_point() with positive size should include point on near face (X axis).");
CHECK_MESSAGE(
aabb.has_point(Vector3(2.5, 3, 0)),
"has_point() with positive size should include point on far face (X axis).");
CHECK_MESSAGE(
aabb.has_point(Vector3(0, 2, 0)),
"has_point() with positive size should include point on near face (Y axis).");
CHECK_MESSAGE(
aabb.has_point(Vector3(0, 7, 0)),
"has_point() with positive size should include point on far face (Y axis).");
CHECK_MESSAGE(
aabb.has_point(Vector3(0, 3, -2.5)),
"has_point() with positive size should include point on near face (Z axis).");
CHECK_MESSAGE(
aabb.has_point(Vector3(0, 3, 3.5)),
"has_point() with positive size should include point on far face (Z axis).");
}
TEST_CASE("[AABB] Expanding") {
constexpr 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.");
}
TEST_CASE("[AABB] Finite number checks") {
constexpr Vector3 x(0, 1, 2);
constexpr Vector3 infinite(Math::NaN, Math::NaN, Math::NaN);
CHECK_MESSAGE(
AABB(x, x).is_finite(),
"AABB with all components finite should be finite");
CHECK_FALSE_MESSAGE(
AABB(infinite, x).is_finite(),
"AABB with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
AABB(x, infinite).is_finite(),
"AABB with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
AABB(infinite, infinite).is_finite(),
"AABB with two components infinite should not be finite.");
}
} // namespace TestAABB

View File

@@ -0,0 +1,359 @@
/**************************************************************************/
/* test_astar.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/a_star.h"
#include "tests/test_macros.h"
namespace TestAStar {
class ABCX : public AStar3D {
public:
enum {
A,
B,
C,
X,
};
ABCX() {
add_point(A, Vector3(0, 0, 0));
add_point(B, Vector3(1, 0, 0));
add_point(C, Vector3(0, 1, 0));
add_point(X, Vector3(0, 0, 1));
connect_points(A, B);
connect_points(A, C);
connect_points(B, C);
connect_points(X, A);
}
// Disable heuristic completely.
real_t _compute_cost(int64_t p_from, int64_t p_to) {
if (p_from == A && p_to == C) {
return 1000;
}
return 100;
}
};
TEST_CASE("[AStar3D] ABC path") {
ABCX abcx;
Vector<int64_t> 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("[AStar3D] ABCX path") {
ABCX abcx;
Vector<int64_t> 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("[AStar3D] Add/Remove") {
AStar3D 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][AStar3D] Find paths") {
// Random stress tests with Floyd-Warshall.
constexpr int N = 30;
Math::seed(0);
for (int test = 0; test < 1000; test++) {
AStar3D 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]) : Math::INF;
}
}
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<int64_t> 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

View File

@@ -0,0 +1,428 @@
/**************************************************************************/
/* test_basis.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/basis.h"
#include "core/math/random_number_generator.h"
#include "tests/test_macros.h"
namespace TestBasis {
Vector3 deg_to_rad(const Vector3 &p_rotation) {
return p_rotation / 180.0 * Math::PI;
}
Vector3 rad2deg(const Vector3 &p_rotation) {
return p_rotation / Math::PI * 180.0;
}
String get_rot_order_name(EulerOrder ro) {
switch (ro) {
case EulerOrder::XYZ:
return "XYZ";
case EulerOrder::XZY:
return "XZY";
case EulerOrder::YZX:
return "YZX";
case EulerOrder::YXZ:
return "YXZ";
case EulerOrder::ZXY:
return "ZXY";
case EulerOrder::ZYX:
return "ZYX";
default:
return "[Not supported]";
}
}
void test_rotation(Vector3 deg_original_euler, EulerOrder 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 = deg_to_rad(deg_original_euler);
const Basis to_rotation = Basis::from_euler(original_euler, rot_order);
// Euler from rotation
const Vector3 euler_from_rotation = to_rotation.get_euler(rot_order);
const Basis rotation_from_computed_euler = Basis::from_euler(euler_from_rotation, rot_order);
Basis res = to_rotation.inverse() * rotation_from_computed_euler;
CHECK_MESSAGE((res.get_column(0) - Vector3(1.0, 0.0, 0.0)).length() <= 0.001, vformat("Fail due to X %s\n", String(res.get_column(0))));
CHECK_MESSAGE((res.get_column(1) - Vector3(0.0, 1.0, 0.0)).length() <= 0.001, vformat("Fail due to Y %s\n", String(res.get_column(1))));
CHECK_MESSAGE((res.get_column(2) - Vector3(0.0, 0.0, 1.0)).length() <= 0.001, vformat("Fail due to Z %s\n", String(res.get_column(2))));
// Double check `to_rotation` decomposing with XYZ rotation order.
const Vector3 euler_xyz_from_rotation = to_rotation.get_euler(EulerOrder::XYZ);
Basis rotation_from_xyz_computed_euler = Basis::from_euler(euler_xyz_from_rotation, EulerOrder::XYZ);
res = to_rotation.inverse() * rotation_from_xyz_computed_euler;
CHECK_MESSAGE((res.get_column(0) - Vector3(1.0, 0.0, 0.0)).length() <= 0.001, vformat("Double check with XYZ rot order failed, due to X %s\n", String(res.get_column(0))));
CHECK_MESSAGE((res.get_column(1) - Vector3(0.0, 1.0, 0.0)).length() <= 0.001, vformat("Double check with XYZ rot order failed, due to Y %s\n", String(res.get_column(1))));
CHECK_MESSAGE((res.get_column(2) - Vector3(0.0, 0.0, 1.0)).length() <= 0.001, vformat("Double check with XYZ rot order failed, due to Z %s\n", String(res.get_column(2))));
INFO(vformat("Rotation order: %s\n.", get_rot_order_name(rot_order)));
INFO(vformat("Original Rotation: %s\n", String(deg_original_euler)));
INFO(vformat("Quaternion to rotation order: %s\n", String(rad2deg(euler_from_rotation))));
}
TEST_CASE("[Basis] Euler conversions") {
Vector<EulerOrder> euler_order_to_test;
euler_order_to_test.push_back(EulerOrder::XYZ);
euler_order_to_test.push_back(EulerOrder::XZY);
euler_order_to_test.push_back(EulerOrder::YZX);
euler_order_to_test.push_back(EulerOrder::YXZ);
euler_order_to_test.push_back(EulerOrder::ZXY);
euler_order_to_test.push_back(EulerOrder::ZYX);
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));
vectors_to_test.push_back(Vector3(89.9, 0.0, 0.0));
vectors_to_test.push_back(Vector3(-89.9, 0.0, 0.0));
vectors_to_test.push_back(Vector3(0.0, 89.9, 0.0));
vectors_to_test.push_back(Vector3(0.0, -89.9, 0.0));
vectors_to_test.push_back(Vector3(0.0, 0.0, 89.9));
vectors_to_test.push_back(Vector3(0.0, 0.0, -89.9));
for (int h = 0; h < euler_order_to_test.size(); h += 1) {
for (int i = 0; i < vectors_to_test.size(); i += 1) {
test_rotation(vectors_to_test[i], euler_order_to_test[h]);
}
}
}
TEST_CASE("[Stress][Basis] Euler conversions") {
Vector<EulerOrder> euler_order_to_test;
euler_order_to_test.push_back(EulerOrder::XYZ);
euler_order_to_test.push_back(EulerOrder::XZY);
euler_order_to_test.push_back(EulerOrder::YZX);
euler_order_to_test.push_back(EulerOrder::YXZ);
euler_order_to_test.push_back(EulerOrder::ZXY);
euler_order_to_test.push_back(EulerOrder::ZYX);
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 < euler_order_to_test.size(); h += 1) {
for (int i = 0; i < vectors_to_test.size(); i += 1) {
test_rotation(vectors_to_test[i], euler_order_to_test[h]);
}
}
}
TEST_CASE("[Basis] Set axis angle") {
Vector3 axis;
real_t angle;
real_t pi = (real_t)Math::PI;
// Testing the singularity when the angle is 0°.
Basis identity(1, 0, 0, 0, 1, 0, 0, 0, 1);
identity.get_axis_angle(axis, angle);
CHECK(angle == 0);
// Testing the singularity when the angle is 180°.
Basis singularityPi(-1, 0, 0, 0, 1, 0, 0, 0, -1);
singularityPi.get_axis_angle(axis, angle);
CHECK(angle == doctest::Approx(pi));
// Testing reversing the an axis (of an 30° angle).
float cos30deg = Math::cos(Math::deg_to_rad((real_t)30.0));
Basis z_positive(cos30deg, -0.5, 0, 0.5, cos30deg, 0, 0, 0, 1);
Basis z_negative(cos30deg, 0.5, 0, -0.5, cos30deg, 0, 0, 0, 1);
z_positive.get_axis_angle(axis, angle);
CHECK(angle == doctest::Approx(Math::deg_to_rad((real_t)30.0)));
CHECK(axis == Vector3(0, 0, 1));
z_negative.get_axis_angle(axis, angle);
CHECK(angle == doctest::Approx(Math::deg_to_rad((real_t)30.0)));
CHECK(axis == Vector3(0, 0, -1));
// Testing a rotation of 90° on x-y-z.
Basis x90deg(1, 0, 0, 0, 0, -1, 0, 1, 0);
x90deg.get_axis_angle(axis, angle);
CHECK(angle == doctest::Approx(pi / (real_t)2));
CHECK(axis == Vector3(1, 0, 0));
Basis y90deg(0, 0, 1, 0, 1, 0, -1, 0, 0);
y90deg.get_axis_angle(axis, angle);
CHECK(axis == Vector3(0, 1, 0));
Basis z90deg(0, -1, 0, 1, 0, 0, 0, 0, 1);
z90deg.get_axis_angle(axis, angle);
CHECK(axis == Vector3(0, 0, 1));
// Regression test: checks that the method returns a small angle (not 0).
Basis tiny(1, 0, 0, 0, 0.9999995, -0.001, 0, 001, 0.9999995); // The min angle possible with float is 0.001rad.
tiny.get_axis_angle(axis, angle);
CHECK(angle == doctest::Approx(0.001).epsilon(0.0001));
// Regression test: checks that the method returns an angle which is a number (not NaN)
Basis bugNan(1.00000024, 0, 0.000100001693, 0, 1, 0, -0.000100009143, 0, 1.00000024);
bugNan.get_axis_angle(axis, angle);
CHECK(!Math::is_nan(angle));
}
TEST_CASE("[Basis] Finite number checks") {
constexpr Vector3 x(0, 1, 2);
constexpr Vector3 infinite(Math::NaN, Math::NaN, Math::NaN);
CHECK_MESSAGE(
Basis(x, x, x).is_finite(),
"Basis with all components finite should be finite");
CHECK_FALSE_MESSAGE(
Basis(infinite, x, x).is_finite(),
"Basis with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Basis(x, infinite, x).is_finite(),
"Basis with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Basis(x, x, infinite).is_finite(),
"Basis with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Basis(infinite, infinite, x).is_finite(),
"Basis with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Basis(infinite, x, infinite).is_finite(),
"Basis with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Basis(x, infinite, infinite).is_finite(),
"Basis with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Basis(infinite, infinite, infinite).is_finite(),
"Basis with three components infinite should not be finite.");
}
TEST_CASE("[Basis] Is conformal checks") {
CHECK_MESSAGE(
Basis().is_conformal(),
"Identity Basis should be conformal.");
CHECK_MESSAGE(
Basis::from_euler(Vector3(1.2, 3.4, 5.6)).is_conformal(),
"Basis with only rotation should be conformal.");
CHECK_MESSAGE(
Basis::from_scale(Vector3(-1, -1, -1)).is_conformal(),
"Basis with only a flip should be conformal.");
CHECK_MESSAGE(
Basis::from_scale(Vector3(1.2, 1.2, 1.2)).is_conformal(),
"Basis with only uniform scale should be conformal.");
CHECK_MESSAGE(
Basis(Vector3(3, 4, 0), Vector3(4, -3, 0.0), Vector3(0, 0, 5)).is_conformal(),
"Basis with a flip, rotation, and uniform scale should be conformal.");
CHECK_FALSE_MESSAGE(
Basis::from_scale(Vector3(1.2, 3.4, 5.6)).is_conformal(),
"Basis with non-uniform scale should not be conformal.");
CHECK_FALSE_MESSAGE(
Basis(Vector3(Math::SQRT12, Math::SQRT12, 0), Vector3(0, 1, 0), Vector3(0, 0, 1)).is_conformal(),
"Basis with the X axis skewed 45 degrees should not be conformal.");
CHECK_MESSAGE(
Basis(0, 0, 0, 0, 0, 0, 0, 0, 0).is_conformal(),
"Edge case: Basis with all zeroes should return true for is_conformal (because a 0 scale is uniform).");
}
TEST_CASE("[Basis] Is orthogonal checks") {
CHECK_MESSAGE(
Basis().is_orthogonal(),
"Identity Basis should be orthogonal.");
CHECK_MESSAGE(
Basis::from_euler(Vector3(1.2, 3.4, 5.6)).is_orthogonal(),
"Basis with only rotation should be orthogonal.");
CHECK_MESSAGE(
Basis::from_scale(Vector3(-1, -1, -1)).is_orthogonal(),
"Basis with only a flip should be orthogonal.");
CHECK_MESSAGE(
Basis::from_scale(Vector3(1.2, 3.4, 5.6)).is_orthogonal(),
"Basis with only scale should be orthogonal.");
CHECK_MESSAGE(
Basis(Vector3(3, 4, 0), Vector3(4, -3, 0), Vector3(0, 0, 5)).is_orthogonal(),
"Basis with a flip, rotation, and uniform scale should be orthogonal.");
CHECK_FALSE_MESSAGE(
Basis(Vector3(Math::SQRT12, Math::SQRT12, 0), Vector3(0, 1, 0), Vector3(0, 0, 1)).is_orthogonal(),
"Basis with the X axis skewed 45 degrees should not be orthogonal.");
CHECK_MESSAGE(
Basis(0, 0, 0, 0, 0, 0, 0, 0, 0).is_orthogonal(),
"Edge case: Basis with all zeroes should return true for is_orthogonal, since zero vectors are orthogonal to all vectors.");
}
TEST_CASE("[Basis] Is orthonormal checks") {
CHECK_MESSAGE(
Basis().is_orthonormal(),
"Identity Basis should be orthonormal.");
CHECK_MESSAGE(
Basis::from_euler(Vector3(1.2, 3.4, 5.6)).is_orthonormal(),
"Basis with only rotation should be orthonormal.");
CHECK_MESSAGE(
Basis::from_scale(Vector3(-1, -1, -1)).is_orthonormal(),
"Basis with only a flip should be orthonormal.");
CHECK_FALSE_MESSAGE(
Basis::from_scale(Vector3(1.2, 3.4, 5.6)).is_orthonormal(),
"Basis with only scale should not be orthonormal.");
CHECK_FALSE_MESSAGE(
Basis(Vector3(3, 4, 0), Vector3(4, -3, 0), Vector3(0, 0, 5)).is_orthonormal(),
"Basis with a flip, rotation, and uniform scale should not be orthonormal.");
CHECK_FALSE_MESSAGE(
Basis(Vector3(Math::SQRT12, Math::SQRT12, 0), Vector3(0, 1, 0), Vector3(0, 0, 1)).is_orthonormal(),
"Basis with the X axis skewed 45 degrees should not be orthonormal.");
CHECK_FALSE_MESSAGE(
Basis(0, 0, 0, 0, 0, 0, 0, 0, 0).is_orthonormal(),
"Edge case: Basis with all zeroes should return false for is_orthonormal, since the vectors do not have a length of 1.");
}
TEST_CASE("[Basis] Is rotation checks") {
CHECK_MESSAGE(
Basis().is_rotation(),
"Identity Basis should be a rotation (a rotation of zero).");
CHECK_MESSAGE(
Basis::from_euler(Vector3(1.2, 3.4, 5.6)).is_rotation(),
"Basis with only rotation should be a rotation.");
CHECK_FALSE_MESSAGE(
Basis::from_scale(Vector3(-1, -1, -1)).is_rotation(),
"Basis with only a flip should not be a rotation.");
CHECK_FALSE_MESSAGE(
Basis::from_scale(Vector3(1.2, 3.4, 5.6)).is_rotation(),
"Basis with only scale should not be a rotation.");
CHECK_FALSE_MESSAGE(
Basis(Vector3(2, 0, 0), Vector3(0, 0.5, 0), Vector3(0, 0, 1)).is_rotation(),
"Basis with a squeeze should not be a rotation.");
CHECK_FALSE_MESSAGE(
Basis(Vector3(Math::SQRT12, Math::SQRT12, 0), Vector3(0, 1, 0), Vector3(0, 0, 1)).is_rotation(),
"Basis with the X axis skewed 45 degrees should not be a rotation.");
CHECK_FALSE_MESSAGE(
Basis(0, 0, 0, 0, 0, 0, 0, 0, 0).is_rotation(),
"Edge case: Basis with all zeroes should return false for is_rotation, because it is not just a rotation (has a scale of 0).");
}
} // namespace TestBasis

View File

@@ -0,0 +1,228 @@
/**************************************************************************/
/* test_color.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/color.h"
#include "tests/test_macros.h"
namespace TestColor {
TEST_CASE("[Color] Constructor methods") {
constexpr 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.");
constexpr 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") {
constexpr Color blue = Color(0.2, 0.2, 1);
constexpr 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") {
constexpr Color dark_blue = Color(0, 0, 0.5, 0.4);
CHECK_MESSAGE(
dark_blue.get_h() == doctest::Approx(240.0f / 360.0f),
"The returned HSV hue should match the expected value.");
CHECK_MESSAGE(
dark_blue.get_s() == doctest::Approx(1.0f),
"The returned HSV saturation should match the expected value.");
CHECK_MESSAGE(
dark_blue.get_v() == doctest::Approx(0.5f),
"The returned HSV value should match the expected value.");
}
TEST_CASE("[Color] Conversion methods") {
constexpr Color cyan = Color(0, 1, 1);
constexpr 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.0, 1.0, 1.0, 1.0)",
"The string representation should match the expected value.");
}
TEST_CASE("[Color] Linear <-> sRGB conversion") {
constexpr Color color = Color(0.35, 0.5, 0.6, 0.7);
const Color color_linear = color.srgb_to_linear();
const Color color_srgb = color.linear_to_srgb();
CHECK_MESSAGE(
color_linear.is_equal_approx(Color(0.100481, 0.214041, 0.318547, 0.7)),
"The color converted to linear color space should match the expected value.");
CHECK_MESSAGE(
color_srgb.is_equal_approx(Color(0.62621, 0.735357, 0.797738, 0.7)),
"The color converted to sRGB color space should match the expected value.");
CHECK_MESSAGE(
color_linear.linear_to_srgb().is_equal_approx(Color(0.35, 0.5, 0.6, 0.7)),
"The linear color converted back to sRGB color space should match the expected value.");
CHECK_MESSAGE(
color_srgb.srgb_to_linear().is_equal_approx(Color(0.35, 0.5, 0.6, 0.7)),
"The sRGB color converted back to linear color space should match the expected value.");
CHECK_MESSAGE(
Color(1.0, 1.0, 1.0, 1.0).srgb_to_linear() == (Color(1.0, 1.0, 1.0, 1.0)),
"White converted from sRGB to linear should remain white.");
CHECK_MESSAGE(
Color(1.0, 1.0, 1.0, 1.0).linear_to_srgb() == (Color(1.0, 1.0, 1.0, 1.0)),
"White converted from linear to sRGB should remain white.");
}
TEST_CASE("[Color] Named colors") {
CHECK_MESSAGE(
Color::named("red").is_equal_approx(Color::hex(0xFF0000FF)),
"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::hex(0xF5F5F5FF)),
"The named color \"white_smoke\" should match the expected value.");
CHECK_MESSAGE(
Color::named("Slate Blue").is_equal_approx(Color::hex(0x6A5ACDFF)),
"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") {
constexpr 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.");
constexpr 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.");
constexpr Color red = Color(1, 0, 0, 0.2);
constexpr 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

View File

@@ -0,0 +1,490 @@
/**************************************************************************/
/* test_expression.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#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(
double(expression.execute()) == doctest::Approx(-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(
double(expression.execute()) == doctest::Approx(5),
"Float addition should return the expected result.");
CHECK_MESSAGE(
expression.parse("3.0 / 10") == OK,
"Float / integer division should parse successfully.");
CHECK_MESSAGE(
double(expression.execute()) == doctest::Approx(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(
double(expression.execute()) == doctest::Approx(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(
double(expression.execute()) == doctest::Approx(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(
double(expression.execute()) == doctest::Approx(20.18905),
"Float multiplication-addition-subtraction-division should return the expected result.");
}
TEST_CASE("[Expression] Floating-point notation") {
Expression expression;
CHECK_MESSAGE(
expression.parse("2.") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
double(expression.execute()) == doctest::Approx(2.0),
"The expression should return the expected result.");
CHECK_MESSAGE(
expression.parse("(2.)") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
double(expression.execute()) == doctest::Approx(2.0),
"The expression should return the expected result.");
CHECK_MESSAGE(
expression.parse(".3") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
double(expression.execute()) == doctest::Approx(0.3),
"The expression should return the expected result.");
CHECK_MESSAGE(
expression.parse("2.+5.") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
double(expression.execute()) == doctest::Approx(7.0),
"The expression should return the expected result.");
CHECK_MESSAGE(
expression.parse(".3-.8") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
double(expression.execute()) == doctest::Approx(-0.5),
"The expression should return the expected result.");
CHECK_MESSAGE(
expression.parse("2.+.2") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
double(expression.execute()) == doctest::Approx(2.2),
"The expression should return the expected result.");
CHECK_MESSAGE(
expression.parse(".0*0.") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
double(expression.execute()) == doctest::Approx(0.0),
"The expression 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(
expression.parse("2.E5") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
double(expression.execute()) == doctest::Approx(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(
double(expression.execute()) == doctest::Approx(2e5),
"The expression should return the expected result.");
CHECK_MESSAGE(
expression.parse("2e.5") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
double(expression.execute()) == doctest::Approx(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.");
CHECK_MESSAGE(
expression.parse("0Xff_99_00") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
expression.parse("0b10_11_00") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
expression.parse("0B10_11_00") == OK,
"The expression should parse successfully.");
}
TEST_CASE("[Expression] Built-in functions") {
Expression expression;
CHECK_MESSAGE(
expression.parse("sqrt(pow(3, 2) + pow(4, 2))") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
int(expression.execute()) == 5,
"`sqrt(pow(3, 2) + pow(4, 2))` should return the expected result.");
CHECK_MESSAGE(
expression.parse("snapped(sin(0.5), 0.01)") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
double(expression.execute()) == doctest::Approx(0.48),
"`snapped(sin(0.5), 0.01)` should return the expected result.");
CHECK_MESSAGE(
expression.parse("pow(2.0, -2500)") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
Math::is_zero_approx(double(expression.execute())),
"`pow(2.0, -2500)` should return the expected result (asymptotically zero).");
}
TEST_CASE("[Expression] Boolean expressions") {
Expression expression;
CHECK_MESSAGE(
expression.parse("24 >= 12") == OK,
"The boolean expression should parse successfully.");
CHECK_MESSAGE(
bool(expression.execute()),
"The boolean expression should evaluate to `true`.");
CHECK_MESSAGE(
expression.parse("1.0 < 1.25 && 1.25 < 2.0") == OK,
"The boolean expression should parse successfully.");
CHECK_MESSAGE(
bool(expression.execute()),
"The boolean expression should evaluate to `true`.");
CHECK_MESSAGE(
expression.parse("!2") == OK,
"The boolean expression should parse successfully.");
CHECK_MESSAGE(
!bool(expression.execute()),
"The boolean expression should evaluate to `false`.");
CHECK_MESSAGE(
expression.parse("!!2") == OK,
"The boolean expression should parse successfully.");
CHECK_MESSAGE(
bool(expression.execute()),
"The boolean expression should evaluate to `true`.");
CHECK_MESSAGE(
expression.parse("!0") == OK,
"The boolean expression should parse successfully.");
CHECK_MESSAGE(
bool(expression.execute()),
"The boolean expression should evaluate to `true`.");
CHECK_MESSAGE(
expression.parse("!!0") == OK,
"The boolean expression should parse successfully.");
CHECK_MESSAGE(
!bool(expression.execute()),
"The boolean expression should evaluate to `false`.");
CHECK_MESSAGE(
expression.parse("2 && 5") == OK,
"The boolean expression should parse successfully.");
CHECK_MESSAGE(
bool(expression.execute()),
"The boolean expression should evaluate to `true`.");
CHECK_MESSAGE(
expression.parse("0 || 0") == OK,
"The boolean expression should parse successfully.");
CHECK_MESSAGE(
!bool(expression.execute()),
"The boolean expression should evaluate to `false`.");
CHECK_MESSAGE(
expression.parse("(2 <= 4) && (2 > 5)") == OK,
"The boolean expression should parse successfully.");
CHECK_MESSAGE(
!bool(expression.execute()),
"The boolean expression should evaluate to `false`.");
}
TEST_CASE("[Expression] Expressions with variables") {
Expression expression;
PackedStringArray parameter_names = { "foo", "bar" };
CHECK_MESSAGE(
expression.parse("foo + bar + 50", parameter_names) == OK,
"The expression should parse successfully.");
Array values = { 60, 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 = { 60, 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 = { "foo", "bar" };
CHECK_MESSAGE(
expression.parse("foo + bar + 50", parameter_names_mismatch) == OK,
"The expression should parse successfully.");
Array values_mismatch = { 60, 20, 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 = { "foo", "bar", "baz" };
CHECK_MESSAGE(
expression.parse("foo + bar + baz + 50", parameter_names_mismatch2) == OK,
"The expression should parse successfully.");
Array values_mismatch2 = { 60, 20 };
// Having more parameters than values will parse successfully but print an
// error message when executing.
ERR_PRINT_OFF;
CHECK_MESSAGE(
int(expression.execute(values_mismatch2)) == 0,
"The expression should return the expected value.");
ERR_PRINT_ON;
}
TEST_CASE("[Expression] Invalid expressions") {
Expression expression;
CHECK_MESSAGE(
expression.parse("\\") == ERR_INVALID_PARAMETER,
"The expression shouldn't parse successfully.");
CHECK_MESSAGE(
expression.parse("0++") == ERR_INVALID_PARAMETER,
"The expression shouldn't parse successfully.");
CHECK_MESSAGE(
expression.parse("()") == ERR_INVALID_PARAMETER,
"The expression shouldn't parse successfully.");
CHECK_MESSAGE(
expression.parse("()()") == ERR_INVALID_PARAMETER,
"The expression shouldn't parse successfully.");
CHECK_MESSAGE(
expression.parse("() - ()") == ERR_INVALID_PARAMETER,
"The expression shouldn't parse successfully.");
CHECK_MESSAGE(
expression.parse("() * 12345") == ERR_INVALID_PARAMETER,
"The expression shouldn't parse successfully.");
CHECK_MESSAGE(
expression.parse("() * 12345") == ERR_INVALID_PARAMETER,
"The expression shouldn't parse successfully.");
CHECK_MESSAGE(
expression.parse("123'456") == ERR_INVALID_PARAMETER,
"The expression shouldn't parse successfully.");
CHECK_MESSAGE(
expression.parse("123\"456") == ERR_INVALID_PARAMETER,
"The expression shouldn't parse successfully.");
}
TEST_CASE("[Expression] Unusual expressions") {
Expression expression;
// Redundant parentheses don't cause a parse error as long as they're matched.
CHECK_MESSAGE(
expression.parse("(((((((((((((((666)))))))))))))))") == OK,
"The expression should parse successfully.");
// Using invalid identifiers doesn't cause a parse error.
ERR_PRINT_OFF;
CHECK_MESSAGE(
expression.parse("hello + hello") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
int(expression.execute()) == 0,
"The expression should return the expected result.");
ERR_PRINT_ON;
ERR_PRINT_OFF;
CHECK_MESSAGE(
expression.parse("$1.00 + ???5") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
int(expression.execute()) == 0,
"The expression should return the expected result.");
ERR_PRINT_ON;
// Commas can't be used as a decimal parameter.
CHECK_MESSAGE(
expression.parse("123,456") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
int(expression.execute()) == 123,
"The expression should return the expected result.");
// Spaces can't be used as a separator for large numbers.
CHECK_MESSAGE(
expression.parse("123 456") == OK,
"The expression should parse successfully.");
CHECK_MESSAGE(
int(expression.execute()) == 123,
"The expression should return the expected result.");
// Division by zero is accepted, even though it prints an error message normally.
CHECK_MESSAGE(
expression.parse("-25.4 / 0") == OK,
"The expression should parse successfully.");
ERR_PRINT_OFF;
CHECK_MESSAGE(
Math::is_inf(double(expression.execute())),
"`-25.4 / 0` should return inf.");
ERR_PRINT_ON;
CHECK_MESSAGE(
expression.parse("0 / 0") == OK,
"The expression should parse successfully.");
ERR_PRINT_OFF;
CHECK_MESSAGE(
int(expression.execute()) == 0,
"`0 / 0` should return 0.");
ERR_PRINT_ON;
// The tests below currently crash the engine.
//
//CHECK_MESSAGE(
// expression.parse("(-9223372036854775807 - 1) % -1") == OK,
// "The expression should parse successfully.");
//CHECK_MESSAGE(
// int64_t(expression.execute()) == 0,
// "`(-9223372036854775807 - 1) % -1` should return the expected result.");
//
//CHECK_MESSAGE(
// expression.parse("(-9223372036854775807 - 1) / -1") == OK,
// "The expression should parse successfully.");
//CHECK_MESSAGE(
// int64_t(expression.execute()) == 0,
// "`(-9223372036854775807 - 1) / -1` should return the expected result.");
}
} // namespace TestExpression

View File

@@ -0,0 +1,892 @@
/**************************************************************************/
/* test_geometry_2d.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/geometry_2d.h"
#include "thirdparty/doctest/doctest.h"
namespace TestGeometry2D {
TEST_CASE("[Geometry2D] Point in circle") {
CHECK(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(0, 0), 1.0));
CHECK(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(11.99, 0), 12));
CHECK(Geometry2D::is_point_in_circle(Vector2(-11.99, 0), Vector2(0, 0), 12));
CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(12.01, 0), 12));
CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(-12.01, 0), Vector2(0, 0), 12));
CHECK(Geometry2D::is_point_in_circle(Vector2(7, -42), Vector2(4, -40), 3.7));
CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(7, -42), Vector2(4, -40), 3.5));
// This tests points on the edge of the circle. They are treated as being inside the circle.
CHECK(Geometry2D::is_point_in_circle(Vector2(1.0, 0.0), Vector2(0, 0), 1.0));
CHECK(Geometry2D::is_point_in_circle(Vector2(0.0, -1.0), Vector2(0, 0), 1.0));
}
TEST_CASE("[Geometry2D] Point in triangle") {
CHECK(Geometry2D::is_point_in_triangle(Vector2(0, 0), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));
CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(-1.01, 1.0), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));
CHECK(Geometry2D::is_point_in_triangle(Vector2(3, 2.5), Vector2(1, 4), Vector2(3, 2), Vector2(5, 4)));
CHECK(Geometry2D::is_point_in_triangle(Vector2(-3, -2.5), Vector2(-1, -4), Vector2(-3, -2), Vector2(-5, -4)));
CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(0, 0), Vector2(1, 4), Vector2(3, 2), Vector2(5, 4)));
// This tests points on the edge of the triangle. They are treated as being outside the triangle.
// In `is_point_in_circle` and `is_point_in_polygon` they are treated as being inside, so in order the make
// the behavior consistent this may change in the future (see issue #44717 and PR #44274).
CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(1, 1), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));
CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(0, 1), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));
}
TEST_CASE("[Geometry2D] Point in polygon") {
Vector<Vector2> p;
CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(0, 0), p));
p.push_back(Vector2(-88, 120));
p.push_back(Vector2(-74, -38));
p.push_back(Vector2(135, -145));
p.push_back(Vector2(425, 70));
p.push_back(Vector2(68, 112));
p.push_back(Vector2(-120, 370));
p.push_back(Vector2(-323, -145));
CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-350, 0), p));
CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-110, 60), p));
CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(412, 96), p));
CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(83, 130), p));
CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-320, -153), p));
CHECK(Geometry2D::is_point_in_polygon(Vector2(0, 0), p));
CHECK(Geometry2D::is_point_in_polygon(Vector2(-230, 0), p));
CHECK(Geometry2D::is_point_in_polygon(Vector2(130, -110), p));
CHECK(Geometry2D::is_point_in_polygon(Vector2(370, 55), p));
CHECK(Geometry2D::is_point_in_polygon(Vector2(-160, 190), p));
// This tests points on the edge of the polygon. They are treated as being inside the polygon.
int c = p.size();
for (int i = 0; i < c; i++) {
const Vector2 &p1 = p[i];
CHECK(Geometry2D::is_point_in_polygon(p1, p));
const Vector2 &p2 = p[(i + 1) % c];
Vector2 midpoint((p1 + p2) * 0.5);
CHECK(Geometry2D::is_point_in_polygon(midpoint, p));
}
}
TEST_CASE("[Geometry2D] Polygon clockwise") {
Vector<Vector2> p;
CHECK_FALSE(Geometry2D::is_polygon_clockwise(p));
p.push_back(Vector2(5, -5));
p.push_back(Vector2(-1, -5));
p.push_back(Vector2(-5, -1));
p.push_back(Vector2(-1, 3));
p.push_back(Vector2(1, 5));
CHECK(Geometry2D::is_polygon_clockwise(p));
p.reverse();
CHECK_FALSE(Geometry2D::is_polygon_clockwise(p));
}
TEST_CASE("[Geometry2D] Line intersection") {
Vector2 r;
CHECK(Geometry2D::line_intersects_line(Vector2(2, 0), Vector2(0, 1), Vector2(0, 2), Vector2(1, 0), r));
CHECK(r.is_equal_approx(Vector2(2, 2)));
CHECK(Geometry2D::line_intersects_line(Vector2(-1, 1), Vector2(1, -1), Vector2(4, 1), Vector2(-1, -1), r));
CHECK(r.is_equal_approx(Vector2(1.5, -1.5)));
CHECK(Geometry2D::line_intersects_line(Vector2(-1, 0), Vector2(-1, -1), Vector2(1, 0), Vector2(1, -1), r));
CHECK(r.is_equal_approx(Vector2(0, 1)));
CHECK_FALSE_MESSAGE(
Geometry2D::line_intersects_line(Vector2(-1, 1), Vector2(1, -1), Vector2(0, 1), Vector2(1, -1), r),
"Parallel lines should not intersect.");
}
TEST_CASE("[Geometry2D] Segment intersection") {
Vector2 r;
CHECK(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(-1, -1), &r));
CHECK(r.is_equal_approx(Vector2(0, 0)));
CHECK_FALSE(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(0.1, 0.1), &r));
CHECK_FALSE(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(0.1, 0.1), Vector2(1, 1), &r));
CHECK_FALSE_MESSAGE(
Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(0, 1), Vector2(2, -1), &r),
"Parallel segments should not intersect.");
CHECK_FALSE_MESSAGE(
Geometry2D::segment_intersects_segment(Vector2(1, 2), Vector2(3, 2), Vector2(0, 2), Vector2(-2, 2), &r),
"Non-overlapping collinear segments should not intersect.");
CHECK_MESSAGE(
Geometry2D::segment_intersects_segment(Vector2(0, 0), Vector2(0, 1), Vector2(0, 0), Vector2(1, 0), &r),
"Touching segments should intersect.");
CHECK(r.is_equal_approx(Vector2(0, 0)));
CHECK_MESSAGE(
Geometry2D::segment_intersects_segment(Vector2(0, 1), Vector2(0, 0), Vector2(0, 0), Vector2(1, 0), &r),
"Touching segments should intersect.");
CHECK(r.is_equal_approx(Vector2(0, 0)));
}
TEST_CASE("[Geometry2D] Segment intersection with circle") {
constexpr real_t minus_one = -1.0;
constexpr real_t zero = 0.0;
constexpr real_t one_quarter = 0.25;
constexpr real_t three_quarters = 0.75;
constexpr real_t one = 1.0;
CHECK_MESSAGE(
Geometry2D::segment_intersects_circle(Vector2(0, 0), Vector2(4, 0), Vector2(0, 0), 1.0) == doctest::Approx(one_quarter),
"Segment from inside to outside of circle should intersect it.");
CHECK_MESSAGE(
Geometry2D::segment_intersects_circle(Vector2(4, 0), Vector2(0, 0), Vector2(0, 0), 1.0) == doctest::Approx(three_quarters),
"Segment from outside to inside of circle should intersect it.");
CHECK_MESSAGE(
Geometry2D::segment_intersects_circle(Vector2(-2, 0), Vector2(2, 0), Vector2(0, 0), 1.0) == doctest::Approx(one_quarter),
"Segment running through circle should intersect it.");
CHECK_MESSAGE(
Geometry2D::segment_intersects_circle(Vector2(2, 0), Vector2(-2, 0), Vector2(0, 0), 1.0) == doctest::Approx(one_quarter),
"Segment running through circle should intersect it.");
CHECK_MESSAGE(
Geometry2D::segment_intersects_circle(Vector2(0, 0), Vector2(1, 0), Vector2(0, 0), 1.0) == doctest::Approx(one),
"Segment starting inside the circle and ending on the circle should intersect it");
CHECK_MESSAGE(
Geometry2D::segment_intersects_circle(Vector2(1, 0), Vector2(0, 0), Vector2(0, 0), 1.0) == doctest::Approx(zero),
"Segment starting on the circle and going inwards should intersect it");
CHECK_MESSAGE(
Geometry2D::segment_intersects_circle(Vector2(1, 0), Vector2(2, 0), Vector2(0, 0), 1.0) == doctest::Approx(zero),
"Segment starting on the circle and going outwards should intersect it");
CHECK_MESSAGE(
Geometry2D::segment_intersects_circle(Vector2(2, 0), Vector2(1, 0), Vector2(0, 0), 1.0) == doctest::Approx(one),
"Segment starting outside the circle and ending on the circle intersect it");
CHECK_MESSAGE(
Geometry2D::segment_intersects_circle(Vector2(-1, 0), Vector2(1, 0), Vector2(0, 0), 2.0) == doctest::Approx(minus_one),
"Segment completely within the circle should not intersect it");
CHECK_MESSAGE(
Geometry2D::segment_intersects_circle(Vector2(1, 0), Vector2(-1, 0), Vector2(0, 0), 2.0) == doctest::Approx(minus_one),
"Segment completely within the circle should not intersect it");
CHECK_MESSAGE(
Geometry2D::segment_intersects_circle(Vector2(2, 0), Vector2(3, 0), Vector2(0, 0), 1.0) == doctest::Approx(minus_one),
"Segment completely outside the circle should not intersect it");
CHECK_MESSAGE(
Geometry2D::segment_intersects_circle(Vector2(3, 0), Vector2(2, 0), Vector2(0, 0), 1.0) == doctest::Approx(minus_one),
"Segment completely outside the circle should not intersect it");
}
TEST_CASE("[Geometry2D] Segment intersection with polygon") {
Vector<Point2> a;
a.push_back(Point2(-2, 2));
a.push_back(Point2(3, 4));
a.push_back(Point2(1, 1));
a.push_back(Point2(2, -2));
a.push_back(Point2(-1, -1));
CHECK_MESSAGE(
Geometry2D::is_segment_intersecting_polygon(Vector2(0, 2), Vector2(2, 2), a),
"Segment from inside to outside of polygon should intersect it.");
CHECK_MESSAGE(
Geometry2D::is_segment_intersecting_polygon(Vector2(2, 2), Vector2(0, 2), a),
"Segment from outside to inside of polygon should intersect it.");
CHECK_MESSAGE(
Geometry2D::is_segment_intersecting_polygon(Vector2(2, 4), Vector2(3, 3), a),
"Segment running through polygon should intersect it.");
CHECK_MESSAGE(
Geometry2D::is_segment_intersecting_polygon(Vector2(3, 3), Vector2(2, 4), a),
"Segment running through polygon should intersect it.");
CHECK_MESSAGE(
Geometry2D::is_segment_intersecting_polygon(Vector2(0, 0), Vector2(1, 1), a),
"Segment starting inside the polygon and ending on the polygon should intersect it");
CHECK_MESSAGE(
Geometry2D::is_segment_intersecting_polygon(Vector2(1, 1), Vector2(0, 0), a),
"Segment starting on the polygon and going inwards should intersect it");
CHECK_MESSAGE(
Geometry2D::is_segment_intersecting_polygon(Vector2(-2, 2), Vector2(-2, -1), a),
"Segment starting on the polygon and going outwards should intersect it");
CHECK_MESSAGE(
Geometry2D::is_segment_intersecting_polygon(Vector2(-2, 1), Vector2(-2, 2), a),
"Segment starting outside the polygon and ending on the polygon intersect it");
CHECK_FALSE_MESSAGE(
Geometry2D::is_segment_intersecting_polygon(Vector2(-1, 2), Vector2(1, -1), a),
"Segment completely within the polygon should not intersect it");
CHECK_FALSE_MESSAGE(
Geometry2D::is_segment_intersecting_polygon(Vector2(1, -1), Vector2(-1, 2), a),
"Segment completely within the polygon should not intersect it");
CHECK_FALSE_MESSAGE(
Geometry2D::is_segment_intersecting_polygon(Vector2(2, 2), Vector2(2, -1), a),
"Segment completely outside the polygon should not intersect it");
CHECK_FALSE_MESSAGE(
Geometry2D::is_segment_intersecting_polygon(Vector2(2, -1), Vector2(2, 2), a),
"Segment completely outside the polygon should not intersect it");
}
TEST_CASE("[Geometry2D] Closest point to segment") {
Vector2 a = Vector2(-4, -4);
Vector2 b = Vector2(4, 4);
CHECK(Geometry2D::get_closest_point_to_segment(Vector2(4.1, 4.1), a, b).is_equal_approx(Vector2(4, 4)));
CHECK(Geometry2D::get_closest_point_to_segment(Vector2(-4.1, -4.1), a, b).is_equal_approx(Vector2(-4, -4)));
CHECK(Geometry2D::get_closest_point_to_segment(Vector2(-1, 1), a, b).is_equal_approx(Vector2(0, 0)));
a = Vector2(1, -2);
b = Vector2(1, -2);
CHECK_MESSAGE(
Geometry2D::get_closest_point_to_segment(Vector2(-3, 4), a, b).is_equal_approx(Vector2(1, -2)),
"Line segment is only a single point. This point should be the closest.");
}
TEST_CASE("[Geometry2D] Closest point to uncapped segment") {
constexpr Vector2 a = Vector2(-4, -4);
constexpr Vector2 b = Vector2(4, 4);
CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(-1, 1), a, b).is_equal_approx(Vector2(0, 0)));
CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(-4, -6), a, b).is_equal_approx(Vector2(-5, -5)));
CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(4, 6), a, b).is_equal_approx(Vector2(5, 5)));
}
TEST_CASE("[Geometry2D] Closest points between segments") {
Vector2 c1, c2;
// Basis Path Testing suite
SUBCASE("[Geometry2D] Both segments degenerate to a point") {
Geometry2D::get_closest_points_between_segments(Vector2(0, 0), Vector2(0, 0), Vector2(0, 0), Vector2(0, 0), c1, c2);
CHECK(c1.is_equal_approx(Vector2(0, 0)));
CHECK(c2.is_equal_approx(Vector2(0, 0)));
}
SUBCASE("[Geometry2D] Closest point on second segment trajectory is above [0,1]") {
Geometry2D::get_closest_points_between_segments(Vector2(50, -25), Vector2(50, -10), Vector2(-50, 10), Vector2(-40, 10), c1, c2);
CHECK(c1.is_equal_approx(Vector2(50, -10)));
CHECK(c2.is_equal_approx(Vector2(-40, 10)));
}
SUBCASE("[Geometry2D] Parallel segments") {
Geometry2D::get_closest_points_between_segments(Vector2(2, 1), Vector2(4, 3), Vector2(2, 3), Vector2(4, 5), c1, c2);
CHECK(c1.is_equal_approx(Vector2(3, 2)));
CHECK(c2.is_equal_approx(Vector2(2, 3)));
}
SUBCASE("[Geometry2D] Closest point on second segment trajectory is within [0,1]") {
Geometry2D::get_closest_points_between_segments(Vector2(2, 4), Vector2(2, 3), Vector2(1, 1), Vector2(4, 4), c1, c2);
CHECK(c1.is_equal_approx(Vector2(2, 3)));
CHECK(c2.is_equal_approx(Vector2(2.5, 2.5)));
}
SUBCASE("[Geometry2D] Closest point on second segment trajectory is below [0,1]") {
Geometry2D::get_closest_points_between_segments(Vector2(-20, -20), Vector2(-10, -40), Vector2(10, 25), Vector2(25, 40), c1, c2);
CHECK(c1.is_equal_approx(Vector2(-20, -20)));
CHECK(c2.is_equal_approx(Vector2(10, 25)));
}
SUBCASE("[Geometry2D] Second segment degenerates to a point") {
Geometry2D::get_closest_points_between_segments(Vector2(1, 2), Vector2(2, 1), Vector2(3, 3), Vector2(3, 3), c1, c2);
CHECK(c1.is_equal_approx(Vector2(1.5, 1.5)));
CHECK(c2.is_equal_approx(Vector2(3, 3)));
}
SUBCASE("[Geometry2D] First segment degenerates to a point") {
Geometry2D::get_closest_points_between_segments(Vector2(1, 1), Vector2(1, 1), Vector2(2, 2), Vector2(4, 4), c1, c2);
CHECK(c1.is_equal_approx(Vector2(1, 1)));
CHECK(c2.is_equal_approx(Vector2(2, 2)));
}
// End Basis Path Testing suite
SUBCASE("[Geometry2D] Segments are equal vectors") {
Geometry2D::get_closest_points_between_segments(Vector2(2, 2), Vector2(3, 3), Vector2(4, 4), Vector2(4, 5), c1, c2);
CHECK(c1.is_equal_approx(Vector2(3, 3)));
CHECK(c2.is_equal_approx(Vector2(4, 4)));
}
SUBCASE("[Geometry2D] Standard case") {
Geometry2D::get_closest_points_between_segments(Vector2(0, 1), Vector2(-2, -1), Vector2(0, 0), Vector2(2, -2), c1, c2);
CHECK(c1.is_equal_approx(Vector2(-0.5, 0.5)));
CHECK(c2.is_equal_approx(Vector2(0, 0)));
}
SUBCASE("[Geometry2D] Segments intersect") {
Geometry2D::get_closest_points_between_segments(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(-1, -1), c1, c2);
CHECK(c1.is_equal_approx(Vector2(0, 0)));
CHECK(c2.is_equal_approx(Vector2(0, 0)));
}
}
TEST_CASE("[Geometry2D] Make atlas") {
Vector<Point2i> result;
Size2i size;
Vector<Size2i> r;
r.push_back(Size2i(2, 2));
Geometry2D::make_atlas(r, result, size);
CHECK(size == Size2i(2, 2));
CHECK(result.size() == r.size());
r.clear();
result.clear();
r.push_back(Size2i(1, 2));
r.push_back(Size2i(3, 4));
r.push_back(Size2i(5, 6));
r.push_back(Size2i(7, 8));
Geometry2D::make_atlas(r, result, size);
CHECK(result.size() == r.size());
}
TEST_CASE("[Geometry2D] Polygon intersection") {
Vector<Point2> a;
Vector<Point2> b;
Vector<Vector<Point2>> r;
a.push_back(Point2(30, 60));
a.push_back(Point2(70, 5));
a.push_back(Point2(200, 40));
a.push_back(Point2(80, 200));
SUBCASE("[Geometry2D] Both polygons are empty") {
r = Geometry2D::intersect_polygons(Vector<Point2>(), Vector<Point2>());
CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The intersection should also be empty.");
}
SUBCASE("[Geometry2D] One polygon is empty") {
r = Geometry2D::intersect_polygons(a, b);
REQUIRE_MESSAGE(r.is_empty(), "One polygon is empty. The intersection should also be empty.");
}
SUBCASE("[Geometry2D] Basic intersection") {
b.push_back(Point2(200, 300));
b.push_back(Point2(90, 200));
b.push_back(Point2(50, 100));
b.push_back(Point2(200, 90));
r = Geometry2D::intersect_polygons(a, b);
REQUIRE_MESSAGE(r.size() == 1, "The polygons should intersect each other with 1 resulting intersection polygon.");
REQUIRE_MESSAGE(r[0].size() == 3, "The resulting intersection polygon should have 3 vertices.");
CHECK(r[0][0].is_equal_approx(Point2(86.52174, 191.30436)));
CHECK(r[0][1].is_equal_approx(Point2(50, 100)));
CHECK(r[0][2].is_equal_approx(Point2(160.52632, 92.63157)));
}
SUBCASE("[Geometry2D] Intersection with one polygon being completely inside the other polygon") {
b.push_back(Point2(80, 100));
b.push_back(Point2(50, 50));
b.push_back(Point2(150, 50));
r = Geometry2D::intersect_polygons(a, b);
REQUIRE_MESSAGE(r.size() == 1, "The polygons should intersect each other with 1 resulting intersection polygon.");
REQUIRE_MESSAGE(r[0].size() == 3, "The resulting intersection polygon should have 3 vertices.");
CHECK(r[0][0].is_equal_approx(b[0]));
CHECK(r[0][1].is_equal_approx(b[1]));
CHECK(r[0][2].is_equal_approx(b[2]));
}
SUBCASE("[Geometry2D] No intersection with 2 non-empty polygons") {
b.push_back(Point2(150, 150));
b.push_back(Point2(250, 100));
b.push_back(Point2(300, 200));
r = Geometry2D::intersect_polygons(a, b);
REQUIRE_MESSAGE(r.is_empty(), "The polygons should not intersect each other.");
}
SUBCASE("[Geometry2D] Intersection with 2 resulting polygons") {
a.clear();
a.push_back(Point2(70, 5));
a.push_back(Point2(140, 7));
a.push_back(Point2(100, 52));
a.push_back(Point2(170, 50));
a.push_back(Point2(60, 125));
b.push_back(Point2(70, 105));
b.push_back(Point2(115, 55));
b.push_back(Point2(90, 15));
b.push_back(Point2(160, 50));
r = Geometry2D::intersect_polygons(a, b);
REQUIRE_MESSAGE(r.size() == 2, "The polygons should intersect each other with 2 resulting intersection polygons.");
REQUIRE_MESSAGE(r[0].size() == 4, "The resulting intersection polygon should have 4 vertices.");
CHECK(r[0][0].is_equal_approx(Point2(70, 105)));
CHECK(r[0][1].is_equal_approx(Point2(115, 55)));
CHECK(r[0][2].is_equal_approx(Point2(112.894737, 51.63158)));
CHECK(r[0][3].is_equal_approx(Point2(159.509537, 50.299728)));
REQUIRE_MESSAGE(r[1].size() == 3, "The intersection polygon should have 3 vertices.");
CHECK(r[1][0].is_equal_approx(Point2(119.692307, 29.846149)));
CHECK(r[1][1].is_equal_approx(Point2(107.706421, 43.33028)));
CHECK(r[1][2].is_equal_approx(Point2(90, 15)));
}
}
TEST_CASE("[Geometry2D] Merge polygons") {
Vector<Point2> a;
Vector<Point2> b;
Vector<Vector<Point2>> r;
a.push_back(Point2(225, 180));
a.push_back(Point2(160, 230));
a.push_back(Point2(20, 212));
a.push_back(Point2(50, 115));
SUBCASE("[Geometry2D] Both polygons are empty") {
r = Geometry2D::merge_polygons(Vector<Point2>(), Vector<Point2>());
REQUIRE_MESSAGE(r.is_empty(), "Both polygons are empty. The union should also be empty.");
}
SUBCASE("[Geometry2D] One polygon is empty") {
r = Geometry2D::merge_polygons(a, b);
REQUIRE_MESSAGE(r.size() == 1, "One polygon is non-empty. There should be 1 resulting merged polygon.");
REQUIRE_MESSAGE(r[0].size() == 4, "The resulting merged polygon should have 4 vertices.");
CHECK(r[0][0].is_equal_approx(a[0]));
CHECK(r[0][1].is_equal_approx(a[1]));
CHECK(r[0][2].is_equal_approx(a[2]));
CHECK(r[0][3].is_equal_approx(a[3]));
}
SUBCASE("[Geometry2D] Basic merge with 2 polygons") {
b.push_back(Point2(180, 190));
b.push_back(Point2(60, 140));
b.push_back(Point2(160, 80));
r = Geometry2D::merge_polygons(a, b);
REQUIRE_MESSAGE(r.size() == 1, "The merged polygons should result in 1 polygon.");
REQUIRE_MESSAGE(r[0].size() == 7, "The resulting merged polygon should have 7 vertices.");
CHECK(r[0][0].is_equal_approx(Point2(174.791077, 161.350967)));
CHECK(r[0][1].is_equal_approx(Point2(225, 180)));
CHECK(r[0][2].is_equal_approx(Point2(160, 230)));
CHECK(r[0][3].is_equal_approx(Point2(20, 212)));
CHECK(r[0][4].is_equal_approx(Point2(50, 115)));
CHECK(r[0][5].is_equal_approx(Point2(81.911758, 126.852943)));
CHECK(r[0][6].is_equal_approx(Point2(160, 80)));
}
SUBCASE("[Geometry2D] Merge with 2 resulting merged polygons (outline and hole)") {
b.push_back(Point2(180, 190));
b.push_back(Point2(140, 125));
b.push_back(Point2(60, 140));
b.push_back(Point2(160, 80));
r = Geometry2D::merge_polygons(a, b);
REQUIRE_MESSAGE(r.size() == 2, "The merged polygons should result in 2 polygons.");
REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The merged polygon (outline) should be counter-clockwise.");
REQUIRE_MESSAGE(r[0].size() == 7, "The resulting merged polygon (outline) should have 7 vertices.");
CHECK(r[0][0].is_equal_approx(Point2(174.791077, 161.350967)));
CHECK(r[0][1].is_equal_approx(Point2(225, 180)));
CHECK(r[0][2].is_equal_approx(Point2(160, 230)));
CHECK(r[0][3].is_equal_approx(Point2(20, 212)));
CHECK(r[0][4].is_equal_approx(Point2(50, 115)));
CHECK(r[0][5].is_equal_approx(Point2(81.911758, 126.852943)));
CHECK(r[0][6].is_equal_approx(Point2(160, 80)));
REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting merged polygon (hole) should be clockwise.");
REQUIRE_MESSAGE(r[1].size() == 3, "The resulting merged polygon (hole) should have 3 vertices.");
CHECK(r[1][0].is_equal_approx(Point2(98.083069, 132.859421)));
CHECK(r[1][1].is_equal_approx(Point2(158.689453, 155.370377)));
CHECK(r[1][2].is_equal_approx(Point2(140, 125)));
}
}
TEST_CASE("[Geometry2D] Clip polygons") {
Vector<Point2> a;
Vector<Point2> b;
Vector<Vector<Point2>> r;
a.push_back(Point2(225, 180));
a.push_back(Point2(160, 230));
a.push_back(Point2(20, 212));
a.push_back(Point2(50, 115));
SUBCASE("[Geometry2D] Both polygons are empty") {
r = Geometry2D::clip_polygons(Vector<Point2>(), Vector<Point2>());
CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The clip should also be empty.");
}
SUBCASE("[Geometry2D] Basic clip with one result polygon") {
b.push_back(Point2(250, 170));
b.push_back(Point2(175, 270));
b.push_back(Point2(120, 260));
b.push_back(Point2(25, 80));
r = Geometry2D::clip_polygons(a, b);
REQUIRE_MESSAGE(r.size() == 1, "The clipped polygons should result in 1 polygon.");
REQUIRE_MESSAGE(r[0].size() == 3, "The resulting clipped polygon should have 3 vertices.");
CHECK(r[0][0].is_equal_approx(Point2(100.102173, 222.298843)));
CHECK(r[0][1].is_equal_approx(Point2(20, 212)));
CHECK(r[0][2].is_equal_approx(Point2(47.588089, 122.798492)));
}
SUBCASE("[Geometry2D] Polygon b completely overlaps polygon a") {
b.push_back(Point2(250, 170));
b.push_back(Point2(175, 270));
b.push_back(Point2(10, 210));
b.push_back(Point2(55, 80));
r = Geometry2D::clip_polygons(a, b);
CHECK_MESSAGE(r.is_empty(), "Polygon 'b' completely overlaps polygon 'a'. This should result in no clipped polygons.");
}
SUBCASE("[Geometry2D] Polygon a completely overlaps polygon b") {
b.push_back(Point2(150, 200));
b.push_back(Point2(65, 190));
b.push_back(Point2(80, 140));
r = Geometry2D::clip_polygons(a, b);
REQUIRE_MESSAGE(r.size() == 2, "Polygon 'a' completely overlaps polygon 'b'. This should result in 2 clipped polygons.");
REQUIRE_MESSAGE(r[0].size() == 4, "The resulting clipped polygon should have 4 vertices.");
REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The resulting clipped polygon (outline) should be counter-clockwise.");
CHECK(r[0][0].is_equal_approx(a[0]));
CHECK(r[0][1].is_equal_approx(a[1]));
CHECK(r[0][2].is_equal_approx(a[2]));
CHECK(r[0][3].is_equal_approx(a[3]));
REQUIRE_MESSAGE(r[1].size() == 3, "The resulting clipped polygon should have 3 vertices.");
REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting clipped polygon (hole) should be clockwise.");
CHECK(r[1][0].is_equal_approx(b[1]));
CHECK(r[1][1].is_equal_approx(b[0]));
CHECK(r[1][2].is_equal_approx(b[2]));
}
}
TEST_CASE("[Geometry2D] Exclude polygons") {
Vector<Point2> a;
Vector<Point2> b;
Vector<Vector<Point2>> r;
a.push_back(Point2(225, 180));
a.push_back(Point2(160, 230));
a.push_back(Point2(20, 212));
a.push_back(Point2(50, 115));
SUBCASE("[Geometry2D] Both polygons are empty") {
r = Geometry2D::exclude_polygons(Vector<Point2>(), Vector<Point2>());
CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The excluded polygon should also be empty.");
}
SUBCASE("[Geometry2D] One polygon is empty") {
r = Geometry2D::exclude_polygons(a, b);
REQUIRE_MESSAGE(r.size() == 1, "One polygon is non-empty. There should be 1 resulting excluded polygon.");
REQUIRE_MESSAGE(r[0].size() == 4, "The resulting excluded polygon should have 4 vertices.");
CHECK(r[0][0].is_equal_approx(a[0]));
CHECK(r[0][1].is_equal_approx(a[1]));
CHECK(r[0][2].is_equal_approx(a[2]));
CHECK(r[0][3].is_equal_approx(a[3]));
}
SUBCASE("[Geometry2D] Exclude with 2 resulting polygons (outline and hole)") {
b.push_back(Point2(140, 160));
b.push_back(Point2(150, 220));
b.push_back(Point2(40, 200));
b.push_back(Point2(60, 140));
r = Geometry2D::exclude_polygons(a, b);
REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting excluded polygons (outline and hole).");
REQUIRE_MESSAGE(r[0].size() == 4, "The resulting excluded polygon should have 4 vertices.");
REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The resulting excluded polygon (outline) should be counter-clockwise.");
CHECK(r[0][0].is_equal_approx(a[0]));
CHECK(r[0][1].is_equal_approx(a[1]));
CHECK(r[0][2].is_equal_approx(a[2]));
CHECK(r[0][3].is_equal_approx(a[3]));
REQUIRE_MESSAGE(r[1].size() == 4, "The resulting excluded polygon should have 4 vertices.");
REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting excluded polygon (hole) should be clockwise.");
CHECK(r[1][0].is_equal_approx(Point2(40, 200)));
CHECK(r[1][1].is_equal_approx(Point2(150, 220)));
CHECK(r[1][2].is_equal_approx(Point2(140, 160)));
CHECK(r[1][3].is_equal_approx(Point2(60, 140)));
}
}
TEST_CASE("[Geometry2D] Intersect polyline with polygon") {
Vector<Vector2> l;
Vector<Vector2> p;
Vector<Vector<Point2>> r;
l.push_back(Vector2(100, 90));
l.push_back(Vector2(120, 250));
p.push_back(Vector2(225, 180));
p.push_back(Vector2(160, 230));
p.push_back(Vector2(20, 212));
p.push_back(Vector2(50, 115));
SUBCASE("[Geometry2D] Both line and polygon are empty") {
r = Geometry2D::intersect_polyline_with_polygon(Vector<Vector2>(), Vector<Vector2>());
CHECK_MESSAGE(r.is_empty(), "Both line and polygon are empty. The intersection line should also be empty.");
}
SUBCASE("[Geometry2D] Line is non-empty and polygon is empty") {
r = Geometry2D::intersect_polyline_with_polygon(l, Vector<Vector2>());
CHECK_MESSAGE(r.is_empty(), "The polygon is empty while the line is non-empty. The intersection line should be empty.");
}
SUBCASE("[Geometry2D] Basic intersection with 1 resulting intersection line") {
r = Geometry2D::intersect_polyline_with_polygon(l, p);
REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting intersection line.");
REQUIRE_MESSAGE(r[0].size() == 2, "The resulting intersection line should have 2 vertices.");
CHECK(r[0][0].is_equal_approx(Vector2(105.711609, 135.692886)));
CHECK(r[0][1].is_equal_approx(Vector2(116.805809, 224.446457)));
}
SUBCASE("[Geometry2D] Complex intersection with 2 resulting intersection lines") {
l.clear();
l.push_back(Vector2(100, 90));
l.push_back(Vector2(190, 255));
l.push_back(Vector2(135, 260));
l.push_back(Vector2(57, 200));
l.push_back(Vector2(50, 170));
l.push_back(Vector2(15, 155));
r = Geometry2D::intersect_polyline_with_polygon(l, p);
REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting intersection lines.");
REQUIRE_MESSAGE(r[0].size() == 2, "The resulting intersection line should have 2 vertices.");
CHECK(r[0][0].is_equal_approx(Vector2(129.804565, 144.641693)));
CHECK(r[0][1].is_equal_approx(Vector2(171.527084, 221.132996)));
REQUIRE_MESSAGE(r[1].size() == 4, "The resulting intersection line should have 4 vertices.");
CHECK(r[1][0].is_equal_approx(Vector2(83.15609, 220.120087)));
CHECK(r[1][1].is_equal_approx(Vector2(57, 200)));
CHECK(r[1][2].is_equal_approx(Vector2(50, 170)));
CHECK(r[1][3].is_equal_approx(Vector2(34.980492, 163.563065)));
}
}
TEST_CASE("[Geometry2D] Clip polyline with polygon") {
Vector<Vector2> l;
Vector<Vector2> p;
Vector<Vector<Point2>> r;
l.push_back(Vector2(70, 140));
l.push_back(Vector2(160, 320));
p.push_back(Vector2(225, 180));
p.push_back(Vector2(160, 230));
p.push_back(Vector2(20, 212));
p.push_back(Vector2(50, 115));
SUBCASE("[Geometry2D] Both line and polygon are empty") {
r = Geometry2D::clip_polyline_with_polygon(Vector<Vector2>(), Vector<Vector2>());
CHECK_MESSAGE(r.is_empty(), "Both line and polygon are empty. The clipped line should also be empty.");
}
SUBCASE("[Geometry2D] Polygon is empty and line is non-empty") {
r = Geometry2D::clip_polyline_with_polygon(l, Vector<Vector2>());
REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting clipped line.");
REQUIRE_MESSAGE(r[0].size() == 2, "The resulting clipped line should have 2 vertices.");
CHECK(r[0][0].is_equal_approx(l[0]));
CHECK(r[0][1].is_equal_approx(l[1]));
}
SUBCASE("[Geometry2D] Basic clip with 1 resulting clipped line") {
r = Geometry2D::clip_polyline_with_polygon(l, p);
REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting clipped line.");
REQUIRE_MESSAGE(r[0].size() == 2, "The resulting clipped line should have 2 vertices.");
CHECK(r[0][0].is_equal_approx(Vector2(111.908401, 223.816803)));
CHECK(r[0][1].is_equal_approx(Vector2(160, 320)));
}
SUBCASE("[Geometry2D] Complex clip with 2 resulting clipped lines") {
l.clear();
l.push_back(Vector2(55, 70));
l.push_back(Vector2(50, 190));
l.push_back(Vector2(120, 165));
l.push_back(Vector2(122, 250));
l.push_back(Vector2(160, 320));
r = Geometry2D::clip_polyline_with_polygon(l, p);
REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting clipped lines.");
REQUIRE_MESSAGE(r[0].size() == 3, "The resulting clipped line should have 3 vertices.");
CHECK(r[0][0].is_equal_approx(Vector2(121.412682, 225.038757)));
CHECK(r[0][1].is_equal_approx(Vector2(122, 250)));
CHECK(r[0][2].is_equal_approx(Vector2(160, 320)));
REQUIRE_MESSAGE(r[1].size() == 2, "The resulting clipped line should have 2 vertices.");
CHECK(r[1][0].is_equal_approx(Vector2(55, 70)));
CHECK(r[1][1].is_equal_approx(Vector2(53.07737, 116.143021)));
}
}
TEST_CASE("[Geometry2D] Convex hull") {
Vector<Point2> a;
Vector<Point2> r;
a.push_back(Point2(-4, -8));
a.push_back(Point2(-10, -4));
a.push_back(Point2(8, 2));
a.push_back(Point2(-6, 10));
a.push_back(Point2(-12, 4));
a.push_back(Point2(10, -8));
a.push_back(Point2(4, 8));
SUBCASE("[Geometry2D] No points") {
r = Geometry2D::convex_hull(Vector<Vector2>());
CHECK_MESSAGE(r.is_empty(), "The convex hull should be empty if there are no input points.");
}
SUBCASE("[Geometry2D] Single point") {
Vector<Point2> b;
b.push_back(Point2(4, -3));
r = Geometry2D::convex_hull(b);
REQUIRE_MESSAGE(r.size() == 1, "Convex hull should contain 1 point.");
CHECK(r[0].is_equal_approx(b[0]));
}
SUBCASE("[Geometry2D] All points form the convex hull") {
r = Geometry2D::convex_hull(a);
REQUIRE_MESSAGE(r.size() == 8, "Convex hull should contain 8 points.");
CHECK(r[0].is_equal_approx(Point2(-12, 4)));
CHECK(r[1].is_equal_approx(Point2(-10, -4)));
CHECK(r[2].is_equal_approx(Point2(-4, -8)));
CHECK(r[3].is_equal_approx(Point2(10, -8)));
CHECK(r[4].is_equal_approx(Point2(8, 2)));
CHECK(r[5].is_equal_approx(Point2(4, 8)));
CHECK(r[6].is_equal_approx(Point2(-6, 10)));
CHECK(r[7].is_equal_approx(Point2(-12, 4)));
}
SUBCASE("[Geometry2D] Add extra points inside original convex hull") {
a.push_back(Point2(-4, -8));
a.push_back(Point2(0, 0));
a.push_back(Point2(0, 8));
a.push_back(Point2(-10, -3));
a.push_back(Point2(9, -4));
a.push_back(Point2(6, 4));
r = Geometry2D::convex_hull(a);
REQUIRE_MESSAGE(r.size() == 8, "Convex hull should contain 8 points.");
CHECK(r[0].is_equal_approx(Point2(-12, 4)));
CHECK(r[1].is_equal_approx(Point2(-10, -4)));
CHECK(r[2].is_equal_approx(Point2(-4, -8)));
CHECK(r[3].is_equal_approx(Point2(10, -8)));
CHECK(r[4].is_equal_approx(Point2(8, 2)));
CHECK(r[5].is_equal_approx(Point2(4, 8)));
CHECK(r[6].is_equal_approx(Point2(-6, 10)));
CHECK(r[7].is_equal_approx(Point2(-12, 4)));
}
SUBCASE("[Geometry2D] Add extra points on border of original convex hull") {
a.push_back(Point2(9, -3));
a.push_back(Point2(-2, -8));
r = Geometry2D::convex_hull(a);
REQUIRE_MESSAGE(r.size() == 8, "Convex hull should contain 8 points.");
CHECK(r[0].is_equal_approx(Point2(-12, 4)));
CHECK(r[1].is_equal_approx(Point2(-10, -4)));
CHECK(r[2].is_equal_approx(Point2(-4, -8)));
CHECK(r[3].is_equal_approx(Point2(10, -8)));
CHECK(r[4].is_equal_approx(Point2(8, 2)));
CHECK(r[5].is_equal_approx(Point2(4, 8)));
CHECK(r[6].is_equal_approx(Point2(-6, 10)));
CHECK(r[7].is_equal_approx(Point2(-12, 4)));
}
SUBCASE("[Geometry2D] Add extra points outside border of original convex hull") {
a.push_back(Point2(-11, -1));
a.push_back(Point2(7, 6));
r = Geometry2D::convex_hull(a);
REQUIRE_MESSAGE(r.size() == 10, "Convex hull should contain 10 points.");
CHECK(r[0].is_equal_approx(Point2(-12, 4)));
CHECK(r[1].is_equal_approx(Point2(-11, -1)));
CHECK(r[2].is_equal_approx(Point2(-10, -4)));
CHECK(r[3].is_equal_approx(Point2(-4, -8)));
CHECK(r[4].is_equal_approx(Point2(10, -8)));
CHECK(r[5].is_equal_approx(Point2(8, 2)));
CHECK(r[6].is_equal_approx(Point2(7, 6)));
CHECK(r[7].is_equal_approx(Point2(4, 8)));
CHECK(r[8].is_equal_approx(Point2(-6, 10)));
CHECK(r[9].is_equal_approx(Point2(-12, 4)));
}
}
TEST_CASE("[Geometry2D] Bresenham line") {
Vector<Vector2i> r;
SUBCASE("[Geometry2D] Single point") {
r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(0, 0));
REQUIRE_MESSAGE(r.size() == 1, "The Bresenham line should contain exactly one point.");
CHECK(r[0] == Vector2i(0, 0));
}
SUBCASE("[Geometry2D] Line parallel to x-axis") {
r = Geometry2D::bresenham_line(Point2i(1, 2), Point2i(5, 2));
REQUIRE_MESSAGE(r.size() == 5, "The Bresenham line should contain exactly five points.");
CHECK(r[0] == Vector2i(1, 2));
CHECK(r[1] == Vector2i(2, 2));
CHECK(r[2] == Vector2i(3, 2));
CHECK(r[3] == Vector2i(4, 2));
CHECK(r[4] == Vector2i(5, 2));
}
SUBCASE("[Geometry2D] 45 degree line from the origin") {
r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(4, 4));
REQUIRE_MESSAGE(r.size() == 5, "The Bresenham line should contain exactly five points.");
CHECK(r[0] == Vector2i(0, 0));
CHECK(r[1] == Vector2i(1, 1));
CHECK(r[2] == Vector2i(2, 2));
CHECK(r[3] == Vector2i(3, 3));
CHECK(r[4] == Vector2i(4, 4));
}
SUBCASE("[Geometry2D] Sloped line going up one unit") {
r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(4, 1));
REQUIRE_MESSAGE(r.size() == 5, "The Bresenham line should contain exactly five points.");
CHECK(r[0] == Vector2i(0, 0));
CHECK(r[1] == Vector2i(1, 0));
CHECK(r[2] == Vector2i(2, 0));
CHECK(r[3] == Vector2i(3, 1));
CHECK(r[4] == Vector2i(4, 1));
}
SUBCASE("[Geometry2D] Sloped line going up two units") {
r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(4, 2));
REQUIRE_MESSAGE(r.size() == 5, "The Bresenham line should contain exactly five points.");
CHECK(r[0] == Vector2i(0, 0));
CHECK(r[1] == Vector2i(1, 0));
CHECK(r[2] == Vector2i(2, 1));
CHECK(r[3] == Vector2i(3, 1));
CHECK(r[4] == Vector2i(4, 2));
}
SUBCASE("[Geometry2D] Long sloped line") {
r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(11, 5));
REQUIRE_MESSAGE(r.size() == 12, "The Bresenham line should contain exactly twelve points.");
CHECK(r[0] == Vector2i(0, 0));
CHECK(r[1] == Vector2i(1, 0));
CHECK(r[2] == Vector2i(2, 1));
CHECK(r[3] == Vector2i(3, 1));
CHECK(r[4] == Vector2i(4, 2));
CHECK(r[5] == Vector2i(5, 2));
CHECK(r[6] == Vector2i(6, 3));
CHECK(r[7] == Vector2i(7, 3));
CHECK(r[8] == Vector2i(8, 4));
CHECK(r[9] == Vector2i(9, 4));
CHECK(r[10] == Vector2i(10, 5));
CHECK(r[11] == Vector2i(11, 5));
}
}
} // namespace TestGeometry2D

View File

@@ -0,0 +1,201 @@
/**************************************************************************/
/* test_geometry_3d.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/geometry_3d.h"
#include "tests/test_macros.h"
namespace TestGeometry3D {
TEST_CASE("[Geometry3D] Closest Points Between Segments") {
Vector3 ps, qt;
Geometry3D::get_closest_points_between_segments(Vector3(1, -1, 1), Vector3(1, 1, -1), Vector3(-1, -2, -1), Vector3(-1, 1, 1), ps, qt);
CHECK(ps.is_equal_approx(Vector3(1, -0.2, 0.2)));
CHECK(qt.is_equal_approx(Vector3(-1, -0.2, 0.2)));
}
TEST_CASE("[Geometry3D] Closest Distance Between Segments") {
CHECK(Geometry3D::get_closest_distance_between_segments(Vector3(1, -2, 0), Vector3(1, 2, 0), Vector3(-1, 2, 0), Vector3(-1, -2, 0)) == 2.0f);
}
TEST_CASE("[Geometry3D] Build Box Planes") {
constexpr Vector3 extents = Vector3(5, 5, 20);
Vector<Plane> box = Geometry3D::build_box_planes(extents);
CHECK(box.size() == 6);
CHECK(extents.x == box[0].d);
CHECK(box[0].normal == Vector3(1, 0, 0));
CHECK(extents.x == box[1].d);
CHECK(box[1].normal == Vector3(-1, 0, 0));
CHECK(extents.y == box[2].d);
CHECK(box[2].normal == Vector3(0, 1, 0));
CHECK(extents.y == box[3].d);
CHECK(box[3].normal == Vector3(0, -1, 0));
CHECK(extents.z == box[4].d);
CHECK(box[4].normal == Vector3(0, 0, 1));
CHECK(extents.z == box[5].d);
CHECK(box[5].normal == Vector3(0, 0, -1));
}
TEST_CASE("[Geometry3D] Build Capsule Planes") {
Vector<Plane> capsule = Geometry3D::build_capsule_planes(10, 20, 6, 10);
CHECK(capsule.size() == 126);
}
TEST_CASE("[Geometry3D] Build Cylinder Planes") {
Vector<Plane> planes = Geometry3D::build_cylinder_planes(3.0f, 10.0f, 10);
CHECK(planes.size() == 12);
}
TEST_CASE("[Geometry3D] Build Sphere Planes") {
Vector<Plane> planes = Geometry3D::build_sphere_planes(10.0f, 10, 3);
CHECK(planes.size() == 63);
}
#if false
// This test has been temporarily disabled because it's really fragile and
// breaks if calculations change very slightly. For example, it breaks when
// using doubles, and it breaks when making Plane calculations more accurate.
TEST_CASE("[Geometry3D] Build Convex Mesh") {
struct Case {
Vector<Plane> object;
int want_faces, want_edges, want_vertices;
Case(){};
Case(Vector<Plane> p_object, int p_want_faces, int p_want_edges, int p_want_vertices) :
object(p_object), want_faces(p_want_faces), want_edges(p_want_edges), want_vertices(p_want_vertices){};
};
Vector<Case> tt;
tt.push_back(Case(Geometry3D::build_box_planes(Vector3(5, 10, 5)), 6, 12, 8));
tt.push_back(Case(Geometry3D::build_capsule_planes(5, 5, 20, 20, Vector3::Axis()), 820, 7603, 6243));
tt.push_back(Case(Geometry3D::build_cylinder_planes(5, 5, 20, Vector3::Axis()), 22, 100, 80));
tt.push_back(Case(Geometry3D::build_sphere_planes(5, 5, 20), 220, 1011, 522));
for (int i = 0; i < tt.size(); ++i) {
Case current_case = tt[i];
Geometry3D::MeshData mesh = Geometry3D::build_convex_mesh(current_case.object);
CHECK(mesh.faces.size() == current_case.want_faces);
CHECK(mesh.edges.size() == current_case.want_edges);
CHECK(mesh.vertices.size() == current_case.want_vertices);
}
}
#endif
TEST_CASE("[Geometry3D] Clip Polygon") {
Vector<Plane> box_planes = Geometry3D::build_box_planes(Vector3(5, 10, 5));
Vector<Vector3> box = Geometry3D::compute_convex_mesh_points(&box_planes[0], box_planes.size());
Vector<Vector3> output = Geometry3D::clip_polygon(box, Plane());
CHECK(output == box);
output = Geometry3D::clip_polygon(box, Plane(Vector3(0, 1, 0), Vector3(0, 3, 0)));
CHECK(output != box);
}
TEST_CASE("[Geometry3D] Compute Convex Mesh Points") {
Vector<Vector3> cube;
cube.push_back(Vector3(-5, -5, -5));
cube.push_back(Vector3(5, -5, -5));
cube.push_back(Vector3(-5, 5, -5));
cube.push_back(Vector3(5, 5, -5));
cube.push_back(Vector3(-5, -5, 5));
cube.push_back(Vector3(5, -5, 5));
cube.push_back(Vector3(-5, 5, 5));
cube.push_back(Vector3(5, 5, 5));
Vector<Plane> box_planes = Geometry3D::build_box_planes(Vector3(5, 5, 5));
CHECK(Geometry3D::compute_convex_mesh_points(&box_planes[0], box_planes.size()) == cube);
}
TEST_CASE("[Geometry3D] Get Closest Point To Segment") {
constexpr Vector3 a = Vector3(1, 1, 1);
constexpr Vector3 b = Vector3(5, 5, 5);
Vector3 output = Geometry3D::get_closest_point_to_segment(Vector3(2, 1, 4), a, b);
CHECK(output.is_equal_approx(Vector3(2.33333, 2.33333, 2.33333)));
}
TEST_CASE("[Geometry3D] Plane and Box Overlap") {
CHECK(Geometry3D::planeBoxOverlap(Vector3(3, 4, 2), 5.0f, Vector3(5, 5, 5)) == true);
CHECK(Geometry3D::planeBoxOverlap(Vector3(0, 1, 0), -10.0f, Vector3(5, 5, 5)) == false);
CHECK(Geometry3D::planeBoxOverlap(Vector3(1, 0, 0), -6.0f, Vector3(5, 5, 5)) == false);
}
TEST_CASE("[Geometry3D] Is Point in Projected Triangle") {
CHECK(Geometry3D::point_in_projected_triangle(Vector3(1, 1, 0), Vector3(3, 0, 0), Vector3(0, 3, 0), Vector3(-3, 0, 0)) == true);
CHECK(Geometry3D::point_in_projected_triangle(Vector3(5, 1, 0), Vector3(3, 0, 0), Vector3(0, 3, 0), Vector3(-3, 0, 0)) == false);
CHECK(Geometry3D::point_in_projected_triangle(Vector3(3, 0, 0), Vector3(3, 0, 0), Vector3(0, 3, 0), Vector3(-3, 0, 0)) == true);
}
TEST_CASE("[Geometry3D] Does Ray Intersect Triangle") {
Vector3 result;
CHECK(Geometry3D::ray_intersects_triangle(Vector3(0, 1, 1), Vector3(0, 0, -10), Vector3(0, 3, 0), Vector3(-3, 0, 0), Vector3(3, 0, 0), &result) == true);
CHECK(Geometry3D::ray_intersects_triangle(Vector3(5, 10, 1), Vector3(0, 0, -10), Vector3(0, 3, 0), Vector3(-3, 0, 0), Vector3(3, 0, 0), &result) == false);
CHECK(Geometry3D::ray_intersects_triangle(Vector3(0, 1, 1), Vector3(0, 0, 10), Vector3(0, 3, 0), Vector3(-3, 0, 0), Vector3(3, 0, 0), &result) == false);
}
TEST_CASE("[Geometry3D] Does Segment Intersect Convex") {
Vector<Plane> box_planes = Geometry3D::build_box_planes(Vector3(5, 5, 5));
Vector3 result, normal;
CHECK(Geometry3D::segment_intersects_convex(Vector3(10, 10, 10), Vector3(0, 0, 0), &box_planes[0], box_planes.size(), &result, &normal) == true);
CHECK(Geometry3D::segment_intersects_convex(Vector3(10, 10, 10), Vector3(5, 5, 5), &box_planes[0], box_planes.size(), &result, &normal) == true);
CHECK(Geometry3D::segment_intersects_convex(Vector3(10, 10, 10), Vector3(6, 5, 5), &box_planes[0], box_planes.size(), &result, &normal) == false);
}
TEST_CASE("[Geometry3D] Segment Intersects Cylinder") {
Vector3 result, normal;
CHECK(Geometry3D::segment_intersects_cylinder(Vector3(10, 10, 10), Vector3(0, 0, 0), 5, 5, &result, &normal) == true);
CHECK(Geometry3D::segment_intersects_cylinder(Vector3(10, 10, 10), Vector3(6, 6, 6), 5, 5, &result, &normal) == false);
}
TEST_CASE("[Geometry3D] Segment Intersects Cylinder") {
Vector3 result, normal;
CHECK(Geometry3D::segment_intersects_sphere(Vector3(10, 10, 10), Vector3(0, 0, 0), Vector3(0, 0, 0), 5, &result, &normal) == true);
CHECK(Geometry3D::segment_intersects_sphere(Vector3(10, 10, 10), Vector3(0, 0, 2.5), Vector3(0, 0, 0), 5, &result, &normal) == true);
CHECK(Geometry3D::segment_intersects_sphere(Vector3(10, 10, 10), Vector3(5, 5, 5), Vector3(0, 0, 0), 5, &result, &normal) == false);
}
TEST_CASE("[Geometry3D] Segment Intersects Triangle") {
Vector3 result;
CHECK(Geometry3D::segment_intersects_triangle(Vector3(1, 1, 1), Vector3(-1, -1, -1), Vector3(-3, 0, 0), Vector3(0, 3, 0), Vector3(3, 0, 0), &result) == true);
CHECK(Geometry3D::segment_intersects_triangle(Vector3(1, 1, 1), Vector3(3, 0, 0), Vector3(-3, 0, 0), Vector3(0, 3, 0), Vector3(3, 0, 0), &result) == true);
CHECK(Geometry3D::segment_intersects_triangle(Vector3(1, 1, 1), Vector3(10, -1, -1), Vector3(-3, 0, 0), Vector3(0, 3, 0), Vector3(3, 0, 0), &result) == false);
}
TEST_CASE("[Geometry3D] Triangle and Box Overlap") {
constexpr Vector3 good_triangle[3] = { Vector3(3, 2, 3), Vector3(2, 2, 1), Vector3(2, 1, 1) };
CHECK(Geometry3D::triangle_box_overlap(Vector3(0, 0, 0), Vector3(5, 5, 5), good_triangle) == true);
constexpr Vector3 bad_triangle[3] = { Vector3(100, 100, 100), Vector3(-100, -100, -100), Vector3(10, 10, 10) };
CHECK(Geometry3D::triangle_box_overlap(Vector3(1000, 1000, 1000), Vector3(1, 1, 1), bad_triangle) == false);
}
TEST_CASE("[Geometry3D] Triangle and Sphere Intersect") {
constexpr Vector3 triangle_a = Vector3(3, 0, 0);
constexpr Vector3 triangle_b = Vector3(-3, 0, 0);
constexpr Vector3 triangle_c = Vector3(0, 3, 0);
Vector3 triangle_contact, sphere_contact;
CHECK(Geometry3D::triangle_sphere_intersection_test(triangle_a, triangle_b, triangle_c, Vector3(0, -1, 0), Vector3(0, 0, 0), 5, triangle_contact, sphere_contact) == true);
CHECK(Geometry3D::triangle_sphere_intersection_test(triangle_a, triangle_b, triangle_c, Vector3(0, 1, 0), Vector3(0, 0, 0), 5, triangle_contact, sphere_contact) == true);
CHECK(Geometry3D::triangle_sphere_intersection_test(triangle_a, triangle_b, triangle_c, Vector3(0, 1, 0), Vector3(20, 0, 0), 5, triangle_contact, sphere_contact) == false);
}
} // namespace TestGeometry3D

View File

@@ -0,0 +1,641 @@
/**************************************************************************/
/* test_math_funcs.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "tests/test_macros.h"
namespace TestMath {
TEST_CASE("[Math] C++ macros") {
CHECK(MIN(-2, 2) == -2);
CHECK(MIN(600, 2) == 2);
CHECK(MAX(-2, 2) == 2);
CHECK(MAX(600, 2) == 600);
CHECK(CLAMP(600, -2, 2) == 2);
CHECK(CLAMP(620, 600, 650) == 620);
// `max` is lower than `min`.
CHECK(CLAMP(620, 600, 50) == 50);
CHECK(Math::abs(-5) == 5);
CHECK(Math::abs(0) == 0);
CHECK(Math::abs(5) == 5);
CHECK(SIGN(-5) == -1.0);
CHECK(SIGN(0) == 0.0);
CHECK(SIGN(5) == 1.0);
// Check that SIGN(Math::NaN) returns 0.0.
CHECK(SIGN(Math::NaN) == 0.0);
}
TEST_CASE("[Math] Power of two functions") {
CHECK(next_power_of_2((uint32_t)0) == 0);
CHECK(next_power_of_2((uint32_t)1) == 1);
CHECK(next_power_of_2((uint32_t)16) == 16);
CHECK(next_power_of_2((uint32_t)17) == 32);
CHECK(next_power_of_2((uint32_t)65535) == 65536);
CHECK(previous_power_of_2((uint32_t)0) == 0);
CHECK(previous_power_of_2((uint32_t)1) == 1);
CHECK(previous_power_of_2((uint32_t)16) == 16);
CHECK(previous_power_of_2((uint32_t)17) == 16);
CHECK(previous_power_of_2((uint32_t)65535) == 32768);
CHECK(closest_power_of_2((uint32_t)0) == 0);
CHECK(closest_power_of_2((uint32_t)1) == 1);
CHECK(closest_power_of_2((uint32_t)16) == 16);
CHECK(closest_power_of_2((uint32_t)17) == 16);
CHECK(closest_power_of_2((uint32_t)65535) == 65536);
CHECK(get_shift_from_power_of_2((uint32_t)0) == -1);
CHECK(get_shift_from_power_of_2((uint32_t)1) == 0);
CHECK(get_shift_from_power_of_2((uint32_t)16) == 4);
CHECK(get_shift_from_power_of_2((uint32_t)17) == -1);
CHECK(get_shift_from_power_of_2((uint32_t)65535) == -1);
CHECK(nearest_shift((uint32_t)0) == 0);
CHECK(nearest_shift((uint32_t)1) == 1);
CHECK(nearest_shift((uint32_t)16) == 5);
CHECK(nearest_shift((uint32_t)17) == 5);
CHECK(nearest_shift((uint32_t)65535) == 16);
}
TEST_CASE_TEMPLATE("[Math] abs", T, int, float, double) {
CHECK(Math::abs((T)-1) == (T)1);
CHECK(Math::abs((T)0) == (T)0);
CHECK(Math::abs((T)1) == (T)1);
CHECK(Math::abs((T)0.1) == (T)0.1);
}
TEST_CASE_TEMPLATE("[Math] round/floor/ceil", T, float, double) {
CHECK(Math::round((T)1.5) == (T)2.0);
CHECK(Math::round((T)1.6) == (T)2.0);
CHECK(Math::round((T)-1.5) == (T)-2.0);
CHECK(Math::round((T)-1.1) == (T)-1.0);
CHECK(Math::floor((T)1.5) == (T)1.0);
CHECK(Math::floor((T)-1.5) == (T)-2.0);
CHECK(Math::ceil((T)1.5) == (T)2.0);
CHECK(Math::ceil((T)-1.9) == (T)-1.0);
}
TEST_CASE_TEMPLATE("[Math] integer division round up unsigned", T, uint32_t, uint64_t) {
CHECK(Math::division_round_up((T)0, (T)64) == 0);
CHECK(Math::division_round_up((T)1, (T)64) == 1);
CHECK(Math::division_round_up((T)63, (T)64) == 1);
CHECK(Math::division_round_up((T)64, (T)64) == 1);
CHECK(Math::division_round_up((T)65, (T)64) == 2);
CHECK(Math::division_round_up((T)65, (T)1) == 65);
}
TEST_CASE_TEMPLATE("[Math] integer division round up signed", T, int32_t, int64_t) {
CHECK(Math::division_round_up((T)0, (T)64) == 0);
CHECK(Math::division_round_up((T)1, (T)64) == 1);
CHECK(Math::division_round_up((T)63, (T)64) == 1);
CHECK(Math::division_round_up((T)64, (T)64) == 1);
CHECK(Math::division_round_up((T)65, (T)64) == 2);
CHECK(Math::division_round_up((T)65, (T)1) == 65);
CHECK(Math::division_round_up((T)-1, (T)64) == 0);
CHECK(Math::division_round_up((T)-1, (T)-1) == 1);
CHECK(Math::division_round_up((T)-1, (T)1) == -1);
CHECK(Math::division_round_up((T)-1, (T)-2) == 1);
CHECK(Math::division_round_up((T)-4, (T)-2) == 2);
}
TEST_CASE_TEMPLATE("[Math] sin/cos/tan", T, float, double) {
CHECK(Math::sin((T)-0.1) == doctest::Approx((T)-0.0998334166));
CHECK(Math::sin((T)0.1) == doctest::Approx((T)0.0998334166));
CHECK(Math::sin((T)0.5) == doctest::Approx((T)0.4794255386));
CHECK(Math::sin((T)1.0) == doctest::Approx((T)0.8414709848));
CHECK(Math::sin((T)1.5) == doctest::Approx((T)0.9974949866));
CHECK(Math::sin((T)450.0) == doctest::Approx((T)-0.683283725));
CHECK(Math::cos((T)-0.1) == doctest::Approx((T)0.99500416530));
CHECK(Math::cos((T)0.1) == doctest::Approx((T)0.9950041653));
CHECK(Math::cos((T)0.5) == doctest::Approx((T)0.8775825619));
CHECK(Math::cos((T)1.0) == doctest::Approx((T)0.5403023059));
CHECK(Math::cos((T)1.5) == doctest::Approx((T)0.0707372017));
CHECK(Math::cos((T)450.0) == doctest::Approx((T)-0.7301529642));
CHECK(Math::tan((T)-0.1) == doctest::Approx((T)-0.1003346721));
CHECK(Math::tan((T)0.1) == doctest::Approx((T)0.1003346721));
CHECK(Math::tan((T)0.5) == doctest::Approx((T)0.5463024898));
CHECK(Math::tan((T)1.0) == doctest::Approx((T)1.5574077247));
CHECK(Math::tan((T)1.5) == doctest::Approx((T)14.1014199472));
CHECK(Math::tan((T)450.0) == doctest::Approx((T)0.9358090134));
}
TEST_CASE_TEMPLATE("[Math] sinh/cosh/tanh", T, float, double) {
CHECK(Math::sinh((T)-0.1) == doctest::Approx((T)-0.10016675));
CHECK(Math::sinh((T)0.1) == doctest::Approx((T)0.10016675));
CHECK(Math::sinh((T)0.5) == doctest::Approx((T)0.5210953055));
CHECK(Math::sinh((T)1.0) == doctest::Approx((T)1.1752011936));
CHECK(Math::sinh((T)1.5) == doctest::Approx((T)2.1292794551));
CHECK(Math::cosh((T)-0.1) == doctest::Approx((T)1.0050041681));
CHECK(Math::cosh((T)0.1) == doctest::Approx((T)1.0050041681));
CHECK(Math::cosh((T)0.5) == doctest::Approx((T)1.1276259652));
CHECK(Math::cosh((T)1.0) == doctest::Approx((T)1.5430806348));
CHECK(Math::cosh((T)1.5) == doctest::Approx((T)2.3524096152));
CHECK(Math::tanh((T)-0.1) == doctest::Approx((T)-0.0996679946));
CHECK(Math::tanh((T)0.1) == doctest::Approx((T)0.0996679946));
CHECK(Math::tanh((T)0.5) == doctest::Approx((T)0.4621171573));
CHECK(Math::tanh((T)1.0) == doctest::Approx((T)0.761594156));
CHECK(Math::tanh((T)1.5) == doctest::Approx((T)0.9051482536));
CHECK(Math::tanh((T)450.0) == doctest::Approx((T)1.0));
}
TEST_CASE_TEMPLATE("[Math] asin/acos/atan", T, float, double) {
CHECK(Math::asin((T)-0.1) == doctest::Approx((T)-0.1001674212));
CHECK(Math::asin((T)0.1) == doctest::Approx((T)0.1001674212));
CHECK(Math::asin((T)0.5) == doctest::Approx((T)0.5235987756));
CHECK(Math::asin((T)1.0) == doctest::Approx((T)1.5707963268));
CHECK(Math::asin((T)2.0) == doctest::Approx((T)1.5707963268));
CHECK(Math::asin((T)-2.0) == doctest::Approx((T)-1.5707963268));
CHECK(Math::acos((T)-0.1) == doctest::Approx((T)1.670963748));
CHECK(Math::acos((T)0.1) == doctest::Approx((T)1.4706289056));
CHECK(Math::acos((T)0.5) == doctest::Approx((T)1.0471975512));
CHECK(Math::acos((T)1.0) == doctest::Approx((T)0.0));
CHECK(Math::acos((T)2.0) == doctest::Approx((T)0.0));
CHECK(Math::acos((T)-2.0) == doctest::Approx((T)Math::PI));
CHECK(Math::atan((T)-0.1) == doctest::Approx((T)-0.0996686525));
CHECK(Math::atan((T)0.1) == doctest::Approx((T)0.0996686525));
CHECK(Math::atan((T)0.5) == doctest::Approx((T)0.463647609));
CHECK(Math::atan((T)1.0) == doctest::Approx((T)0.7853981634));
CHECK(Math::atan((T)1.5) == doctest::Approx((T)0.9827937232));
CHECK(Math::atan((T)450.0) == doctest::Approx((T)1.5685741082));
}
TEST_CASE_TEMPLATE("[Math] asinh/acosh/atanh", T, float, double) {
CHECK(Math::asinh((T)-2.0) == doctest::Approx((T)-1.4436354751));
CHECK(Math::asinh((T)-0.1) == doctest::Approx((T)-0.0998340788));
CHECK(Math::asinh((T)0.1) == doctest::Approx((T)0.0998340788));
CHECK(Math::asinh((T)0.5) == doctest::Approx((T)0.4812118250));
CHECK(Math::asinh((T)1.0) == doctest::Approx((T)0.8813735870));
CHECK(Math::asinh((T)2.0) == doctest::Approx((T)1.4436354751));
CHECK(Math::acosh((T)-2.0) == doctest::Approx((T)0.0));
CHECK(Math::acosh((T)-0.1) == doctest::Approx((T)0.0));
CHECK(Math::acosh((T)0.1) == doctest::Approx((T)0.0));
CHECK(Math::acosh((T)0.5) == doctest::Approx((T)0.0));
CHECK(Math::acosh((T)1.0) == doctest::Approx((T)0.0));
CHECK(Math::acosh((T)2.0) == doctest::Approx((T)1.3169578969));
CHECK(Math::acosh((T)450.0) == doctest::Approx((T)6.8023935287));
CHECK(Math::is_inf(Math::atanh((T)-2.0)));
CHECK(Math::atanh((T)-2.0) < (T)0.0);
CHECK(Math::is_inf(Math::atanh((T)-1.0)));
CHECK(Math::atanh((T)-1.0) < (T)0.0);
CHECK(Math::atanh((T)-0.1) == doctest::Approx((T)-0.1003353477));
CHECK(Math::atanh((T)0.1) == doctest::Approx((T)0.1003353477));
CHECK(Math::atanh((T)0.5) == doctest::Approx((T)0.5493061443));
CHECK(Math::is_inf(Math::atanh((T)1.0)));
CHECK(Math::atanh((T)1.0) > (T)0.0);
CHECK(Math::is_inf(Math::atanh((T)1.5)));
CHECK(Math::atanh((T)1.5) > (T)0.0);
CHECK(Math::is_inf(Math::atanh((T)450.0)));
CHECK(Math::atanh((T)450.0) > (T)0.0);
}
TEST_CASE_TEMPLATE("[Math] sinc/sincn/atan2", T, float, double) {
CHECK(Math::sinc((T)-0.1) == doctest::Approx((T)0.9983341665));
CHECK(Math::sinc((T)0.1) == doctest::Approx((T)0.9983341665));
CHECK(Math::sinc((T)0.5) == doctest::Approx((T)0.9588510772));
CHECK(Math::sinc((T)1.0) == doctest::Approx((T)0.8414709848));
CHECK(Math::sinc((T)1.5) == doctest::Approx((T)0.6649966577));
CHECK(Math::sinc((T)450.0) == doctest::Approx((T)-0.0015184083));
CHECK(Math::sincn((T)-0.1) == doctest::Approx((T)0.9836316431));
CHECK(Math::sincn((T)0.1) == doctest::Approx((T)0.9836316431));
CHECK(Math::sincn((T)0.5) == doctest::Approx((T)0.6366197724));
CHECK(Math::sincn((T)1.0) == doctest::Approx((T)0.0));
CHECK(Math::sincn((T)1.5) == doctest::Approx((T)-0.2122065908));
CHECK(Math::sincn((T)450.0) == doctest::Approx((T)0.0));
CHECK(Math::atan2((T)-0.1, (T)0.5) == doctest::Approx((T)-0.1973955598));
CHECK(Math::atan2((T)0.1, (T)-0.5) == doctest::Approx((T)2.9441970937));
CHECK(Math::atan2((T)0.5, (T)1.5) == doctest::Approx((T)0.3217505544));
CHECK(Math::atan2((T)1.0, (T)2.5) == doctest::Approx((T)0.3805063771));
CHECK(Math::atan2((T)1.5, (T)1.0) == doctest::Approx((T)0.9827937232));
CHECK(Math::atan2((T)450.0, (T)1.0) == doctest::Approx((T)1.5685741082));
}
TEST_CASE_TEMPLATE("[Math] pow/log/log2/exp/sqrt", T, float, double) {
CHECK(Math::pow((T)-0.1, (T)2.0) == doctest::Approx((T)0.01));
CHECK(Math::pow((T)0.1, (T)2.5) == doctest::Approx((T)0.0031622777));
CHECK(Math::pow((T)0.5, (T)0.5) == doctest::Approx((T)0.7071067812));
CHECK(Math::pow((T)1.0, (T)1.0) == doctest::Approx((T)1.0));
CHECK(Math::pow((T)1.5, (T)-1.0) == doctest::Approx((T)0.6666666667));
CHECK(Math::pow((T)450.0, (T)-2.0) == doctest::Approx((T)0.0000049383));
CHECK(Math::pow((T)450.0, (T)0.0) == doctest::Approx((T)1.0));
CHECK(Math::is_nan(Math::log((T)-0.1)));
CHECK(Math::log((T)0.1) == doctest::Approx((T)-2.302585093));
CHECK(Math::log((T)0.5) == doctest::Approx((T)-0.6931471806));
CHECK(Math::log((T)1.0) == doctest::Approx((T)0.0));
CHECK(Math::log((T)1.5) == doctest::Approx((T)0.4054651081));
CHECK(Math::log((T)450.0) == doctest::Approx((T)6.1092475828));
CHECK(Math::is_nan(Math::log2((T)-0.1)));
CHECK(Math::log2((T)0.1) == doctest::Approx((T)-3.3219280949));
CHECK(Math::log2((T)0.5) == doctest::Approx((T)-1.0));
CHECK(Math::log2((T)1.0) == doctest::Approx((T)0.0));
CHECK(Math::log2((T)1.5) == doctest::Approx((T)0.5849625007));
CHECK(Math::log2((T)450.0) == doctest::Approx((T)8.8137811912));
CHECK(Math::exp((T)-0.1) == doctest::Approx((T)0.904837418));
CHECK(Math::exp((T)0.1) == doctest::Approx((T)1.1051709181));
CHECK(Math::exp((T)0.5) == doctest::Approx((T)1.6487212707));
CHECK(Math::exp((T)1.0) == doctest::Approx((T)2.7182818285));
CHECK(Math::exp((T)1.5) == doctest::Approx((T)4.4816890703));
CHECK(Math::is_nan(Math::sqrt((T)-0.1)));
CHECK(Math::sqrt((T)0.1) == doctest::Approx((T)0.316228));
CHECK(Math::sqrt((T)0.5) == doctest::Approx((T)0.707107));
CHECK(Math::sqrt((T)1.0) == doctest::Approx((T)1.0));
CHECK(Math::sqrt((T)1.5) == doctest::Approx((T)1.224745));
}
TEST_CASE_TEMPLATE("[Math] is_nan/is_inf", T, float, double) {
CHECK(!Math::is_nan((T)0.0));
CHECK(Math::is_nan((T)Math::NaN));
CHECK(!Math::is_inf((T)0.0));
CHECK(Math::is_inf((T)Math::INF));
}
TEST_CASE_TEMPLATE("[Math] linear_to_db", T, float, double) {
CHECK(Math::linear_to_db((T)1.0) == doctest::Approx((T)0.0));
CHECK(Math::linear_to_db((T)20.0) == doctest::Approx((T)26.0206));
CHECK(Math::is_inf(Math::linear_to_db((T)0.0)));
CHECK(Math::is_nan(Math::linear_to_db((T)-20.0)));
}
TEST_CASE_TEMPLATE("[Math] db_to_linear", T, float, double) {
CHECK(Math::db_to_linear((T)0.0) == doctest::Approx((T)1.0));
CHECK(Math::db_to_linear((T)1.0) == doctest::Approx((T)1.122018));
CHECK(Math::db_to_linear((T)20.0) == doctest::Approx((T)10.0));
CHECK(Math::db_to_linear((T)-20.0) == doctest::Approx((T)0.1));
}
TEST_CASE_TEMPLATE("[Math] step_decimals", T, float, double) {
CHECK(Math::step_decimals((T)-0.5) == 1);
CHECK(Math::step_decimals((T)0) == 0);
CHECK(Math::step_decimals((T)1) == 0);
CHECK(Math::step_decimals((T)0.1) == 1);
CHECK(Math::step_decimals((T)0.01) == 2);
CHECK(Math::step_decimals((T)0.001) == 3);
CHECK(Math::step_decimals((T)0.0001) == 4);
CHECK(Math::step_decimals((T)0.00001) == 5);
CHECK(Math::step_decimals((T)0.000001) == 6);
CHECK(Math::step_decimals((T)0.0000001) == 7);
CHECK(Math::step_decimals((T)0.00000001) == 8);
CHECK(Math::step_decimals((T)0.000000001) == 9);
// Too many decimals to handle.
CHECK(Math::step_decimals((T)0.0000000001) == 0);
}
TEST_CASE_TEMPLATE("[Math] range_step_decimals", T, float, double) {
CHECK(Math::range_step_decimals((T)0.000000001) == 9);
// Too many decimals to handle.
CHECK(Math::range_step_decimals((T)0.0000000001) == 0);
// Should be treated as a step of 0 for use by the editor.
CHECK(Math::range_step_decimals((T)0.0) == 16);
CHECK(Math::range_step_decimals((T)-0.5) == 16);
}
TEST_CASE_TEMPLATE("[Math] lerp", T, float, double) {
CHECK(Math::lerp((T)2.0, (T)5.0, (T)-0.1) == doctest::Approx((T)1.7));
CHECK(Math::lerp((T)2.0, (T)5.0, (T)0.0) == doctest::Approx((T)2.0));
CHECK(Math::lerp((T)2.0, (T)5.0, (T)0.1) == doctest::Approx((T)2.3));
CHECK(Math::lerp((T)2.0, (T)5.0, (T)1.0) == doctest::Approx((T)5.0));
CHECK(Math::lerp((T)2.0, (T)5.0, (T)2.0) == doctest::Approx((T)8.0));
CHECK(Math::lerp((T)-2.0, (T)-5.0, (T)-0.1) == doctest::Approx((T)-1.7));
CHECK(Math::lerp((T)-2.0, (T)-5.0, (T)0.0) == doctest::Approx((T)-2.0));
CHECK(Math::lerp((T)-2.0, (T)-5.0, (T)0.1) == doctest::Approx((T)-2.3));
CHECK(Math::lerp((T)-2.0, (T)-5.0, (T)1.0) == doctest::Approx((T)-5.0));
CHECK(Math::lerp((T)-2.0, (T)-5.0, (T)2.0) == doctest::Approx((T)-8.0));
}
TEST_CASE_TEMPLATE("[Math] inverse_lerp", T, float, double) {
CHECK(Math::inverse_lerp((T)2.0, (T)5.0, (T)1.7) == doctest::Approx((T)-0.1));
CHECK(Math::inverse_lerp((T)2.0, (T)5.0, (T)2.0) == doctest::Approx((T)0.0));
CHECK(Math::inverse_lerp((T)2.0, (T)5.0, (T)2.3) == doctest::Approx((T)0.1));
CHECK(Math::inverse_lerp((T)2.0, (T)5.0, (T)5.0) == doctest::Approx((T)1.0));
CHECK(Math::inverse_lerp((T)2.0, (T)5.0, (T)8.0) == doctest::Approx((T)2.0));
CHECK(Math::inverse_lerp((T)-2.0, (T)-5.0, (T)-1.7) == doctest::Approx((T)-0.1));
CHECK(Math::inverse_lerp((T)-2.0, (T)-5.0, (T)-2.0) == doctest::Approx((T)0.0));
CHECK(Math::inverse_lerp((T)-2.0, (T)-5.0, (T)-2.3) == doctest::Approx((T)0.1));
CHECK(Math::inverse_lerp((T)-2.0, (T)-5.0, (T)-5.0) == doctest::Approx((T)1.0));
CHECK(Math::inverse_lerp((T)-2.0, (T)-5.0, (T)-8.0) == doctest::Approx((T)2.0));
}
TEST_CASE_TEMPLATE("[Math] remap", T, float, double) {
CHECK(Math::remap((T)50.0, (T)100.0, (T)200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)-500.0));
CHECK(Math::remap((T)100.0, (T)100.0, (T)200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)0.0));
CHECK(Math::remap((T)200.0, (T)100.0, (T)200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)1000.0));
CHECK(Math::remap((T)250.0, (T)100.0, (T)200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)1500.0));
CHECK(Math::remap((T)-50.0, (T)-100.0, (T)-200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)-500.0));
CHECK(Math::remap((T)-100.0, (T)-100.0, (T)-200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)0.0));
CHECK(Math::remap((T)-200.0, (T)-100.0, (T)-200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)1000.0));
CHECK(Math::remap((T)-250.0, (T)-100.0, (T)-200.0, (T)0.0, (T)1000.0) == doctest::Approx((T)1500.0));
CHECK(Math::remap((T)-50.0, (T)-100.0, (T)-200.0, (T)0.0, (T)-1000.0) == doctest::Approx((T)500.0));
CHECK(Math::remap((T)-100.0, (T)-100.0, (T)-200.0, (T)0.0, (T)-1000.0) == doctest::Approx((T)0.0));
CHECK(Math::remap((T)-200.0, (T)-100.0, (T)-200.0, (T)0.0, (T)-1000.0) == doctest::Approx((T)-1000.0));
CHECK(Math::remap((T)-250.0, (T)-100.0, (T)-200.0, (T)0.0, (T)-1000.0) == doctest::Approx((T)-1500.0));
// Note: undefined behavior can happen when `p_istart == p_istop`. We don't bother testing this as it will
// vary between hardware and compilers properly implementing IEEE 754.
}
TEST_CASE_TEMPLATE("[Math] angle_difference", T, float, double) {
// Loops around, should return 0.0.
CHECK(Math::angle_difference((T)0.0, (T)Math::TAU) == doctest::Approx((T)0.0));
CHECK(Math::angle_difference((T)Math::PI, (T)-Math::PI) == doctest::Approx((T)0.0));
CHECK(Math::angle_difference((T)0.0, (T)Math::TAU * (T)4.0) == doctest::Approx((T)0.0));
// Rotation is clockwise, so it should return -PI.
CHECK(Math::angle_difference((T)0.0, (T)Math::PI) == doctest::Approx((T)-Math::PI));
CHECK(Math::angle_difference((T)0.0, (T)-Math::PI) == doctest::Approx((T)Math::PI));
CHECK(Math::angle_difference((T)Math::PI, (T)0.0) == doctest::Approx((T)Math::PI));
CHECK(Math::angle_difference((T)-Math::PI, (T)0.0) == doctest::Approx((T)-Math::PI));
CHECK(Math::angle_difference((T)0.0, (T)3.0) == doctest::Approx((T)3.0));
CHECK(Math::angle_difference((T)1.0, (T)-2.0) == doctest::Approx((T)-3.0));
CHECK(Math::angle_difference((T)-1.0, (T)2.0) == doctest::Approx((T)3.0));
CHECK(Math::angle_difference((T)-2.0, (T)-4.5) == doctest::Approx((T)-2.5));
CHECK(Math::angle_difference((T)100.0, (T)102.5) == doctest::Approx((T)2.5));
}
TEST_CASE_TEMPLATE("[Math] lerp_angle", T, float, double) {
// Counter-clockwise rotation.
CHECK(Math::lerp_angle((T)0.24 * Math::TAU, 0.75 * Math::TAU, 0.5) == doctest::Approx((T)-0.005 * Math::TAU));
// Counter-clockwise rotation.
CHECK(Math::lerp_angle((T)0.25 * Math::TAU, 0.75 * Math::TAU, 0.5) == doctest::Approx((T)0.0));
// Clockwise rotation.
CHECK(Math::lerp_angle((T)0.26 * Math::TAU, 0.75 * Math::TAU, 0.5) == doctest::Approx((T)0.505 * Math::TAU));
CHECK(Math::lerp_angle((T)-0.25 * Math::TAU, 1.25 * Math::TAU, 0.5) == doctest::Approx((T)-0.5 * Math::TAU));
CHECK(Math::lerp_angle((T)0.72 * Math::TAU, 1.44 * Math::TAU, 0.96) == doctest::Approx((T)0.4512 * Math::TAU));
CHECK(Math::lerp_angle((T)0.72 * Math::TAU, 1.44 * Math::TAU, 1.04) == doctest::Approx((T)0.4288 * Math::TAU));
// Initial and final angles are effectively identical, so the value returned
// should always be the same regardless of the `weight` parameter.
CHECK(Math::lerp_angle((T)-4 * Math::TAU, 4 * Math::TAU, -1.0) == doctest::Approx((T)-4.0 * Math::TAU));
CHECK(Math::lerp_angle((T)-4 * Math::TAU, 4 * Math::TAU, 0.0) == doctest::Approx((T)-4.0 * Math::TAU));
CHECK(Math::lerp_angle((T)-4 * Math::TAU, 4 * Math::TAU, 0.5) == doctest::Approx((T)-4.0 * Math::TAU));
CHECK(Math::lerp_angle((T)-4 * Math::TAU, 4 * Math::TAU, 1.0) == doctest::Approx((T)-4.0 * Math::TAU));
CHECK(Math::lerp_angle((T)-4 * Math::TAU, 4 * Math::TAU, 500.0) == doctest::Approx((T)-4.0 * Math::TAU));
}
TEST_CASE_TEMPLATE("[Math] move_toward", T, float, double) {
CHECK(Math::move_toward(2.0, 5.0, -1.0) == doctest::Approx((T)1.0));
CHECK(Math::move_toward(2.0, 5.0, 2.5) == doctest::Approx((T)4.5));
CHECK(Math::move_toward(2.0, 5.0, 4.0) == doctest::Approx((T)5.0));
CHECK(Math::move_toward(-2.0, -5.0, -1.0) == doctest::Approx((T)-1.0));
CHECK(Math::move_toward(-2.0, -5.0, 2.5) == doctest::Approx((T)-4.5));
CHECK(Math::move_toward(-2.0, -5.0, 4.0) == doctest::Approx((T)-5.0));
}
TEST_CASE_TEMPLATE("[Math] rotate_toward", T, float, double) {
// Rotate toward.
CHECK(Math::rotate_toward((T)0.0, (T)Math::PI * (T)0.75, (T)1.5) == doctest::Approx((T)1.5));
CHECK(Math::rotate_toward((T)-2.0, (T)1.0, (T)2.5) == doctest::Approx((T)0.5));
CHECK(Math::rotate_toward((T)-2.0, (T)Math::PI, (T)Math::PI) == doctest::Approx((T)-Math::PI));
CHECK(Math::rotate_toward((T)1.0, (T)Math::PI, (T)20.0) == doctest::Approx((T)Math::PI));
// Rotate away.
CHECK(Math::rotate_toward((T)0.0, (T)0.0, (T)-1.5) == doctest::Approx((T)-1.5));
CHECK(Math::rotate_toward((T)0.0, (T)0.0, (T)-Math::PI) == doctest::Approx((T)-Math::PI));
CHECK(Math::rotate_toward((T)3.0, (T)Math::PI, (T)-Math::PI) == doctest::Approx((T)0.0));
CHECK(Math::rotate_toward((T)2.0, (T)Math::PI, (T)-1.5) == doctest::Approx((T)0.5));
CHECK(Math::rotate_toward((T)1.0, (T)2.0, (T)-0.5) == doctest::Approx((T)0.5));
CHECK(Math::rotate_toward((T)2.5, (T)2.0, (T)-0.5) == doctest::Approx((T)3.0));
CHECK(Math::rotate_toward((T)-1.0, (T)1.0, (T)-1.0) == doctest::Approx((T)-2.0));
}
TEST_CASE_TEMPLATE("[Math] smoothstep", T, float, double) {
CHECK(Math::smoothstep((T)0.0, (T)2.0, (T)-5.0) == doctest::Approx((T)0.0));
CHECK(Math::smoothstep((T)0.0, (T)2.0, (T)0.5) == doctest::Approx((T)0.15625));
CHECK(Math::smoothstep((T)0.0, (T)2.0, (T)1.0) == doctest::Approx((T)0.5));
CHECK(Math::smoothstep((T)0.0, (T)2.0, (T)2.0) == doctest::Approx((T)1.0));
}
TEST_CASE("[Math] ease") {
CHECK(Math::ease(0.1, 1.0) == doctest::Approx(0.1));
CHECK(Math::ease(0.1, 2.0) == doctest::Approx(0.01));
CHECK(Math::ease(0.1, 0.5) == doctest::Approx(0.19));
CHECK(Math::ease(0.1, 0.0) == doctest::Approx(0));
CHECK(Math::ease(0.1, -0.5) == doctest::Approx(0.2236067977));
CHECK(Math::ease(0.1, -1.0) == doctest::Approx(0.1));
CHECK(Math::ease(0.1, -2.0) == doctest::Approx(0.02));
CHECK(Math::ease(-1.0, 1.0) == doctest::Approx(0));
CHECK(Math::ease(-1.0, 2.0) == doctest::Approx(0));
CHECK(Math::ease(-1.0, 0.5) == doctest::Approx(0));
CHECK(Math::ease(-1.0, 0.0) == doctest::Approx(0));
CHECK(Math::ease(-1.0, -0.5) == doctest::Approx(0));
CHECK(Math::ease(-1.0, -1.0) == doctest::Approx(0));
CHECK(Math::ease(-1.0, -2.0) == doctest::Approx(0));
}
TEST_CASE("[Math] snapped") {
CHECK(Math::snapped(0.5, 0.04) == doctest::Approx(0.52));
CHECK(Math::snapped(-0.5, 0.04) == doctest::Approx(-0.48));
CHECK(Math::snapped(0.0, 0.04) == doctest::Approx(0));
CHECK(Math::snapped(128'000.025, 0.04) == doctest::Approx(128'000.04));
CHECK(Math::snapped(0.5, 400) == doctest::Approx(0));
CHECK(Math::snapped(-0.5, 400) == doctest::Approx(0));
CHECK(Math::snapped(0.0, 400) == doctest::Approx(0));
CHECK(Math::snapped(128'000.025, 400) == doctest::Approx(128'000.0));
CHECK(Math::snapped(0.5, 0.0) == doctest::Approx(0.5));
CHECK(Math::snapped(-0.5, 0.0) == doctest::Approx(-0.5));
CHECK(Math::snapped(0.0, 0.0) == doctest::Approx(0.0));
CHECK(Math::snapped(128'000.025, 0.0) == doctest::Approx(128'000.0));
CHECK(Math::snapped(0.5, -1.0) == doctest::Approx(0));
CHECK(Math::snapped(-0.5, -1.0) == doctest::Approx(-1.0));
CHECK(Math::snapped(0.0, -1.0) == doctest::Approx(0));
CHECK(Math::snapped(128'000.025, -1.0) == doctest::Approx(128'000.0));
}
TEST_CASE("[Math] larger_prime") {
CHECK(Math::larger_prime(0) == 5);
CHECK(Math::larger_prime(1) == 5);
CHECK(Math::larger_prime(2) == 5);
CHECK(Math::larger_prime(5) == 13);
CHECK(Math::larger_prime(500) == 769);
CHECK(Math::larger_prime(1'000'000) == 1'572'869);
CHECK(Math::larger_prime(1'000'000'000) == 1'610'612'741);
// The next prime is larger than `INT32_MAX` and is not present in the built-in prime table.
ERR_PRINT_OFF;
CHECK(Math::larger_prime(2'000'000'000) == 0);
ERR_PRINT_ON;
}
TEST_CASE_TEMPLATE("[Math] fmod", T, float, double) {
CHECK(Math::fmod((T)-2.0, (T)0.3) == doctest::Approx((T)-0.2));
CHECK(Math::fmod((T)0.0, (T)0.3) == doctest::Approx((T)0.0));
CHECK(Math::fmod((T)2.0, (T)0.3) == doctest::Approx((T)0.2));
CHECK(Math::fmod((T)-2.0, (T)-0.3) == doctest::Approx((T)-0.2));
CHECK(Math::fmod((T)0.0, (T)-0.3) == doctest::Approx((T)0.0));
CHECK(Math::fmod((T)2.0, (T)-0.3) == doctest::Approx((T)0.2));
}
TEST_CASE_TEMPLATE("[Math] fposmod", T, float, double) {
CHECK(Math::fposmod((T)-2.0, (T)0.3) == doctest::Approx((T)0.1));
CHECK(Math::fposmod((T)0.0, (T)0.3) == doctest::Approx((T)0.0));
CHECK(Math::fposmod((T)2.0, (T)0.3) == doctest::Approx((T)0.2));
CHECK(Math::fposmod((T)-2.0, (T)-0.3) == doctest::Approx((T)-0.2));
CHECK(Math::fposmod((T)0.0, (T)-0.3) == doctest::Approx((T)0.0));
CHECK(Math::fposmod((T)2.0, (T)-0.3) == doctest::Approx((T)-0.1));
}
TEST_CASE_TEMPLATE("[Math] fposmodp", T, float, double) {
CHECK(Math::fposmodp((T)-2.0, (T)0.3) == doctest::Approx((T)0.1));
CHECK(Math::fposmodp((T)0.0, (T)0.3) == doctest::Approx((T)0.0));
CHECK(Math::fposmodp((T)2.0, (T)0.3) == doctest::Approx((T)0.2));
CHECK(Math::fposmodp((T)-2.0, (T)-0.3) == doctest::Approx((T)-0.5));
CHECK(Math::fposmodp((T)0.0, (T)-0.3) == doctest::Approx((T)0.0));
CHECK(Math::fposmodp((T)2.0, (T)-0.3) == doctest::Approx((T)0.2));
}
TEST_CASE("[Math] posmod") {
CHECK(Math::posmod(-20, 3) == 1);
CHECK(Math::posmod(0, 3) == 0);
CHECK(Math::posmod(20, 3) == 2);
CHECK(Math::posmod(-20, -3) == -2);
CHECK(Math::posmod(0, -3) == 0);
CHECK(Math::posmod(20, -3) == -1);
}
TEST_CASE("[Math] wrapi") {
CHECK(Math::wrapi(-30, -20, 160) == 150);
CHECK(Math::wrapi(30, -20, 160) == 30);
CHECK(Math::wrapi(300, -20, 160) == 120);
CHECK(Math::wrapi(300'000'000'000, -20, 160) == 120);
}
TEST_CASE_TEMPLATE("[Math] wrapf", T, float, double) {
CHECK(Math::wrapf((T)-30.0, (T)-20.0, (T)160.0) == doctest::Approx((T)150.0));
CHECK(Math::wrapf((T)30.0, (T)-2.0, (T)160.0) == doctest::Approx((T)30.0));
CHECK(Math::wrapf((T)300.0, (T)-20.0, (T)160.0) == doctest::Approx((T)120.0));
CHECK(Math::wrapf(300'000'000'000.0, -20.0, 160.0) == doctest::Approx((T)120.0));
// float's precision is too low for 300'000'000'000.0, so we reduce it by a factor of 1000.
CHECK(Math::wrapf((float)15'000'000.0, (float)-20.0, (float)160.0) == doctest::Approx((T)60.0));
}
TEST_CASE_TEMPLATE("[Math] fract", T, float, double) {
CHECK(Math::fract((T)1.0) == doctest::Approx((T)0.0));
CHECK(Math::fract((T)77.8) == doctest::Approx((T)0.8));
CHECK(Math::fract((T)-10.1) == doctest::Approx((T)0.9));
}
TEST_CASE_TEMPLATE("[Math] pingpong", T, float, double) {
CHECK(Math::pingpong((T)0.0, (T)0.0) == doctest::Approx((T)0.0));
CHECK(Math::pingpong((T)1.0, (T)1.0) == doctest::Approx((T)1.0));
CHECK(Math::pingpong((T)0.5, (T)2.0) == doctest::Approx((T)0.5));
CHECK(Math::pingpong((T)3.5, (T)2.0) == doctest::Approx((T)0.5));
CHECK(Math::pingpong((T)11.5, (T)2.0) == doctest::Approx((T)0.5));
CHECK(Math::pingpong((T)-2.5, (T)2.0) == doctest::Approx((T)1.5));
}
TEST_CASE_TEMPLATE("[Math] deg_to_rad/rad_to_deg", T, float, double) {
CHECK(Math::deg_to_rad((T)180.0) == doctest::Approx((T)Math::PI));
CHECK(Math::deg_to_rad((T)-27.0) == doctest::Approx((T)-0.471239));
CHECK(Math::rad_to_deg((T)Math::PI) == doctest::Approx((T)180.0));
CHECK(Math::rad_to_deg((T)-1.5) == doctest::Approx((T)-85.94366927));
}
TEST_CASE_TEMPLATE("[Math] cubic_interpolate", T, float, double) {
CHECK(Math::cubic_interpolate((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.0) == doctest::Approx((T)0.2));
CHECK(Math::cubic_interpolate((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.25) == doctest::Approx((T)0.33125));
CHECK(Math::cubic_interpolate((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.5) == doctest::Approx((T)0.5));
CHECK(Math::cubic_interpolate((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.75) == doctest::Approx((T)0.66875));
CHECK(Math::cubic_interpolate((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)1.0) == doctest::Approx((T)0.8));
CHECK(Math::cubic_interpolate((T)20.2, (T)30.1, (T)-100.0, (T)32.0, (T)-50.0) == doctest::Approx((T)-6662732.3));
CHECK(Math::cubic_interpolate((T)20.2, (T)30.1, (T)-100.0, (T)32.0, (T)-5.0) == doctest::Approx((T)-9356.3));
CHECK(Math::cubic_interpolate((T)20.2, (T)30.1, (T)-100.0, (T)32.0, (T)0.0) == doctest::Approx((T)20.2));
CHECK(Math::cubic_interpolate((T)20.2, (T)30.1, (T)-100.0, (T)32.0, (T)1.0) == doctest::Approx((T)30.1));
CHECK(Math::cubic_interpolate((T)20.2, (T)30.1, (T)-100.0, (T)32.0, (T)4.0) == doctest::Approx((T)1853.2));
}
TEST_CASE_TEMPLATE("[Math] cubic_interpolate_angle", T, float, double) {
CHECK(Math::cubic_interpolate_angle((T)(Math::PI * (1.0 / 6.0)), (T)(Math::PI * (5.0 / 6.0)), (T)0.0, (T)Math::PI, (T)0.0) == doctest::Approx((T)Math::PI * (1.0 / 6.0)));
CHECK(Math::cubic_interpolate_angle((T)(Math::PI * (1.0 / 6.0)), (T)(Math::PI * (5.0 / 6.0)), (T)0.0, (T)Math::PI, (T)0.25) == doctest::Approx((T)0.973566));
CHECK(Math::cubic_interpolate_angle((T)(Math::PI * (1.0 / 6.0)), (T)(Math::PI * (5.0 / 6.0)), (T)0.0, (T)Math::PI, (T)0.5) == doctest::Approx((T)Math::PI / 2.0));
CHECK(Math::cubic_interpolate_angle((T)(Math::PI * (1.0 / 6.0)), (T)(Math::PI * (5.0 / 6.0)), (T)0.0, (T)Math::PI, (T)0.75) == doctest::Approx((T)2.16803));
CHECK(Math::cubic_interpolate_angle((T)(Math::PI * (1.0 / 6.0)), (T)(Math::PI * (5.0 / 6.0)), (T)0.0, (T)Math::PI, (T)1.0) == doctest::Approx((T)Math::PI * (5.0 / 6.0)));
}
TEST_CASE_TEMPLATE("[Math] cubic_interpolate_in_time", T, float, double) {
CHECK(Math::cubic_interpolate_in_time((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.0, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.0));
CHECK(Math::cubic_interpolate_in_time((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.25, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.1625));
CHECK(Math::cubic_interpolate_in_time((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.5, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.4));
CHECK(Math::cubic_interpolate_in_time((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)0.75, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.6375));
CHECK(Math::cubic_interpolate_in_time((T)0.2, (T)0.8, (T)0.0, (T)1.0, (T)1.0, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.8));
}
TEST_CASE_TEMPLATE("[Math] cubic_interpolate_angle_in_time", T, float, double) {
CHECK(Math::cubic_interpolate_angle_in_time((T)(Math::PI * (1.0 / 6.0)), (T)(Math::PI * (5.0 / 6.0)), (T)0.0, (T)Math::PI, (T)0.0, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.0));
CHECK(Math::cubic_interpolate_angle_in_time((T)(Math::PI * (1.0 / 6.0)), (T)(Math::PI * (5.0 / 6.0)), (T)0.0, (T)Math::PI, (T)0.25, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)0.494964));
CHECK(Math::cubic_interpolate_angle_in_time((T)(Math::PI * (1.0 / 6.0)), (T)(Math::PI * (5.0 / 6.0)), (T)0.0, (T)Math::PI, (T)0.5, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)1.27627));
CHECK(Math::cubic_interpolate_angle_in_time((T)(Math::PI * (1.0 / 6.0)), (T)(Math::PI * (5.0 / 6.0)), (T)0.0, (T)Math::PI, (T)0.75, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)2.07394));
CHECK(Math::cubic_interpolate_angle_in_time((T)(Math::PI * (1.0 / 6.0)), (T)(Math::PI * (5.0 / 6.0)), (T)0.0, (T)Math::PI, (T)1.0, (T)0.5, (T)0.0, (T)1.0) == doctest::Approx((T)Math::PI * (5.0 / 6.0)));
}
TEST_CASE_TEMPLATE("[Math] bezier_interpolate", T, float, double) {
CHECK(Math::bezier_interpolate((T)0.0, (T)0.2, (T)0.8, (T)1.0, (T)0.0) == doctest::Approx((T)0.0));
CHECK(Math::bezier_interpolate((T)0.0, (T)0.2, (T)0.8, (T)1.0, (T)0.25) == doctest::Approx((T)0.2125));
CHECK(Math::bezier_interpolate((T)0.0, (T)0.2, (T)0.8, (T)1.0, (T)0.5) == doctest::Approx((T)0.5));
CHECK(Math::bezier_interpolate((T)0.0, (T)0.2, (T)0.8, (T)1.0, (T)0.75) == doctest::Approx((T)0.7875));
CHECK(Math::bezier_interpolate((T)0.0, (T)0.2, (T)0.8, (T)1.0, (T)1.0) == doctest::Approx((T)1.0));
}
} // namespace TestMath

View File

@@ -0,0 +1,192 @@
/**************************************************************************/
/* test_plane.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/plane.h"
#include "thirdparty/doctest/doctest.h"
namespace TestPlane {
// Plane
TEST_CASE("[Plane] Constructor methods") {
constexpr Plane plane = Plane(32, 22, 16, 3);
constexpr Plane plane_vector = Plane(Vector3(32, 22, 16), 3);
constexpr Plane plane_copy_plane = Plane(plane);
static_assert(
plane == plane_vector,
"Planes created with same values but different methods should be equal.");
static_assert(
plane == plane_copy_plane,
"Planes created with same values but different methods should be equal.");
}
TEST_CASE("[Plane] Basic getters") {
constexpr Plane plane = Plane(32, 22, 16, 3);
constexpr Plane plane_normalized = Plane(32.0 / 42, 22.0 / 42, 16.0 / 42, 3.0 / 42);
CHECK_MESSAGE(
plane.get_normal().is_equal_approx(Vector3(32, 22, 16)),
"get_normal() should return the expected value.");
CHECK_MESSAGE(
plane.normalized().is_equal_approx(plane_normalized),
"normalized() should return a copy of the normalized value.");
}
TEST_CASE("[Plane] Basic setters") {
Plane plane = Plane(32, 22, 16, 3);
plane.set_normal(Vector3(4, 2, 3));
CHECK_MESSAGE(
plane.is_equal_approx(Plane(4, 2, 3, 3)),
"set_normal() should result in the expected plane.");
plane = Plane(32, 22, 16, 3);
plane.normalize();
CHECK_MESSAGE(
plane.is_equal_approx(Plane(32.0 / 42, 22.0 / 42, 16.0 / 42, 3.0 / 42)),
"normalize() should result in the expected plane.");
}
TEST_CASE("[Plane] Plane-point operations") {
constexpr Plane plane = Plane(32, 22, 16, 3);
constexpr Plane y_facing_plane = Plane(0, 1, 0, 4);
CHECK_MESSAGE(
plane.get_center().is_equal_approx(Vector3(32 * 3, 22 * 3, 16 * 3)),
"get_center() should return a vector pointing to the center of the plane.");
CHECK_MESSAGE(
y_facing_plane.is_point_over(Vector3(0, 5, 0)),
"is_point_over() should return the expected result.");
CHECK_MESSAGE(
y_facing_plane.get_any_perpendicular_normal().is_equal_approx(Vector3(1, 0, 0)),
"get_any_perpendicular_normal() should return the expected result.");
// TODO distance_to()
}
TEST_CASE("[Plane] Has point") {
constexpr Plane x_facing_plane = Plane(1, 0, 0, 0);
constexpr Plane y_facing_plane = Plane(0, 1, 0, 0);
constexpr Plane z_facing_plane = Plane(0, 0, 1, 0);
constexpr Vector3 x_axis_point = Vector3(10, 0, 0);
constexpr Vector3 y_axis_point = Vector3(0, 10, 0);
constexpr Vector3 z_axis_point = Vector3(0, 0, 10);
constexpr Plane x_facing_plane_with_d_offset = Plane(1, 0, 0, 1);
constexpr Vector3 y_axis_point_with_d_offset = Vector3(1, 10, 0);
CHECK_MESSAGE(
x_facing_plane.has_point(y_axis_point),
"has_point() with contained Vector3 should return the expected result.");
CHECK_MESSAGE(
x_facing_plane.has_point(z_axis_point),
"has_point() with contained Vector3 should return the expected result.");
CHECK_MESSAGE(
y_facing_plane.has_point(x_axis_point),
"has_point() with contained Vector3 should return the expected result.");
CHECK_MESSAGE(
y_facing_plane.has_point(z_axis_point),
"has_point() with contained Vector3 should return the expected result.");
CHECK_MESSAGE(
z_facing_plane.has_point(y_axis_point),
"has_point() with contained Vector3 should return the expected result.");
CHECK_MESSAGE(
z_facing_plane.has_point(x_axis_point),
"has_point() with contained Vector3 should return the expected result.");
CHECK_MESSAGE(
x_facing_plane_with_d_offset.has_point(y_axis_point_with_d_offset),
"has_point() with passed Vector3 should return the expected result.");
}
TEST_CASE("[Plane] Intersection") {
constexpr Plane x_facing_plane = Plane(1, 0, 0, 1);
constexpr Plane y_facing_plane = Plane(0, 1, 0, 2);
constexpr Plane z_facing_plane = Plane(0, 0, 1, 3);
Vector3 vec_out;
CHECK_MESSAGE(
x_facing_plane.intersect_3(y_facing_plane, z_facing_plane, &vec_out),
"intersect_3() should return the expected result.");
CHECK_MESSAGE(
vec_out.is_equal_approx(Vector3(1, 2, 3)),
"intersect_3() should modify vec_out to the expected result.");
CHECK_MESSAGE(
x_facing_plane.intersects_ray(Vector3(0, 1, 1), Vector3(2, 0, 0), &vec_out),
"intersects_ray() should return the expected result.");
CHECK_MESSAGE(
vec_out.is_equal_approx(Vector3(1, 1, 1)),
"intersects_ray() should modify vec_out to the expected result.");
CHECK_MESSAGE(
x_facing_plane.intersects_segment(Vector3(0, 1, 1), Vector3(2, 1, 1), &vec_out),
"intersects_segment() should return the expected result.");
CHECK_MESSAGE(
vec_out.is_equal_approx(Vector3(1, 1, 1)),
"intersects_segment() should modify vec_out to the expected result.");
}
TEST_CASE("[Plane] Finite number checks") {
constexpr Vector3 x(0, 1, 2);
constexpr Vector3 infinite_vec(Math::NaN, Math::NaN, Math::NaN);
constexpr real_t y = 0;
constexpr real_t infinite_y = Math::NaN;
CHECK_MESSAGE(
Plane(x, y).is_finite(),
"Plane with all components finite should be finite");
CHECK_FALSE_MESSAGE(
Plane(x, infinite_y).is_finite(),
"Plane with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Plane(infinite_vec, y).is_finite(),
"Plane with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Plane(infinite_vec, infinite_y).is_finite(),
"Plane with two components infinite should not be finite.");
}
} // namespace TestPlane

View File

@@ -0,0 +1,694 @@
/**************************************************************************/
/* test_projection.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/aabb.h"
#include "core/math/plane.h"
#include "core/math/projection.h"
#include "core/math/rect2.h"
#include "core/math/transform_3d.h"
#include "thirdparty/doctest/doctest.h"
namespace TestProjection {
TEST_CASE("[Projection] Construction") {
Projection default_proj;
CHECK(default_proj[0].is_equal_approx(Vector4(1, 0, 0, 0)));
CHECK(default_proj[1].is_equal_approx(Vector4(0, 1, 0, 0)));
CHECK(default_proj[2].is_equal_approx(Vector4(0, 0, 1, 0)));
CHECK(default_proj[3].is_equal_approx(Vector4(0, 0, 0, 1)));
Projection from_vec4(
Vector4(1, 2, 3, 4),
Vector4(5, 6, 7, 8),
Vector4(9, 10, 11, 12),
Vector4(13, 14, 15, 16));
CHECK(from_vec4[0].is_equal_approx(Vector4(1, 2, 3, 4)));
CHECK(from_vec4[1].is_equal_approx(Vector4(5, 6, 7, 8)));
CHECK(from_vec4[2].is_equal_approx(Vector4(9, 10, 11, 12)));
CHECK(from_vec4[3].is_equal_approx(Vector4(13, 14, 15, 16)));
Transform3D transform(
Basis(
Vector3(1, 0, 0),
Vector3(0, 2, 0),
Vector3(0, 0, 3)),
Vector3(4, 5, 6));
Projection from_transform(transform);
CHECK(from_transform[0].is_equal_approx(Vector4(1, 0, 0, 0)));
CHECK(from_transform[1].is_equal_approx(Vector4(0, 2, 0, 0)));
CHECK(from_transform[2].is_equal_approx(Vector4(0, 0, 3, 0)));
CHECK(from_transform[3].is_equal_approx(Vector4(4, 5, 6, 1)));
}
TEST_CASE("[Projection] set_zero()") {
Projection proj;
proj.set_zero();
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
CHECK(proj.columns[i][j] == 0);
}
}
}
TEST_CASE("[Projection] set_identity()") {
Projection proj;
proj.set_identity();
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
CHECK(proj.columns[i][j] == (i == j ? 1 : 0));
}
}
}
TEST_CASE("[Projection] determinant()") {
Projection proj(
Vector4(1, 5, 9, 13),
Vector4(2, 6, 11, 15),
Vector4(4, 7, 11, 15),
Vector4(4, 8, 12, 16));
CHECK(proj.determinant() == -12);
}
TEST_CASE("[Projection] Inverse and invert") {
SUBCASE("[Projection] Arbitrary projection matrix inversion") {
Projection proj(
Vector4(1, 5, 9, 13),
Vector4(2, 6, 11, 15),
Vector4(4, 7, 11, 15),
Vector4(4, 8, 12, 16));
Projection inverse_truth(
Vector4(-4.0 / 12, 0, 1, -8.0 / 12),
Vector4(8.0 / 12, -1, -1, 16.0 / 12),
Vector4(-20.0 / 12, 2, -1, 5.0 / 12),
Vector4(1, -1, 1, -0.75));
Projection inverse = proj.inverse();
CHECK(inverse[0].is_equal_approx(inverse_truth[0]));
CHECK(inverse[1].is_equal_approx(inverse_truth[1]));
CHECK(inverse[2].is_equal_approx(inverse_truth[2]));
CHECK(inverse[3].is_equal_approx(inverse_truth[3]));
proj.invert();
CHECK(proj[0].is_equal_approx(inverse_truth[0]));
CHECK(proj[1].is_equal_approx(inverse_truth[1]));
CHECK(proj[2].is_equal_approx(inverse_truth[2]));
CHECK(proj[3].is_equal_approx(inverse_truth[3]));
}
SUBCASE("[Projection] Orthogonal projection matrix inversion") {
Projection p = Projection::create_orthogonal(-125.0f, 125.0f, -125.0f, 125.0f, 0.01f, 25.0f);
p = p.inverse() * p;
CHECK(p[0].is_equal_approx(Vector4(1, 0, 0, 0)));
CHECK(p[1].is_equal_approx(Vector4(0, 1, 0, 0)));
CHECK(p[2].is_equal_approx(Vector4(0, 0, 1, 0)));
CHECK(p[3].is_equal_approx(Vector4(0, 0, 0, 1)));
}
SUBCASE("[Projection] Perspective projection matrix inversion") {
Projection p = Projection::create_perspective(90.0f, 1.77777f, 0.05f, 4000.0f);
p = p.inverse() * p;
CHECK(p[0].is_equal_approx(Vector4(1, 0, 0, 0)));
CHECK(p[1].is_equal_approx(Vector4(0, 1, 0, 0)));
CHECK(p[2].is_equal_approx(Vector4(0, 0, 1, 0)));
CHECK(p[3].is_equal_approx(Vector4(0, 0, 0, 1)));
}
}
TEST_CASE("[Projection] Matrix product") {
Projection proj1(
Vector4(1, 5, 9, 13),
Vector4(2, 6, 11, 15),
Vector4(4, 7, 11, 15),
Vector4(4, 8, 12, 16));
Projection proj2(
Vector4(0, 1, 2, 3),
Vector4(10, 11, 12, 13),
Vector4(20, 21, 22, 23),
Vector4(30, 31, 32, 33));
Projection prod = proj1 * proj2;
CHECK(prod[0].is_equal_approx(Vector4(22, 44, 69, 93)));
CHECK(prod[1].is_equal_approx(Vector4(132, 304, 499, 683)));
CHECK(prod[2].is_equal_approx(Vector4(242, 564, 929, 1273)));
CHECK(prod[3].is_equal_approx(Vector4(352, 824, 1359, 1863)));
}
TEST_CASE("[Projection] Vector transformation") {
Projection proj(
Vector4(1, 5, 9, 13),
Vector4(2, 6, 11, 15),
Vector4(4, 7, 11, 15),
Vector4(4, 8, 12, 16));
Vector4 vec4(1, 2, 3, 4);
CHECK(proj.xform(vec4).is_equal_approx(Vector4(33, 70, 112, 152)));
CHECK(proj.xform_inv(vec4).is_equal_approx(Vector4(90, 107, 111, 120)));
Vector3 vec3(1, 2, 3);
CHECK(proj.xform(vec3).is_equal_approx(Vector3(21, 46, 76) / 104));
}
TEST_CASE("[Projection] Plane transformation") {
Projection proj(
Vector4(1, 5, 9, 13),
Vector4(2, 6, 11, 15),
Vector4(4, 7, 11, 15),
Vector4(4, 8, 12, 16));
Plane plane(1, 2, 3, 4);
CHECK(proj.xform4(plane).is_equal_approx(Plane(33, 70, 112, 152)));
}
TEST_CASE("[Projection] Values access") {
Projection proj(
Vector4(00, 01, 02, 03),
Vector4(10, 11, 12, 13),
Vector4(20, 21, 22, 23),
Vector4(30, 31, 32, 33));
CHECK(proj[0] == Vector4(00, 01, 02, 03));
CHECK(proj[1] == Vector4(10, 11, 12, 13));
CHECK(proj[2] == Vector4(20, 21, 22, 23));
CHECK(proj[3] == Vector4(30, 31, 32, 33));
}
TEST_CASE("[Projection] flip_y() and flipped_y()") {
Projection proj(
Vector4(00, 01, 02, 03),
Vector4(10, 11, 12, 13),
Vector4(20, 21, 22, 23),
Vector4(30, 31, 32, 33));
Projection flipped = proj.flipped_y();
CHECK(flipped[0] == proj[0]);
CHECK(flipped[1] == -proj[1]);
CHECK(flipped[2] == proj[2]);
CHECK(flipped[3] == proj[3]);
proj.flip_y();
CHECK(proj[0] == flipped[0]);
CHECK(proj[1] == flipped[1]);
CHECK(proj[2] == flipped[2]);
CHECK(proj[3] == flipped[3]);
}
TEST_CASE("[Projection] Jitter offset") {
Projection proj(
Vector4(00, 01, 02, 03),
Vector4(10, 11, 12, 13),
Vector4(20, 21, 22, 23),
Vector4(30, 31, 32, 33));
Projection offsetted = proj.jitter_offseted(Vector2(1, 2));
CHECK(offsetted[0] == proj[0]);
CHECK(offsetted[1] == proj[1]);
CHECK(offsetted[2] == proj[2]);
CHECK(offsetted[3] == proj[3] + Vector4(1, 2, 0, 0));
proj.add_jitter_offset(Vector2(1, 2));
CHECK(proj[0] == offsetted[0]);
CHECK(proj[1] == offsetted[1]);
CHECK(proj[2] == offsetted[2]);
CHECK(proj[3] == offsetted[3]);
}
TEST_CASE("[Projection] Adjust znear") {
Projection persp = Projection::create_perspective(90, 0.5, 1, 50, false);
Projection adjusted = persp.perspective_znear_adjusted(2);
CHECK(adjusted[0] == persp[0]);
CHECK(adjusted[1] == persp[1]);
CHECK(adjusted[2].is_equal_approx(Vector4(persp[2][0], persp[2][1], -1.083333, persp[2][3])));
CHECK(adjusted[3].is_equal_approx(Vector4(persp[3][0], persp[3][1], -4.166666, persp[3][3])));
persp.adjust_perspective_znear(2);
CHECK(persp[0] == adjusted[0]);
CHECK(persp[1] == adjusted[1]);
CHECK(persp[2] == adjusted[2]);
CHECK(persp[3] == adjusted[3]);
}
TEST_CASE("[Projection] Set light bias") {
Projection proj;
proj.set_light_bias();
CHECK(proj[0] == Vector4(0.5, 0, 0, 0));
CHECK(proj[1] == Vector4(0, 0.5, 0, 0));
CHECK(proj[2] == Vector4(0, 0, 0.5, 0));
CHECK(proj[3] == Vector4(0.5, 0.5, 0.5, 1));
}
TEST_CASE("[Projection] Depth correction") {
Projection corrected = Projection::create_depth_correction(true);
CHECK(corrected[0] == Vector4(1, 0, 0, 0));
CHECK(corrected[1] == Vector4(0, -1, 0, 0));
CHECK(corrected[2] == Vector4(0, 0, -0.5, 0));
CHECK(corrected[3] == Vector4(0, 0, 0.5, 1));
Projection proj;
proj.set_depth_correction(true, true, true);
CHECK(proj[0] == corrected[0]);
CHECK(proj[1] == corrected[1]);
CHECK(proj[2] == corrected[2]);
CHECK(proj[3] == corrected[3]);
proj.set_depth_correction(false, true, true);
CHECK(proj[0] == Vector4(1, 0, 0, 0));
CHECK(proj[1] == Vector4(0, 1, 0, 0));
CHECK(proj[2] == Vector4(0, 0, -0.5, 0));
CHECK(proj[3] == Vector4(0, 0, 0.5, 1));
proj.set_depth_correction(false, false, true);
CHECK(proj[0] == Vector4(1, 0, 0, 0));
CHECK(proj[1] == Vector4(0, 1, 0, 0));
CHECK(proj[2] == Vector4(0, 0, 0.5, 0));
CHECK(proj[3] == Vector4(0, 0, 0.5, 1));
proj.set_depth_correction(false, false, false);
CHECK(proj[0] == Vector4(1, 0, 0, 0));
CHECK(proj[1] == Vector4(0, 1, 0, 0));
CHECK(proj[2] == Vector4(0, 0, 1, 0));
CHECK(proj[3] == Vector4(0, 0, 0, 1));
proj.set_depth_correction(true, true, false);
CHECK(proj[0] == Vector4(1, 0, 0, 0));
CHECK(proj[1] == Vector4(0, -1, 0, 0));
CHECK(proj[2] == Vector4(0, 0, -1, 0));
CHECK(proj[3] == Vector4(0, 0, 0, 1));
}
TEST_CASE("[Projection] Light atlas rect") {
Projection rect = Projection::create_light_atlas_rect(Rect2(1, 2, 30, 40));
CHECK(rect[0] == Vector4(30, 0, 0, 0));
CHECK(rect[1] == Vector4(0, 40, 0, 0));
CHECK(rect[2] == Vector4(0, 0, 1, 0));
CHECK(rect[3] == Vector4(1, 2, 0, 1));
Projection proj;
proj.set_light_atlas_rect(Rect2(1, 2, 30, 40));
CHECK(proj[0] == rect[0]);
CHECK(proj[1] == rect[1]);
CHECK(proj[2] == rect[2]);
CHECK(proj[3] == rect[3]);
}
TEST_CASE("[Projection] Make scale") {
Projection proj;
proj.make_scale(Vector3(2, 3, 4));
CHECK(proj[0] == Vector4(2, 0, 0, 0));
CHECK(proj[1] == Vector4(0, 3, 0, 0));
CHECK(proj[2] == Vector4(0, 0, 4, 0));
CHECK(proj[3] == Vector4(0, 0, 0, 1));
}
TEST_CASE("[Projection] Scale translate to fit aabb") {
Projection fit = Projection::create_fit_aabb(AABB(Vector3(), Vector3(0.1, 0.2, 0.4)));
CHECK(fit[0] == Vector4(20, 0, 0, 0));
CHECK(fit[1] == Vector4(0, 10, 0, 0));
CHECK(fit[2] == Vector4(0, 0, 5, 0));
CHECK(fit[3] == Vector4(-1, -1, -1, 1));
Projection proj;
proj.scale_translate_to_fit(AABB(Vector3(), Vector3(0.1, 0.2, 0.4)));
CHECK(proj[0] == fit[0]);
CHECK(proj[1] == fit[1]);
CHECK(proj[2] == fit[2]);
CHECK(proj[3] == fit[3]);
}
TEST_CASE("[Projection] Perspective") {
Projection persp = Projection::create_perspective(90, 0.5, 5, 15, false);
CHECK(persp[0].is_equal_approx(Vector4(2, 0, 0, 0)));
CHECK(persp[1].is_equal_approx(Vector4(0, 1, 0, 0)));
CHECK(persp[2].is_equal_approx(Vector4(0, 0, -2, -1)));
CHECK(persp[3].is_equal_approx(Vector4(0, 0, -15, 0)));
Projection proj;
proj.set_perspective(90, 0.5, 5, 15, false);
CHECK(proj[0] == persp[0]);
CHECK(proj[1] == persp[1]);
CHECK(proj[2] == persp[2]);
CHECK(proj[3] == persp[3]);
}
TEST_CASE("[Projection] Frustum") {
Projection frustum = Projection::create_frustum(15, 20, 10, 12, 5, 15);
CHECK(frustum[0].is_equal_approx(Vector4(2, 0, 0, 0)));
CHECK(frustum[1].is_equal_approx(Vector4(0, 5, 0, 0)));
CHECK(frustum[2].is_equal_approx(Vector4(7, 11, -2, -1)));
CHECK(frustum[3].is_equal_approx(Vector4(0, 0, -15, 0)));
Projection proj;
proj.set_frustum(15, 20, 10, 12, 5, 15);
CHECK(proj[0] == frustum[0]);
CHECK(proj[1] == frustum[1]);
CHECK(proj[2] == frustum[2]);
CHECK(proj[3] == frustum[3]);
}
TEST_CASE("[Projection] Ortho") {
Projection ortho = Projection::create_orthogonal(15, 20, 10, 12, 5, 15);
CHECK(ortho[0].is_equal_approx(Vector4(0.4, 0, 0, 0)));
CHECK(ortho[1].is_equal_approx(Vector4(0, 1, 0, 0)));
CHECK(ortho[2].is_equal_approx(Vector4(0, 0, -0.2, 0)));
CHECK(ortho[3].is_equal_approx(Vector4(-7, -11, -2, 1)));
Projection proj;
proj.set_orthogonal(15, 20, 10, 12, 5, 15);
CHECK(proj[0] == ortho[0]);
CHECK(proj[1] == ortho[1]);
CHECK(proj[2] == ortho[2]);
CHECK(proj[3] == ortho[3]);
}
TEST_CASE("[Projection] get_fovy()") {
double fov = Projection::get_fovy(90, 0.5);
CHECK(fov == doctest::Approx(53.1301));
}
TEST_CASE("[Projection] Perspective values extraction") {
Projection persp = Projection::create_perspective(90, 0.5, 1, 50, true);
double znear = persp.get_z_near();
double zfar = persp.get_z_far();
double aspect = persp.get_aspect();
double fov = persp.get_fov();
CHECK(znear == doctest::Approx(1));
CHECK(zfar == doctest::Approx(50));
CHECK(aspect == doctest::Approx(0.5));
CHECK(fov == doctest::Approx(90));
persp.set_perspective(38, 1.3, 0.2, 8, false);
znear = persp.get_z_near();
zfar = persp.get_z_far();
aspect = persp.get_aspect();
fov = persp.get_fov();
CHECK(znear == doctest::Approx(0.2));
CHECK(zfar == doctest::Approx(8));
CHECK(aspect == doctest::Approx(1.3));
CHECK(fov == doctest::Approx(Projection::get_fovy(38, 1.3)));
persp.set_perspective(47, 2.5, 0.9, 14, true);
znear = persp.get_z_near();
zfar = persp.get_z_far();
aspect = persp.get_aspect();
fov = persp.get_fov();
CHECK(znear == doctest::Approx(0.9));
CHECK(zfar == doctest::Approx(14));
CHECK(aspect == doctest::Approx(2.5));
CHECK(fov == doctest::Approx(47));
}
TEST_CASE("[Projection] Frustum values extraction") {
Projection frustum = Projection::create_frustum_aspect(1.0, 4.0 / 3.0, Vector2(0.5, -0.25), 0.5, 50, true);
double znear = frustum.get_z_near();
double zfar = frustum.get_z_far();
double aspect = frustum.get_aspect();
double fov = frustum.get_fov();
CHECK(znear == doctest::Approx(0.5));
CHECK(zfar == doctest::Approx(50));
CHECK(aspect == doctest::Approx(4.0 / 3.0));
CHECK(fov == doctest::Approx(Math::rad_to_deg(Math::atan(2.0))));
frustum.set_frustum(2.0, 1.5, Vector2(-0.5, 2), 2, 12, false);
znear = frustum.get_z_near();
zfar = frustum.get_z_far();
aspect = frustum.get_aspect();
fov = frustum.get_fov();
CHECK(znear == doctest::Approx(2));
CHECK(zfar == doctest::Approx(12));
CHECK(aspect == doctest::Approx(1.5));
CHECK(fov == doctest::Approx(Math::rad_to_deg(Math::atan(1.0) + Math::atan(0.5))));
}
TEST_CASE("[Projection] Orthographic values extraction") {
Projection ortho = Projection::create_orthogonal(-2, 3, -0.5, 1.5, 1.2, 15);
double znear = ortho.get_z_near();
double zfar = ortho.get_z_far();
double aspect = ortho.get_aspect();
CHECK(znear == doctest::Approx(1.2));
CHECK(zfar == doctest::Approx(15));
CHECK(aspect == doctest::Approx(2.5));
ortho.set_orthogonal(-7, 2, 2.5, 5.5, 0.5, 6);
znear = ortho.get_z_near();
zfar = ortho.get_z_far();
aspect = ortho.get_aspect();
CHECK(znear == doctest::Approx(0.5));
CHECK(zfar == doctest::Approx(6));
CHECK(aspect == doctest::Approx(3));
}
TEST_CASE("[Projection] Orthographic check") {
Projection persp = Projection::create_perspective(90, 0.5, 1, 50, false);
Projection ortho = Projection::create_orthogonal(15, 20, 10, 12, 5, 15);
CHECK(!persp.is_orthogonal());
CHECK(ortho.is_orthogonal());
}
TEST_CASE("[Projection] Planes extraction") {
Projection persp = Projection::create_perspective(90, 1, 1, 40, false);
Vector<Plane> planes = persp.get_projection_planes(Transform3D());
CHECK(planes[Projection::PLANE_NEAR].normalized().is_equal_approx(Plane(0, 0, 1, -1)));
CHECK(planes[Projection::PLANE_FAR].normalized().is_equal_approx(Plane(0, 0, -1, 40)));
CHECK(planes[Projection::PLANE_LEFT].normalized().is_equal_approx(Plane(-0.707107, 0, 0.707107, 0)));
CHECK(planes[Projection::PLANE_TOP].normalized().is_equal_approx(Plane(0, 0.707107, 0.707107, 0)));
CHECK(planes[Projection::PLANE_RIGHT].normalized().is_equal_approx(Plane(0.707107, 0, 0.707107, 0)));
CHECK(planes[Projection::PLANE_BOTTOM].normalized().is_equal_approx(Plane(0, -0.707107, 0.707107, 0)));
Plane plane_array[6]{
persp.get_projection_plane(Projection::PLANE_NEAR),
persp.get_projection_plane(Projection::PLANE_FAR),
persp.get_projection_plane(Projection::PLANE_LEFT),
persp.get_projection_plane(Projection::PLANE_TOP),
persp.get_projection_plane(Projection::PLANE_RIGHT),
persp.get_projection_plane(Projection::PLANE_BOTTOM)
};
CHECK(plane_array[Projection::PLANE_NEAR].normalized().is_equal_approx(planes[Projection::PLANE_NEAR].normalized()));
CHECK(plane_array[Projection::PLANE_FAR].normalized().is_equal_approx(planes[Projection::PLANE_FAR].normalized()));
CHECK(plane_array[Projection::PLANE_LEFT].normalized().is_equal_approx(planes[Projection::PLANE_LEFT].normalized()));
CHECK(plane_array[Projection::PLANE_TOP].normalized().is_equal_approx(planes[Projection::PLANE_TOP].normalized()));
CHECK(plane_array[Projection::PLANE_RIGHT].normalized().is_equal_approx(planes[Projection::PLANE_RIGHT].normalized()));
CHECK(plane_array[Projection::PLANE_BOTTOM].normalized().is_equal_approx(planes[Projection::PLANE_BOTTOM].normalized()));
}
TEST_CASE("[Projection] Perspective Half extents") {
constexpr real_t sqrt3 = 1.7320508;
Projection persp = Projection::create_perspective(90, 1, 1, 40, false);
Vector2 ne = persp.get_viewport_half_extents();
Vector2 fe = persp.get_far_plane_half_extents();
CHECK(ne.is_equal_approx(Vector2(1, 1) * 1));
CHECK(fe.is_equal_approx(Vector2(1, 1) * 40));
persp.set_perspective(120, sqrt3, 0.8, 10, true);
ne = persp.get_viewport_half_extents();
fe = persp.get_far_plane_half_extents();
CHECK(ne.is_equal_approx(Vector2(sqrt3, 1.0) * 0.8));
CHECK(fe.is_equal_approx(Vector2(sqrt3, 1.0) * 10));
persp.set_perspective(60, 1.2, 0.5, 15, false);
ne = persp.get_viewport_half_extents();
fe = persp.get_far_plane_half_extents();
CHECK(ne.is_equal_approx(Vector2(sqrt3 / 3 * 1.2, sqrt3 / 3) * 0.5));
CHECK(fe.is_equal_approx(Vector2(sqrt3 / 3 * 1.2, sqrt3 / 3) * 15));
}
TEST_CASE("[Projection] Orthographic Half extents") {
Projection ortho = Projection::create_orthogonal(-3, 3, -1.5, 1.5, 1.2, 15);
Vector2 ne = ortho.get_viewport_half_extents();
Vector2 fe = ortho.get_far_plane_half_extents();
CHECK(ne.is_equal_approx(Vector2(3, 1.5)));
CHECK(fe.is_equal_approx(Vector2(3, 1.5)));
ortho.set_orthogonal(-7, 7, -2.5, 2.5, 0.5, 6);
ne = ortho.get_viewport_half_extents();
fe = ortho.get_far_plane_half_extents();
CHECK(ne.is_equal_approx(Vector2(7, 2.5)));
CHECK(fe.is_equal_approx(Vector2(7, 2.5)));
}
TEST_CASE("[Projection] Endpoints") {
constexpr real_t sqrt3 = 1.7320508;
Projection persp = Projection::create_perspective(90, 1, 1, 40, false);
Vector3 ep[8];
persp.get_endpoints(Transform3D(), ep);
CHECK(ep[0].is_equal_approx(Vector3(-1, 1, -1) * 40));
CHECK(ep[1].is_equal_approx(Vector3(-1, -1, -1) * 40));
CHECK(ep[2].is_equal_approx(Vector3(1, 1, -1) * 40));
CHECK(ep[3].is_equal_approx(Vector3(1, -1, -1) * 40));
CHECK(ep[4].is_equal_approx(Vector3(-1, 1, -1) * 1));
CHECK(ep[5].is_equal_approx(Vector3(-1, -1, -1) * 1));
CHECK(ep[6].is_equal_approx(Vector3(1, 1, -1) * 1));
CHECK(ep[7].is_equal_approx(Vector3(1, -1, -1) * 1));
persp.set_perspective(120, sqrt3, 0.8, 10, true);
persp.get_endpoints(Transform3D(), ep);
CHECK(ep[0].is_equal_approx(Vector3(-sqrt3, 1, -1) * 10));
CHECK(ep[1].is_equal_approx(Vector3(-sqrt3, -1, -1) * 10));
CHECK(ep[2].is_equal_approx(Vector3(sqrt3, 1, -1) * 10));
CHECK(ep[3].is_equal_approx(Vector3(sqrt3, -1, -1) * 10));
CHECK(ep[4].is_equal_approx(Vector3(-sqrt3, 1, -1) * 0.8));
CHECK(ep[5].is_equal_approx(Vector3(-sqrt3, -1, -1) * 0.8));
CHECK(ep[6].is_equal_approx(Vector3(sqrt3, 1, -1) * 0.8));
CHECK(ep[7].is_equal_approx(Vector3(sqrt3, -1, -1) * 0.8));
persp.set_perspective(60, 1.2, 0.5, 15, false);
persp.get_endpoints(Transform3D(), ep);
CHECK(ep[0].is_equal_approx(Vector3(-sqrt3 / 3 * 1.2, sqrt3 / 3, -1) * 15));
CHECK(ep[1].is_equal_approx(Vector3(-sqrt3 / 3 * 1.2, -sqrt3 / 3, -1) * 15));
CHECK(ep[2].is_equal_approx(Vector3(sqrt3 / 3 * 1.2, sqrt3 / 3, -1) * 15));
CHECK(ep[3].is_equal_approx(Vector3(sqrt3 / 3 * 1.2, -sqrt3 / 3, -1) * 15));
CHECK(ep[4].is_equal_approx(Vector3(-sqrt3 / 3 * 1.2, sqrt3 / 3, -1) * 0.5));
CHECK(ep[5].is_equal_approx(Vector3(-sqrt3 / 3 * 1.2, -sqrt3 / 3, -1) * 0.5));
CHECK(ep[6].is_equal_approx(Vector3(sqrt3 / 3 * 1.2, sqrt3 / 3, -1) * 0.5));
CHECK(ep[7].is_equal_approx(Vector3(sqrt3 / 3 * 1.2, -sqrt3 / 3, -1) * 0.5));
}
TEST_CASE("[Projection] LOD multiplier") {
constexpr real_t sqrt3 = 1.7320508;
Projection proj;
real_t multiplier;
proj.set_perspective(60, 1, 1, 40, false);
multiplier = proj.get_lod_multiplier();
CHECK(multiplier == doctest::Approx(2 * sqrt3 / 3));
proj.set_perspective(120, 1.5, 0.5, 20, false);
multiplier = proj.get_lod_multiplier();
CHECK(multiplier == doctest::Approx(3 * sqrt3));
proj.set_orthogonal(15, 20, 10, 12, 5, 15);
multiplier = proj.get_lod_multiplier();
CHECK(multiplier == doctest::Approx(5));
proj.set_orthogonal(-5, 15, -8, 10, 1.5, 10);
multiplier = proj.get_lod_multiplier();
CHECK(multiplier == doctest::Approx(20));
proj.set_frustum(1.0, 4.0 / 3.0, Vector2(0.5, -0.25), 0.5, 50, false);
multiplier = proj.get_lod_multiplier();
CHECK(multiplier == doctest::Approx(8.0 / 3.0));
proj.set_frustum(2.0, 1.2, Vector2(-0.1, 0.8), 1, 10, true);
multiplier = proj.get_lod_multiplier();
CHECK(multiplier == doctest::Approx(2));
}
TEST_CASE("[Projection] Pixels per meter") {
constexpr real_t sqrt3 = 1.7320508;
Projection proj;
int ppm;
proj.set_perspective(60, 1, 1, 40, false);
ppm = proj.get_pixels_per_meter(1024);
CHECK(ppm == int(1536.0f / sqrt3));
proj.set_perspective(120, 1.5, 0.5, 20, false);
ppm = proj.get_pixels_per_meter(1200);
CHECK(ppm == int(800.0f / sqrt3));
proj.set_orthogonal(15, 20, 10, 12, 5, 15);
ppm = proj.get_pixels_per_meter(500);
CHECK(ppm == 100);
proj.set_orthogonal(-5, 15, -8, 10, 1.5, 10);
ppm = proj.get_pixels_per_meter(640);
CHECK(ppm == 32);
proj.set_frustum(1.0, 4.0 / 3.0, Vector2(0.5, -0.25), 0.5, 50, false);
ppm = proj.get_pixels_per_meter(2048);
CHECK(ppm == 1536);
proj.set_frustum(2.0, 1.2, Vector2(-0.1, 0.8), 1, 10, true);
ppm = proj.get_pixels_per_meter(800);
CHECK(ppm == 400);
}
} //namespace TestProjection

View File

@@ -0,0 +1,493 @@
/**************************************************************************/
/* test_quaternion.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/math_defs.h"
#include "core/math/math_funcs.h"
#include "core/math/quaternion.h"
#include "core/math/vector3.h"
#include "tests/test_macros.h"
namespace TestQuaternion {
Quaternion quat_euler_yxz_deg(Vector3 angle) {
double yaw = Math::deg_to_rad(angle[1]);
double pitch = Math::deg_to_rad(angle[0]);
double roll = Math::deg_to_rad(angle[2]);
// Generate YXZ (Z-then-X-then-Y) Quaternion using single-axis Euler
// constructor and quaternion product, both tested separately.
Quaternion q_y = Quaternion::from_euler(Vector3(0.0, yaw, 0.0));
Quaternion q_p = Quaternion::from_euler(Vector3(pitch, 0.0, 0.0));
Quaternion q_r = Quaternion::from_euler(Vector3(0.0, 0.0, roll));
// Roll-Z is followed by Pitch-X, then Yaw-Y.
Quaternion q_yxz = q_y * q_p * q_r;
return q_yxz;
}
TEST_CASE("[Quaternion] Default Construct") {
constexpr Quaternion q;
CHECK(q[0] == 0.0);
CHECK(q[1] == 0.0);
CHECK(q[2] == 0.0);
CHECK(q[3] == 1.0);
}
TEST_CASE("[Quaternion] Construct x,y,z,w") {
// Values are taken from actual use in another project & are valid (except roundoff error).
constexpr Quaternion q(0.2391, 0.099, 0.3696, 0.8924);
CHECK(q[0] == doctest::Approx(0.2391));
CHECK(q[1] == doctest::Approx(0.099));
CHECK(q[2] == doctest::Approx(0.3696));
CHECK(q[3] == doctest::Approx(0.8924));
}
TEST_CASE("[Quaternion] Construct AxisAngle 1") {
// Easy to visualize: 120 deg about X-axis.
Quaternion q(Vector3(1.0, 0.0, 0.0), Math::deg_to_rad(120.0));
// 0.866 isn't close enough; doctest::Approx doesn't cut much slack!
CHECK(q[0] == doctest::Approx(0.866025)); // Sine of half the angle.
CHECK(q[1] == doctest::Approx(0.0));
CHECK(q[2] == doctest::Approx(0.0));
CHECK(q[3] == doctest::Approx(0.5)); // Cosine of half the angle.
}
TEST_CASE("[Quaternion] Construct AxisAngle 2") {
// Easy to visualize: 30 deg about Y-axis.
Quaternion q(Vector3(0.0, 1.0, 0.0), Math::deg_to_rad(30.0));
CHECK(q[0] == doctest::Approx(0.0));
CHECK(q[1] == doctest::Approx(0.258819)); // Sine of half the angle.
CHECK(q[2] == doctest::Approx(0.0));
CHECK(q[3] == doctest::Approx(0.965926)); // Cosine of half the angle.
}
TEST_CASE("[Quaternion] Construct AxisAngle 3") {
// Easy to visualize: 60 deg about Z-axis.
Quaternion q(Vector3(0.0, 0.0, 1.0), Math::deg_to_rad(60.0));
CHECK(q[0] == doctest::Approx(0.0));
CHECK(q[1] == doctest::Approx(0.0));
CHECK(q[2] == doctest::Approx(0.5)); // Sine of half the angle.
CHECK(q[3] == doctest::Approx(0.866025)); // Cosine of half the angle.
}
TEST_CASE("[Quaternion] Construct AxisAngle 4") {
// More complex & hard to visualize, so test w/ data from online calculator.
constexpr Vector3 axis(1.0, 2.0, 0.5);
Quaternion q(axis.normalized(), Math::deg_to_rad(35.0));
CHECK(q[0] == doctest::Approx(0.131239));
CHECK(q[1] == doctest::Approx(0.262478));
CHECK(q[2] == doctest::Approx(0.0656194));
CHECK(q[3] == doctest::Approx(0.953717));
}
TEST_CASE("[Quaternion] Construct from Quaternion") {
constexpr Vector3 axis(1.0, 2.0, 0.5);
Quaternion q_src(axis.normalized(), Math::deg_to_rad(35.0));
Quaternion q(q_src);
CHECK(q[0] == doctest::Approx(0.131239));
CHECK(q[1] == doctest::Approx(0.262478));
CHECK(q[2] == doctest::Approx(0.0656194));
CHECK(q[3] == doctest::Approx(0.953717));
}
TEST_CASE("[Quaternion] Construct Euler SingleAxis") {
double yaw = Math::deg_to_rad(45.0);
double pitch = Math::deg_to_rad(30.0);
double roll = Math::deg_to_rad(10.0);
Vector3 euler_y(0.0, yaw, 0.0);
Quaternion q_y = Quaternion::from_euler(euler_y);
CHECK(q_y[0] == doctest::Approx(0.0));
CHECK(q_y[1] == doctest::Approx(0.382684));
CHECK(q_y[2] == doctest::Approx(0.0));
CHECK(q_y[3] == doctest::Approx(0.923879));
Vector3 euler_p(pitch, 0.0, 0.0);
Quaternion q_p = Quaternion::from_euler(euler_p);
CHECK(q_p[0] == doctest::Approx(0.258819));
CHECK(q_p[1] == doctest::Approx(0.0));
CHECK(q_p[2] == doctest::Approx(0.0));
CHECK(q_p[3] == doctest::Approx(0.965926));
Vector3 euler_r(0.0, 0.0, roll);
Quaternion q_r = Quaternion::from_euler(euler_r);
CHECK(q_r[0] == doctest::Approx(0.0));
CHECK(q_r[1] == doctest::Approx(0.0));
CHECK(q_r[2] == doctest::Approx(0.0871558));
CHECK(q_r[3] == doctest::Approx(0.996195));
}
TEST_CASE("[Quaternion] Construct Euler YXZ dynamic axes") {
double yaw = Math::deg_to_rad(45.0);
double pitch = Math::deg_to_rad(30.0);
double roll = Math::deg_to_rad(10.0);
// Generate YXZ comparison data (Z-then-X-then-Y) using single-axis Euler
// constructor and quaternion product, both tested separately.
Vector3 euler_y(0.0, yaw, 0.0);
Quaternion q_y = Quaternion::from_euler(euler_y);
Vector3 euler_p(pitch, 0.0, 0.0);
Quaternion q_p = Quaternion::from_euler(euler_p);
Vector3 euler_r(0.0, 0.0, roll);
Quaternion q_r = Quaternion::from_euler(euler_r);
// Instrinsically, Yaw-Y then Pitch-X then Roll-Z.
// Extrinsically, Roll-Z is followed by Pitch-X, then Yaw-Y.
Quaternion check_yxz = q_y * q_p * q_r;
// Test construction from YXZ Euler angles.
Vector3 euler_yxz(pitch, yaw, roll);
Quaternion q = Quaternion::from_euler(euler_yxz);
CHECK(q[0] == doctest::Approx(check_yxz[0]));
CHECK(q[1] == doctest::Approx(check_yxz[1]));
CHECK(q[2] == doctest::Approx(check_yxz[2]));
CHECK(q[3] == doctest::Approx(check_yxz[3]));
CHECK(q.is_equal_approx(check_yxz));
CHECK(q.get_euler().is_equal_approx(euler_yxz));
CHECK(check_yxz.get_euler().is_equal_approx(euler_yxz));
}
TEST_CASE("[Quaternion] Construct Basis Euler") {
double yaw = Math::deg_to_rad(45.0);
double pitch = Math::deg_to_rad(30.0);
double roll = Math::deg_to_rad(10.0);
Vector3 euler_yxz(pitch, yaw, roll);
Quaternion q_yxz = Quaternion::from_euler(euler_yxz);
Basis basis_axes = Basis::from_euler(euler_yxz);
Quaternion q(basis_axes);
CHECK(q.is_equal_approx(q_yxz));
}
TEST_CASE("[Quaternion] Construct Basis Axes") {
// Arbitrary Euler angles.
const Vector3 euler_yxz(Math::deg_to_rad(31.41), Math::deg_to_rad(-49.16), Math::deg_to_rad(12.34));
// Basis vectors from online calculation of rotation matrix.
constexpr Vector3 i_unit(0.5545787, 0.1823950, 0.8118957);
constexpr Vector3 j_unit(-0.5249245, 0.8337420, 0.1712555);
constexpr Vector3 k_unit(-0.6456754, -0.5211586, 0.5581192);
// Quaternion from online calculation.
constexpr Quaternion q_calc(0.2016913, -0.4245716, 0.206033, 0.8582598);
// Quaternion from local calculation.
const Quaternion q_local = quat_euler_yxz_deg(Vector3(31.41, -49.16, 12.34));
// Quaternion from Euler angles constructor.
const Quaternion q_euler = Quaternion::from_euler(euler_yxz);
CHECK(q_calc.is_equal_approx(q_local));
CHECK(q_local.is_equal_approx(q_euler));
// Calculate Basis and construct Quaternion.
// When this is written, C++ Basis class does not construct from basis vectors.
// This is by design, but may be subject to change.
// Workaround by constructing Basis from Euler angles.
// basis_axes = Basis(i_unit, j_unit, k_unit);
Basis basis_axes = Basis::from_euler(euler_yxz);
Quaternion q(basis_axes);
CHECK(basis_axes.get_column(0).is_equal_approx(i_unit));
CHECK(basis_axes.get_column(1).is_equal_approx(j_unit));
CHECK(basis_axes.get_column(2).is_equal_approx(k_unit));
CHECK(q.is_equal_approx(q_calc));
CHECK_FALSE(q.inverse().is_equal_approx(q_calc));
CHECK(q.is_equal_approx(q_local));
CHECK(q.is_equal_approx(q_euler));
CHECK(q[0] == doctest::Approx(0.2016913));
CHECK(q[1] == doctest::Approx(-0.4245716));
CHECK(q[2] == doctest::Approx(0.206033));
CHECK(q[3] == doctest::Approx(0.8582598));
}
TEST_CASE("[Quaternion] Construct Shortest Arc For 180 Degree Arc") {
Vector3 up(0, 1, 0);
Vector3 down(0, -1, 0);
Vector3 left(-1, 0, 0);
Vector3 right(1, 0, 0);
Vector3 forward(0, 0, -1);
Vector3 back(0, 0, 1);
// When we have a 180 degree rotation quaternion which was defined as
// A to B, logically when we transform A we expect to get B.
Quaternion left_to_right(left, right);
Quaternion right_to_left(right, left);
CHECK(left_to_right.xform(left).is_equal_approx(right));
CHECK(Quaternion(right, left).xform(right).is_equal_approx(left));
CHECK(Quaternion(up, down).xform(up).is_equal_approx(down));
CHECK(Quaternion(down, up).xform(down).is_equal_approx(up));
CHECK(Quaternion(forward, back).xform(forward).is_equal_approx(back));
CHECK(Quaternion(back, forward).xform(back).is_equal_approx(forward));
// With (arbitrary) opposite vectors that are not axis-aligned as parameters.
Vector3 diagonal_up = Vector3(1.2, 2.3, 4.5).normalized();
Vector3 diagonal_down = -diagonal_up;
Quaternion q1(diagonal_up, diagonal_down);
CHECK(q1.xform(diagonal_down).is_equal_approx(diagonal_up));
CHECK(q1.xform(diagonal_up).is_equal_approx(diagonal_down));
// For the consistency of the rotation direction, they should be symmetrical to the plane.
CHECK(left_to_right.is_equal_approx(right_to_left.inverse()));
// If vectors are same, no rotation.
CHECK(Quaternion(diagonal_up, diagonal_up).is_equal_approx(Quaternion()));
}
TEST_CASE("[Quaternion] Get Euler Orders") {
double x = Math::deg_to_rad(30.0);
double y = Math::deg_to_rad(45.0);
double z = Math::deg_to_rad(10.0);
Vector3 euler(x, y, z);
for (int i = 0; i < 6; i++) {
EulerOrder order = (EulerOrder)i;
Basis basis = Basis::from_euler(euler, order);
Quaternion q = Quaternion(basis);
Vector3 check = q.get_euler(order);
CHECK_MESSAGE(check.is_equal_approx(euler),
"Quaternion get_euler method should return the original angles.");
CHECK_MESSAGE(check.is_equal_approx(basis.get_euler(order)),
"Quaternion get_euler method should behave the same as Basis get_euler.");
}
}
TEST_CASE("[Quaternion] Product (book)") {
// Example from "Quaternions and Rotation Sequences" by Jack Kuipers, p. 108.
constexpr Quaternion p(1.0, -2.0, 1.0, 3.0);
constexpr Quaternion q(-1.0, 2.0, 3.0, 2.0);
constexpr Quaternion pq = p * q;
CHECK(pq[0] == doctest::Approx(-9.0));
CHECK(pq[1] == doctest::Approx(-2.0));
CHECK(pq[2] == doctest::Approx(11.0));
CHECK(pq[3] == doctest::Approx(8.0));
}
TEST_CASE("[Quaternion] Product") {
double yaw = Math::deg_to_rad(45.0);
double pitch = Math::deg_to_rad(30.0);
double roll = Math::deg_to_rad(10.0);
Vector3 euler_y(0.0, yaw, 0.0);
Quaternion q_y = Quaternion::from_euler(euler_y);
CHECK(q_y[0] == doctest::Approx(0.0));
CHECK(q_y[1] == doctest::Approx(0.382684));
CHECK(q_y[2] == doctest::Approx(0.0));
CHECK(q_y[3] == doctest::Approx(0.923879));
Vector3 euler_p(pitch, 0.0, 0.0);
Quaternion q_p = Quaternion::from_euler(euler_p);
CHECK(q_p[0] == doctest::Approx(0.258819));
CHECK(q_p[1] == doctest::Approx(0.0));
CHECK(q_p[2] == doctest::Approx(0.0));
CHECK(q_p[3] == doctest::Approx(0.965926));
Vector3 euler_r(0.0, 0.0, roll);
Quaternion q_r = Quaternion::from_euler(euler_r);
CHECK(q_r[0] == doctest::Approx(0.0));
CHECK(q_r[1] == doctest::Approx(0.0));
CHECK(q_r[2] == doctest::Approx(0.0871558));
CHECK(q_r[3] == doctest::Approx(0.996195));
// Test ZYX dynamic-axes since test data is available online.
// Rotate first about X axis, then new Y axis, then new Z axis.
// (Godot uses YXZ Yaw-Pitch-Roll order).
Quaternion q_yp = q_y * q_p;
CHECK(q_yp[0] == doctest::Approx(0.239118));
CHECK(q_yp[1] == doctest::Approx(0.369644));
CHECK(q_yp[2] == doctest::Approx(-0.099046));
CHECK(q_yp[3] == doctest::Approx(0.892399));
Quaternion q_ryp = q_r * q_yp;
CHECK(q_ryp[0] == doctest::Approx(0.205991));
CHECK(q_ryp[1] == doctest::Approx(0.389078));
CHECK(q_ryp[2] == doctest::Approx(-0.0208912));
CHECK(q_ryp[3] == doctest::Approx(0.897636));
}
TEST_CASE("[Quaternion] xform unit vectors") {
// Easy to visualize: 120 deg about X-axis.
// Transform the i, j, & k unit vectors.
Quaternion q(Vector3(1.0, 0.0, 0.0), Math::deg_to_rad(120.0));
Vector3 i_t = q.xform(Vector3(1.0, 0.0, 0.0));
Vector3 j_t = q.xform(Vector3(0.0, 1.0, 0.0));
Vector3 k_t = q.xform(Vector3(0.0, 0.0, 1.0));
//
CHECK(i_t.is_equal_approx(Vector3(1.0, 0.0, 0.0)));
CHECK(j_t.is_equal_approx(Vector3(0.0, -0.5, 0.866025)));
CHECK(k_t.is_equal_approx(Vector3(0.0, -0.866025, -0.5)));
CHECK(i_t.length_squared() == doctest::Approx(1.0));
CHECK(j_t.length_squared() == doctest::Approx(1.0));
CHECK(k_t.length_squared() == doctest::Approx(1.0));
// Easy to visualize: 30 deg about Y-axis.
q = Quaternion(Vector3(0.0, 1.0, 0.0), Math::deg_to_rad(30.0));
i_t = q.xform(Vector3(1.0, 0.0, 0.0));
j_t = q.xform(Vector3(0.0, 1.0, 0.0));
k_t = q.xform(Vector3(0.0, 0.0, 1.0));
//
CHECK(i_t.is_equal_approx(Vector3(0.866025, 0.0, -0.5)));
CHECK(j_t.is_equal_approx(Vector3(0.0, 1.0, 0.0)));
CHECK(k_t.is_equal_approx(Vector3(0.5, 0.0, 0.866025)));
CHECK(i_t.length_squared() == doctest::Approx(1.0));
CHECK(j_t.length_squared() == doctest::Approx(1.0));
CHECK(k_t.length_squared() == doctest::Approx(1.0));
// Easy to visualize: 60 deg about Z-axis.
q = Quaternion(Vector3(0.0, 0.0, 1.0), Math::deg_to_rad(60.0));
i_t = q.xform(Vector3(1.0, 0.0, 0.0));
j_t = q.xform(Vector3(0.0, 1.0, 0.0));
k_t = q.xform(Vector3(0.0, 0.0, 1.0));
//
CHECK(i_t.is_equal_approx(Vector3(0.5, 0.866025, 0.0)));
CHECK(j_t.is_equal_approx(Vector3(-0.866025, 0.5, 0.0)));
CHECK(k_t.is_equal_approx(Vector3(0.0, 0.0, 1.0)));
CHECK(i_t.length_squared() == doctest::Approx(1.0));
CHECK(j_t.length_squared() == doctest::Approx(1.0));
CHECK(k_t.length_squared() == doctest::Approx(1.0));
}
TEST_CASE("[Quaternion] xform vector") {
// Arbitrary quaternion rotates an arbitrary vector.
const Vector3 euler_yzx(Math::deg_to_rad(31.41), Math::deg_to_rad(-49.16), Math::deg_to_rad(12.34));
const Basis basis_axes = Basis::from_euler(euler_yzx);
const Quaternion q(basis_axes);
constexpr Vector3 v_arb(3.0, 4.0, 5.0);
const Vector3 v_rot = q.xform(v_arb);
const Vector3 v_compare = basis_axes.xform(v_arb);
CHECK(v_rot.length_squared() == doctest::Approx(v_arb.length_squared()));
CHECK(v_rot.is_equal_approx(v_compare));
}
// Test vector xform for a single combination of Quaternion and Vector.
void test_quat_vec_rotate(Vector3 euler_yzx, Vector3 v_in) {
const Basis basis_axes = Basis::from_euler(euler_yzx);
const Quaternion q(basis_axes);
const Vector3 v_rot = q.xform(v_in);
const Vector3 v_compare = basis_axes.xform(v_in);
CHECK(v_rot.length_squared() == doctest::Approx(v_in.length_squared()));
CHECK(v_rot.is_equal_approx(v_compare));
}
TEST_CASE("[Stress][Quaternion] Many vector xforms") {
// Many arbitrary quaternions rotate many arbitrary vectors.
// For each trial, check that rotation by Quaternion yields same result as
// rotation by Basis.
constexpr int STEPS = 100; // Number of test steps in each dimension
constexpr double delta = 2.0 * Math::PI / STEPS; // Angle increment per step
constexpr double delta_vec = 20.0 / STEPS; // Vector increment per step
Vector3 vec_arb(1.0, 1.0, 1.0);
double x_angle = -Math::PI;
double y_angle = -Math::PI;
double z_angle = -Math::PI;
for (double i = 0; i < STEPS; ++i) {
vec_arb[0] = -10.0 + i * delta_vec;
x_angle = i * delta - Math::PI;
for (double j = 0; j < STEPS; ++j) {
vec_arb[1] = -10.0 + j * delta_vec;
y_angle = j * delta - Math::PI;
for (double k = 0; k < STEPS; ++k) {
vec_arb[2] = -10.0 + k * delta_vec;
z_angle = k * delta - Math::PI;
Vector3 euler_yzx(x_angle, y_angle, z_angle);
test_quat_vec_rotate(euler_yzx, vec_arb);
}
}
}
}
TEST_CASE("[Quaternion] Finite number checks") {
constexpr real_t x = Math::NaN;
CHECK_MESSAGE(
Quaternion(0, 1, 2, 3).is_finite(),
"Quaternion with all components finite should be finite");
CHECK_FALSE_MESSAGE(
Quaternion(x, 1, 2, 3).is_finite(),
"Quaternion with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(0, x, 2, 3).is_finite(),
"Quaternion with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(0, 1, x, 3).is_finite(),
"Quaternion with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(0, 1, 2, x).is_finite(),
"Quaternion with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(x, x, 2, 3).is_finite(),
"Quaternion with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(x, 1, x, 3).is_finite(),
"Quaternion with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(x, 1, 2, x).is_finite(),
"Quaternion with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(0, x, x, 3).is_finite(),
"Quaternion with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(0, x, 2, x).is_finite(),
"Quaternion with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(0, 1, x, x).is_finite(),
"Quaternion with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(0, x, x, x).is_finite(),
"Quaternion with three components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(x, 1, x, x).is_finite(),
"Quaternion with three components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(x, x, 2, x).is_finite(),
"Quaternion with three components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(x, x, x, 3).is_finite(),
"Quaternion with three components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Quaternion(x, x, x, x).is_finite(),
"Quaternion with four components infinite should not be finite.");
}
} // namespace TestQuaternion

View File

@@ -0,0 +1,272 @@
/**************************************************************************/
/* test_random_number_generator.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/random_number_generator.h"
#include "tests/test_macros.h"
namespace TestRandomNumberGenerator {
TEST_CASE("[RandomNumberGenerator] Float") {
Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
rng->set_seed(0);
INFO("Should give float between 0.0 and 1.0.");
for (int i = 0; i < 1000; i++) {
real_t n = rng->randf();
CHECK(n >= 0.0);
CHECK(n <= 1.0);
}
}
TEST_CASE("[RandomNumberGenerator] Integer range via modulo") {
Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
rng->set_seed(0);
INFO("Should give integer between 0 and 100.");
for (int i = 0; i < 1000; i++) {
uint32_t n = rng->randi() % 100;
CHECK(n >= 0);
CHECK(n <= 100);
}
}
TEST_CASE_MAY_FAIL("[RandomNumberGenerator] Integer 32 bit") {
Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
rng->set_seed(0); // Change the seed if this fails.
bool higher = false;
int i;
for (i = 0; i < 1000; i++) {
uint32_t n = rng->randi();
if (n > 0x0fff'ffff) {
higher = true;
break;
}
}
INFO("Current seed: ", rng->get_seed());
INFO("Current iteration: ", i);
CHECK_MESSAGE(higher, "Given current seed, this should give an integer higher than 0x0fff'ffff at least once.");
}
TEST_CASE("[RandomNumberGenerator] Float and integer range") {
Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
rng->set_seed(0);
uint64_t initial_state = rng->get_state();
uint32_t initial_seed = rng->get_seed();
INFO("Should give float between -100.0 and 100.0, base test.");
for (int i = 0; i < 1000; i++) {
real_t n0 = rng->randf_range(-100.0, 100.0);
CHECK(n0 >= -100);
CHECK(n0 <= 100);
}
rng->randomize();
INFO("Should give float between -75.0 and 75.0.");
INFO("Shouldn't be affected by randomize.");
for (int i = 0; i < 1000; i++) {
real_t n1 = rng->randf_range(-75.0, 75.0);
CHECK(n1 >= -75);
CHECK(n1 <= 75);
}
rng->set_state(initial_state);
INFO("Should give integer between -50 and 50.");
INFO("Shouldn't be affected by set_state.");
for (int i = 0; i < 1000; i++) {
real_t n2 = rng->randi_range(-50, 50);
CHECK(n2 >= -50);
CHECK(n2 <= 50);
}
rng->set_seed(initial_seed);
INFO("Should give integer between -25 and 25.");
INFO("Shouldn't be affected by set_seed.");
for (int i = 0; i < 1000; i++) {
int32_t n3 = rng->randi_range(-25, 25);
CHECK(n3 >= -25);
CHECK(n3 <= 25);
}
rng->randf();
rng->randf();
INFO("Should give float between -10.0 and 10.0.");
INFO("Shouldn't be affected after generating new numbers.");
for (int i = 0; i < 1000; i++) {
real_t n4 = rng->randf_range(-10.0, 10.0);
CHECK(n4 >= -10);
CHECK(n4 <= 10);
}
rng->randi();
rng->randi();
INFO("Should give integer between -5 and 5.");
INFO("Shouldn't be affected after generating new numbers.");
for (int i = 0; i < 1000; i++) {
real_t n5 = rng->randf_range(-5, 5);
CHECK(n5 >= -5);
CHECK(n5 <= 5);
}
}
TEST_CASE_MAY_FAIL("[RandomNumberGenerator] Normal distribution") {
Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
rng->set_seed(1); // Change the seed if this fails.
INFO("Should give a number between -5 to 5 (5 std deviations away; above 99.7% chance it will be in this range).");
INFO("Standard randfn function call.");
for (int i = 0; i < 100; i++) {
real_t n = rng->randfn();
CHECK(n >= -5);
CHECK(n <= 5);
}
INFO("Should give number between -5 to 5 after multiple randi/randf calls.");
INFO("5 std deviations away; above 99.7% chance it will be in this range.");
rng->randf();
rng->randi();
for (int i = 0; i < 100; i++) {
real_t n = rng->randfn();
CHECK(n >= -5);
CHECK(n <= 5);
}
INFO("Checks if user defined mean and deviation work properly.");
INFO("5 std deviations away; above 99.7% chance it will be in this range.");
for (int i = 0; i < 100; i++) {
real_t n = rng->randfn(5, 10);
CHECK(n >= -45);
CHECK(n <= 55);
}
INFO("Checks if randfn works with changed seeds.");
INFO("5 std deviations away; above 99.7% chance it will be in this range.");
rng->randomize();
for (int i = 0; i < 100; i++) {
real_t n = rng->randfn(3, 3);
CHECK(n >= -12);
CHECK(n <= 18);
}
}
TEST_CASE("[RandomNumberGenerator] Zero for first number immediately after seeding") {
Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
rng->set_seed(0);
uint32_t n1 = rng->randi();
uint32_t n2 = rng->randi();
INFO("Initial random values: ", n1, " ", n2);
CHECK(n1 != 0);
rng->set_seed(1);
uint32_t n3 = rng->randi();
uint32_t n4 = rng->randi();
INFO("Values after changing the seed: ", n3, " ", n4);
CHECK(n3 != 0);
}
TEST_CASE("[RandomNumberGenerator] Restore state") {
Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
rng->randomize();
uint64_t last_seed = rng->get_seed();
INFO("Current seed: ", last_seed);
rng->randi();
rng->randi();
CHECK_MESSAGE(rng->get_seed() == last_seed,
"The seed should remain the same after generating some numbers");
uint64_t saved_state = rng->get_state();
INFO("Current state: ", saved_state);
real_t f1_before = rng->randf();
real_t f2_before = rng->randf();
INFO("This seed produces: ", f1_before, " ", f2_before);
// Restore now.
rng->set_state(saved_state);
real_t f1_after = rng->randf();
real_t f2_after = rng->randf();
INFO("Resetting the state produces: ", f1_after, " ", f2_after);
String msg = "Should restore the sequence of numbers after resetting the state";
CHECK_MESSAGE(f1_before == f1_after, msg);
CHECK_MESSAGE(f2_before == f2_after, msg);
}
TEST_CASE("[RandomNumberGenerator] Restore from seed") {
Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
rng->set_seed(0);
INFO("Current seed: ", rng->get_seed());
uint32_t s0_1_before = rng->randi();
uint32_t s0_2_before = rng->randi();
INFO("This seed produces: ", s0_1_before, " ", s0_2_before);
rng->set_seed(9000);
INFO("Current seed: ", rng->get_seed());
uint32_t s9000_1 = rng->randi();
uint32_t s9000_2 = rng->randi();
INFO("This seed produces: ", s9000_1, " ", s9000_2);
rng->set_seed(0);
INFO("Current seed: ", rng->get_seed());
uint32_t s0_1_after = rng->randi();
uint32_t s0_2_after = rng->randi();
INFO("This seed produces: ", s0_1_after, " ", s0_2_after);
String msg = "Should restore the sequence of numbers after resetting the seed";
CHECK_MESSAGE(s0_1_before == s0_1_after, msg);
CHECK_MESSAGE(s0_2_before == s0_2_after, msg);
}
TEST_CASE_MAY_FAIL("[RandomNumberGenerator] randi_range bias check") {
int zeros = 0;
int ones = 0;
Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator);
for (int i = 0; i < 10000; i++) {
int val = rng->randi_range(0, 1);
val == 0 ? zeros++ : ones++;
}
CHECK_MESSAGE(std::abs(zeros * 1.0 / ones - 1.0) < 0.1, "The ratio of zeros to ones should be nearly 1");
int vals[10] = { 0 };
for (int i = 0; i < 1000000; i++) {
vals[rng->randi_range(0, 9)]++;
}
for (int i = 0; i < 10; i++) {
CHECK_MESSAGE(std::abs(vals[i] / 1000000.0 - 0.1) < 0.01, "Each element should appear roughly 10% of the time");
}
}
} // namespace TestRandomNumberGenerator

View File

@@ -0,0 +1,345 @@
/**************************************************************************/
/* test_rect2.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/rect2.h"
#include "core/math/rect2i.h"
#include "thirdparty/doctest/doctest.h"
namespace TestRect2 {
TEST_CASE("[Rect2] Constructor methods") {
constexpr Rect2 rect = Rect2(0, 100, 1280, 720);
constexpr Rect2 rect_vector = Rect2(Vector2(0, 100), Vector2(1280, 720));
constexpr Rect2 rect_copy_rect = Rect2(rect);
const Rect2 rect_copy_recti = Rect2(Rect2i(0, 100, 1280, 720));
static_assert(
rect == rect_vector,
"Rect2s created with the same dimensions but by different methods should be equal.");
static_assert(
rect == rect_copy_rect,
"Rect2s created with the same dimensions but by different methods should be equal.");
CHECK_MESSAGE(
rect == rect_copy_recti,
"Rect2s created with the same dimensions but by different methods should be equal.");
}
TEST_CASE("[Rect2] String conversion") {
// Note: This also depends on the Vector2 string representation.
CHECK_MESSAGE(
String(Rect2(0, 100, 1280, 720)) == "[P: (0.0, 100.0), S: (1280.0, 720.0)]",
"The string representation should match the expected value.");
}
TEST_CASE("[Rect2] Basic getters") {
constexpr Rect2 rect = Rect2(0, 100, 1280, 720);
CHECK_MESSAGE(
rect.get_position().is_equal_approx(Vector2(0, 100)),
"get_position() should return the expected value.");
CHECK_MESSAGE(
rect.get_size().is_equal_approx(Vector2(1280, 720)),
"get_size() should return the expected value.");
CHECK_MESSAGE(
rect.get_end().is_equal_approx(Vector2(1280, 820)),
"get_end() should return the expected value.");
CHECK_MESSAGE(
rect.get_center().is_equal_approx(Vector2(640, 460)),
"get_center() should return the expected value.");
CHECK_MESSAGE(
Rect2(0, 100, 1281, 721).get_center().is_equal_approx(Vector2(640.5, 460.5)),
"get_center() should return the expected value.");
}
TEST_CASE("[Rect2] Basic setters") {
Rect2 rect = Rect2(0, 100, 1280, 720);
rect.set_end(Vector2(4000, 4000));
CHECK_MESSAGE(
rect.is_equal_approx(Rect2(0, 100, 4000, 3900)),
"set_end() should result in the expected Rect2.");
rect = Rect2(0, 100, 1280, 720);
rect.set_position(Vector2(4000, 4000));
CHECK_MESSAGE(
rect.is_equal_approx(Rect2(4000, 4000, 1280, 720)),
"set_position() should result in the expected Rect2.");
rect = Rect2(0, 100, 1280, 720);
rect.set_size(Vector2(4000, 4000));
CHECK_MESSAGE(
rect.is_equal_approx(Rect2(0, 100, 4000, 4000)),
"set_size() should result in the expected Rect2.");
}
TEST_CASE("[Rect2] Area getters") {
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).get_area() == doctest::Approx(921'600),
"get_area() should return the expected value.");
CHECK_MESSAGE(
Rect2(0, 100, -1280, -720).get_area() == doctest::Approx(921'600),
"get_area() should return the expected value.");
CHECK_MESSAGE(
Rect2(0, 100, 1280, -720).get_area() == doctest::Approx(-921'600),
"get_area() should return the expected value.");
CHECK_MESSAGE(
Rect2(0, 100, -1280, 720).get_area() == doctest::Approx(-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_area(),
"has_area() should return the expected value on Rect2 with an area.");
CHECK_MESSAGE(
!Rect2(0, 100, 0, 500).has_area(),
"has_area() should return the expected value on Rect2 with no area.");
CHECK_MESSAGE(
!Rect2(0, 100, 500, 0).has_area(),
"has_area() should return the expected value on Rect2 with no area.");
CHECK_MESSAGE(
!Rect2(0, 100, 0, 0).has_area(),
"has_area() should return the expected value on Rect2 with no area.");
}
TEST_CASE("[Rect2] Absolute coordinates") {
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).abs().is_equal_approx(Rect2(0, 100, 1280, 720)),
"abs() should return the expected Rect2.");
CHECK_MESSAGE(
Rect2(0, -100, 1280, 720).abs().is_equal_approx(Rect2(0, -100, 1280, 720)),
"abs() should return the expected Rect2.");
CHECK_MESSAGE(
Rect2(0, -100, -1280, -720).abs().is_equal_approx(Rect2(-1280, -820, 1280, 720)),
"abs() should return the expected Rect2.");
CHECK_MESSAGE(
Rect2(0, 100, -1280, 720).abs().is_equal_approx(Rect2(-1280, 100, 1280, 720)),
"abs() should return the expected Rect2.");
}
TEST_CASE("[Rect2] Intersection") {
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).intersection(Rect2(0, 300, 100, 100)).is_equal_approx(Rect2(0, 300, 100, 100)),
"intersection() with fully enclosed Rect2 should return the expected result.");
// The resulting Rect2 is 100 pixels high because the first Rect2 is vertically offset by 100 pixels.
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).intersection(Rect2(1200, 700, 100, 100)).is_equal_approx(Rect2(1200, 700, 80, 100)),
"intersection() with partially enclosed Rect2 should return the expected result.");
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).intersection(Rect2(-4000, -4000, 100, 100)).is_equal_approx(Rect2()),
"intersection() with non-enclosed Rect2 should return the expected result.");
}
TEST_CASE("[Rect2] Enclosing") {
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).encloses(Rect2(0, 300, 100, 100)),
"encloses() with fully contained Rect2 should return the expected result.");
CHECK_MESSAGE(
!Rect2(0, 100, 1280, 720).encloses(Rect2(1200, 700, 100, 100)),
"encloses() with partially contained Rect2 should return the expected result.");
CHECK_MESSAGE(
!Rect2(0, 100, 1280, 720).encloses(Rect2(-4000, -4000, 100, 100)),
"encloses() with non-contained Rect2 should return the expected result.");
}
TEST_CASE("[Rect2] Expanding") {
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).expand(Vector2(500, 600)).is_equal_approx(Rect2(0, 100, 1280, 720)),
"expand() with contained Vector2 should return the expected result.");
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).expand(Vector2(0, 0)).is_equal_approx(Rect2(0, 0, 1280, 820)),
"expand() with non-contained Vector2 should return the expected result.");
}
TEST_CASE("[Rect2] Get support") {
constexpr Rect2 rect = Rect2(Vector2(-1.5, 2), Vector2(4, 5));
CHECK_MESSAGE(
rect.get_support(Vector2(1, 0)) == Vector2(2.5, 2),
"get_support() should return the expected value.");
CHECK_MESSAGE(
rect.get_support(Vector2(0.5, 1)) == Vector2(2.5, 7),
"get_support() should return the expected value.");
CHECK_MESSAGE(
rect.get_support(Vector2(0.5, 1)) == Vector2(2.5, 7),
"get_support() should return the expected value.");
CHECK_MESSAGE(
rect.get_support(Vector2(0, -1)) == Vector2(-1.5, 2),
"get_support() should return the expected value.");
CHECK_MESSAGE(
rect.get_support(Vector2(0, -0.1)) == Vector2(-1.5, 2),
"get_support() should return the expected value.");
CHECK_MESSAGE(
rect.get_support(Vector2()) == Vector2(-1.5, 2),
"get_support() should return the Rect2 position when given a zero vector.");
}
TEST_CASE("[Rect2] Growing") {
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).grow(100).is_equal_approx(Rect2(-100, 0, 1480, 920)),
"grow() with positive value should return the expected Rect2.");
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).grow(-100).is_equal_approx(Rect2(100, 200, 1080, 520)),
"grow() with negative value should return the expected Rect2.");
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).grow(-4000).is_equal_approx(Rect2(4000, 4100, -6720, -7280)),
"grow() with large negative value should return the expected Rect2.");
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).grow_individual(100, 200, 300, 400).is_equal_approx(Rect2(-100, -100, 1680, 1320)),
"grow_individual() with positive values should return the expected Rect2.");
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).grow_individual(-100, 200, 300, -400).is_equal_approx(Rect2(100, -100, 1480, 520)),
"grow_individual() with positive and negative values should return the expected Rect2.");
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).grow_side(SIDE_TOP, 500).is_equal_approx(Rect2(0, -400, 1280, 1220)),
"grow_side() with positive value should return the expected Rect2.");
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).grow_side(SIDE_TOP, -500).is_equal_approx(Rect2(0, 600, 1280, 220)),
"grow_side() with negative value should return the expected Rect2.");
}
TEST_CASE("[Rect2] Has point") {
Rect2 rect = Rect2(0, 100, 1280, 720);
CHECK_MESSAGE(
rect.has_point(Vector2(500, 600)),
"has_point() with contained Vector2 should return the expected result.");
CHECK_MESSAGE(
!rect.has_point(Vector2(0, 0)),
"has_point() with non-contained Vector2 should return the expected result.");
CHECK_MESSAGE(
rect.has_point(rect.position),
"has_point() with positive size should include `position`.");
CHECK_MESSAGE(
rect.has_point(rect.position + Vector2(1, 1)),
"has_point() with positive size should include `position + (1, 1)`.");
CHECK_MESSAGE(
!rect.has_point(rect.position + Vector2(1, -1)),
"has_point() with positive size should not include `position + (1, -1)`.");
CHECK_MESSAGE(
!rect.has_point(rect.position + rect.size),
"has_point() with positive size should not include `position + size`.");
CHECK_MESSAGE(
!rect.has_point(rect.position + rect.size + Vector2(1, 1)),
"has_point() with positive size should not include `position + size + (1, 1)`.");
CHECK_MESSAGE(
rect.has_point(rect.position + rect.size + Vector2(-1, -1)),
"has_point() with positive size should include `position + size + (-1, -1)`.");
CHECK_MESSAGE(
!rect.has_point(rect.position + rect.size + Vector2(-1, 1)),
"has_point() with positive size should not include `position + size + (-1, 1)`.");
CHECK_MESSAGE(
rect.has_point(rect.position + Vector2(0, 10)),
"has_point() with point located on left edge should return true.");
CHECK_MESSAGE(
!rect.has_point(rect.position + Vector2(rect.size.x, 10)),
"has_point() with point located on right edge should return false.");
CHECK_MESSAGE(
rect.has_point(rect.position + Vector2(10, 0)),
"has_point() with point located on top edge should return true.");
CHECK_MESSAGE(
!rect.has_point(rect.position + Vector2(10, rect.size.y)),
"has_point() with point located on bottom edge should return false.");
/*
// FIXME: Disabled for now until GH-37617 is fixed one way or another.
// More tests should then be written like for the positive size case.
rect = Rect2(0, 100, -1280, -720);
CHECK_MESSAGE(
rect.has_point(rect.position),
"has_point() with negative size should include `position`.");
CHECK_MESSAGE(
!rect.has_point(rect.position + rect.size),
"has_point() with negative size should not include `position + size`.");
*/
rect = Rect2(-4000, -200, 1280, 720);
CHECK_MESSAGE(
rect.has_point(rect.position + Vector2(0, 10)),
"has_point() with negative position and point located on left edge should return true.");
CHECK_MESSAGE(
!rect.has_point(rect.position + Vector2(rect.size.x, 10)),
"has_point() with negative position and point located on right edge should return false.");
CHECK_MESSAGE(
rect.has_point(rect.position + Vector2(10, 0)),
"has_point() with negative position and point located on top edge should return true.");
CHECK_MESSAGE(
!rect.has_point(rect.position + Vector2(10, rect.size.y)),
"has_point() with negative position and point located on bottom edge should return false.");
}
TEST_CASE("[Rect2] Intersection") {
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).intersects(Rect2(0, 300, 100, 100)),
"intersects() with fully enclosed Rect2 should return the expected result.");
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).intersects(Rect2(1200, 700, 100, 100)),
"intersects() with partially enclosed Rect2 should return the expected result.");
CHECK_MESSAGE(
!Rect2(0, 100, 1280, 720).intersects(Rect2(-4000, -4000, 100, 100)),
"intersects() with non-enclosed Rect2 should return the expected result.");
}
TEST_CASE("[Rect2] Merging") {
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).merge(Rect2(0, 300, 100, 100)).is_equal_approx(Rect2(0, 100, 1280, 720)),
"merge() with fully enclosed Rect2 should return the expected result.");
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).merge(Rect2(1200, 700, 100, 100)).is_equal_approx(Rect2(0, 100, 1300, 720)),
"merge() with partially enclosed Rect2 should return the expected result.");
CHECK_MESSAGE(
Rect2(0, 100, 1280, 720).merge(Rect2(-4000, -4000, 100, 100)).is_equal_approx(Rect2(-4000, -4000, 5280, 4820)),
"merge() with non-enclosed Rect2 should return the expected result.");
}
TEST_CASE("[Rect2] Finite number checks") {
constexpr Vector2 x(0, 1);
constexpr Vector2 infinite(Math::NaN, Math::NaN);
CHECK_MESSAGE(
Rect2(x, x).is_finite(),
"Rect2 with all components finite should be finite");
CHECK_FALSE_MESSAGE(
Rect2(infinite, x).is_finite(),
"Rect2 with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Rect2(x, infinite).is_finite(),
"Rect2 with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Rect2(infinite, infinite).is_finite(),
"Rect2 with two components infinite should not be finite.");
}
} // namespace TestRect2

View File

@@ -0,0 +1,308 @@
/**************************************************************************/
/* test_rect2i.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/rect2.h"
#include "core/math/rect2i.h"
#include "thirdparty/doctest/doctest.h"
namespace TestRect2i {
TEST_CASE("[Rect2i] Constructor methods") {
constexpr Rect2i recti = Rect2i(0, 100, 1280, 720);
constexpr Rect2i recti_vector = Rect2i(Vector2i(0, 100), Vector2i(1280, 720));
constexpr Rect2i recti_copy_recti = Rect2i(recti);
const Rect2i recti_copy_rect = Rect2i(Rect2(0, 100, 1280, 720));
static_assert(
recti == recti_vector,
"Rect2is created with the same dimensions but by different methods should be equal.");
static_assert(
recti == recti_copy_recti,
"Rect2is created with the same dimensions but by different methods should be equal.");
CHECK_MESSAGE(
recti == recti_copy_rect,
"Rect2is created with the same dimensions but by different methods should be equal.");
}
TEST_CASE("[Rect2i] String conversion") {
// Note: This also depends on the Vector2 string representation.
CHECK_MESSAGE(
String(Rect2i(0, 100, 1280, 720)) == "[P: (0, 100), S: (1280, 720)]",
"The string representation should match the expected value.");
}
TEST_CASE("[Rect2i] Basic getters") {
constexpr Rect2i rect = Rect2i(0, 100, 1280, 720);
CHECK_MESSAGE(
rect.get_position() == Vector2i(0, 100),
"get_position() should return the expected value.");
CHECK_MESSAGE(
rect.get_size() == Vector2i(1280, 720),
"get_size() should return the expected value.");
CHECK_MESSAGE(
rect.get_end() == Vector2i(1280, 820),
"get_end() should return the expected value.");
CHECK_MESSAGE(
rect.get_center() == Vector2i(640, 460),
"get_center() should return the expected value.");
CHECK_MESSAGE(
Rect2i(0, 100, 1281, 721).get_center() == Vector2i(640, 460),
"get_center() should return the expected value.");
}
TEST_CASE("[Rect2i] Basic setters") {
Rect2i rect = Rect2i(0, 100, 1280, 720);
rect.set_end(Vector2i(4000, 4000));
CHECK_MESSAGE(
rect == Rect2i(0, 100, 4000, 3900),
"set_end() should result in the expected Rect2i.");
rect = Rect2i(0, 100, 1280, 720);
rect.set_position(Vector2i(4000, 4000));
CHECK_MESSAGE(
rect == Rect2i(4000, 4000, 1280, 720),
"set_position() should result in the expected Rect2i.");
rect = Rect2i(0, 100, 1280, 720);
rect.set_size(Vector2i(4000, 4000));
CHECK_MESSAGE(
rect == Rect2i(0, 100, 4000, 4000),
"set_size() should result in the expected Rect2i.");
}
TEST_CASE("[Rect2i] Area getters") {
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).get_area() == 921'600,
"get_area() should return the expected value.");
CHECK_MESSAGE(
Rect2i(0, 100, -1280, -720).get_area() == 921'600,
"get_area() should return the expected value.");
CHECK_MESSAGE(
Rect2i(0, 100, 1280, -720).get_area() == -921'600,
"get_area() should return the expected value.");
CHECK_MESSAGE(
Rect2i(0, 100, -1280, 720).get_area() == -921'600,
"get_area() should return the expected value.");
CHECK_MESSAGE(
Rect2i(0, 100, 0, 720).get_area() == 0,
"get_area() should return the expected value.");
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).has_area(),
"has_area() should return the expected value on Rect2i with an area.");
CHECK_MESSAGE(
!Rect2i(0, 100, 0, 500).has_area(),
"has_area() should return the expected value on Rect2i with no area.");
CHECK_MESSAGE(
!Rect2i(0, 100, 500, 0).has_area(),
"has_area() should return the expected value on Rect2i with no area.");
CHECK_MESSAGE(
!Rect2i(0, 100, 0, 0).has_area(),
"has_area() should return the expected value on Rect2i with no area.");
}
TEST_CASE("[Rect2i] Absolute coordinates") {
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).abs() == Rect2i(0, 100, 1280, 720),
"abs() should return the expected Rect2i.");
CHECK_MESSAGE(
Rect2i(0, -100, 1280, 720).abs() == Rect2i(0, -100, 1280, 720),
"abs() should return the expected Rect2i.");
CHECK_MESSAGE(
Rect2i(0, -100, -1280, -720).abs() == Rect2i(-1280, -820, 1280, 720),
"abs() should return the expected Rect2i.");
CHECK_MESSAGE(
Rect2i(0, 100, -1280, 720).abs() == Rect2i(-1280, 100, 1280, 720),
"abs() should return the expected Rect2i.");
}
TEST_CASE("[Rect2i] Intersection") {
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).intersection(Rect2i(0, 300, 100, 100)) == Rect2i(0, 300, 100, 100),
"intersection() with fully enclosed Rect2i should return the expected result.");
// The resulting Rect2i is 100 pixels high because the first Rect2i is vertically offset by 100 pixels.
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).intersection(Rect2i(1200, 700, 100, 100)) == Rect2i(1200, 700, 80, 100),
"intersection() with partially enclosed Rect2i should return the expected result.");
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).intersection(Rect2i(-4000, -4000, 100, 100)) == Rect2i(),
"intersection() with non-enclosed Rect2i should return the expected result.");
}
TEST_CASE("[Rect2i] Enclosing") {
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).encloses(Rect2i(0, 300, 100, 100)),
"encloses() with fully contained Rect2i should return the expected result.");
CHECK_MESSAGE(
!Rect2i(0, 100, 1280, 720).encloses(Rect2i(1200, 700, 100, 100)),
"encloses() with partially contained Rect2i should return the expected result.");
CHECK_MESSAGE(
!Rect2i(0, 100, 1280, 720).encloses(Rect2i(-4000, -4000, 100, 100)),
"encloses() with non-contained Rect2i should return the expected result.");
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).encloses(Rect2i(0, 100, 1280, 720)),
"encloses() with identical Rect2i should return the expected result.");
}
TEST_CASE("[Rect2i] Expanding") {
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).expand(Vector2i(500, 600)) == Rect2i(0, 100, 1280, 720),
"expand() with contained Vector2i should return the expected result.");
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).expand(Vector2i(0, 0)) == Rect2i(0, 0, 1280, 820),
"expand() with non-contained Vector2i should return the expected result.");
}
TEST_CASE("[Rect2i] Growing") {
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).grow(100) == Rect2i(-100, 0, 1480, 920),
"grow() with positive value should return the expected Rect2i.");
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).grow(-100) == Rect2i(100, 200, 1080, 520),
"grow() with negative value should return the expected Rect2i.");
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).grow(-4000) == Rect2i(4000, 4100, -6720, -7280),
"grow() with large negative value should return the expected Rect2i.");
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).grow_individual(100, 200, 300, 400) == Rect2i(-100, -100, 1680, 1320),
"grow_individual() with positive values should return the expected Rect2i.");
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).grow_individual(-100, 200, 300, -400) == Rect2i(100, -100, 1480, 520),
"grow_individual() with positive and negative values should return the expected Rect2i.");
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).grow_side(SIDE_TOP, 500) == Rect2i(0, -400, 1280, 1220),
"grow_side() with positive value should return the expected Rect2i.");
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).grow_side(SIDE_TOP, -500) == Rect2i(0, 600, 1280, 220),
"grow_side() with negative value should return the expected Rect2i.");
}
TEST_CASE("[Rect2i] Has point") {
Rect2i rect = Rect2i(0, 100, 1280, 720);
CHECK_MESSAGE(
rect.has_point(Vector2i(500, 600)),
"has_point() with contained Vector2i should return the expected result.");
CHECK_MESSAGE(
!rect.has_point(Vector2i(0, 0)),
"has_point() with non-contained Vector2i should return the expected result.");
CHECK_MESSAGE(
rect.has_point(rect.position),
"has_point() with positive size should include `position`.");
CHECK_MESSAGE(
rect.has_point(rect.position + Vector2i(1, 1)),
"has_point() with positive size should include `position + (1, 1)`.");
CHECK_MESSAGE(
!rect.has_point(rect.position + Vector2i(1, -1)),
"has_point() with positive size should not include `position + (1, -1)`.");
CHECK_MESSAGE(
!rect.has_point(rect.position + rect.size),
"has_point() with positive size should not include `position + size`.");
CHECK_MESSAGE(
!rect.has_point(rect.position + rect.size + Vector2i(1, 1)),
"has_point() with positive size should not include `position + size + (1, 1)`.");
CHECK_MESSAGE(
rect.has_point(rect.position + rect.size + Vector2i(-1, -1)),
"has_point() with positive size should include `position + size + (-1, -1)`.");
CHECK_MESSAGE(
!rect.has_point(rect.position + rect.size + Vector2i(-1, 1)),
"has_point() with positive size should not include `position + size + (-1, 1)`.");
CHECK_MESSAGE(
rect.has_point(rect.position + Vector2i(0, 10)),
"has_point() with point located on left edge should return true.");
CHECK_MESSAGE(
!rect.has_point(rect.position + Vector2i(rect.size.x, 10)),
"has_point() with point located on right edge should return false.");
CHECK_MESSAGE(
rect.has_point(rect.position + Vector2i(10, 0)),
"has_point() with point located on top edge should return true.");
CHECK_MESSAGE(
!rect.has_point(rect.position + Vector2i(10, rect.size.y)),
"has_point() with point located on bottom edge should return false.");
/*
// FIXME: Disabled for now until GH-37617 is fixed one way or another.
// More tests should then be written like for the positive size case.
rect = Rect2i(0, 100, -1280, -720);
CHECK_MESSAGE(
rect.has_point(rect.position),
"has_point() with negative size should include `position`.");
CHECK_MESSAGE(
!rect.has_point(rect.position + rect.size),
"has_point() with negative size should not include `position + size`.");
*/
rect = Rect2i(-4000, -200, 1280, 720);
CHECK_MESSAGE(
rect.has_point(rect.position + Vector2i(0, 10)),
"has_point() with negative position and point located on left edge should return true.");
CHECK_MESSAGE(
!rect.has_point(rect.position + Vector2i(rect.size.x, 10)),
"has_point() with negative position and point located on right edge should return false.");
CHECK_MESSAGE(
rect.has_point(rect.position + Vector2i(10, 0)),
"has_point() with negative position and point located on top edge should return true.");
CHECK_MESSAGE(
!rect.has_point(rect.position + Vector2i(10, rect.size.y)),
"has_point() with negative position and point located on bottom edge should return false.");
}
TEST_CASE("[Rect2i] Intersection") {
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).intersects(Rect2i(0, 300, 100, 100)),
"intersects() with fully enclosed Rect2i should return the expected result.");
CHECK_MESSAGE(
Rect2i(0, 100, 1280, 720).intersects(Rect2i(1200, 700, 100, 100)),
"intersects() with partially enclosed Rect2i should return the expected result.");
CHECK_MESSAGE(
!Rect2i(0, 100, 1280, 720).intersects(Rect2i(-4000, -4000, 100, 100)),
"intersects() with non-enclosed Rect2i should return the expected result.");
CHECK_MESSAGE(
!Rect2i(0, 0, 2, 2).intersects(Rect2i(2, 2, 2, 2)),
"intersects() with adjacent 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 TestRect2i

View File

@@ -0,0 +1,246 @@
/**************************************************************************/
/* test_transform_2d.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/transform_2d.h"
#include "tests/test_macros.h"
namespace TestTransform2D {
Transform2D create_dummy_transform() {
return Transform2D(Vector2(1, 2), Vector2(3, 4), Vector2(5, 6));
}
Transform2D identity() {
return Transform2D();
}
TEST_CASE("[Transform2D] Default constructor") {
Transform2D default_constructor = Transform2D();
CHECK(default_constructor == Transform2D(Vector2(1, 0), Vector2(0, 1), Vector2(0, 0)));
}
TEST_CASE("[Transform2D] Copy constructor") {
Transform2D T = create_dummy_transform();
Transform2D copy_constructor = Transform2D(T);
CHECK(T == copy_constructor);
}
TEST_CASE("[Transform2D] Constructor from angle and position") {
constexpr float ROTATION = Math::PI / 4;
constexpr Vector2 TRANSLATION = Vector2(20, -20);
const Transform2D test = Transform2D(ROTATION, TRANSLATION);
const Transform2D expected = Transform2D().rotated(ROTATION).translated(TRANSLATION);
CHECK(test == expected);
}
TEST_CASE("[Transform2D] Constructor from angle, scale, skew and position") {
constexpr float ROTATION = Math::PI / 2;
constexpr Vector2 SCALE = Vector2(2, 0.5);
constexpr float SKEW = Math::PI / 4;
constexpr Vector2 TRANSLATION = Vector2(30, 0);
const Transform2D test = Transform2D(ROTATION, SCALE, SKEW, TRANSLATION);
Transform2D expected = Transform2D().scaled(SCALE).rotated(ROTATION).translated(TRANSLATION);
expected.set_skew(SKEW);
CHECK(test.is_equal_approx(expected));
}
TEST_CASE("[Transform2D] Constructor from raw values") {
constexpr Transform2D test = Transform2D(1, 2, 3, 4, 5, 6);
constexpr Transform2D expected = Transform2D(Vector2(1, 2), Vector2(3, 4), Vector2(5, 6));
static_assert(test == expected);
}
TEST_CASE("[Transform2D] xform") {
constexpr Vector2 v = Vector2(2, 3);
constexpr Transform2D T = Transform2D(Vector2(1, 2), Vector2(3, 4), Vector2(5, 6));
constexpr Vector2 expected = Vector2(1 * 2 + 3 * 3 + 5 * 1, 2 * 2 + 4 * 3 + 6 * 1);
CHECK(T.xform(v) == expected);
}
TEST_CASE("[Transform2D] Basis xform") {
constexpr Vector2 v = Vector2(2, 2);
constexpr Transform2D T1 = Transform2D(Vector2(1, 2), Vector2(3, 4), Vector2(0, 0));
// Both versions should be the same when the origin is (0,0).
CHECK(T1.basis_xform(v) == T1.xform(v));
constexpr Transform2D T2 = Transform2D(Vector2(1, 2), Vector2(3, 4), Vector2(5, 6));
// Each version should be different when the origin is not (0,0).
CHECK_FALSE(T2.basis_xform(v) == T2.xform(v));
}
TEST_CASE("[Transform2D] Affine inverse") {
const Transform2D orig = create_dummy_transform();
const Transform2D affine_inverted = orig.affine_inverse();
const Transform2D affine_inverted_again = affine_inverted.affine_inverse();
CHECK(affine_inverted_again == orig);
}
TEST_CASE("[Transform2D] Orthonormalized") {
const Transform2D T = create_dummy_transform();
const Transform2D orthonormalized_T = T.orthonormalized();
// Check each basis has length 1.
CHECK(Math::is_equal_approx(orthonormalized_T[0].length_squared(), 1));
CHECK(Math::is_equal_approx(orthonormalized_T[1].length_squared(), 1));
const Vector2 vx = Vector2(orthonormalized_T[0].x, orthonormalized_T[1].x);
const Vector2 vy = Vector2(orthonormalized_T[0].y, orthonormalized_T[1].y);
// Check the basis are orthogonal.
CHECK(Math::is_equal_approx(orthonormalized_T.tdotx(vx), 1));
CHECK(Math::is_equal_approx(orthonormalized_T.tdotx(vy), 0));
CHECK(Math::is_equal_approx(orthonormalized_T.tdoty(vx), 0));
CHECK(Math::is_equal_approx(orthonormalized_T.tdoty(vy), 1));
}
TEST_CASE("[Transform2D] translation") {
constexpr Vector2 offset = Vector2(1, 2);
// Both versions should give the same result applied to identity.
CHECK(identity().translated(offset) == identity().translated_local(offset));
// Check both versions against left and right multiplications.
const Transform2D orig = create_dummy_transform();
const Transform2D T = identity().translated(offset);
CHECK(orig.translated(offset) == T * orig);
CHECK(orig.translated_local(offset) == orig * T);
}
TEST_CASE("[Transform2D] scaling") {
constexpr Vector2 scaling = Vector2(1, 2);
// Both versions should give the same result applied to identity.
CHECK(identity().scaled(scaling) == identity().scaled_local(scaling));
// Check both versions against left and right multiplications.
const Transform2D orig = create_dummy_transform();
const Transform2D S = identity().scaled(scaling);
CHECK(orig.scaled(scaling) == S * orig);
CHECK(orig.scaled_local(scaling) == orig * S);
}
TEST_CASE("[Transform2D] rotation") {
constexpr real_t phi = 1.0;
// Both versions should give the same result applied to identity.
CHECK(identity().rotated(phi) == identity().rotated_local(phi));
// Check both versions against left and right multiplications.
const Transform2D orig = create_dummy_transform();
const Transform2D R = identity().rotated(phi);
CHECK(orig.rotated(phi) == R * orig);
CHECK(orig.rotated_local(phi) == orig * R);
}
TEST_CASE("[Transform2D] Interpolation") {
const Transform2D rotate_scale_skew_pos = Transform2D(Math::deg_to_rad(170.0), Vector2(3.6, 8.0), Math::deg_to_rad(20.0), Vector2(2.4, 6.8));
const Transform2D rotate_scale_skew_pos_halfway = Transform2D(Math::deg_to_rad(85.0), Vector2(2.3, 4.5), Math::deg_to_rad(10.0), Vector2(1.2, 3.4));
Transform2D interpolated = Transform2D().interpolate_with(rotate_scale_skew_pos, 0.5);
CHECK(interpolated.get_origin().is_equal_approx(rotate_scale_skew_pos_halfway.get_origin()));
CHECK(interpolated.get_rotation() == doctest::Approx(rotate_scale_skew_pos_halfway.get_rotation()));
CHECK(interpolated.get_scale().is_equal_approx(rotate_scale_skew_pos_halfway.get_scale()));
CHECK(interpolated.get_skew() == doctest::Approx(rotate_scale_skew_pos_halfway.get_skew()));
CHECK(interpolated.is_equal_approx(rotate_scale_skew_pos_halfway));
interpolated = rotate_scale_skew_pos.interpolate_with(Transform2D(), 0.5);
CHECK(interpolated.is_equal_approx(rotate_scale_skew_pos_halfway));
}
TEST_CASE("[Transform2D] Finite number checks") {
constexpr Vector2 x = Vector2(0, 1);
constexpr Vector2 infinite = Vector2(Math::NaN, Math::NaN);
CHECK_MESSAGE(
Transform2D(x, x, x).is_finite(),
"Transform2D with all components finite should be finite");
CHECK_FALSE_MESSAGE(
Transform2D(infinite, x, x).is_finite(),
"Transform2D with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Transform2D(x, infinite, x).is_finite(),
"Transform2D with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Transform2D(x, x, infinite).is_finite(),
"Transform2D with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Transform2D(infinite, infinite, x).is_finite(),
"Transform2D with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Transform2D(infinite, x, infinite).is_finite(),
"Transform2D with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Transform2D(x, infinite, infinite).is_finite(),
"Transform2D with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Transform2D(infinite, infinite, infinite).is_finite(),
"Transform2D with three components infinite should not be finite.");
}
TEST_CASE("[Transform2D] Is conformal checks") {
CHECK_MESSAGE(
Transform2D().is_conformal(),
"Identity Transform2D should be conformal.");
CHECK_MESSAGE(
Transform2D(1.2, Vector2()).is_conformal(),
"Transform2D with only rotation should be conformal.");
CHECK_MESSAGE(
Transform2D(Vector2(1, 0), Vector2(0, -1), Vector2()).is_conformal(),
"Transform2D with only a flip should be conformal.");
CHECK_MESSAGE(
Transform2D(Vector2(1.2, 0), Vector2(0, 1.2), Vector2()).is_conformal(),
"Transform2D with only uniform scale should be conformal.");
CHECK_MESSAGE(
Transform2D(Vector2(1.2, 3.4), Vector2(3.4, -1.2), Vector2()).is_conformal(),
"Transform2D with a flip, rotation, and uniform scale should be conformal.");
CHECK_FALSE_MESSAGE(
Transform2D(Vector2(1.2, 0), Vector2(0, 3.4), Vector2()).is_conformal(),
"Transform2D with non-uniform scale should not be conformal.");
CHECK_FALSE_MESSAGE(
Transform2D(Vector2(Math::SQRT12, Math::SQRT12), Vector2(0, 1), Vector2()).is_conformal(),
"Transform2D with the X axis skewed 45 degrees should not be conformal.");
}
} // namespace TestTransform2D

View File

@@ -0,0 +1,138 @@
/**************************************************************************/
/* test_transform_3d.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/transform_3d.h"
#include "tests/test_macros.h"
namespace TestTransform3D {
Transform3D create_dummy_transform() {
return Transform3D(Basis(Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9)), Vector3(10, 11, 12));
}
Transform3D identity() {
return Transform3D();
}
TEST_CASE("[Transform3D] translation") {
constexpr Vector3 offset = Vector3(1, 2, 3);
// Both versions should give the same result applied to identity.
CHECK(identity().translated(offset) == identity().translated_local(offset));
// Check both versions against left and right multiplications.
Transform3D orig = create_dummy_transform();
Transform3D T = identity().translated(offset);
CHECK(orig.translated(offset) == T * orig);
CHECK(orig.translated_local(offset) == orig * T);
}
TEST_CASE("[Transform3D] scaling") {
constexpr Vector3 scaling = Vector3(1, 2, 3);
// Both versions should give the same result applied to identity.
CHECK(identity().scaled(scaling) == identity().scaled_local(scaling));
// Check both versions against left and right multiplications.
Transform3D orig = create_dummy_transform();
Transform3D S = identity().scaled(scaling);
CHECK(orig.scaled(scaling) == S * orig);
CHECK(orig.scaled_local(scaling) == orig * S);
}
TEST_CASE("[Transform3D] rotation") {
const Vector3 axis = Vector3(1, 2, 3).normalized();
constexpr real_t phi = 1.0;
// Both versions should give the same result applied to identity.
CHECK(identity().rotated(axis, phi) == identity().rotated_local(axis, phi));
// Check both versions against left and right multiplications.
Transform3D orig = create_dummy_transform();
Transform3D R = identity().rotated(axis, phi);
CHECK(orig.rotated(axis, phi) == R * orig);
CHECK(orig.rotated_local(axis, phi) == orig * R);
}
TEST_CASE("[Transform3D] Finite number checks") {
constexpr Vector3 y(0, 1, 2);
constexpr Vector3 infinite_vec(Math::NaN, Math::NaN, Math::NaN);
constexpr Basis x(y, y, y);
constexpr Basis infinite_basis(infinite_vec, infinite_vec, infinite_vec);
CHECK_MESSAGE(
Transform3D(x, y).is_finite(),
"Transform3D with all components finite should be finite");
CHECK_FALSE_MESSAGE(
Transform3D(x, infinite_vec).is_finite(),
"Transform3D with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Transform3D(infinite_basis, y).is_finite(),
"Transform3D with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Transform3D(infinite_basis, infinite_vec).is_finite(),
"Transform3D with two components infinite should not be finite.");
}
TEST_CASE("[Transform3D] Rotate around global origin") {
// Start with the default orientation, but not centered on the origin.
// Rotating should rotate both our basis and the origin.
Transform3D transform = Transform3D();
transform.origin = Vector3(0, 0, 1);
Transform3D expected = Transform3D();
expected.origin = Vector3(0, 0, -1);
expected.basis[0] = Vector3(-1, 0, 0);
expected.basis[2] = Vector3(0, 0, -1);
const Transform3D rotated_transform = transform.rotated(Vector3(0, 1, 0), Math::PI);
CHECK_MESSAGE(rotated_transform.is_equal_approx(expected), "The rotated transform should have a new orientation and basis.");
}
TEST_CASE("[Transform3D] Rotate in-place (local rotation)") {
// Start with the default orientation.
// Local rotation should not change the origin, only the basis.
Transform3D transform = Transform3D();
transform.origin = Vector3(1, 2, 3);
Transform3D expected = Transform3D();
expected.origin = Vector3(1, 2, 3);
expected.basis[0] = Vector3(-1, 0, 0);
expected.basis[2] = Vector3(0, 0, -1);
const Transform3D rotated_transform = Transform3D(transform.rotated_local(Vector3(0, 1, 0), Math::PI));
CHECK_MESSAGE(rotated_transform.is_equal_approx(expected), "The rotated transform should have a new orientation but still be based on the same origin.");
}
} // namespace TestTransform3D

View File

@@ -0,0 +1,82 @@
/**************************************************************************/
/* test_triangle_mesh.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/triangle_mesh.h"
#include "scene/resources/3d/primitive_meshes.h"
#include "tests/test_macros.h"
namespace TestTriangleMesh {
TEST_CASE("[SceneTree][TriangleMesh] BVH creation and intersection") {
Ref<BoxMesh> box_mesh;
box_mesh.instantiate();
const Vector<Face3> faces = box_mesh->get_faces();
Ref<TriangleMesh> triangle_mesh;
triangle_mesh.instantiate();
CHECK(triangle_mesh->create_from_faces(Variant(faces)));
const Vector3 begin = Vector3(0.0, 2.0, 0.0);
const Vector3 end = Vector3(0.0, -2.0, 0.0);
{
Vector3 point;
Vector3 normal;
int32_t *surf_index = nullptr;
int32_t face_index = -1;
const bool has_result = triangle_mesh->intersect_segment(begin, end, point, normal, surf_index, &face_index);
CHECK(has_result);
CHECK(point.is_equal_approx(Vector3(0.0, 0.5, 0.0)));
CHECK(normal.is_equal_approx(Vector3(0.0, 1.0, 0.0)));
CHECK(surf_index == nullptr);
REQUIRE(face_index != -1);
CHECK(face_index == 8);
}
{
Vector3 dir = begin.direction_to(end);
Vector3 point;
Vector3 normal;
int32_t *surf_index = nullptr;
int32_t face_index = -1;
const bool has_result = triangle_mesh->intersect_ray(begin, dir, point, normal, surf_index, &face_index);
CHECK(has_result);
CHECK(point.is_equal_approx(Vector3(0.0, 0.5, 0.0)));
CHECK(normal.is_equal_approx(Vector3(0.0, 1.0, 0.0)));
CHECK(surf_index == nullptr);
REQUIRE(face_index != -1);
CHECK(face_index == 8);
}
}
} // namespace TestTriangleMesh

View File

@@ -0,0 +1,499 @@
/**************************************************************************/
/* test_vector2.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/vector2.h"
#include "core/math/vector2i.h"
#include "tests/test_macros.h"
namespace TestVector2 {
TEST_CASE("[Vector2] Constructor methods") {
constexpr Vector2 vector_empty = Vector2();
constexpr Vector2 vector_zero = Vector2(0.0, 0.0);
static_assert(
vector_empty == vector_zero,
"Vector2 Constructor with no inputs should return a zero Vector2.");
}
TEST_CASE("[Vector2] Angle methods") {
constexpr Vector2 vector_x = Vector2(1, 0);
constexpr Vector2 vector_y = Vector2(0, 1);
CHECK_MESSAGE(
vector_x.angle_to(vector_y) == doctest::Approx((real_t)Math::TAU / 4),
"Vector2 angle_to should work as expected.");
CHECK_MESSAGE(
vector_y.angle_to(vector_x) == doctest::Approx((real_t)-Math::TAU / 4),
"Vector2 angle_to should work as expected.");
CHECK_MESSAGE(
vector_x.angle_to_point(vector_y) == doctest::Approx((real_t)Math::TAU * 3 / 8),
"Vector2 angle_to_point should work as expected.");
CHECK_MESSAGE(
vector_y.angle_to_point(vector_x) == doctest::Approx((real_t)-Math::TAU / 8),
"Vector2 angle_to_point should work as expected.");
}
TEST_CASE("[Vector2] Axis methods") {
Vector2 vector = Vector2(1.2, 3.4);
CHECK_MESSAGE(
vector.max_axis_index() == Vector2::Axis::AXIS_Y,
"Vector2 max_axis_index should work as expected.");
CHECK_MESSAGE(
vector.min_axis_index() == Vector2::Axis::AXIS_X,
"Vector2 min_axis_index should work as expected.");
CHECK_MESSAGE(
vector[vector.min_axis_index()] == (real_t)1.2,
"Vector2 array operator should work as expected.");
vector[Vector2::Axis::AXIS_Y] = 3.7;
CHECK_MESSAGE(
vector[Vector2::Axis::AXIS_Y] == (real_t)3.7,
"Vector2 array operator setter should work as expected.");
}
TEST_CASE("[Vector2] Interpolation methods") {
constexpr Vector2 vector1 = Vector2(1, 2);
constexpr Vector2 vector2 = Vector2(4, 5);
CHECK_MESSAGE(
vector1.lerp(vector2, 0.5) == Vector2(2.5, 3.5),
"Vector2 lerp should work as expected.");
CHECK_MESSAGE(
vector1.lerp(vector2, 1.0 / 3.0).is_equal_approx(Vector2(2, 3)),
"Vector2 lerp should work as expected.");
CHECK_MESSAGE(
vector1.normalized().slerp(vector2.normalized(), 0.5).is_equal_approx(Vector2(0.538953602313995361, 0.84233558177947998)),
"Vector2 slerp should work as expected.");
CHECK_MESSAGE(
vector1.normalized().slerp(vector2.normalized(), 1.0 / 3.0).is_equal_approx(Vector2(0.508990883827209473, 0.860771894454956055)),
"Vector2 slerp should work as expected.");
CHECK_MESSAGE(
Vector2(5, 0).slerp(Vector2(0, 5), 0.5).is_equal_approx(Vector2(5, 5) * Math::SQRT12),
"Vector2 slerp with non-normalized values should work as expected.");
CHECK_MESSAGE(
Vector2(1, 1).slerp(Vector2(2, 2), 0.5).is_equal_approx(Vector2(1.5, 1.5)),
"Vector2 slerp with colinear inputs should behave as expected.");
CHECK_MESSAGE(
Vector2().slerp(Vector2(), 0.5) == Vector2(),
"Vector2 slerp with both inputs as zero vectors should return a zero vector.");
CHECK_MESSAGE(
Vector2().slerp(Vector2(1, 1), 0.5) == Vector2(0.5, 0.5),
"Vector2 slerp with one input as zero should behave like a regular lerp.");
CHECK_MESSAGE(
Vector2(1, 1).slerp(Vector2(), 0.5) == Vector2(0.5, 0.5),
"Vector2 slerp with one input as zero should behave like a regular lerp.");
CHECK_MESSAGE(
Vector2(4, 6).slerp(Vector2(8, 10), 0.5).is_equal_approx(Vector2(5.9076470794008017626, 8.07918879020090480697)),
"Vector2 slerp should work as expected.");
CHECK_MESSAGE(
vector1.slerp(vector2, 0.5).length() == doctest::Approx((real_t)4.31959610746631919),
"Vector2 slerp with different length input should return a vector with an interpolated length.");
CHECK_MESSAGE(
vector1.angle_to(vector1.slerp(vector2, 0.5)) * 2 == doctest::Approx(vector1.angle_to(vector2)),
"Vector2 slerp with different length input should return a vector with an interpolated angle.");
CHECK_MESSAGE(
vector1.cubic_interpolate(vector2, Vector2(), Vector2(7, 7), 0.5) == Vector2(2.375, 3.5),
"Vector2 cubic_interpolate should work as expected.");
CHECK_MESSAGE(
vector1.cubic_interpolate(vector2, Vector2(), Vector2(7, 7), 1.0 / 3.0).is_equal_approx(Vector2(1.851851940155029297, 2.962963104248046875)),
"Vector2 cubic_interpolate should work as expected.");
CHECK_MESSAGE(
Vector2(1, 0).move_toward(Vector2(10, 0), 3) == Vector2(4, 0),
"Vector2 move_toward should work as expected.");
}
TEST_CASE("[Vector2] Length methods") {
constexpr Vector2 vector1 = Vector2(10, 10);
constexpr Vector2 vector2 = Vector2(20, 30);
CHECK_MESSAGE(
vector1.length_squared() == 200,
"Vector2 length_squared should work as expected and return exact result.");
CHECK_MESSAGE(
vector1.length() == doctest::Approx(10 * (real_t)Math::SQRT2),
"Vector2 length should work as expected.");
CHECK_MESSAGE(
vector2.length_squared() == 1300,
"Vector2 length_squared should work as expected and return exact result.");
CHECK_MESSAGE(
vector2.length() == doctest::Approx((real_t)36.05551275463989293119),
"Vector2 length should work as expected.");
CHECK_MESSAGE(
vector1.distance_squared_to(vector2) == 500,
"Vector2 distance_squared_to should work as expected and return exact result.");
CHECK_MESSAGE(
vector1.distance_to(vector2) == doctest::Approx((real_t)22.36067977499789696409),
"Vector2 distance_to should work as expected.");
}
TEST_CASE("[Vector2] Limiting methods") {
constexpr Vector2 vector = Vector2(10, 10);
CHECK_MESSAGE(
vector.limit_length().is_equal_approx(Vector2(Math::SQRT12, Math::SQRT12)),
"Vector2 limit_length should work as expected.");
CHECK_MESSAGE(
vector.limit_length(5).is_equal_approx(5 * Vector2(Math::SQRT12, Math::SQRT12)),
"Vector2 limit_length should work as expected.");
CHECK_MESSAGE(
Vector2(-5, 15).clamp(Vector2(), vector).is_equal_approx(Vector2(0, 10)),
"Vector2 clamp should work as expected.");
CHECK_MESSAGE(
vector.clamp(Vector2(0, 15), Vector2(5, 20)).is_equal_approx(Vector2(5, 15)),
"Vector2 clamp should work as expected.");
}
TEST_CASE("[Vector2] Normalization methods") {
CHECK_MESSAGE(
Vector2(1, 0).is_normalized() == true,
"Vector2 is_normalized should return true for a normalized vector.");
CHECK_MESSAGE(
Vector2(1, 1).is_normalized() == false,
"Vector2 is_normalized should return false for a non-normalized vector.");
CHECK_MESSAGE(
Vector2(1, 0).normalized() == Vector2(1, 0),
"Vector2 normalized should return the same vector for a normalized vector.");
CHECK_MESSAGE(
Vector2(1, 1).normalized().is_equal_approx(Vector2(Math::SQRT12, Math::SQRT12)),
"Vector2 normalized should work as expected.");
Vector2 vector = Vector2(3.2, -5.4);
vector.normalize();
CHECK_MESSAGE(
vector == Vector2(3.2, -5.4).normalized(),
"Vector2 normalize should convert same way as Vector2 normalized.");
CHECK_MESSAGE(
vector.is_equal_approx(Vector2(0.509802390301732898898, -0.860291533634174266891)),
"Vector2 normalize should work as expected.");
}
TEST_CASE("[Vector2] Operators") {
constexpr Vector2 decimal1 = Vector2(2.3, 4.9);
constexpr Vector2 decimal2 = Vector2(1.2, 3.4);
constexpr Vector2 power1 = Vector2(0.75, 1.5);
constexpr Vector2 power2 = Vector2(0.5, 0.125);
constexpr Vector2 int1 = Vector2(4, 5);
constexpr Vector2 int2 = Vector2(1, 2);
CHECK_MESSAGE(
(decimal1 + decimal2).is_equal_approx(Vector2(3.5, 8.3)),
"Vector2 addition should behave as expected.");
static_assert(
(power1 + power2) == Vector2(1.25, 1.625),
"Vector2 addition with powers of two should give exact results.");
static_assert(
(int1 + int2) == Vector2(5, 7),
"Vector2 addition with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 - decimal2).is_equal_approx(Vector2(1.1, 1.5)),
"Vector2 subtraction should behave as expected.");
static_assert(
(power1 - power2) == Vector2(0.25, 1.375),
"Vector2 subtraction with powers of two should give exact results.");
static_assert(
(int1 - int2) == Vector2(3, 3),
"Vector2 subtraction with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 * decimal2).is_equal_approx(Vector2(2.76, 16.66)),
"Vector2 multiplication should behave as expected.");
static_assert(
(power1 * power2) == Vector2(0.375, 0.1875),
"Vector2 multiplication with powers of two should give exact results.");
static_assert(
(int1 * int2) == Vector2(4, 10),
"Vector2 multiplication with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 / decimal2).is_equal_approx(Vector2(1.91666666666666666, 1.44117647058823529)),
"Vector2 division should behave as expected.");
static_assert(
(power1 / power2) == Vector2(1.5, 12.0),
"Vector2 division with powers of two should give exact results.");
static_assert(
(int1 / int2) == Vector2(4, 2.5),
"Vector2 division with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 * 2).is_equal_approx(Vector2(4.6, 9.8)),
"Vector2 multiplication should behave as expected.");
static_assert(
(power1 * 2) == Vector2(1.5, 3),
"Vector2 multiplication with powers of two should give exact results.");
static_assert(
(int1 * 2) == Vector2(8, 10),
"Vector2 multiplication with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 / 2).is_equal_approx(Vector2(1.15, 2.45)),
"Vector2 division should behave as expected.");
static_assert(
(power1 / 2) == Vector2(0.375, 0.75),
"Vector2 division with powers of two should give exact results.");
static_assert(
(int1 / 2) == Vector2(2, 2.5),
"Vector2 division with integers should give exact results.");
CHECK_MESSAGE(
((Vector2i)decimal1) == Vector2i(2, 4),
"Vector2 cast to Vector2i should work as expected.");
CHECK_MESSAGE(
((Vector2i)decimal2) == Vector2i(1, 3),
"Vector2 cast to Vector2i should work as expected.");
CHECK_MESSAGE(
Vector2(Vector2i(1, 2)) == Vector2(1, 2),
"Vector2 constructed from Vector2i should work as expected.");
CHECK_MESSAGE(
((String)decimal1) == "(2.3, 4.9)",
"Vector2 cast to String should work as expected.");
CHECK_MESSAGE(
((String)decimal2) == "(1.2, 3.4)",
"Vector2 cast to String should work as expected.");
CHECK_MESSAGE(
((String)Vector2(9.8, 9.9)) == "(9.8, 9.9)",
"Vector2 cast to String should work as expected.");
#ifdef REAL_T_IS_DOUBLE
CHECK_MESSAGE(
((String)Vector2(Math::PI, Math::TAU)) == "(3.14159265358979, 6.28318530717959)",
"Vector2 cast to String should print the correct amount of digits for real_t = double.");
#else
CHECK_MESSAGE(
((String)Vector2(Math::PI, Math::TAU)) == "(3.141593, 6.283185)",
"Vector2 cast to String should print the correct amount of digits for real_t = float.");
#endif // REAL_T_IS_DOUBLE
}
TEST_CASE("[Vector2] Other methods") {
constexpr Vector2 vector = Vector2(1.2, 3.4);
CHECK_MESSAGE(
vector.aspect() == doctest::Approx((real_t)1.2 / (real_t)3.4),
"Vector2 aspect should work as expected.");
CHECK_MESSAGE(
vector.direction_to(Vector2()).is_equal_approx(-vector.normalized()),
"Vector2 direction_to should work as expected.");
CHECK_MESSAGE(
Vector2(1, 1).direction_to(Vector2(2, 2)).is_equal_approx(Vector2(Math::SQRT12, Math::SQRT12)),
"Vector2 direction_to should work as expected.");
CHECK_MESSAGE(
vector.posmod(2).is_equal_approx(Vector2(1.2, 1.4)),
"Vector2 posmod should work as expected.");
CHECK_MESSAGE(
(-vector).posmod(2).is_equal_approx(Vector2(0.8, 0.6)),
"Vector2 posmod should work as expected.");
CHECK_MESSAGE(
vector.posmodv(Vector2(1, 2)).is_equal_approx(Vector2(0.2, 1.4)),
"Vector2 posmodv should work as expected.");
CHECK_MESSAGE(
(-vector).posmodv(Vector2(2, 3)).is_equal_approx(Vector2(0.8, 2.6)),
"Vector2 posmodv should work as expected.");
CHECK_MESSAGE(
vector.rotated(Math::TAU).is_equal_approx(Vector2(1.2, 3.4)),
"Vector2 rotated should work as expected.");
CHECK_MESSAGE(
vector.rotated(Math::TAU / 4).is_equal_approx(Vector2(-3.4, 1.2)),
"Vector2 rotated should work as expected.");
CHECK_MESSAGE(
vector.rotated(Math::TAU / 3).is_equal_approx(Vector2(-3.544486372867091398996, -0.660769515458673623883)),
"Vector2 rotated should work as expected.");
CHECK_MESSAGE(
vector.rotated(Math::TAU / 2).is_equal_approx(vector.rotated(Math::TAU / -2)),
"Vector2 rotated should work as expected.");
CHECK_MESSAGE(
vector.snapped(Vector2(1, 1)) == Vector2(1, 3),
"Vector2 snapped to integers should be the same as rounding.");
CHECK_MESSAGE(
Vector2(3.4, 5.6).snapped(Vector2(1, 1)) == Vector2(3, 6),
"Vector2 snapped to integers should be the same as rounding.");
CHECK_MESSAGE(
vector.snapped(Vector2(0.25, 0.25)) == Vector2(1.25, 3.5),
"Vector2 snapped to 0.25 should give exact results.");
CHECK_MESSAGE(
Vector2(1.2, 2.5).is_equal_approx(vector.min(Vector2(3.0, 2.5))),
"Vector2 min should return expected value.");
CHECK_MESSAGE(
Vector2(5.3, 3.4).is_equal_approx(vector.max(Vector2(5.3, 2.0))),
"Vector2 max should return expected value.");
}
TEST_CASE("[Vector2] Plane methods") {
constexpr Vector2 vector = Vector2(1.2, 3.4);
constexpr Vector2 vector_y = Vector2(0, 1);
constexpr Vector2 vector_normal = Vector2(0.95879811270838721622267, 0.2840883296913739899919);
constexpr real_t p_d = 99.1;
CHECK_MESSAGE(
vector.bounce(vector_y) == Vector2(1.2, -3.4),
"Vector2 bounce on a plane with normal of the Y axis should.");
CHECK_MESSAGE(
vector.bounce(vector_normal).is_equal_approx(Vector2(-2.85851197982345523329, 2.197477931904161412358)),
"Vector2 bounce with normal should return expected value.");
CHECK_MESSAGE(
vector.reflect(vector_y) == Vector2(-1.2, 3.4),
"Vector2 reflect on a plane with normal of the Y axis should.");
CHECK_MESSAGE(
vector.reflect(vector_normal).is_equal_approx(Vector2(2.85851197982345523329, -2.197477931904161412358)),
"Vector2 reflect with normal should return expected value.");
CHECK_MESSAGE(
vector.project(vector_y) == Vector2(0, 3.4),
"Vector2 projected on the Y axis should only give the Y component.");
CHECK_MESSAGE(
vector.project(vector_normal).is_equal_approx(Vector2(2.0292559899117276166, 0.60126103404791929382)),
"Vector2 projected on a normal should return expected value.");
CHECK_MESSAGE(
vector_normal.plane_project(p_d, vector).is_equal_approx(Vector2(94.187635516479631, 30.951892004882851)),
"Vector2 plane_project should return expected value.");
CHECK_MESSAGE(
vector.slide(vector_y) == Vector2(1.2, 0),
"Vector2 slide on a plane with normal of the Y axis should set the Y to zero.");
CHECK_MESSAGE(
vector.slide(vector_normal).is_equal_approx(Vector2(-0.8292559899117276166456, 2.798738965952080706179)),
"Vector2 slide with normal should return expected value.");
// There's probably a better way to test these ones?
#ifdef MATH_CHECKS
constexpr Vector2 vector_non_normal = Vector2(5.4, 1.6);
ERR_PRINT_OFF;
CHECK_MESSAGE(
vector.bounce(vector_non_normal).is_equal_approx(Vector2()),
"Vector2 bounce should return empty Vector2 with non-normalized input.");
CHECK_MESSAGE(
vector.reflect(vector_non_normal).is_equal_approx(Vector2()),
"Vector2 reflect should return empty Vector2 with non-normalized input.");
CHECK_MESSAGE(
vector.slide(vector_non_normal).is_equal_approx(Vector2()),
"Vector2 slide should return empty Vector2 with non-normalized input.");
ERR_PRINT_ON;
#endif // MATH_CHECKS
}
TEST_CASE("[Vector2] Rounding methods") {
constexpr Vector2 vector1 = Vector2(1.2, 5.6);
constexpr Vector2 vector2 = Vector2(1.2, -5.6);
CHECK_MESSAGE(
vector1.abs() == vector1,
"Vector2 abs should work as expected.");
CHECK_MESSAGE(
vector2.abs() == vector1,
"Vector2 abs should work as expected.");
CHECK_MESSAGE(
vector1.ceil() == Vector2(2, 6),
"Vector2 ceil should work as expected.");
CHECK_MESSAGE(
vector2.ceil() == Vector2(2, -5),
"Vector2 ceil should work as expected.");
CHECK_MESSAGE(
vector1.floor() == Vector2(1, 5),
"Vector2 floor should work as expected.");
CHECK_MESSAGE(
vector2.floor() == Vector2(1, -6),
"Vector2 floor should work as expected.");
CHECK_MESSAGE(
vector1.round() == Vector2(1, 6),
"Vector2 round should work as expected.");
CHECK_MESSAGE(
vector2.round() == Vector2(1, -6),
"Vector2 round should work as expected.");
CHECK_MESSAGE(
vector1.sign() == Vector2(1, 1),
"Vector2 sign should work as expected.");
CHECK_MESSAGE(
vector2.sign() == Vector2(1, -1),
"Vector2 sign should work as expected.");
}
TEST_CASE("[Vector2] Linear algebra methods") {
constexpr Vector2 vector_x = Vector2(1, 0);
constexpr Vector2 vector_y = Vector2(0, 1);
constexpr Vector2 a = Vector2(3.5, 8.5);
constexpr Vector2 b = Vector2(5.2, 4.6);
CHECK_MESSAGE(
vector_x.cross(vector_y) == 1,
"Vector2 cross product of X and Y should give 1.");
CHECK_MESSAGE(
vector_y.cross(vector_x) == -1,
"Vector2 cross product of Y and X should give negative 1.");
CHECK_MESSAGE(
a.cross(b) == doctest::Approx((real_t)-28.1),
"Vector2 cross should return expected value.");
CHECK_MESSAGE(
Vector2(-a.x, a.y).cross(Vector2(b.x, -b.y)) == doctest::Approx((real_t)-28.1),
"Vector2 cross should return expected value.");
CHECK_MESSAGE(
vector_x.dot(vector_y) == 0.0,
"Vector2 dot product of perpendicular vectors should be zero.");
CHECK_MESSAGE(
vector_x.dot(vector_x) == 1.0,
"Vector2 dot product of identical unit vectors should be one.");
CHECK_MESSAGE(
(vector_x * 10).dot(vector_x * 10) == 100.0,
"Vector2 dot product of same direction vectors should behave as expected.");
CHECK_MESSAGE(
a.dot(b) == doctest::Approx((real_t)57.3),
"Vector2 dot should return expected value.");
CHECK_MESSAGE(
Vector2(-a.x, a.y).dot(Vector2(b.x, -b.y)) == doctest::Approx((real_t)-57.3),
"Vector2 dot should return expected value.");
}
TEST_CASE("[Vector2] Finite number checks") {
constexpr double infinite[] = { Math::NaN, Math::INF, -Math::INF };
CHECK_MESSAGE(
Vector2(0, 1).is_finite(),
"Vector2(0, 1) should be finite");
for (double x : infinite) {
CHECK_FALSE_MESSAGE(
Vector2(x, 1).is_finite(),
"Vector2 with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector2(0, x).is_finite(),
"Vector2 with one component infinite should not be finite.");
}
for (double x : infinite) {
for (double y : infinite) {
CHECK_FALSE_MESSAGE(
Vector2(x, y).is_finite(),
"Vector2 with two components infinite should not be finite.");
}
}
}
} // namespace TestVector2

View File

@@ -0,0 +1,168 @@
/**************************************************************************/
/* test_vector2i.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/vector2.h"
#include "core/math/vector2i.h"
#include "tests/test_macros.h"
namespace TestVector2i {
TEST_CASE("[Vector2i] Constructor methods") {
constexpr Vector2i vector_empty = Vector2i();
constexpr Vector2i vector_zero = Vector2i(0, 0);
static_assert(
vector_empty == vector_zero,
"Vector2i Constructor with no inputs should return a zero Vector2i.");
}
TEST_CASE("[Vector2i] Axis methods") {
Vector2i vector = Vector2i(2, 3);
CHECK_MESSAGE(
vector.max_axis_index() == Vector2i::Axis::AXIS_Y,
"Vector2i max_axis_index should work as expected.");
CHECK_MESSAGE(
vector.min_axis_index() == Vector2i::Axis::AXIS_X,
"Vector2i min_axis_index should work as expected.");
CHECK_MESSAGE(
vector[vector.min_axis_index()] == 2,
"Vector2i array operator should work as expected.");
vector[Vector2i::Axis::AXIS_Y] = 5;
CHECK_MESSAGE(
vector[Vector2i::Axis::AXIS_Y] == 5,
"Vector2i array operator setter should work as expected.");
}
TEST_CASE("[Vector2i] Clamp method") {
constexpr Vector2i vector = Vector2i(10, 10);
CHECK_MESSAGE(
Vector2i(-5, 15).clamp(Vector2i(), vector) == Vector2i(0, 10),
"Vector2i clamp should work as expected.");
CHECK_MESSAGE(
vector.clamp(Vector2i(0, 15), Vector2i(5, 20)) == Vector2i(5, 15),
"Vector2i clamp should work as expected.");
}
TEST_CASE("[Vector2i] Length methods") {
constexpr Vector2i vector1 = Vector2i(10, 10);
constexpr Vector2i vector2 = Vector2i(20, 30);
CHECK_MESSAGE(
vector1.length_squared() == 200,
"Vector2i length_squared should work as expected and return exact result.");
CHECK_MESSAGE(
vector1.length() == doctest::Approx(10 * Math::SQRT2),
"Vector2i length should work as expected.");
CHECK_MESSAGE(
vector2.length_squared() == 1300,
"Vector2i length_squared should work as expected and return exact result.");
CHECK_MESSAGE(
vector2.length() == doctest::Approx(36.05551275463989293119),
"Vector2i length should work as expected.");
CHECK_MESSAGE(
vector1.distance_squared_to(vector2) == 500,
"Vector2i distance_squared_to should work as expected and return exact result.");
CHECK_MESSAGE(
vector1.distance_to(vector2) == doctest::Approx(22.36067977499789696409),
"Vector2i distance_to should work as expected.");
}
TEST_CASE("[Vector2i] Operators") {
constexpr Vector2i vector1 = Vector2i(5, 9);
constexpr Vector2i vector2 = Vector2i(2, 3);
static_assert(
(vector1 + vector2) == Vector2i(7, 12),
"Vector2i addition with integers should give exact results.");
static_assert(
(vector1 - vector2) == Vector2i(3, 6),
"Vector2i subtraction with integers should give exact results.");
static_assert(
(vector1 * vector2) == Vector2i(10, 27),
"Vector2i multiplication with integers should give exact results.");
static_assert(
(vector1 / vector2) == Vector2i(2, 3),
"Vector2i division with integers should give exact results.");
static_assert(
(vector1 * 2) == Vector2i(10, 18),
"Vector2i multiplication with integers should give exact results.");
static_assert(
(vector1 / 2) == Vector2i(2, 4),
"Vector2i division with integers should give exact results.");
CHECK_MESSAGE(
((Vector2)vector1) == Vector2(5, 9),
"Vector2i cast to Vector2 should work as expected.");
CHECK_MESSAGE(
((Vector2)vector2) == Vector2(2, 3),
"Vector2i cast to Vector2 should work as expected.");
CHECK_MESSAGE(
Vector2i(Vector2(1.1, 2.9)) == Vector2i(1, 2),
"Vector2i constructed from Vector2 should work as expected.");
}
TEST_CASE("[Vector2i] Other methods") {
constexpr Vector2i vector = Vector2i(1, 3);
CHECK_MESSAGE(
vector.aspect() == doctest::Approx((real_t)1.0 / (real_t)3.0),
"Vector2i aspect should work as expected.");
CHECK_MESSAGE(
vector.min(Vector2i(3, 2)) == Vector2i(1, 2),
"Vector2i min should return expected value.");
CHECK_MESSAGE(
vector.max(Vector2i(5, 2)) == Vector2i(5, 3),
"Vector2i max should return expected value.");
CHECK_MESSAGE(
vector.snapped(Vector2i(4, 2)) == Vector2i(0, 4),
"Vector2i snapped should work as expected.");
}
TEST_CASE("[Vector2i] Abs and sign methods") {
constexpr Vector2i vector1 = Vector2i(1, 3);
constexpr Vector2i vector2 = Vector2i(1, -3);
CHECK_MESSAGE(
vector1.abs() == vector1,
"Vector2i abs should work as expected.");
CHECK_MESSAGE(
vector2.abs() == vector1,
"Vector2i abs should work as expected.");
CHECK_MESSAGE(
vector1.sign() == Vector2i(1, 1),
"Vector2i sign should work as expected.");
CHECK_MESSAGE(
vector2.sign() == Vector2i(1, -1),
"Vector2i sign should work as expected.");
}
} // namespace TestVector2i

View File

@@ -0,0 +1,533 @@
/**************************************************************************/
/* test_vector3.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/vector3.h"
#include "tests/test_macros.h"
namespace TestVector3 {
TEST_CASE("[Vector3] Constructor methods") {
constexpr Vector3 vector_empty = Vector3();
constexpr Vector3 vector_zero = Vector3(0.0, 0.0, 0.0);
static_assert(
vector_empty == vector_zero,
"Vector3 Constructor with no inputs should return a zero Vector3.");
}
TEST_CASE("[Vector3] Angle methods") {
constexpr Vector3 vector_x = Vector3(1, 0, 0);
constexpr Vector3 vector_y = Vector3(0, 1, 0);
constexpr Vector3 vector_yz = Vector3(0, 1, 1);
CHECK_MESSAGE(
vector_x.angle_to(vector_y) == doctest::Approx((real_t)Math::TAU / 4),
"Vector3 angle_to should work as expected.");
CHECK_MESSAGE(
vector_x.angle_to(vector_yz) == doctest::Approx((real_t)Math::TAU / 4),
"Vector3 angle_to should work as expected.");
CHECK_MESSAGE(
vector_yz.angle_to(vector_x) == doctest::Approx((real_t)Math::TAU / 4),
"Vector3 angle_to should work as expected.");
CHECK_MESSAGE(
vector_y.angle_to(vector_yz) == doctest::Approx((real_t)Math::TAU / 8),
"Vector3 angle_to should work as expected.");
CHECK_MESSAGE(
vector_x.signed_angle_to(vector_y, vector_y) == doctest::Approx((real_t)Math::TAU / 4),
"Vector3 signed_angle_to edge case should be positive.");
CHECK_MESSAGE(
vector_x.signed_angle_to(vector_yz, vector_y) == doctest::Approx((real_t)Math::TAU / -4),
"Vector3 signed_angle_to should work as expected.");
CHECK_MESSAGE(
vector_yz.signed_angle_to(vector_x, vector_y) == doctest::Approx((real_t)Math::TAU / 4),
"Vector3 signed_angle_to should work as expected.");
}
TEST_CASE("[Vector3] Axis methods") {
Vector3 vector = Vector3(1.2, 3.4, 5.6);
CHECK_MESSAGE(
vector.max_axis_index() == Vector3::Axis::AXIS_Z,
"Vector3 max_axis_index should work as expected.");
CHECK_MESSAGE(
vector.min_axis_index() == Vector3::Axis::AXIS_X,
"Vector3 min_axis_index should work as expected.");
CHECK_MESSAGE(
vector[vector.max_axis_index()] == (real_t)5.6,
"Vector3 array operator should work as expected.");
CHECK_MESSAGE(
vector[vector.min_axis_index()] == (real_t)1.2,
"Vector3 array operator should work as expected.");
vector[Vector3::Axis::AXIS_Y] = 3.7;
CHECK_MESSAGE(
vector[Vector3::Axis::AXIS_Y] == (real_t)3.7,
"Vector3 array operator setter should work as expected.");
}
TEST_CASE("[Vector3] Interpolation methods") {
constexpr Vector3 vector1 = Vector3(1, 2, 3);
constexpr Vector3 vector2 = Vector3(4, 5, 6);
CHECK_MESSAGE(
vector1.lerp(vector2, 0.5) == Vector3(2.5, 3.5, 4.5),
"Vector3 lerp should work as expected.");
CHECK_MESSAGE(
vector1.lerp(vector2, 1.0 / 3.0).is_equal_approx(Vector3(2, 3, 4)),
"Vector3 lerp should work as expected.");
CHECK_MESSAGE(
vector1.normalized().slerp(vector2.normalized(), 0.5).is_equal_approx(Vector3(0.363866806030273438, 0.555698215961456299, 0.747529566287994385)),
"Vector3 slerp should work as expected.");
CHECK_MESSAGE(
vector1.normalized().slerp(vector2.normalized(), 1.0 / 3.0).is_equal_approx(Vector3(0.332119762897491455, 0.549413740634918213, 0.766707837581634521)),
"Vector3 slerp should work as expected.");
CHECK_MESSAGE(
Vector3(5, 0, 0).slerp(Vector3(0, 3, 4), 0.5).is_equal_approx(Vector3(3.535533905029296875, 2.121320486068725586, 2.828427314758300781)),
"Vector3 slerp with non-normalized values should work as expected.");
CHECK_MESSAGE(
Vector3(1, 1, 1).slerp(Vector3(2, 2, 2), 0.5).is_equal_approx(Vector3(1.5, 1.5, 1.5)),
"Vector3 slerp with colinear inputs should behave as expected.");
CHECK_MESSAGE(
Vector3().slerp(Vector3(), 0.5) == Vector3(),
"Vector3 slerp with both inputs as zero vectors should return a zero vector.");
CHECK_MESSAGE(
Vector3().slerp(Vector3(1, 1, 1), 0.5) == Vector3(0.5, 0.5, 0.5),
"Vector3 slerp with one input as zero should behave like a regular lerp.");
CHECK_MESSAGE(
Vector3(1, 1, 1).slerp(Vector3(), 0.5) == Vector3(0.5, 0.5, 0.5),
"Vector3 slerp with one input as zero should behave like a regular lerp.");
CHECK_MESSAGE(
Vector3(4, 6, 2).slerp(Vector3(8, 10, 3), 0.5).is_equal_approx(Vector3(5.90194219811429941053, 8.06758688849378394534, 2.558307894718317120038)),
"Vector3 slerp should work as expected.");
CHECK_MESSAGE(
vector1.slerp(vector2, 0.5).length() == doctest::Approx((real_t)6.25831088708303172),
"Vector3 slerp with different length input should return a vector with an interpolated length.");
CHECK_MESSAGE(
vector1.angle_to(vector1.slerp(vector2, 0.5)) * 2 == doctest::Approx(vector1.angle_to(vector2)),
"Vector3 slerp with different length input should return a vector with an interpolated angle.");
CHECK_MESSAGE(
vector1.cubic_interpolate(vector2, Vector3(), Vector3(7, 7, 7), 0.5) == Vector3(2.375, 3.5, 4.625),
"Vector3 cubic_interpolate should work as expected.");
CHECK_MESSAGE(
vector1.cubic_interpolate(vector2, Vector3(), Vector3(7, 7, 7), 1.0 / 3.0).is_equal_approx(Vector3(1.851851940155029297, 2.962963104248046875, 4.074074268341064453)),
"Vector3 cubic_interpolate should work as expected.");
CHECK_MESSAGE(
Vector3(1, 0, 0).move_toward(Vector3(10, 0, 0), 3) == Vector3(4, 0, 0),
"Vector3 move_toward should work as expected.");
}
TEST_CASE("[Vector3] Length methods") {
constexpr Vector3 vector1 = Vector3(10, 10, 10);
constexpr Vector3 vector2 = Vector3(20, 30, 40);
CHECK_MESSAGE(
vector1.length_squared() == 300,
"Vector3 length_squared should work as expected and return exact result.");
CHECK_MESSAGE(
vector1.length() == doctest::Approx(10 * (real_t)Math::SQRT3),
"Vector3 length should work as expected.");
CHECK_MESSAGE(
vector2.length_squared() == 2900,
"Vector3 length_squared should work as expected and return exact result.");
CHECK_MESSAGE(
vector2.length() == doctest::Approx((real_t)53.8516480713450403125),
"Vector3 length should work as expected.");
CHECK_MESSAGE(
vector1.distance_squared_to(vector2) == 1400,
"Vector3 distance_squared_to should work as expected and return exact result.");
CHECK_MESSAGE(
vector1.distance_to(vector2) == doctest::Approx((real_t)37.41657386773941385584),
"Vector3 distance_to should work as expected.");
}
TEST_CASE("[Vector3] Limiting methods") {
constexpr Vector3 vector = Vector3(10, 10, 10);
CHECK_MESSAGE(
vector.limit_length().is_equal_approx(Vector3(Math::SQRT13, Math::SQRT13, Math::SQRT13)),
"Vector3 limit_length should work as expected.");
CHECK_MESSAGE(
vector.limit_length(5).is_equal_approx(5 * Vector3(Math::SQRT13, Math::SQRT13, Math::SQRT13)),
"Vector3 limit_length should work as expected.");
CHECK_MESSAGE(
Vector3(-5, 5, 15).clamp(Vector3(), vector) == Vector3(0, 5, 10),
"Vector3 clamp should work as expected.");
CHECK_MESSAGE(
vector.clamp(Vector3(0, 10, 15), Vector3(5, 10, 20)) == Vector3(5, 10, 15),
"Vector3 clamp should work as expected.");
}
TEST_CASE("[Vector3] Normalization methods") {
CHECK_MESSAGE(
Vector3(1, 0, 0).is_normalized() == true,
"Vector3 is_normalized should return true for a normalized vector.");
CHECK_MESSAGE(
Vector3(1, 1, 1).is_normalized() == false,
"Vector3 is_normalized should return false for a non-normalized vector.");
CHECK_MESSAGE(
Vector3(1, 0, 0).normalized() == Vector3(1, 0, 0),
"Vector3 normalized should return the same vector for a normalized vector.");
CHECK_MESSAGE(
Vector3(1, 1, 0).normalized().is_equal_approx(Vector3(Math::SQRT12, Math::SQRT12, 0)),
"Vector3 normalized should work as expected.");
CHECK_MESSAGE(
Vector3(1, 1, 1).normalized().is_equal_approx(Vector3(Math::SQRT13, Math::SQRT13, Math::SQRT13)),
"Vector3 normalized should work as expected.");
Vector3 vector = Vector3(3.2, -5.4, 6);
vector.normalize();
CHECK_MESSAGE(
vector == Vector3(3.2, -5.4, 6).normalized(),
"Vector3 normalize should convert same way as Vector3 normalized.");
CHECK_MESSAGE(
vector.is_equal_approx(Vector3(0.368522751763902980457, -0.621882143601586279522, 0.6909801595573180883585)),
"Vector3 normalize should work as expected.");
}
TEST_CASE("[Vector3] Operators") {
constexpr Vector3 decimal1 = Vector3(2.3, 4.9, 7.8);
constexpr Vector3 decimal2 = Vector3(1.2, 3.4, 5.6);
constexpr Vector3 power1 = Vector3(0.75, 1.5, 0.625);
constexpr Vector3 power2 = Vector3(0.5, 0.125, 0.25);
constexpr Vector3 int1 = Vector3(4, 5, 9);
constexpr Vector3 int2 = Vector3(1, 2, 3);
CHECK_MESSAGE(
(decimal1 + decimal2).is_equal_approx(Vector3(3.5, 8.3, 13.4)),
"Vector3 addition should behave as expected.");
static_assert(
(power1 + power2) == Vector3(1.25, 1.625, 0.875),
"Vector3 addition with powers of two should give exact results.");
static_assert(
(int1 + int2) == Vector3(5, 7, 12),
"Vector3 addition with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 - decimal2).is_equal_approx(Vector3(1.1, 1.5, 2.2)),
"Vector3 subtraction should behave as expected.");
static_assert(
(power1 - power2) == Vector3(0.25, 1.375, 0.375),
"Vector3 subtraction with powers of two should give exact results.");
static_assert(
(int1 - int2) == Vector3(3, 3, 6),
"Vector3 subtraction with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 * decimal2).is_equal_approx(Vector3(2.76, 16.66, 43.68)),
"Vector3 multiplication should behave as expected.");
static_assert(
(power1 * power2) == Vector3(0.375, 0.1875, 0.15625),
"Vector3 multiplication with powers of two should give exact results.");
static_assert(
(int1 * int2) == Vector3(4, 10, 27),
"Vector3 multiplication with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 / decimal2).is_equal_approx(Vector3(1.91666666666666666, 1.44117647058823529, 1.39285714285714286)),
"Vector3 division should behave as expected.");
static_assert(
(power1 / power2) == Vector3(1.5, 12.0, 2.5),
"Vector3 division with powers of two should give exact results.");
static_assert(
(int1 / int2) == Vector3(4, 2.5, 3),
"Vector3 division with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 * 2).is_equal_approx(Vector3(4.6, 9.8, 15.6)),
"Vector3 multiplication should behave as expected.");
static_assert(
(power1 * 2) == Vector3(1.5, 3, 1.25),
"Vector3 multiplication with powers of two should give exact results.");
static_assert(
(int1 * 2) == Vector3(8, 10, 18),
"Vector3 multiplication with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 / 2).is_equal_approx(Vector3(1.15, 2.45, 3.9)),
"Vector3 division should behave as expected.");
static_assert(
(power1 / 2) == Vector3(0.375, 0.75, 0.3125),
"Vector3 division with powers of two should give exact results.");
static_assert(
(int1 / 2) == Vector3(2, 2.5, 4.5),
"Vector3 division with integers should give exact results.");
CHECK_MESSAGE(
((Vector3i)decimal1) == Vector3i(2, 4, 7),
"Vector3 cast to Vector3i should work as expected.");
CHECK_MESSAGE(
((Vector3i)decimal2) == Vector3i(1, 3, 5),
"Vector3 cast to Vector3i should work as expected.");
CHECK_MESSAGE(
Vector3(Vector3i(1, 2, 3)) == Vector3(1, 2, 3),
"Vector3 constructed from Vector3i should work as expected.");
CHECK_MESSAGE(
((String)decimal1) == "(2.3, 4.9, 7.8)",
"Vector3 cast to String should work as expected.");
CHECK_MESSAGE(
((String)decimal2) == "(1.2, 3.4, 5.6)",
"Vector3 cast to String should work as expected.");
CHECK_MESSAGE(
((String)Vector3(9.7, 9.8, 9.9)) == "(9.7, 9.8, 9.9)",
"Vector3 cast to String should work as expected.");
#ifdef REAL_T_IS_DOUBLE
CHECK_MESSAGE(
((String)Vector3(Math::E, Math::SQRT2, Math::SQRT3)) == "(2.71828182845905, 1.4142135623731, 1.73205080756888)",
"Vector3 cast to String should print the correct amount of digits for real_t = double.");
#else
CHECK_MESSAGE(
((String)Vector3(Math::E, Math::SQRT2, Math::SQRT3)) == "(2.718282, 1.414214, 1.732051)",
"Vector3 cast to String should print the correct amount of digits for real_t = float.");
#endif // REAL_T_IS_DOUBLE
}
TEST_CASE("[Vector3] Other methods") {
constexpr Vector3 vector = Vector3(1.2, 3.4, 5.6);
CHECK_MESSAGE(
vector.direction_to(Vector3()).is_equal_approx(-vector.normalized()),
"Vector3 direction_to should work as expected.");
CHECK_MESSAGE(
Vector3(1, 1, 1).direction_to(Vector3(2, 2, 2)).is_equal_approx(Vector3(Math::SQRT13, Math::SQRT13, Math::SQRT13)),
"Vector3 direction_to should work as expected.");
CHECK_MESSAGE(
vector.inverse().is_equal_approx(Vector3(1 / 1.2, 1 / 3.4, 1 / 5.6)),
"Vector3 inverse should work as expected.");
CHECK_MESSAGE(
vector.posmod(2).is_equal_approx(Vector3(1.2, 1.4, 1.6)),
"Vector3 posmod should work as expected.");
CHECK_MESSAGE(
(-vector).posmod(2).is_equal_approx(Vector3(0.8, 0.6, 0.4)),
"Vector3 posmod should work as expected.");
CHECK_MESSAGE(
vector.posmodv(Vector3(1, 2, 3)).is_equal_approx(Vector3(0.2, 1.4, 2.6)),
"Vector3 posmodv should work as expected.");
CHECK_MESSAGE(
(-vector).posmodv(Vector3(2, 3, 4)).is_equal_approx(Vector3(0.8, 2.6, 2.4)),
"Vector3 posmodv should work as expected.");
CHECK_MESSAGE(
vector.rotated(Vector3(0, 1, 0), Math::TAU).is_equal_approx(vector),
"Vector3 rotated should work as expected.");
CHECK_MESSAGE(
vector.rotated(Vector3(0, 1, 0), Math::TAU / 4).is_equal_approx(Vector3(5.6, 3.4, -1.2)),
"Vector3 rotated should work as expected.");
CHECK_MESSAGE(
vector.rotated(Vector3(1, 0, 0), Math::TAU / 3).is_equal_approx(Vector3(1.2, -6.54974226119285642, 0.1444863728670914)),
"Vector3 rotated should work as expected.");
CHECK_MESSAGE(
vector.rotated(Vector3(0, 0, 1), Math::TAU / 2).is_equal_approx(vector.rotated(Vector3(0, 0, 1), Math::TAU / -2)),
"Vector3 rotated should work as expected.");
CHECK_MESSAGE(
vector.snapped(Vector3(1, 1, 1)) == Vector3(1, 3, 6),
"Vector3 snapped to integers should be the same as rounding.");
CHECK_MESSAGE(
vector.snapped(Vector3(0.25, 0.25, 0.25)) == Vector3(1.25, 3.5, 5.5),
"Vector3 snapped to 0.25 should give exact results.");
CHECK_MESSAGE(
Vector3(1.2, 2.5, 2.0).is_equal_approx(vector.min(Vector3(3.0, 2.5, 2.0))),
"Vector3 min should return expected value.");
CHECK_MESSAGE(
Vector3(5.3, 3.4, 5.6).is_equal_approx(vector.max(Vector3(5.3, 2.0, 3.0))),
"Vector3 max should return expected value.");
}
TEST_CASE("[Vector3] Plane methods") {
constexpr Vector3 vector = Vector3(1.2, 3.4, 5.6);
constexpr Vector3 vector_y = Vector3(0, 1, 0);
constexpr Vector3 vector_normal = Vector3(0.88763458893247992491, 0.26300284116517923701, 0.37806658417494515320);
CHECK_MESSAGE(
vector.bounce(vector_y) == Vector3(1.2, -3.4, 5.6),
"Vector3 bounce on a plane with normal of the Y axis should.");
CHECK_MESSAGE(
vector.bounce(vector_normal).is_equal_approx(Vector3(-6.0369629829775736287, 1.25571467171034855444, 2.517589840583626047)),
"Vector3 bounce with normal should return expected value.");
CHECK_MESSAGE(
vector.reflect(vector_y) == Vector3(-1.2, 3.4, -5.6),
"Vector3 reflect on a plane with normal of the Y axis should.");
CHECK_MESSAGE(
vector.reflect(vector_normal).is_equal_approx(Vector3(6.0369629829775736287, -1.25571467171034855444, -2.517589840583626047)),
"Vector3 reflect with normal should return expected value.");
CHECK_MESSAGE(
vector.project(vector_y) == Vector3(0, 3.4, 0),
"Vector3 projected on the Y axis should only give the Y component.");
CHECK_MESSAGE(
vector.project(vector_normal).is_equal_approx(Vector3(3.61848149148878681437, 1.0721426641448257227776, 1.54120507970818697649)),
"Vector3 projected on a normal should return expected value.");
CHECK_MESSAGE(
vector.slide(vector_y) == Vector3(1.2, 0, 5.6),
"Vector3 slide on a plane with normal of the Y axis should set the Y to zero.");
CHECK_MESSAGE(
vector.slide(vector_normal).is_equal_approx(Vector3(-2.41848149148878681437, 2.32785733585517427722237, 4.0587949202918130235)),
"Vector3 slide with normal should return expected value.");
// There's probably a better way to test these ones?
#ifdef MATH_CHECKS
constexpr Vector3 vector_non_normal = Vector3(5.4, 1.6, 2.3);
ERR_PRINT_OFF;
CHECK_MESSAGE(
vector.bounce(vector_non_normal).is_equal_approx(Vector3()),
"Vector3 bounce should return empty Vector3 with non-normalized input.");
CHECK_MESSAGE(
vector.reflect(vector_non_normal).is_equal_approx(Vector3()),
"Vector3 reflect should return empty Vector3 with non-normalized input.");
CHECK_MESSAGE(
vector.slide(vector_non_normal).is_equal_approx(Vector3()),
"Vector3 slide should return empty Vector3 with non-normalized input.");
ERR_PRINT_ON;
#endif // MATH_CHECKS
}
TEST_CASE("[Vector3] Rounding methods") {
constexpr Vector3 vector1 = Vector3(1.2, 3.4, 5.6);
constexpr Vector3 vector2 = Vector3(1.2, -3.4, -5.6);
CHECK_MESSAGE(
vector1.abs() == vector1,
"Vector3 abs should work as expected.");
CHECK_MESSAGE(
vector2.abs() == vector1,
"Vector3 abs should work as expected.");
CHECK_MESSAGE(
vector1.ceil() == Vector3(2, 4, 6),
"Vector3 ceil should work as expected.");
CHECK_MESSAGE(
vector2.ceil() == Vector3(2, -3, -5),
"Vector3 ceil should work as expected.");
CHECK_MESSAGE(
vector1.floor() == Vector3(1, 3, 5),
"Vector3 floor should work as expected.");
CHECK_MESSAGE(
vector2.floor() == Vector3(1, -4, -6),
"Vector3 floor should work as expected.");
CHECK_MESSAGE(
vector1.round() == Vector3(1, 3, 6),
"Vector3 round should work as expected.");
CHECK_MESSAGE(
vector2.round() == Vector3(1, -3, -6),
"Vector3 round should work as expected.");
CHECK_MESSAGE(
vector1.sign() == Vector3(1, 1, 1),
"Vector3 sign should work as expected.");
CHECK_MESSAGE(
vector2.sign() == Vector3(1, -1, -1),
"Vector3 sign should work as expected.");
}
TEST_CASE("[Vector3] Linear algebra methods") {
constexpr Vector3 vector_x = Vector3(1, 0, 0);
constexpr Vector3 vector_y = Vector3(0, 1, 0);
constexpr Vector3 vector_z = Vector3(0, 0, 1);
constexpr Vector3 a = Vector3(3.5, 8.5, 2.3);
constexpr Vector3 b = Vector3(5.2, 4.6, 7.8);
CHECK_MESSAGE(
vector_x.cross(vector_y) == vector_z,
"Vector3 cross product of X and Y should give Z.");
CHECK_MESSAGE(
vector_y.cross(vector_x) == -vector_z,
"Vector3 cross product of Y and X should give negative Z.");
CHECK_MESSAGE(
vector_y.cross(vector_z) == vector_x,
"Vector3 cross product of Y and Z should give X.");
CHECK_MESSAGE(
vector_z.cross(vector_x) == vector_y,
"Vector3 cross product of Z and X should give Y.");
CHECK_MESSAGE(
a.cross(b).is_equal_approx(Vector3(55.72, -15.34, -28.1)),
"Vector3 cross should return expected value.");
CHECK_MESSAGE(
Vector3(-a.x, a.y, -a.z).cross(Vector3(b.x, -b.y, b.z)).is_equal_approx(Vector3(55.72, 15.34, -28.1)),
"Vector2 cross should return expected value.");
CHECK_MESSAGE(
vector_x.dot(vector_y) == 0.0,
"Vector3 dot product of perpendicular vectors should be zero.");
CHECK_MESSAGE(
vector_x.dot(vector_x) == 1.0,
"Vector3 dot product of identical unit vectors should be one.");
CHECK_MESSAGE(
(vector_x * 10).dot(vector_x * 10) == 100.0,
"Vector3 dot product of same direction vectors should behave as expected.");
CHECK_MESSAGE(
a.dot(b) == doctest::Approx((real_t)75.24),
"Vector3 dot should return expected value.");
CHECK_MESSAGE(
Vector3(-a.x, a.y, -a.z).dot(Vector3(b.x, -b.y, b.z)) == doctest::Approx((real_t)-75.24),
"Vector3 dot should return expected value.");
}
TEST_CASE("[Vector3] Finite number checks") {
constexpr double infinite[] = { Math::NaN, Math::INF, -Math::INF };
CHECK_MESSAGE(
Vector3(0, 1, 2).is_finite(),
"Vector3(0, 1, 2) should be finite");
for (double x : infinite) {
CHECK_FALSE_MESSAGE(
Vector3(x, 1, 2).is_finite(),
"Vector3 with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector3(0, x, 2).is_finite(),
"Vector3 with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector3(0, 1, x).is_finite(),
"Vector3 with one component infinite should not be finite.");
}
for (double x : infinite) {
for (double y : infinite) {
CHECK_FALSE_MESSAGE(
Vector3(x, y, 2).is_finite(),
"Vector3 with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector3(x, 1, y).is_finite(),
"Vector3 with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector3(0, x, y).is_finite(),
"Vector3 with two components infinite should not be finite.");
}
}
for (double x : infinite) {
for (double y : infinite) {
for (double z : infinite) {
CHECK_FALSE_MESSAGE(
Vector3(x, y, z).is_finite(),
"Vector3 with three components infinite should not be finite.");
}
}
}
}
} // namespace TestVector3

View File

@@ -0,0 +1,167 @@
/**************************************************************************/
/* test_vector3i.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/vector3i.h"
#include "tests/test_macros.h"
namespace TestVector3i {
TEST_CASE("[Vector3i] Constructor methods") {
constexpr Vector3i vector_empty = Vector3i();
constexpr Vector3i vector_zero = Vector3i(0, 0, 0);
static_assert(
vector_empty == vector_zero,
"Vector3i Constructor with no inputs should return a zero Vector3i.");
}
TEST_CASE("[Vector3i] Axis methods") {
Vector3i vector = Vector3i(1, 2, 3);
CHECK_MESSAGE(
vector.max_axis_index() == Vector3i::Axis::AXIS_Z,
"Vector3i max_axis_index should work as expected.");
CHECK_MESSAGE(
vector.min_axis_index() == Vector3i::Axis::AXIS_X,
"Vector3i min_axis_index should work as expected.");
CHECK_MESSAGE(
vector[vector.max_axis_index()] == 3,
"Vector3i array operator should work as expected.");
CHECK_MESSAGE(
vector[vector.min_axis_index()] == 1,
"Vector3i array operator should work as expected.");
vector[Vector3i::Axis::AXIS_Y] = 5;
CHECK_MESSAGE(
vector[Vector3i::Axis::AXIS_Y] == 5,
"Vector3i array operator setter should work as expected.");
}
TEST_CASE("[Vector3i] Clamp method") {
constexpr Vector3i vector = Vector3i(10, 10, 10);
CHECK_MESSAGE(
Vector3i(-5, 5, 15).clamp(Vector3i(), vector) == Vector3i(0, 5, 10),
"Vector3i clamp should work as expected.");
CHECK_MESSAGE(
vector.clamp(Vector3i(0, 10, 15), Vector3i(5, 10, 20)) == Vector3i(5, 10, 15),
"Vector3i clamp should work as expected.");
}
TEST_CASE("[Vector3i] Length methods") {
constexpr Vector3i vector1 = Vector3i(10, 10, 10);
constexpr Vector3i vector2 = Vector3i(20, 30, 40);
CHECK_MESSAGE(
vector1.length_squared() == 300,
"Vector3i length_squared should work as expected and return exact result.");
CHECK_MESSAGE(
vector1.length() == doctest::Approx(10 * Math::SQRT3),
"Vector3i length should work as expected.");
CHECK_MESSAGE(
vector2.length_squared() == 2900,
"Vector3i length_squared should work as expected and return exact result.");
CHECK_MESSAGE(
vector2.length() == doctest::Approx(53.8516480713450403125),
"Vector3i length should work as expected.");
CHECK_MESSAGE(
vector1.distance_squared_to(vector2) == 1400,
"Vector3i distance_squared_to should work as expected and return exact result.");
CHECK_MESSAGE(
vector1.distance_to(vector2) == doctest::Approx(37.41657386773941385584),
"Vector3i distance_to should work as expected.");
}
TEST_CASE("[Vector3i] Operators") {
constexpr Vector3i vector1 = Vector3i(4, 5, 9);
constexpr Vector3i vector2 = Vector3i(1, 2, 3);
static_assert(
(vector1 + vector2) == Vector3i(5, 7, 12),
"Vector3i addition with integers should give exact results.");
static_assert(
(vector1 - vector2) == Vector3i(3, 3, 6),
"Vector3i subtraction with integers should give exact results.");
static_assert(
(vector1 * vector2) == Vector3i(4, 10, 27),
"Vector3i multiplication with integers should give exact results.");
static_assert(
(vector1 / vector2) == Vector3i(4, 2, 3),
"Vector3i division with integers should give exact results.");
static_assert(
(vector1 * 2) == Vector3i(8, 10, 18),
"Vector3i multiplication with integers should give exact results.");
static_assert(
(vector1 / 2) == Vector3i(2, 2, 4),
"Vector3i division with integers should give exact results.");
CHECK_MESSAGE(
((Vector3)vector1) == Vector3(4, 5, 9),
"Vector3i cast to Vector3 should work as expected.");
CHECK_MESSAGE(
((Vector3)vector2) == Vector3(1, 2, 3),
"Vector3i cast to Vector3 should work as expected.");
CHECK_MESSAGE(
Vector3i(Vector3(1.1, 2.9, 3.9)) == Vector3i(1, 2, 3),
"Vector3i constructed from Vector3 should work as expected.");
}
TEST_CASE("[Vector3i] Other methods") {
constexpr Vector3i vector = Vector3i(1, 3, -7);
CHECK_MESSAGE(
vector.min(Vector3i(3, 2, 5)) == Vector3i(1, 2, -7),
"Vector3i min should return expected value.");
CHECK_MESSAGE(
vector.max(Vector3i(5, 2, 4)) == Vector3i(5, 3, 4),
"Vector3i max should return expected value.");
CHECK_MESSAGE(
vector.snapped(Vector3i(4, 2, 5)) == Vector3i(0, 4, -5),
"Vector3i snapped should work as expected.");
}
TEST_CASE("[Vector3i] Abs and sign methods") {
constexpr Vector3i vector1 = Vector3i(1, 3, 5);
constexpr Vector3i vector2 = Vector3i(1, -3, -5);
CHECK_MESSAGE(
vector1.abs() == vector1,
"Vector3i abs should work as expected.");
CHECK_MESSAGE(
vector2.abs() == vector1,
"Vector3i abs should work as expected.");
CHECK_MESSAGE(
vector1.sign() == Vector3i(1, 1, 1),
"Vector3i sign should work as expected.");
CHECK_MESSAGE(
vector2.sign() == Vector3i(1, -1, -1),
"Vector3i sign should work as expected.");
}
} // namespace TestVector3i

View File

@@ -0,0 +1,400 @@
/**************************************************************************/
/* test_vector4.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/vector4.h"
#include "tests/test_macros.h"
namespace TestVector4 {
TEST_CASE("[Vector4] Constructor methods") {
constexpr Vector4 vector_empty = Vector4();
constexpr Vector4 vector_zero = Vector4(0.0, 0.0, 0.0, 0.0);
static_assert(
vector_empty == vector_zero,
"Vector4 Constructor with no inputs should return a zero Vector4.");
}
TEST_CASE("[Vector4] Axis methods") {
Vector4 vector = Vector4(1.2, 3.4, 5.6, -0.9);
CHECK_MESSAGE(
vector.max_axis_index() == Vector4::Axis::AXIS_Z,
"Vector4 max_axis_index should work as expected.");
CHECK_MESSAGE(
vector.min_axis_index() == Vector4::Axis::AXIS_W,
"Vector4 min_axis_index should work as expected.");
CHECK_MESSAGE(
vector[vector.max_axis_index()] == (real_t)5.6,
"Vector4 array operator should work as expected.");
CHECK_MESSAGE(
vector[vector.min_axis_index()] == (real_t)-0.9,
"Vector4 array operator should work as expected.");
vector[Vector4::Axis::AXIS_Y] = 3.7;
CHECK_MESSAGE(
vector[Vector4::Axis::AXIS_Y] == (real_t)3.7,
"Vector4 array operator setter should work as expected.");
}
TEST_CASE("[Vector4] Interpolation methods") {
constexpr Vector4 vector1 = Vector4(1, 2, 3, 4);
constexpr Vector4 vector2 = Vector4(4, 5, 6, 7);
CHECK_MESSAGE(
vector1.lerp(vector2, 0.5) == Vector4(2.5, 3.5, 4.5, 5.5),
"Vector4 lerp should work as expected.");
CHECK_MESSAGE(
vector1.lerp(vector2, 1.0 / 3.0).is_equal_approx(Vector4(2, 3, 4, 5)),
"Vector4 lerp should work as expected.");
CHECK_MESSAGE(
vector1.cubic_interpolate(vector2, Vector4(), Vector4(7, 7, 7, 7), 0.5) == Vector4(2.375, 3.5, 4.625, 5.75),
"Vector4 cubic_interpolate should work as expected.");
CHECK_MESSAGE(
vector1.cubic_interpolate(vector2, Vector4(), Vector4(7, 7, 7, 7), 1.0 / 3.0).is_equal_approx(Vector4(1.851851940155029297, 2.962963104248046875, 4.074074268341064453, 5.185185185185)),
"Vector4 cubic_interpolate should work as expected.");
}
TEST_CASE("[Vector4] Length methods") {
constexpr Vector4 vector1 = Vector4(10, 10, 10, 10);
constexpr Vector4 vector2 = Vector4(20, 30, 40, 50);
CHECK_MESSAGE(
vector1.length_squared() == 400,
"Vector4 length_squared should work as expected and return exact result.");
CHECK_MESSAGE(
vector1.length() == doctest::Approx(20),
"Vector4 length should work as expected.");
CHECK_MESSAGE(
vector2.length_squared() == 5400,
"Vector4 length_squared should work as expected and return exact result.");
CHECK_MESSAGE(
vector2.length() == doctest::Approx((real_t)73.484692283495),
"Vector4 length should work as expected.");
CHECK_MESSAGE(
vector1.distance_to(vector2) == doctest::Approx((real_t)54.772255750517),
"Vector4 distance_to should work as expected.");
CHECK_MESSAGE(
vector1.distance_squared_to(vector2) == doctest::Approx(3000),
"Vector4 distance_squared_to should work as expected.");
}
TEST_CASE("[Vector4] Limiting methods") {
constexpr Vector4 vector = Vector4(10, 10, 10, 10);
CHECK_MESSAGE(
Vector4(-5, 5, 15, -15).clamp(Vector4(), vector) == Vector4(0, 5, 10, 0),
"Vector4 clamp should work as expected.");
CHECK_MESSAGE(
vector.clamp(Vector4(0, 10, 15, 18), Vector4(5, 10, 20, 25)) == Vector4(5, 10, 15, 18),
"Vector4 clamp should work as expected.");
}
TEST_CASE("[Vector4] Normalization methods") {
CHECK_MESSAGE(
Vector4(1, 0, 0, 0).is_normalized() == true,
"Vector4 is_normalized should return true for a normalized vector.");
CHECK_MESSAGE(
Vector4(1, 1, 1, 1).is_normalized() == false,
"Vector4 is_normalized should return false for a non-normalized vector.");
CHECK_MESSAGE(
Vector4(1, 0, 0, 0).normalized() == Vector4(1, 0, 0, 0),
"Vector4 normalized should return the same vector for a normalized vector.");
CHECK_MESSAGE(
Vector4(1, 1, 0, 0).normalized().is_equal_approx(Vector4(Math::SQRT12, Math::SQRT12, 0, 0)),
"Vector4 normalized should work as expected.");
CHECK_MESSAGE(
Vector4(1, 1, 1, 1).normalized().is_equal_approx(Vector4(0.5, 0.5, 0.5, 0.5)),
"Vector4 normalized should work as expected.");
}
TEST_CASE("[Vector4] Operators") {
constexpr Vector4 decimal1 = Vector4(2.3, 4.9, 7.8, 3.2);
constexpr Vector4 decimal2 = Vector4(1.2, 3.4, 5.6, 1.7);
constexpr Vector4 power1 = Vector4(0.75, 1.5, 0.625, 0.125);
constexpr Vector4 power2 = Vector4(0.5, 0.125, 0.25, 0.75);
constexpr Vector4 int1 = Vector4(4, 5, 9, 2);
constexpr Vector4 int2 = Vector4(1, 2, 3, 1);
static_assert(
-decimal1 == Vector4(-2.3, -4.9, -7.8, -3.2),
"Vector4 change of sign should work as expected.");
CHECK_MESSAGE(
(decimal1 + decimal2).is_equal_approx(Vector4(3.5, 8.3, 13.4, 4.9)),
"Vector4 addition should behave as expected.");
static_assert(
(power1 + power2) == Vector4(1.25, 1.625, 0.875, 0.875),
"Vector4 addition with powers of two should give exact results.");
static_assert(
(int1 + int2) == Vector4(5, 7, 12, 3),
"Vector4 addition with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 - decimal2).is_equal_approx(Vector4(1.1, 1.5, 2.2, 1.5)),
"Vector4 subtraction should behave as expected.");
static_assert(
(power1 - power2) == Vector4(0.25, 1.375, 0.375, -0.625),
"Vector4 subtraction with powers of two should give exact results.");
static_assert(
(int1 - int2) == Vector4(3, 3, 6, 1),
"Vector4 subtraction with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 * decimal2).is_equal_approx(Vector4(2.76, 16.66, 43.68, 5.44)),
"Vector4 multiplication should behave as expected.");
static_assert(
(power1 * power2) == Vector4(0.375, 0.1875, 0.15625, 0.09375),
"Vector4 multiplication with powers of two should give exact results.");
static_assert(
(int1 * int2) == Vector4(4, 10, 27, 2),
"Vector4 multiplication with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 / decimal2).is_equal_approx(Vector4(1.91666666666666666, 1.44117647058823529, 1.39285714285714286, 1.88235294118)),
"Vector4 division should behave as expected.");
static_assert(
(power1 / power2) == Vector4(1.5, 12.0, 2.5, 1.0 / 6.0),
"Vector4 division with powers of two should give exact results.");
static_assert(
(int1 / int2) == Vector4(4, 2.5, 3, 2),
"Vector4 division with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 * 2).is_equal_approx(Vector4(4.6, 9.8, 15.6, 6.4)),
"Vector4 multiplication should behave as expected.");
static_assert(
(power1 * 2) == Vector4(1.5, 3, 1.25, 0.25),
"Vector4 multiplication with powers of two should give exact results.");
static_assert(
(int1 * 2) == Vector4(8, 10, 18, 4),
"Vector4 multiplication with integers should give exact results.");
CHECK_MESSAGE(
(decimal1 / 2).is_equal_approx(Vector4(1.15, 2.45, 3.9, 1.6)),
"Vector4 division should behave as expected.");
static_assert(
(power1 / 2) == Vector4(0.375, 0.75, 0.3125, 0.0625),
"Vector4 division with powers of two should give exact results.");
static_assert(
(int1 / 2) == Vector4(2, 2.5, 4.5, 1),
"Vector4 division with integers should give exact results.");
CHECK_MESSAGE(
((String)decimal1) == "(2.3, 4.9, 7.8, 3.2)",
"Vector4 cast to String should work as expected.");
CHECK_MESSAGE(
((String)decimal2) == "(1.2, 3.4, 5.6, 1.7)",
"Vector4 cast to String should work as expected.");
CHECK_MESSAGE(
((String)Vector4(9.7, 9.8, 9.9, -1.8)) == "(9.7, 9.8, 9.9, -1.8)",
"Vector4 cast to String should work as expected.");
#ifdef REAL_T_IS_DOUBLE
CHECK_MESSAGE(
((String)Vector4(Math::E, Math::SQRT2, Math::SQRT3, Math::SQRT3)) == "(2.71828182845905, 1.4142135623731, 1.73205080756888, 1.73205080756888)",
"Vector4 cast to String should print the correct amount of digits for real_t = double.");
#else
CHECK_MESSAGE(
((String)Vector4(Math::E, Math::SQRT2, Math::SQRT3, Math::SQRT3)) == "(2.718282, 1.414214, 1.732051, 1.732051)",
"Vector4 cast to String should print the correct amount of digits for real_t = float.");
#endif // REAL_T_IS_DOUBLE
}
TEST_CASE("[Vector4] Other methods") {
constexpr Vector4 vector = Vector4(1.2, 3.4, 5.6, 1.6);
CHECK_MESSAGE(
vector.direction_to(Vector4()).is_equal_approx(-vector.normalized()),
"Vector4 direction_to should work as expected.");
CHECK_MESSAGE(
Vector4(1, 1, 1, 1).direction_to(Vector4(2, 2, 2, 2)).is_equal_approx(Vector4(0.5, 0.5, 0.5, 0.5)),
"Vector4 direction_to should work as expected.");
CHECK_MESSAGE(
vector.inverse().is_equal_approx(Vector4(1 / 1.2, 1 / 3.4, 1 / 5.6, 1 / 1.6)),
"Vector4 inverse should work as expected.");
CHECK_MESSAGE(
vector.posmod(2).is_equal_approx(Vector4(1.2, 1.4, 1.6, 1.6)),
"Vector4 posmod should work as expected.");
CHECK_MESSAGE(
(-vector).posmod(2).is_equal_approx(Vector4(0.8, 0.6, 0.4, 0.4)),
"Vector4 posmod should work as expected.");
CHECK_MESSAGE(
vector.posmodv(Vector4(1, 2, 3, 4)).is_equal_approx(Vector4(0.2, 1.4, 2.6, 1.6)),
"Vector4 posmodv should work as expected.");
CHECK_MESSAGE(
(-vector).posmodv(Vector4(2, 3, 4, 5)).is_equal_approx(Vector4(0.8, 2.6, 2.4, 3.4)),
"Vector4 posmodv should work as expected.");
CHECK_MESSAGE(
vector.snapped(Vector4(1, 1, 1, 1)) == Vector4(1, 3, 6, 2),
"Vector4 snapped to integers should be the same as rounding.");
CHECK_MESSAGE(
vector.snapped(Vector4(0.25, 0.25, 0.25, 0.25)) == Vector4(1.25, 3.5, 5.5, 1.5),
"Vector4 snapped to 0.25 should give exact results.");
CHECK_MESSAGE(
Vector4(1.2, 2.5, 2.0, 1.6).is_equal_approx(vector.min(Vector4(3.0, 2.5, 2.0, 3.4))),
"Vector4 min should return expected value.");
CHECK_MESSAGE(
Vector4(5.3, 3.4, 5.6, 4.2).is_equal_approx(vector.max(Vector4(5.3, 2.0, 3.0, 4.2))),
"Vector4 max should return expected value.");
}
TEST_CASE("[Vector4] Rounding methods") {
constexpr Vector4 vector1 = Vector4(1.2, 3.4, 5.6, 1.6);
constexpr Vector4 vector2 = Vector4(1.2, -3.4, -5.6, -1.6);
CHECK_MESSAGE(
vector1.abs() == vector1,
"Vector4 abs should work as expected.");
CHECK_MESSAGE(
vector2.abs() == vector1,
"Vector4 abs should work as expected.");
CHECK_MESSAGE(
vector1.ceil() == Vector4(2, 4, 6, 2),
"Vector4 ceil should work as expected.");
CHECK_MESSAGE(
vector2.ceil() == Vector4(2, -3, -5, -1),
"Vector4 ceil should work as expected.");
CHECK_MESSAGE(
vector1.floor() == Vector4(1, 3, 5, 1),
"Vector4 floor should work as expected.");
CHECK_MESSAGE(
vector2.floor() == Vector4(1, -4, -6, -2),
"Vector4 floor should work as expected.");
CHECK_MESSAGE(
vector1.round() == Vector4(1, 3, 6, 2),
"Vector4 round should work as expected.");
CHECK_MESSAGE(
vector2.round() == Vector4(1, -3, -6, -2),
"Vector4 round should work as expected.");
CHECK_MESSAGE(
vector1.sign() == Vector4(1, 1, 1, 1),
"Vector4 sign should work as expected.");
CHECK_MESSAGE(
vector2.sign() == Vector4(1, -1, -1, -1),
"Vector4 sign should work as expected.");
}
TEST_CASE("[Vector4] Linear algebra methods") {
constexpr Vector4 vector_x = Vector4(1, 0, 0, 0);
constexpr Vector4 vector_y = Vector4(0, 1, 0, 0);
constexpr Vector4 vector1 = Vector4(1.7, 2.3, 1, 9.1);
constexpr Vector4 vector2 = Vector4(-8.2, -16, 3, 2.4);
CHECK_MESSAGE(
vector_x.dot(vector_y) == 0.0,
"Vector4 dot product of perpendicular vectors should be zero.");
CHECK_MESSAGE(
vector_x.dot(vector_x) == 1.0,
"Vector4 dot product of identical unit vectors should be one.");
CHECK_MESSAGE(
(vector_x * 10).dot(vector_x * 10) == 100.0,
"Vector4 dot product of same direction vectors should behave as expected.");
CHECK_MESSAGE(
(vector1 * 2).dot(vector2 * 4) == doctest::Approx((real_t)-25.9 * 8),
"Vector4 dot product should work as expected.");
}
TEST_CASE("[Vector4] Finite number checks") {
constexpr double infinite[] = { Math::NaN, Math::INF, -Math::INF };
CHECK_MESSAGE(
Vector4(0, 1, 2, 3).is_finite(),
"Vector4(0, 1, 2, 3) should be finite");
for (double x : infinite) {
CHECK_FALSE_MESSAGE(
Vector4(x, 1, 2, 3).is_finite(),
"Vector4 with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector4(0, x, 2, 3).is_finite(),
"Vector4 with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector4(0, 1, x, 3).is_finite(),
"Vector4 with one component infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector4(0, 1, 2, x).is_finite(),
"Vector4 with one component infinite should not be finite.");
}
for (double x : infinite) {
for (double y : infinite) {
CHECK_FALSE_MESSAGE(
Vector4(x, y, 2, 3).is_finite(),
"Vector4 with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector4(x, 1, y, 3).is_finite(),
"Vector4 with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector4(x, 1, 2, y).is_finite(),
"Vector4 with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector4(0, x, y, 3).is_finite(),
"Vector4 with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector4(0, x, 2, y).is_finite(),
"Vector4 with two components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector4(0, 1, x, y).is_finite(),
"Vector4 with two components infinite should not be finite.");
}
}
for (double x : infinite) {
for (double y : infinite) {
for (double z : infinite) {
CHECK_FALSE_MESSAGE(
Vector4(0, x, y, z).is_finite(),
"Vector4 with three components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector4(x, 1, y, z).is_finite(),
"Vector4 with three components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector4(x, y, 2, z).is_finite(),
"Vector4 with three components infinite should not be finite.");
CHECK_FALSE_MESSAGE(
Vector4(x, y, z, 3).is_finite(),
"Vector4 with three components infinite should not be finite.");
}
}
}
for (double x : infinite) {
for (double y : infinite) {
for (double z : infinite) {
for (double w : infinite) {
CHECK_FALSE_MESSAGE(
Vector4(x, y, z, w).is_finite(),
"Vector4 with four components infinite should not be finite.");
}
}
}
}
}
} // namespace TestVector4

View File

@@ -0,0 +1,171 @@
/**************************************************************************/
/* test_vector4i.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/math/vector4i.h"
#include "tests/test_macros.h"
namespace TestVector4i {
TEST_CASE("[Vector4i] Constructor methods") {
constexpr Vector4i vector_empty = Vector4i();
constexpr Vector4i vector_zero = Vector4i(0, 0, 0, 0);
static_assert(
vector_empty == vector_zero,
"Vector4i Constructor with no inputs should return a zero Vector4i.");
}
TEST_CASE("[Vector4i] Axis methods") {
Vector4i vector = Vector4i(1, 2, 3, 4);
CHECK_MESSAGE(
vector.max_axis_index() == Vector4i::Axis::AXIS_W,
"Vector4i max_axis_index should work as expected.");
CHECK_MESSAGE(
vector.min_axis_index() == Vector4i::Axis::AXIS_X,
"Vector4i min_axis_index should work as expected.");
CHECK_MESSAGE(
vector[vector.max_axis_index()] == 4,
"Vector4i array operator should work as expected.");
CHECK_MESSAGE(
vector[vector.min_axis_index()] == 1,
"Vector4i array operator should work as expected.");
vector[Vector4i::Axis::AXIS_Y] = 5;
CHECK_MESSAGE(
vector[Vector4i::Axis::AXIS_Y] == 5,
"Vector4i array operator setter should work as expected.");
}
TEST_CASE("[Vector4i] Clamp method") {
constexpr Vector4i vector = Vector4i(10, 10, 10, 10);
CHECK_MESSAGE(
Vector4i(-5, 5, 15, INT_MAX).clamp(Vector4i(), vector) == Vector4i(0, 5, 10, 10),
"Vector4i clamp should work as expected.");
CHECK_MESSAGE(
vector.clamp(Vector4i(0, 10, 15, -10), Vector4i(5, 10, 20, -5)) == Vector4i(5, 10, 15, -5),
"Vector4i clamp should work as expected.");
}
TEST_CASE("[Vector4i] Length methods") {
constexpr Vector4i vector1 = Vector4i(10, 10, 10, 10);
constexpr Vector4i vector2 = Vector4i(20, 30, 40, 50);
CHECK_MESSAGE(
vector1.length_squared() == 400,
"Vector4i length_squared should work as expected and return exact result.");
CHECK_MESSAGE(
vector1.length() == doctest::Approx(20),
"Vector4i length should work as expected.");
CHECK_MESSAGE(
vector2.length_squared() == 5400,
"Vector4i length_squared should work as expected and return exact result.");
CHECK_MESSAGE(
vector2.length() == doctest::Approx(73.4846922835),
"Vector4i length should work as expected.");
CHECK_MESSAGE(
vector1.distance_squared_to(vector2) == 3000,
"Vector4i distance_squared_to should work as expected.");
CHECK_MESSAGE(
vector1.distance_to(vector2) == doctest::Approx(54.772255750517),
"Vector4i distance_to should work as expected.");
}
TEST_CASE("[Vector4i] Operators") {
constexpr Vector4i vector1 = Vector4i(4, 5, 9, 2);
constexpr Vector4i vector2 = Vector4i(1, 2, 3, 4);
static_assert(
-vector1 == Vector4i(-4, -5, -9, -2),
"Vector4i change of sign should work as expected.");
static_assert(
(vector1 + vector2) == Vector4i(5, 7, 12, 6),
"Vector4i addition with integers should give exact results.");
static_assert(
(vector1 - vector2) == Vector4i(3, 3, 6, -2),
"Vector4i subtraction with integers should give exact results.");
static_assert(
(vector1 * vector2) == Vector4i(4, 10, 27, 8),
"Vector4i multiplication with integers should give exact results.");
static_assert(
(vector1 / vector2) == Vector4i(4, 2, 3, 0),
"Vector4i division with integers should give exact results.");
static_assert(
(vector1 * 2) == Vector4i(8, 10, 18, 4),
"Vector4i multiplication with integers should give exact results.");
static_assert(
(vector1 / 2) == Vector4i(2, 2, 4, 1),
"Vector4i division with integers should give exact results.");
CHECK_MESSAGE(
((Vector4)vector1) == Vector4(4, 5, 9, 2),
"Vector4i cast to Vector4 should work as expected.");
CHECK_MESSAGE(
((Vector4)vector2) == Vector4(1, 2, 3, 4),
"Vector4i cast to Vector4 should work as expected.");
CHECK_MESSAGE(
Vector4i(Vector4(1.1, 2.9, 3.9, 100.5)) == Vector4i(1, 2, 3, 100),
"Vector4i constructed from Vector4 should work as expected.");
}
TEST_CASE("[Vector3i] Other methods") {
constexpr Vector4i vector = Vector4i(1, 3, -7, 13);
CHECK_MESSAGE(
vector.min(Vector4i(3, 2, 5, 8)) == Vector4i(1, 2, -7, 8),
"Vector4i min should return expected value.");
CHECK_MESSAGE(
vector.max(Vector4i(5, 2, 4, 8)) == Vector4i(5, 3, 4, 13),
"Vector4i max should return expected value.");
CHECK_MESSAGE(
vector.snapped(Vector4i(4, 2, 5, 8)) == Vector4i(0, 4, -5, 16),
"Vector4i snapped should work as expected.");
}
TEST_CASE("[Vector4i] Abs and sign methods") {
constexpr Vector4i vector1 = Vector4i(1, 3, 5, 7);
constexpr Vector4i vector2 = Vector4i(1, -3, -5, 7);
CHECK_MESSAGE(
vector1.abs() == vector1,
"Vector4i abs should work as expected.");
CHECK_MESSAGE(
vector2.abs() == vector1,
"Vector4i abs should work as expected.");
CHECK_MESSAGE(
vector1.sign() == Vector4i(1, 1, 1, 1),
"Vector4i sign should work as expected.");
CHECK_MESSAGE(
vector2.sign() == Vector4i(1, -1, -1, 1),
"Vector4i sign should work as expected.");
}
} // namespace TestVector4i

View File

@@ -0,0 +1,898 @@
/**************************************************************************/
/* test_class_db.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/core_bind.h"
#include "core/core_constants.h"
#include "core/object/class_db.h"
#include "tests/test_macros.h"
namespace TestClassDB {
struct TypeReference {
StringName name;
bool is_enum = false;
};
struct ConstantData {
String name;
int64_t 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;
int position;
};
struct MethodData {
StringName name;
TypeReference return_type;
List<ArgumentData> arguments;
bool is_virtual = false;
bool is_vararg = false;
};
struct SignalData {
StringName name;
List<ArgumentData> arguments;
};
struct ExposedClass {
StringName name;
StringName base;
bool is_singleton = false;
bool is_instantiable = false;
bool is_ref_counted = false;
ClassDB::APIType api_type;
List<ConstantData> constants;
List<EnumData> enums;
List<PropertyData> properties;
List<MethodData> methods;
List<SignalData> signals_;
const PropertyData *find_property_by_name(const StringName &p_name) const {
for (const PropertyData &E : properties) {
if (E.name == p_name) {
return &E;
}
}
return nullptr;
}
const MethodData *find_method_by_name(const StringName &p_name) const {
for (const MethodData &E : methods) {
if (E.name == p_name) {
return &E;
}
}
return nullptr;
}
};
struct NamesCache {
StringName variant_type = StringName("Variant");
StringName object_class = StringName("Object");
StringName ref_counted_class = StringName("RefCounted");
StringName string_type = StringName("String");
StringName string_name_type = StringName("StringName");
StringName node_path_type = StringName("NodePath");
StringName bool_type = StringName("bool");
StringName int_type = StringName("int");
StringName float_type = StringName("float");
StringName void_type = StringName("void");
StringName vararg_stub_type = StringName("@VarArg@");
StringName vector2_type = StringName("Vector2");
StringName rect2_type = StringName("Rect2");
StringName vector3_type = StringName("Vector3");
StringName vector4_type = StringName("Vector4");
// Object not included as it must be checked for all derived classes
static constexpr int nullable_types_count = 18;
StringName nullable_types[nullable_types_count] = {
string_type,
string_name_type,
node_path_type,
StringName(_STR(Array)),
StringName(_STR(Dictionary)),
StringName(_STR(Callable)),
StringName(_STR(Signal)),
StringName(_STR(PackedByteArray)),
StringName(_STR(PackedInt32Array)),
StringName(_STR(PackedInt64rray)),
StringName(_STR(PackedFloat32Array)),
StringName(_STR(PackedFloat64Array)),
StringName(_STR(PackedStringArray)),
StringName(_STR(PackedVector2Array)),
StringName(_STR(PackedVector3Array)),
StringName(_STR(PackedColorArray)),
StringName(_STR(PackedVector4Array)),
};
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 HashMap<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::ConstIterator elem = exposed_classes.find(p_name);
return elem ? &elem->value : nullptr;
}
const ExposedClass *find_exposed_class(const TypeReference &p_type_ref) const {
ExposedClasses::ConstIterator 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.has(p_type_ref.name)) {
return true;
}
if (p_type_ref.is_enum) {
if (enum_types.has(p_type_ref.name)) {
return true;
}
// Enum not found. Most likely because none of its constants were bound, so it's empty. That's fine. Use int instead.
return builtin_types.find(names_cache.int_type);
}
return false;
}
};
bool arg_default_value_is_assignable_to_type(const Context &p_context, const Variant &p_val, const TypeReference &p_arg_type, String *r_err_msg = nullptr) {
if (p_arg_type.name == p_context.names_cache.variant_type) {
// Variant can take anything
return true;
}
switch (p_val.get_type()) {
case Variant::NIL:
return p_context.find_exposed_class(p_arg_type) ||
p_context.names_cache.is_nullable_type(p_arg_type.name);
case Variant::BOOL:
return p_arg_type.name == p_context.names_cache.bool_type;
case Variant::INT:
return p_arg_type.name == p_context.names_cache.int_type ||
p_arg_type.name == p_context.names_cache.float_type ||
p_arg_type.is_enum;
case Variant::FLOAT:
return p_arg_type.name == p_context.names_cache.float_type;
case Variant::STRING:
case Variant::STRING_NAME:
return p_arg_type.name == p_context.names_cache.string_type ||
p_arg_type.name == p_context.names_cache.string_name_type ||
p_arg_type.name == p_context.names_cache.node_path_type;
case Variant::NODE_PATH:
return p_arg_type.name == p_context.names_cache.node_path_type;
case Variant::TRANSFORM3D:
case Variant::TRANSFORM2D:
case Variant::BASIS:
case Variant::QUATERNION:
case Variant::PLANE:
case Variant::AABB:
case Variant::COLOR:
case Variant::VECTOR2:
case Variant::RECT2:
case Variant::VECTOR3:
case Variant::VECTOR4:
case Variant::PROJECTION:
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::PACKED_VECTOR4_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());
case Variant::VECTOR4I:
return p_arg_type.name == p_context.names_cache.vector4_type ||
p_arg_type.name == Variant::get_type_name(p_val.get_type());
case Variant::VARIANT_MAX:
break;
}
if (r_err_msg) {
*r_err_msg = "Unexpected Variant type: " + itos(p_val.get_type());
}
return false;
}
bool arg_default_value_is_valid_data(const Variant &p_val, String *r_err_msg = nullptr) {
switch (p_val.get_type()) {
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::PACKED_VECTOR4_ARRAY:
case Variant::CALLABLE:
case Variant::SIGNAL:
case Variant::OBJECT:
if (p_val.is_zero()) {
return true;
}
if (r_err_msg) {
*r_err_msg = "Must be zero.";
}
break;
default:
return true;
}
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) {
TEST_FAIL(
"Return type from getter doesn't match first argument of setter, for property: '", p_class.name, ".", String(p_prop.name), "'.");
}
}
const TypeReference &prop_type_ref = getter ? getter->return_type : setter->arguments.back()->get().type;
const ExposedClass *prop_class = p_context.find_exposed_class(prop_type_ref);
if (prop_class) {
TEST_COND(prop_class->is_singleton,
"Property type is a singleton: '", p_class.name, ".", String(p_prop.name), "'.");
if (p_class.api_type == ClassDB::API_CORE) {
TEST_COND(prop_class->api_type == ClassDB::API_EDITOR,
"Property '", p_class.name, ".", p_prop.name, "' has type '", prop_class->name,
"' from the editor API. Core API cannot have dependencies on the editor API.");
}
} else {
// Look for types that don't inherit Object
TEST_FAIL_COND(!p_context.has_type(prop_type_ref),
"Property type '", prop_type_ref.name, "' not found: '", p_class.name, ".", String(p_prop.name), "'.");
}
if (getter) {
if (p_prop.index != -1) {
const ArgumentData &idx_arg = getter->arguments.front()->get();
if (idx_arg.type.name != p_context.names_cache.int_type) {
// If not an int, it can be an enum
TEST_COND(!p_context.enum_types.has(idx_arg.type.name),
"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.has(idx_arg.type.name),
"Invalid type '", idx_arg.type.name, "' for index argument of property setter: '", p_class.name, ".", String(p_prop.name), "'.");
}
}
}
}
void validate_argument(const Context &p_context, const ExposedClass &p_class, const String &p_owner_name, const String &p_owner_type, const ArgumentData &p_arg) {
#ifdef DEBUG_ENABLED
TEST_COND((p_arg.name.is_empty() || p_arg.name.begins_with("_unnamed_arg")),
vformat("Unnamed argument in position %d of %s '%s.%s'.", p_arg.position, p_owner_type, p_class.name, p_owner_name));
TEST_FAIL_COND((p_arg.name != "@varargs@" && !p_arg.name.is_valid_ascii_identifier()),
vformat("Invalid argument name '%s' of %s '%s.%s'.", p_arg.name, p_owner_type, p_class.name, p_owner_name));
#endif // DEBUG_ENABLED
const ExposedClass *arg_class = p_context.find_exposed_class(p_arg.type);
if (arg_class) {
TEST_COND(arg_class->is_singleton,
vformat("Argument type is a singleton: '%s' of %s '%s.%s'.", p_arg.name, p_owner_type, p_class.name, p_owner_name));
if (p_class.api_type == ClassDB::API_CORE) {
TEST_COND(arg_class->api_type == ClassDB::API_EDITOR,
vformat("Argument '%s' of %s '%s.%s' has type '%s' from the editor API. Core API cannot have dependencies on the editor API.",
p_arg.name, p_owner_type, p_class.name, p_owner_name, arg_class->name));
}
} else {
// Look for types that don't inherit Object.
TEST_FAIL_COND(!p_context.has_type(p_arg.type),
vformat("Argument type '%s' not found: '%s' of %s '%s.%s'.", p_arg.type.name, p_arg.name, p_owner_type, p_class.name, p_owner_name));
}
if (p_arg.has_defval) {
String type_error_msg;
bool arg_defval_assignable_to_type = arg_default_value_is_assignable_to_type(p_context, p_arg.defval, p_arg.type, &type_error_msg);
String err_msg = vformat("Invalid default value for parameter '%s' of %s '%s.%s'.", p_arg.name, p_owner_type, p_class.name, p_owner_name);
if (!type_error_msg.is_empty()) {
err_msg += " " + type_error_msg;
}
TEST_COND(!arg_defval_assignable_to_type, err_msg);
bool arg_defval_valid_data = arg_default_value_is_valid_data(p_arg.defval, &type_error_msg);
if (!type_error_msg.is_empty()) {
err_msg += " " + type_error_msg;
}
TEST_COND(!arg_defval_valid_data, err_msg);
}
}
void validate_method(const Context &p_context, const ExposedClass &p_class, const MethodData &p_method) {
if (p_method.return_type.name != StringName()) {
const ExposedClass *return_class = p_context.find_exposed_class(p_method.return_type);
if (return_class) {
if (p_class.api_type == ClassDB::API_CORE) {
TEST_COND(return_class->api_type == ClassDB::API_EDITOR,
"Method '", p_class.name, ".", p_method.name, "' has return type '", return_class->name,
"' from the editor API. Core API cannot have dependencies on the editor API.");
}
} else {
// Look for types that don't inherit Object
TEST_FAIL_COND(!p_context.has_type(p_method.return_type),
"Method return type '", p_method.return_type.name, "' not found: '", p_class.name, ".", p_method.name, "'.");
}
}
for (const ArgumentData &F : p_method.arguments) {
const ArgumentData &arg = F;
validate_argument(p_context, p_class, p_method.name, "method", arg);
}
}
void validate_signal(const Context &p_context, const ExposedClass &p_class, const SignalData &p_signal) {
for (const ArgumentData &F : p_signal.arguments) {
const ArgumentData &arg = F;
validate_argument(p_context, p_class, p_signal.name, "signal", arg);
}
}
void validate_class(const Context &p_context, const ExposedClass &p_exposed_class) {
bool is_derived_type = p_exposed_class.base != StringName();
if (!is_derived_type) {
// Asserts about the base Object class
TEST_FAIL_COND(p_exposed_class.name != p_context.names_cache.object_class,
"Class '", p_exposed_class.name, "' has no base class.");
TEST_FAIL_COND(!p_exposed_class.is_instantiable,
"Object class is not instantiable.");
TEST_FAIL_COND(p_exposed_class.api_type != ClassDB::API_CORE,
"Object class is API is not API_CORE.");
TEST_FAIL_COND(p_exposed_class.is_singleton,
"Object class is registered as a singleton.");
}
TEST_FAIL_COND((p_exposed_class.is_singleton && p_exposed_class.base != p_context.names_cache.object_class),
"Singleton base class '", String(p_exposed_class.base), "' is not Object, for class '", p_exposed_class.name, "'.");
TEST_FAIL_COND((is_derived_type && !p_context.exposed_classes.has(p_exposed_class.base)),
"Base type '", p_exposed_class.base.operator String(), "' does not exist, for class '", p_exposed_class.name, "'.");
for (const PropertyData &F : p_exposed_class.properties) {
validate_property(p_context, p_exposed_class, F);
}
for (const MethodData &F : p_exposed_class.methods) {
validate_method(p_context, p_exposed_class, F);
}
for (const SignalData &F : p_exposed_class.signals_) {
validate_signal(p_context, p_exposed_class, F);
}
}
void add_exposed_classes(Context &r_context) {
List<StringName> class_list;
ClassDB::get_class_list(&class_list);
class_list.sort_custom<StringName::AlphCompare>();
while (class_list.size()) {
StringName class_name = class_list.front()->get();
ClassDB::APIType api_type = ClassDB::get_api_type(class_name);
if (api_type == ClassDB::API_NONE) {
class_list.pop_front();
continue;
}
if (!ClassDB::is_class_exposed(class_name)) {
INFO(vformat("Ignoring class '%s' because it's not exposed.", class_name));
class_list.pop_front();
continue;
}
if (!ClassDB::is_class_enabled(class_name)) {
INFO(vformat("Ignoring class '%s' because it's not enabled.", class_name));
class_list.pop_front();
continue;
}
ClassDB::ClassInfo *class_info = ClassDB::classes.getptr(class_name);
ExposedClass exposed_class;
exposed_class.name = class_name;
exposed_class.api_type = api_type;
exposed_class.is_singleton = Engine::get_singleton()->has_singleton(class_name);
exposed_class.is_instantiable = class_info->creation_func && !exposed_class.is_singleton;
exposed_class.is_ref_counted = ClassDB::is_parent_class(class_name, "RefCounted");
exposed_class.base = ClassDB::get_parent_class(class_name);
// Add properties
List<PropertyInfo> property_list;
ClassDB::get_property_list(class_name, &property_list, true);
HashMap<StringName, StringName> accessor_methods;
for (const PropertyInfo &property : property_list) {
if (property.usage & PROPERTY_USAGE_GROUP || property.usage & PROPERTY_USAGE_SUBGROUP || property.usage & PROPERTY_USAGE_CATEGORY || (property.type == Variant::NIL && property.usage & PROPERTY_USAGE_ARRAY)) {
continue;
}
PropertyData prop;
prop.name = property.name;
prop.setter = ClassDB::get_property_setter(class_name, prop.name);
prop.getter = ClassDB::get_property_getter(class_name, prop.name);
if (prop.setter != StringName()) {
accessor_methods[prop.setter] = prop.name;
}
if (prop.getter != StringName()) {
accessor_methods[prop.getter] = prop.name;
}
bool valid = false;
prop.index = ClassDB::get_property_index(class_name, prop.name, &valid);
TEST_FAIL_COND(!valid, "Invalid property: '", exposed_class.name, ".", String(prop.name), "'.");
exposed_class.properties.push_back(prop);
}
// Add methods
List<MethodInfo> virtual_method_list;
ClassDB::get_virtual_methods(class_name, &virtual_method_list, true);
List<MethodInfo> method_list;
ClassDB::get_method_list(class_name, &method_list, true);
method_list.sort();
for (const MethodInfo &E : method_list) {
const MethodInfo &method_info = E;
if (method_info.name.is_empty()) {
continue;
}
MethodData method;
method.name = method_info.name;
TEST_FAIL_COND(!String(method.name).is_valid_ascii_identifier(),
"Method name is not a valid identifier: '", exposed_class.name, ".", method.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);
} else if (return_info.type == Variant::INT && return_info.usage & (PROPERTY_USAGE_CLASS_IS_ENUM | PROPERTY_USAGE_CLASS_IS_BITFIELD)) {
method.return_type.name = return_info.class_name;
method.return_type.is_enum = true;
} else if (return_info.class_name != StringName()) {
method.return_type.name = return_info.class_name;
bool bad_reference_hint = !method.is_virtual && return_info.hint != PROPERTY_HINT_RESOURCE_TYPE &&
ClassDB::is_parent_class(return_info.class_name, r_context.names_cache.ref_counted_class);
TEST_COND(bad_reference_hint, "Return type is reference but hint is not '" _STR(PROPERTY_HINT_RESOURCE_TYPE) "'.", " Are you returning a reference type by pointer? Method: '",
exposed_class.name, ".", method.name, "'.");
} else if (return_info.hint == PROPERTY_HINT_RESOURCE_TYPE) {
method.return_type.name = return_info.hint_string;
} else if (return_info.type == Variant::NIL && return_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT) {
method.return_type.name = r_context.names_cache.variant_type;
} else if (return_info.type == Variant::NIL) {
method.return_type.name = r_context.names_cache.void_type;
} else {
// NOTE: We don't care about the size and sign of int and float in these tests
method.return_type.name = Variant::get_type_name(return_info.type);
}
for (int64_t i = 0; i < method_info.arguments.size(); ++i) {
const PropertyInfo &arg_info = method_info.arguments[i];
String orig_arg_name = arg_info.name;
ArgumentData arg;
arg.name = orig_arg_name;
arg.position = i;
if (arg_info.type == Variant::INT && arg_info.usage & (PROPERTY_USAGE_CLASS_IS_ENUM | PROPERTY_USAGE_CLASS_IS_BITFIELD)) {
arg.type.name = arg_info.class_name;
arg.type.is_enum = true;
} else if (arg_info.class_name != StringName()) {
arg.type.name = arg_info.class_name;
} else if (arg_info.hint == PROPERTY_HINT_RESOURCE_TYPE) {
arg.type.name = arg_info.hint_string;
} else if (arg_info.type == Variant::NIL) {
arg.type.name = r_context.names_cache.variant_type;
} else {
// NOTE: We don't care about the size and sign of int and float in these tests
arg.type.name = Variant::get_type_name(arg_info.type);
}
if (m && m->has_default_argument(i)) {
arg.has_defval = true;
arg.defval = m->get_default_argument(i);
}
method.arguments.push_back(arg);
}
if (method.is_vararg) {
ArgumentData vararg;
vararg.type.name = r_context.names_cache.vararg_stub_type;
vararg.name = "@varargs@";
method.arguments.push_back(vararg);
}
TEST_COND(exposed_class.find_property_by_name(method.name),
"Method name conflicts with property: '", String(class_name), ".", String(method.name), "'.");
// Methods starting with an underscore are ignored unless they're virtual or used as a property setter or getter.
if (!method.is_virtual && String(method.name)[0] == '_') {
for (const PropertyData &F : exposed_class.properties) {
const PropertyData &prop = F;
if (prop.setter == method.name || prop.getter == method.name) {
exposed_class.methods.push_back(method);
break;
}
}
} else {
exposed_class.methods.push_back(method);
}
if (method.is_virtual) {
TEST_COND(String(method.name)[0] != '_', "Virtual method ", String(method.name), " does not start with underscore.");
}
}
// Add signals
const HashMap<StringName, MethodInfo> &signal_map = class_info->signal_map;
for (const KeyValue<StringName, MethodInfo> &K : signal_map) {
SignalData signal;
const MethodInfo &method_info = signal_map.get(K.key);
signal.name = method_info.name;
TEST_FAIL_COND(!String(signal.name).is_valid_ascii_identifier(),
"Signal name is not a valid identifier: '", exposed_class.name, ".", signal.name, "'.");
for (int64_t i = 0; i < method_info.arguments.size(); ++i) {
const PropertyInfo &arg_info = method_info.arguments[i];
String orig_arg_name = arg_info.name;
ArgumentData arg;
arg.name = orig_arg_name;
arg.position = i;
if (arg_info.type == Variant::INT && arg_info.usage & (PROPERTY_USAGE_CLASS_IS_ENUM | PROPERTY_USAGE_CLASS_IS_BITFIELD)) {
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);
String warn_msg = vformat(
"Signal name conflicts with %s: '%s.%s.",
method_conflict ? "method" : "property", class_name, signal.name);
TEST_FAIL_COND((method_conflict || exposed_class.find_method_by_name(signal.name)),
warn_msg);
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, ClassDB::ClassInfo::EnumInfo> &enum_map = class_info->enum_map;
for (const KeyValue<StringName, ClassDB::ClassInfo::EnumInfo> &K : enum_map) {
EnumData enum_;
enum_.name = K.key;
for (const StringName &E : K.value.constants) {
const StringName &constant_name = E;
TEST_FAIL_COND(String(constant_name).contains("::"),
"Enum constant contains '::', check bindings to remove the scope: '",
String(class_name), ".", String(enum_.name), ".", String(constant_name), "'.");
int64_t *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.key));
}
for (const String &E : constants) {
const String &constant_name = E;
TEST_FAIL_COND(constant_name.contains("::"),
"Constant contains '::', check bindings to remove the scope: '",
String(class_name), ".", constant_name, "'.");
int64_t *value = class_info->constant_map.getptr(StringName(E));
TEST_FAIL_COND(!value, "Missing constant value: '", String(class_name), ".", String(constant_name), "'.");
ConstantData constant;
constant.name = constant_name;
constant.value = *value;
exposed_class.constants.push_back(constant);
}
r_context.exposed_classes.insert(class_name, exposed_class);
class_list.pop_front();
}
}
void add_builtin_types(Context &r_context) {
// NOTE: We don't care about the size and sign of int and float in these tests
for (int i = 0; i < Variant::VARIANT_MAX; i++) {
r_context.builtin_types.push_back(Variant::get_type_name(Variant::Type(i)));
}
r_context.builtin_types.push_back(_STR(Variant));
r_context.builtin_types.push_back(r_context.names_cache.vararg_stub_type);
r_context.builtin_types.push_back("void");
}
void add_global_enums(Context &r_context) {
int global_constants_count = CoreConstants::get_global_constant_count();
if (global_constants_count > 0) {
for (int i = 0; i < global_constants_count; i++) {
StringName enum_name = CoreConstants::get_global_constant_enum(i);
if (enum_name != StringName()) {
ConstantData constant;
constant.name = CoreConstants::get_global_constant_name(i);
constant.value = CoreConstants::get_global_constant_value(i);
EnumData enum_;
enum_.name = enum_name;
List<EnumData>::Element *enum_match = r_context.global_enums.find(enum_);
if (enum_match) {
enum_match->get().constants.push_back(constant);
} else {
enum_.constants.push_back(constant);
r_context.global_enums.push_back(enum_);
}
}
}
for (const EnumData &E : r_context.global_enums) {
r_context.enum_types.push_back(E.name);
}
}
for (int i = 0; i < Variant::VARIANT_MAX; i++) {
if (i == Variant::OBJECT) {
continue;
}
const Variant::Type type = Variant::Type(i);
List<StringName> enum_names;
Variant::get_enums_for_type(type, &enum_names);
for (const StringName &enum_name : enum_names) {
r_context.enum_types.push_back(Variant::get_type_name(type) + "." + enum_name);
}
}
}
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] Validate exposed classes") {
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 (const KeyValue<StringName, ExposedClass> &E : context.exposed_classes) {
validate_class(context, E.value);
}
}
}
}
} // namespace TestClassDB

View File

@@ -0,0 +1,174 @@
/**************************************************************************/
/* test_method_bind.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/object/class_db.h"
#include "tests/test_macros.h"
namespace TestMethodBind {
class MethodBindTester : public Object {
GDCLASS(MethodBindTester, Object);
public:
enum Test {
TEST_METHOD,
TEST_METHOD_ARGS,
TEST_METHODC,
TEST_METHODC_ARGS,
TEST_METHODR,
TEST_METHODR_ARGS,
TEST_METHODRC,
TEST_METHODRC_ARGS,
TEST_METHOD_DEFARGS,
TEST_METHOD_OBJECT_CAST,
TEST_MAX
};
class ObjectSubclass : public Object {
GDSOFTCLASS(ObjectSubclass, Object);
public:
int value = 1;
};
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
}
void test_method_object_cast(ObjectSubclass *p_object) {
test_valid[TEST_METHOD_OBJECT_CAST] = p_object->value == 1;
}
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));
ClassDB::bind_method(D_METHOD("test_method_object_cast", "object"), &MethodBindTester::test_method_object_cast);
}
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);
ObjectSubclass *obj = memnew(ObjectSubclass);
call("test_method_object_cast", obj);
memdelete(obj);
}
};
TEST_CASE("[MethodBind] check all method binds") {
MethodBindTester *mbt = memnew(MethodBindTester);
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]);
CHECK(mbt->test_valid[MethodBindTester::TEST_METHOD_OBJECT_CAST]);
memdelete(mbt);
}
} // namespace TestMethodBind

View File

@@ -0,0 +1,597 @@
/**************************************************************************/
/* test_object.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/object/class_db.h"
#include "core/object/object.h"
#include "core/object/script_language.h"
#include "tests/test_macros.h"
#ifdef SANITIZERS_ENABLED
#ifdef __has_feature
#if __has_feature(address_sanitizer) || __has_feature(thread_sanitizer)
#define ASAN_OR_TSAN_ENABLED
#endif
#elif defined(__SANITIZE_ADDRESS__) || defined(__SANITIZE_THREAD__)
#define ASAN_OR_TSAN_ENABLED
#endif
#endif
// Declared in global namespace because of GDCLASS macro warning (Windows):
// "Unqualified friend declaration referring to type outside of the nearest enclosing namespace
// is a Microsoft extension; add a nested name specifier".
class _TestDerivedObject : public Object {
GDCLASS(_TestDerivedObject, Object);
int property_value;
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("set_property", "property"), &_TestDerivedObject::set_property);
ClassDB::bind_method(D_METHOD("get_property"), &_TestDerivedObject::get_property);
ADD_PROPERTY(PropertyInfo(Variant::INT, "property"), "set_property", "get_property");
}
public:
void set_property(int value) { property_value = value; }
int get_property() const { return property_value; }
};
namespace TestObject {
class _MockScriptInstance : public ScriptInstance {
StringName property_name = "NO_NAME";
Variant property_value;
public:
bool set(const StringName &p_name, const Variant &p_value) override {
property_name = p_name;
property_value = p_value;
return true;
}
bool get(const StringName &p_name, Variant &r_ret) const override {
if (property_name == p_name) {
r_ret = property_value;
return true;
}
return false;
}
void get_property_list(List<PropertyInfo> *p_properties) const override {
}
Variant::Type get_property_type(const StringName &p_name, bool *r_is_valid) const override {
return Variant::PACKED_FLOAT32_ARRAY;
}
virtual void validate_property(PropertyInfo &p_property) const override {
}
bool property_can_revert(const StringName &p_name) const override {
return false;
}
bool property_get_revert(const StringName &p_name, Variant &r_ret) const override {
return false;
}
void get_method_list(List<MethodInfo> *p_list) const override {
}
bool has_method(const StringName &p_method) const override {
return false;
}
int get_method_argument_count(const StringName &p_method, bool *r_is_valid = nullptr) const override {
if (r_is_valid) {
*r_is_valid = false;
}
return 0;
}
Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override {
return Variant();
}
void notification(int p_notification, bool p_reversed = false) override {
}
Ref<Script> get_script() const override {
return Ref<Script>();
}
const Variant get_rpc_config() const override {
return Variant();
}
ScriptLanguage *get_language() override {
return nullptr;
}
};
TEST_CASE("[Object] Core getters") {
Object object;
CHECK_MESSAGE(
object.is_class("Object"),
"is_class() should return the expected value.");
CHECK_MESSAGE(
object.get_class() == "Object",
"The returned class should match the expected value.");
CHECK_MESSAGE(
object.get_class_name() == "Object",
"The returned class name should match the expected value.");
CHECK_MESSAGE(
object.get_class_static() == "Object",
"The returned static class should match the expected value.");
CHECK_MESSAGE(
object.get_save_class() == "Object",
"The returned save class should match the expected value.");
}
TEST_CASE("[Object] Metadata") {
const String meta_path = "complex_metadata_path";
Object object;
object.set_meta(meta_path, Color(0, 1, 0));
CHECK_MESSAGE(
Color(object.get_meta(meta_path)).is_equal_approx(Color(0, 1, 0)),
"The returned object metadata after setting should match the expected value.");
List<StringName> meta_list;
object.get_meta_list(&meta_list);
CHECK_MESSAGE(
meta_list.size() == 1,
"The metadata list should only contain 1 item after adding one metadata item.");
object.remove_meta(meta_path);
// Also try removing nonexistent metadata (it should do nothing, without printing an error message).
object.remove_meta("I don't exist");
ERR_PRINT_OFF;
CHECK_MESSAGE(
object.get_meta(meta_path) == Variant(),
"The returned object metadata after removing should match the expected value.");
ERR_PRINT_ON;
List<StringName> meta_list2;
object.get_meta_list(&meta_list2);
CHECK_MESSAGE(
meta_list2.size() == 0,
"The metadata list should contain 0 items after removing all metadata items.");
Object other;
object.set_meta("conflicting_meta", "string");
object.set_meta("not_conflicting_meta", 123);
other.set_meta("conflicting_meta", Color(0, 1, 0));
other.set_meta("other_meta", "other");
object.merge_meta_from(&other);
CHECK_MESSAGE(
Color(object.get_meta("conflicting_meta")).is_equal_approx(Color(0, 1, 0)),
"String meta should be overwritten with Color after merging.");
CHECK_MESSAGE(
int(object.get_meta("not_conflicting_meta")) == 123,
"Not conflicting meta on destination should be kept intact.");
CHECK_MESSAGE(
object.get_meta("other_meta", String()) == "other",
"Not conflicting meta name on source should merged.");
List<StringName> meta_list3;
object.get_meta_list(&meta_list3);
CHECK_MESSAGE(
meta_list3.size() == 3,
"The metadata list should contain 3 items after merging meta from two objects.");
}
TEST_CASE("[Object] Construction") {
Object object;
CHECK_MESSAGE(
!object.is_ref_counted(),
"Object is not a RefCounted.");
Object *p_db = ObjectDB::get_instance(object.get_instance_id());
CHECK_MESSAGE(
p_db == &object,
"The database pointer returned by the object id should reference same object.");
}
TEST_CASE("[Object] Script instance property setter") {
Object object;
_MockScriptInstance *script_instance = memnew(_MockScriptInstance);
object.set_script_instance(script_instance);
bool valid = false;
object.set("some_name", 100, &valid);
CHECK(valid);
Variant actual_value;
CHECK_MESSAGE(
script_instance->get("some_name", actual_value),
"The assigned script instance should successfully retrieve value by name.");
CHECK_MESSAGE(
actual_value == Variant(100),
"The returned value should equal the one which was set by the object.");
}
TEST_CASE("[Object] Script instance property getter") {
Object object;
_MockScriptInstance *script_instance = memnew(_MockScriptInstance);
script_instance->set("some_name", 100); // Make sure script instance has the property
object.set_script_instance(script_instance);
bool valid = false;
const Variant &actual_value = object.get("some_name", &valid);
CHECK(valid);
CHECK_MESSAGE(
actual_value == Variant(100),
"The returned value should equal the one which was set by the script instance.");
}
TEST_CASE("[Object] Built-in property setter") {
GDREGISTER_CLASS(_TestDerivedObject);
_TestDerivedObject derived_object;
bool valid = false;
derived_object.set("property", 100, &valid);
CHECK(valid);
CHECK_MESSAGE(
derived_object.get_property() == 100,
"The property value should equal the one which was set with built-in setter.");
}
TEST_CASE("[Object] Built-in property getter") {
GDREGISTER_CLASS(_TestDerivedObject);
_TestDerivedObject derived_object;
derived_object.set_property(100);
bool valid = false;
const Variant &actual_value = derived_object.get("property", &valid);
CHECK(valid);
CHECK_MESSAGE(
actual_value == Variant(100),
"The returned value should equal the one which was set with built-in setter.");
}
TEST_CASE("[Object] Script property setter") {
Object object;
Variant script;
bool valid = false;
object.set(CoreStringName(script), script, &valid);
CHECK(valid);
CHECK_MESSAGE(
object.get_script() == script,
"The object script should be equal to the assigned one.");
}
TEST_CASE("[Object] Script property getter") {
Object object;
Variant script;
object.set_script(script);
bool valid = false;
const Variant &actual_value = object.get(CoreStringName(script), &valid);
CHECK(valid);
CHECK_MESSAGE(
actual_value == script,
"The returned value should be equal to the assigned script.");
}
TEST_CASE("[Object] Absent name setter") {
Object object;
bool valid = true;
object.set("absent_name", 100, &valid);
CHECK(!valid);
}
TEST_CASE("[Object] Absent name getter") {
Object object;
bool valid = true;
const Variant &actual_value = object.get("absent_name", &valid);
CHECK(!valid);
CHECK_MESSAGE(
actual_value == Variant(),
"The returned value should equal nil variant.");
}
TEST_CASE("[Object] Signals") {
Object object;
CHECK_FALSE(object.has_signal("my_custom_signal"));
List<MethodInfo> signals_before;
object.get_signal_list(&signals_before);
object.add_user_signal(MethodInfo("my_custom_signal"));
CHECK(object.has_signal("my_custom_signal"));
List<MethodInfo> signals_after;
object.get_signal_list(&signals_after);
// Should be one more signal.
CHECK_EQ(signals_before.size() + 1, signals_after.size());
SUBCASE("Adding the same user signal again should not have any effect") {
CHECK(object.has_signal("my_custom_signal"));
ERR_PRINT_OFF;
object.add_user_signal(MethodInfo("my_custom_signal"));
ERR_PRINT_ON;
CHECK(object.has_signal("my_custom_signal"));
List<MethodInfo> signals_after_existing_added;
object.get_signal_list(&signals_after_existing_added);
CHECK_EQ(signals_after.size(), signals_after_existing_added.size());
}
SUBCASE("Trying to add a preexisting signal should not have any effect") {
CHECK(object.has_signal("script_changed"));
ERR_PRINT_OFF;
object.add_user_signal(MethodInfo("script_changed"));
ERR_PRINT_ON;
CHECK(object.has_signal("script_changed"));
List<MethodInfo> signals_after_existing_added;
object.get_signal_list(&signals_after_existing_added);
CHECK_EQ(signals_after.size(), signals_after_existing_added.size());
}
SUBCASE("Adding an empty signal should not have any effect") {
CHECK_FALSE(object.has_signal(""));
ERR_PRINT_OFF;
object.add_user_signal(MethodInfo(""));
ERR_PRINT_ON;
CHECK_FALSE(object.has_signal(""));
List<MethodInfo> signals_after_empty_added;
object.get_signal_list(&signals_after_empty_added);
CHECK_EQ(signals_after.size(), signals_after_empty_added.size());
}
SUBCASE("Deleting an object with connected signals should disconnect them") {
List<Object::Connection> signal_connections;
{
Object target;
target.add_user_signal(MethodInfo("my_custom_signal"));
ERR_PRINT_OFF;
target.connect("nonexistent_signal1", callable_mp(&object, &Object::notify_property_list_changed));
target.connect("my_custom_signal", callable_mp(&object, &Object::notify_property_list_changed));
target.connect("nonexistent_signal2", callable_mp(&object, &Object::notify_property_list_changed));
ERR_PRINT_ON;
signal_connections.clear();
object.get_all_signal_connections(&signal_connections);
CHECK(signal_connections.size() == 0);
signal_connections.clear();
object.get_signals_connected_to_this(&signal_connections);
CHECK(signal_connections.size() == 1);
ERR_PRINT_OFF;
object.connect("nonexistent_signal1", callable_mp(&target, &Object::notify_property_list_changed));
object.connect("my_custom_signal", callable_mp(&target, &Object::notify_property_list_changed));
object.connect("nonexistent_signal2", callable_mp(&target, &Object::notify_property_list_changed));
ERR_PRINT_ON;
signal_connections.clear();
object.get_all_signal_connections(&signal_connections);
CHECK(signal_connections.size() == 1);
signal_connections.clear();
object.get_signals_connected_to_this(&signal_connections);
CHECK(signal_connections.size() == 1);
}
signal_connections.clear();
object.get_all_signal_connections(&signal_connections);
CHECK(signal_connections.size() == 0);
signal_connections.clear();
object.get_signals_connected_to_this(&signal_connections);
CHECK(signal_connections.size() == 0);
}
SUBCASE("Emitting a non existing signal will return an error") {
Error err = object.emit_signal("some_signal");
CHECK(err == ERR_UNAVAILABLE);
}
SUBCASE("Emitting an existing signal should call the connected method") {
Array empty_signal_args = { {} };
SIGNAL_WATCH(&object, "my_custom_signal");
SIGNAL_CHECK_FALSE("my_custom_signal");
Error err = object.emit_signal("my_custom_signal");
CHECK(err == OK);
SIGNAL_CHECK("my_custom_signal", empty_signal_args);
SIGNAL_UNWATCH(&object, "my_custom_signal");
}
SUBCASE("Connecting and then disconnecting many signals should not leave anything behind") {
List<Object::Connection> signal_connections;
Object targets[100];
for (int i = 0; i < 10; i++) {
ERR_PRINT_OFF;
for (Object &target : targets) {
object.connect("my_custom_signal", callable_mp(&target, &Object::notify_property_list_changed));
}
ERR_PRINT_ON;
signal_connections.clear();
object.get_all_signal_connections(&signal_connections);
CHECK(signal_connections.size() == 100);
}
for (Object &target : targets) {
object.disconnect("my_custom_signal", callable_mp(&target, &Object::notify_property_list_changed));
}
signal_connections.clear();
object.get_all_signal_connections(&signal_connections);
CHECK(signal_connections.size() == 0);
}
}
class NotificationObjectSuperclass : public Object {
GDCLASS(NotificationObjectSuperclass, Object);
protected:
void _notification(int p_what) {
order_superclass = ++order_global;
}
public:
static inline int order_global = 0;
int order_superclass = -1;
};
class NotificationObjectSubclass : public NotificationObjectSuperclass {
GDCLASS(NotificationObjectSubclass, NotificationObjectSuperclass);
protected:
void _notification(int p_what) {
order_subclass = ++order_global;
}
public:
int order_subclass = -1;
};
class NotificationScriptInstance : public _MockScriptInstance {
void notification(int p_notification, bool p_reversed) override {
order_script = ++NotificationObjectSuperclass::order_global;
}
public:
int order_script = -1;
};
TEST_CASE("[Object] Notification order") { // GH-52325
NotificationObjectSubclass *object = memnew(NotificationObjectSubclass);
NotificationScriptInstance *script = memnew(NotificationScriptInstance);
object->set_script_instance(script);
SUBCASE("regular order") {
NotificationObjectSubclass::order_global = 0;
object->order_superclass = -1;
object->order_subclass = -1;
script->order_script = -1;
object->notification(12345, false);
CHECK_EQ(object->order_superclass, 1);
CHECK_EQ(object->order_subclass, 2);
// TODO If an extension is attached, it should come here.
CHECK_EQ(script->order_script, 3);
CHECK_EQ(NotificationObjectSubclass::order_global, 3);
}
SUBCASE("reverse order") {
NotificationObjectSubclass::order_global = 0;
object->order_superclass = -1;
object->order_subclass = -1;
script->order_script = -1;
object->notification(12345, true);
CHECK_EQ(script->order_script, 1);
// TODO If an extension is attached, it should come here.
CHECK_EQ(object->order_subclass, 2);
CHECK_EQ(object->order_superclass, 3);
CHECK_EQ(NotificationObjectSubclass::order_global, 3);
}
memdelete(object);
}
TEST_CASE("[Object] Destruction at the end of the call chain is safe") {
Object *object = memnew(Object);
ObjectID obj_id = object->get_instance_id();
class _SelfDestroyingScriptInstance : public _MockScriptInstance {
Object *self = nullptr;
// This has to be static because ~Object() also destroys the script instance.
static void free_self(Object *p_self) {
#if defined(ASAN_OR_TSAN_ENABLED)
// Regular deletion is enough becausa asan/tsan will catch a potential heap-after-use.
memdelete(p_self);
#else
// Without asan/tsan, try at least to force a crash by replacing the otherwise seemingly good data with garbage.
// Operations such as dereferencing pointers or decreasing a refcount would fail.
// Unfortunately, we may not poison the memory after the deletion, because the memory would no longer belong to us
// and on doing so we may cause a more generalized crash on some platforms (allocator implementations).
p_self->~Object();
memset((void *)p_self, 0, sizeof(Object));
Memory::free_static(p_self, false);
#endif
}
public:
Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override {
free_self(self);
return Variant();
}
Variant call_const(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override {
free_self(self);
return Variant();
}
bool has_method(const StringName &p_method) const override {
return p_method == "some_method";
}
public:
_SelfDestroyingScriptInstance(Object *p_self) :
self(p_self) {}
};
_SelfDestroyingScriptInstance *script_instance = memnew(_SelfDestroyingScriptInstance(object));
object->set_script_instance(script_instance);
SUBCASE("Within callp()") {
SUBCASE("Through call()") {
object->call("some_method");
}
SUBCASE("Through callv()") {
object->callv("some_method", Array());
}
}
SUBCASE("Within call_const()") {
Callable::CallError call_error;
object->call_const("some_method", nullptr, 0, call_error);
}
SUBCASE("Within signal handling (from emit_signalp(), through emit_signal())") {
Object emitter;
emitter.add_user_signal(MethodInfo("some_signal"));
emitter.connect("some_signal", Callable(object, "some_method"));
emitter.emit_signal("some_signal");
}
CHECK_MESSAGE(
ObjectDB::get_instance(obj_id) == nullptr,
"Object was tail-deleted without crashes.");
}
} // namespace TestObject

View File

@@ -0,0 +1,199 @@
/**************************************************************************/
/* test_undo_redo.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/object/undo_redo.h"
#include "tests/test_macros.h"
// Declared in global namespace because of GDCLASS macro warning (Windows):
// "Unqualified friend declaration referring to type outside of the nearest enclosing namespace
// is a Microsoft extension; add a nested name specifier".
class _TestUndoRedoObject : public Object {
GDCLASS(_TestUndoRedoObject, Object);
int property_value = 0;
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("set_property", "property"), &_TestUndoRedoObject::set_property);
ClassDB::bind_method(D_METHOD("get_property"), &_TestUndoRedoObject::get_property);
ADD_PROPERTY(PropertyInfo(Variant::INT, "property"), "set_property", "get_property");
}
public:
void set_property(int value) { property_value = value; }
int get_property() const { return property_value; }
void add_to_property(int value) { property_value += value; }
void subtract_from_property(int value) { property_value -= value; }
};
namespace TestUndoRedo {
void set_property_action(UndoRedo *undo_redo, const String &name, _TestUndoRedoObject *test_object, int value, UndoRedo::MergeMode merge_mode = UndoRedo::MERGE_DISABLE) {
undo_redo->create_action(name, merge_mode);
undo_redo->add_do_property(test_object, "property", value);
undo_redo->add_undo_property(test_object, "property", test_object->get_property());
undo_redo->commit_action();
}
void increment_property_action(UndoRedo *undo_redo, const String &name, _TestUndoRedoObject *test_object, int value, UndoRedo::MergeMode merge_mode = UndoRedo::MERGE_DISABLE) {
undo_redo->create_action(name, merge_mode);
undo_redo->add_do_method(callable_mp(test_object, &_TestUndoRedoObject::add_to_property).bind(value));
undo_redo->add_undo_method(callable_mp(test_object, &_TestUndoRedoObject::subtract_from_property).bind(value));
undo_redo->commit_action();
}
TEST_CASE("[UndoRedo] Simple Property UndoRedo") {
GDREGISTER_CLASS(_TestUndoRedoObject);
UndoRedo *undo_redo = memnew(UndoRedo());
_TestUndoRedoObject *test_object = memnew(_TestUndoRedoObject());
CHECK(test_object->get_property() == 0);
CHECK(undo_redo->get_version() == 1);
CHECK(undo_redo->get_history_count() == 0);
set_property_action(undo_redo, "Set Property", test_object, 10);
CHECK(test_object->get_property() == 10);
CHECK(undo_redo->get_version() == 2);
CHECK(undo_redo->get_history_count() == 1);
undo_redo->undo();
CHECK(test_object->get_property() == 0);
CHECK(undo_redo->get_version() == 1);
CHECK(undo_redo->get_history_count() == 1);
undo_redo->redo();
CHECK(test_object->get_property() == 10);
CHECK(undo_redo->get_version() == 2);
CHECK(undo_redo->get_history_count() == 1);
set_property_action(undo_redo, "Set Property", test_object, 100);
CHECK(test_object->get_property() == 100);
CHECK(undo_redo->get_version() == 3);
CHECK(undo_redo->get_history_count() == 2);
set_property_action(undo_redo, "Set Property", test_object, 1000);
CHECK(test_object->get_property() == 1000);
CHECK(undo_redo->get_version() == 4);
CHECK(undo_redo->get_history_count() == 3);
undo_redo->undo();
CHECK(test_object->get_property() == 100);
CHECK(undo_redo->get_version() == 3);
CHECK(undo_redo->get_history_count() == 3);
memdelete(test_object);
memdelete(undo_redo);
}
TEST_CASE("[UndoRedo] Merge Property UndoRedo") {
GDREGISTER_CLASS(_TestUndoRedoObject);
UndoRedo *undo_redo = memnew(UndoRedo());
_TestUndoRedoObject *test_object = memnew(_TestUndoRedoObject());
CHECK(test_object->get_property() == 0);
CHECK(undo_redo->get_version() == 1);
CHECK(undo_redo->get_history_count() == 0);
set_property_action(undo_redo, "Merge Action 1", test_object, 10, UndoRedo::MERGE_ALL);
CHECK(test_object->get_property() == 10);
CHECK(undo_redo->get_version() == 2);
CHECK(undo_redo->get_history_count() == 1);
set_property_action(undo_redo, "Merge Action 1", test_object, 100, UndoRedo::MERGE_ALL);
CHECK(test_object->get_property() == 100);
CHECK(undo_redo->get_version() == 2);
CHECK(undo_redo->get_history_count() == 1);
set_property_action(undo_redo, "Merge Action 1", test_object, 1000, UndoRedo::MERGE_ALL);
CHECK(test_object->get_property() == 1000);
CHECK(undo_redo->get_version() == 2);
CHECK(undo_redo->get_history_count() == 1);
memdelete(test_object);
memdelete(undo_redo);
}
TEST_CASE("[UndoRedo] Merge Method UndoRedo") {
GDREGISTER_CLASS(_TestUndoRedoObject);
UndoRedo *undo_redo = memnew(UndoRedo());
_TestUndoRedoObject *test_object = memnew(_TestUndoRedoObject());
CHECK(test_object->get_property() == 0);
CHECK(undo_redo->get_version() == 1);
CHECK(undo_redo->get_history_count() == 0);
increment_property_action(undo_redo, "Merge Increment 1", test_object, 10, UndoRedo::MERGE_ALL);
CHECK(test_object->get_property() == 10);
CHECK(undo_redo->get_version() == 2);
CHECK(undo_redo->get_history_count() == 1);
increment_property_action(undo_redo, "Merge Increment 1", test_object, 10, UndoRedo::MERGE_ALL);
CHECK(test_object->get_property() == 20);
CHECK(undo_redo->get_version() == 2);
CHECK(undo_redo->get_history_count() == 1);
increment_property_action(undo_redo, "Merge Increment 1", test_object, 10, UndoRedo::MERGE_ALL);
CHECK(test_object->get_property() == 30);
CHECK(undo_redo->get_version() == 2);
CHECK(undo_redo->get_history_count() == 1);
undo_redo->undo();
CHECK(test_object->get_property() == 0);
CHECK(undo_redo->get_version() == 1);
CHECK(undo_redo->get_history_count() == 1);
undo_redo->redo();
CHECK(test_object->get_property() == 30);
CHECK(undo_redo->get_version() == 2);
CHECK(undo_redo->get_history_count() == 1);
memdelete(test_object);
memdelete(undo_redo);
}
} //namespace TestUndoRedo

203
tests/core/os/test_os.h Normal file
View File

@@ -0,0 +1,203 @@
/**************************************************************************/
/* test_os.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/os/os.h"
#include "thirdparty/doctest/doctest.h"
namespace TestOS {
TEST_CASE("[OS] Environment variables") {
#ifdef WINDOWS_ENABLED
CHECK_MESSAGE(
OS::get_singleton()->has_environment("USERPROFILE"),
"The USERPROFILE environment variable should be present.");
#else
CHECK_MESSAGE(
OS::get_singleton()->has_environment("HOME"),
"The HOME environment variable should be present.");
#endif
}
TEST_CASE("[OS] UTF-8 environment variables") {
String value = String::utf8("hell\xc3\xb6"); // "hellö", UTF-8 encoded
OS::get_singleton()->set_environment("HELLO", value);
String val = OS::get_singleton()->get_environment("HELLO");
CHECK_MESSAGE(
val == value,
"The previously-set HELLO environment variable should return the expected value.");
CHECK_MESSAGE(
val.length() == 5,
"The previously-set HELLO environment variable was decoded as UTF-8 and should have a length of 5.");
OS::get_singleton()->unset_environment("HELLO");
}
TEST_CASE("[OS] Non-UTF-8 environment variables") {
String value = String("\xff t\xf6rkylempij\xe4vongahdus"); // hex FF and a Finnish pangram, latin-1
OS::get_singleton()->set_environment("HELLO", value);
String val = OS::get_singleton()->get_environment("HELLO");
CHECK_MESSAGE(
val == value,
"The previously-set HELLO environment variable should return the expected value.");
CHECK_MESSAGE(
val.length() == 23,
"The previously-set HELLO environment variable was not decoded from Latin-1.");
OS::get_singleton()->unset_environment("HELLO");
}
TEST_CASE("[OS] Command line arguments") {
List<String> arguments = OS::get_singleton()->get_cmdline_args();
bool found = false;
for (const String &arg : arguments) {
if (arg == "--test") {
found = true;
break;
}
}
CHECK_MESSAGE(
found,
"The `--test` option must be present in the list of command line arguments.");
}
TEST_CASE("[OS] Executable and data paths") {
CHECK_MESSAGE(
OS::get_singleton()->get_executable_path().is_absolute_path(),
"The executable path returned should be an absolute path.");
CHECK_MESSAGE(
OS::get_singleton()->get_data_path().is_absolute_path(),
"The user data path returned should be an absolute path.");
CHECK_MESSAGE(
OS::get_singleton()->get_config_path().is_absolute_path(),
"The user configuration path returned should be an absolute path.");
CHECK_MESSAGE(
OS::get_singleton()->get_cache_path().is_absolute_path(),
"The cache path returned should be an absolute path.");
}
TEST_CASE("[OS] Ticks") {
CHECK_MESSAGE(
OS::get_singleton()->get_ticks_usec() > 1000,
"The returned ticks (in microseconds) must be greater than 1,000.");
CHECK_MESSAGE(
OS::get_singleton()->get_ticks_msec() > 1,
"The returned ticks (in milliseconds) must be greater than 1.");
}
TEST_CASE("[OS] Feature tags") {
#ifdef TOOLS_ENABLED
CHECK_MESSAGE(
OS::get_singleton()->has_feature("editor"),
"The binary has the \"editor\" feature tag.");
CHECK_MESSAGE(
!OS::get_singleton()->has_feature("template"),
"The binary does not have the \"template\" feature tag.");
CHECK_MESSAGE(
!OS::get_singleton()->has_feature("template_debug"),
"The binary does not have the \"template_debug\" feature tag.");
CHECK_MESSAGE(
!OS::get_singleton()->has_feature("template_release"),
"The binary does not have the \"template_release\" feature tag.");
#else
CHECK_MESSAGE(
!OS::get_singleton()->has_feature("editor"),
"The binary does not have the \"editor\" feature tag.");
CHECK_MESSAGE(
OS::get_singleton()->has_feature("template"),
"The binary has the \"template\" feature tag.");
#ifdef DEBUG_ENABLED
CHECK_MESSAGE(
OS::get_singleton()->has_feature("template_debug"),
"The binary has the \"template_debug\" feature tag.");
CHECK_MESSAGE(
!OS::get_singleton()->has_feature("template_release"),
"The binary does not have the \"template_release\" feature tag.");
#else
CHECK_MESSAGE(
!OS::get_singleton()->has_feature("template_debug"),
"The binary does not have the \"template_debug\" feature tag.");
CHECK_MESSAGE(
OS::get_singleton()->has_feature("template_release"),
"The binary has the \"template_release\" feature tag.");
#endif // DEBUG_ENABLED
#endif // TOOLS_ENABLED
}
TEST_CASE("[OS] Process ID") {
CHECK_MESSAGE(
OS::get_singleton()->get_process_id() >= 1,
"The returned process ID should be greater than zero.");
}
TEST_CASE("[OS] Processor count and memory information") {
CHECK_MESSAGE(
OS::get_singleton()->get_processor_count() >= 1,
"The returned processor count should be greater than zero.");
#ifdef DEBUG_ENABLED
CHECK_MESSAGE(
OS::get_singleton()->get_static_memory_usage() >= 1,
"The returned static memory usage should be greater than zero.");
CHECK_MESSAGE(
OS::get_singleton()->get_static_memory_peak_usage() >= 1,
"The returned static memory peak usage should be greater than zero.");
#endif // DEBUG_ENABLED
}
TEST_CASE("[OS] Execute") {
#ifdef WINDOWS_ENABLED
List<String> arguments;
arguments.push_back("/C");
arguments.push_back("dir > NUL");
int exit_code;
const Error err = OS::get_singleton()->execute("cmd", arguments, nullptr, &exit_code);
CHECK_MESSAGE(
err == OK,
"(Running the command `cmd /C \"dir > NUL\"` returns the expected Godot error code (OK).");
CHECK_MESSAGE(
exit_code == 0,
"Running the command `cmd /C \"dir > NUL\"` returns a zero (successful) exit code.");
#else
List<String> arguments;
arguments.push_back("-c");
arguments.push_back("ls > /dev/null");
int exit_code;
const Error err = OS::get_singleton()->execute("sh", arguments, nullptr, &exit_code);
CHECK_MESSAGE(
err == OK,
"(Running the command `sh -c \"ls > /dev/null\"` returns the expected Godot error code (OK).");
CHECK_MESSAGE(
exit_code == 0,
"Running the command `sh -c \"ls > /dev/null\"` returns a zero (successful) exit code.");
#endif
}
} // namespace TestOS

View File

@@ -0,0 +1,80 @@
/**************************************************************************/
/* test_fuzzy_search.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/string/fuzzy_search.h"
#include "tests/test_macros.h"
namespace TestFuzzySearch {
struct FuzzySearchTestCase {
String query;
String expected;
};
// Ideally each of these test queries should represent a different aspect, and potentially bottleneck, of the search process.
const FuzzySearchTestCase test_cases[] = {
// Short query, many matches, few adjacent characters
{ "///gd", "./menu/hud/hud.gd" },
// Filename match with typo
{ "sm.png", "./entity/blood_sword/sam.png" },
// Multipart filename word matches
{ "ham ", "./entity/game_trap/ha_missed_me.wav" },
// Single word token matches
{ "push background", "./entity/background_zone1/background/push.png" },
// Long token matches
{ "background_freighter background png", "./entity/background_freighter/background/background.png" },
// Many matches, many short tokens
{ "menu menu characters wav", "./menu/menu/characters/smoker/0.wav" },
// Maximize total matches
{ "entity gd", "./entity/entity_man.gd" }
};
Vector<String> load_test_data() {
Ref<FileAccess> fp = FileAccess::open(TestUtils::get_data_path("fuzzy_search/project_dir_tree.txt"), FileAccess::READ);
REQUIRE(fp.is_valid());
return fp->get_as_utf8_string().split("\n");
}
TEST_CASE("[FuzzySearch] Test fuzzy search results") {
FuzzySearch search;
Vector<FuzzySearchResult> results;
Vector<String> targets = load_test_data();
for (FuzzySearchTestCase test_case : test_cases) {
search.set_query(test_case.query);
search.search_all(targets, results);
CHECK_GT(results.size(), 0);
CHECK_EQ(results[0].target, test_case.expected);
}
}
} //namespace TestFuzzySearch

View File

@@ -0,0 +1,225 @@
/**************************************************************************/
/* test_node_path.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/string/node_path.h"
#include "tests/test_macros.h"
namespace TestNodePath {
TEST_CASE("[NodePath] Relative path") {
const NodePath node_path_relative = NodePath("Path2D/PathFollow2D/Sprite2D:position:x");
CHECK_MESSAGE(
node_path_relative.get_as_property_path() == NodePath(":Path2D/PathFollow2D/Sprite2D:position:x"),
"The returned property path should match the expected value.");
CHECK_MESSAGE(
node_path_relative.get_concatenated_subnames() == "position:x",
"The returned concatenated subnames should match the expected value.");
CHECK_MESSAGE(
node_path_relative.get_name(0) == "Path2D",
"The returned name at index 0 should match the expected value.");
CHECK_MESSAGE(
node_path_relative.get_name(1) == "PathFollow2D",
"The returned name at index 1 should match the expected value.");
CHECK_MESSAGE(
node_path_relative.get_name(2) == "Sprite2D",
"The returned name at index 2 should match the expected value.");
ERR_PRINT_OFF;
CHECK_MESSAGE(
node_path_relative.get_name(3) == "",
"The returned name at invalid index 3 should match the expected value.");
CHECK_MESSAGE(
node_path_relative.get_name(-1) == "",
"The returned name at invalid index -1 should match the expected value.");
ERR_PRINT_ON;
CHECK_MESSAGE(
node_path_relative.get_name_count() == 3,
"The returned number of names should match the expected value.");
CHECK_MESSAGE(
node_path_relative.get_subname(0) == "position",
"The returned subname at index 0 should match the expected value.");
CHECK_MESSAGE(
node_path_relative.get_subname(1) == "x",
"The returned subname at index 1 should match the expected value.");
ERR_PRINT_OFF;
CHECK_MESSAGE(
node_path_relative.get_subname(2) == "",
"The returned subname at invalid index 2 should match the expected value.");
CHECK_MESSAGE(
node_path_relative.get_subname(-1) == "",
"The returned subname at invalid index -1 should match the expected value.");
ERR_PRINT_ON;
CHECK_MESSAGE(
node_path_relative.get_subname_count() == 2,
"The returned number of subnames should match the expected value.");
CHECK_MESSAGE(
!node_path_relative.is_absolute(),
"The node path should be considered relative.");
CHECK_MESSAGE(
!node_path_relative.is_empty(),
"The node path shouldn't be considered empty.");
}
TEST_CASE("[NodePath] Absolute path") {
const NodePath node_path_absolute = NodePath("/root/Sprite2D");
CHECK_MESSAGE(
node_path_absolute.get_as_property_path() == NodePath(":root/Sprite2D"),
"The returned property path should match the expected value.");
CHECK_MESSAGE(
node_path_absolute.get_concatenated_subnames() == "",
"The returned concatenated subnames should match the expected value.");
CHECK_MESSAGE(
node_path_absolute.get_name(0) == "root",
"The returned name at index 0 should match the expected value.");
CHECK_MESSAGE(
node_path_absolute.get_name(1) == "Sprite2D",
"The returned name at index 1 should match the expected value.");
ERR_PRINT_OFF;
CHECK_MESSAGE(
node_path_absolute.get_name(2) == "",
"The returned name at invalid index 2 should match the expected value.");
CHECK_MESSAGE(
node_path_absolute.get_name(-1) == "",
"The returned name at invalid index -1 should match the expected value.");
ERR_PRINT_ON;
CHECK_MESSAGE(
node_path_absolute.get_name_count() == 2,
"The returned number of names should match the expected value.");
CHECK_MESSAGE(
node_path_absolute.get_subname_count() == 0,
"The returned number of subnames should match the expected value.");
CHECK_MESSAGE(
node_path_absolute.is_absolute(),
"The node path should be considered absolute.");
CHECK_MESSAGE(
!node_path_absolute.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.");
}
TEST_CASE("[NodePath] Slice") {
const NodePath node_path_relative = NodePath("Parent/Child:prop:subprop");
const NodePath node_path_absolute = NodePath("/root/Parent/Child:prop");
CHECK_MESSAGE(
node_path_relative.slice(0, 2) == NodePath("Parent/Child"),
"The slice lower bound should be inclusive and the slice upper bound should be exclusive.");
CHECK_MESSAGE(
node_path_relative.slice(3) == NodePath(":subprop"),
"Slicing on the last index (length - 1) should return the last entry.");
CHECK_MESSAGE(
node_path_relative.slice(1) == NodePath("Child:prop:subprop"),
"Slicing without upper bound should return remaining entries after index.");
CHECK_MESSAGE(
node_path_relative.slice(1, 3) == NodePath("Child:prop"),
"Slicing should include names and subnames.");
CHECK_MESSAGE(
node_path_relative.slice(-1) == NodePath(":subprop"),
"Slicing on -1 should return the last entry.");
CHECK_MESSAGE(
node_path_relative.slice(0, -1) == NodePath("Parent/Child:prop"),
"Slicing up to -1 should include the second-to-last entry.");
CHECK_MESSAGE(
node_path_relative.slice(-2, -1) == NodePath(":prop"),
"Slicing from negative to negative should treat lower bound as inclusive and upper bound as exclusive.");
CHECK_MESSAGE(
node_path_relative.slice(0, 10) == NodePath("Parent/Child:prop:subprop"),
"Slicing past the length of the path should work like slicing up to the last entry.");
CHECK_MESSAGE(
node_path_relative.slice(-10, 2) == NodePath("Parent/Child"),
"Slicing negatively past the length of the path should work like slicing from the first entry.");
CHECK_MESSAGE(
node_path_relative.slice(1, 1) == NodePath(""),
"Slicing with a lower bound equal to upper bound should return empty path.");
CHECK_MESSAGE(
node_path_absolute.slice(0, 2) == NodePath("/root/Parent"),
"Slice from beginning of an absolute path should be an absolute path.");
CHECK_MESSAGE(
node_path_absolute.slice(1, 4) == NodePath("Parent/Child:prop"),
"Slice of an absolute path that does not start at the beginning should be a relative path.");
CHECK_MESSAGE(
node_path_absolute.slice(3, 4) == NodePath(":prop"),
"Slice of an absolute path that does not start at the beginning should be a relative path.");
CHECK_MESSAGE(
NodePath("").slice(0, 1) == NodePath(""),
"Slice of an empty path should be an empty path.");
CHECK_MESSAGE(
NodePath("").slice(-1, 2) == NodePath(""),
"Slice of an empty path should be an empty path.");
CHECK_MESSAGE(
NodePath("/").slice(-1, 2) == NodePath("/"),
"Slice of an empty absolute path should be an empty absolute path.");
}
} // namespace TestNodePath

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,202 @@
/**************************************************************************/
/* test_translation.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/string/optimized_translation.h"
#include "core/string/translation.h"
#include "core/string/translation_po.h"
#include "core/string/translation_server.h"
#ifdef TOOLS_ENABLED
#include "editor/import/resource_importer_csv_translation.h"
#endif
#include "tests/test_macros.h"
#include "tests/test_utils.h"
namespace TestTranslation {
TEST_CASE("[Translation] Messages") {
Ref<Translation> translation = memnew(Translation);
translation->set_locale("fr");
translation->add_message("Hello", "Bonjour");
CHECK(translation->get_message("Hello") == "Bonjour");
translation->erase_message("Hello");
// The message no longer exists, so it returns an empty string instead.
CHECK(translation->get_message("Hello") == "");
List<StringName> messages;
translation->get_message_list(&messages);
CHECK(translation->get_message_count() == 0);
CHECK(messages.size() == 0);
translation->add_message("Hello2", "Bonjour2");
translation->add_message("Hello3", "Bonjour3");
messages.clear();
translation->get_message_list(&messages);
CHECK(translation->get_message_count() == 2);
CHECK(messages.size() == 2);
// Messages are stored in a Map, don't assume ordering.
CHECK(messages.find("Hello2"));
CHECK(messages.find("Hello3"));
}
TEST_CASE("[TranslationPO] Messages with context") {
Ref<TranslationPO> translation = memnew(TranslationPO);
translation->set_locale("fr");
translation->add_message("Hello", "Bonjour");
translation->add_message("Hello", "Salut", "friendly");
CHECK(translation->get_message("Hello") == "Bonjour");
CHECK(translation->get_message("Hello", "friendly") == "Salut");
CHECK(translation->get_message("Hello", "nonexistent_context") == "");
// Only remove the message for the default context, not the "friendly" context.
translation->erase_message("Hello");
// The message no longer exists, so it returns an empty string instead.
CHECK(translation->get_message("Hello") == "");
CHECK(translation->get_message("Hello", "friendly") == "Salut");
CHECK(translation->get_message("Hello", "nonexistent_context") == "");
List<StringName> messages;
translation->get_message_list(&messages);
// `get_message_count()` takes all contexts into account.
CHECK(translation->get_message_count() == 1);
// Only the default context is taken into account.
// Since "Hello" is now only present in a non-default context, it is not counted in the list of messages.
CHECK(messages.size() == 0);
translation->add_message("Hello2", "Bonjour2");
translation->add_message("Hello2", "Salut2", "friendly");
translation->add_message("Hello3", "Bonjour3");
messages.clear();
translation->get_message_list(&messages);
// `get_message_count()` takes all contexts into account.
CHECK(translation->get_message_count() == 4);
// Only the default context is taken into account.
CHECK(messages.size() == 2);
// Messages are stored in a Map, don't assume ordering.
CHECK(messages.find("Hello2"));
CHECK(messages.find("Hello3"));
}
TEST_CASE("[TranslationPO] Plural messages") {
Ref<TranslationPO> translation = memnew(TranslationPO);
translation->set_locale("fr");
translation->set_plural_rule("Plural-Forms: nplurals=2; plural=(n >= 2);");
CHECK(translation->get_plural_forms() == 2);
PackedStringArray plurals;
plurals.push_back("Il y a %d pomme");
plurals.push_back("Il y a %d pommes");
translation->add_plural_message("There are %d apples", plurals);
ERR_PRINT_OFF;
// This is invalid, as the number passed to `get_plural_message()` may not be negative.
CHECK(vformat(translation->get_plural_message("There are %d apples", "", -1), -1) == "");
ERR_PRINT_ON;
CHECK(vformat(translation->get_plural_message("There are %d apples", "", 0), 0) == "Il y a 0 pomme");
CHECK(vformat(translation->get_plural_message("There are %d apples", "", 1), 1) == "Il y a 1 pomme");
CHECK(vformat(translation->get_plural_message("There are %d apples", "", 2), 2) == "Il y a 2 pommes");
}
#ifdef TOOLS_ENABLED
TEST_CASE("[OptimizedTranslation] Generate from Translation and read messages") {
Ref<Translation> translation = memnew(Translation);
translation->set_locale("fr");
translation->add_message("Hello", "Bonjour");
translation->add_message("Hello2", "Bonjour2");
translation->add_message("Hello3", "Bonjour3");
Ref<OptimizedTranslation> optimized_translation = memnew(OptimizedTranslation);
optimized_translation->generate(translation);
CHECK(optimized_translation->get_message("Hello") == "Bonjour");
CHECK(optimized_translation->get_message("Hello2") == "Bonjour2");
CHECK(optimized_translation->get_message("Hello3") == "Bonjour3");
CHECK(optimized_translation->get_message("DoesNotExist") == "");
List<StringName> messages;
// `get_message_list()` can't return the list of messages stored in an OptimizedTranslation.
optimized_translation->get_message_list(&messages);
CHECK(optimized_translation->get_message_count() == 0);
CHECK(messages.size() == 0);
}
TEST_CASE("[TranslationCSV] CSV import") {
Ref<ResourceImporterCSVTranslation> import_csv_translation = memnew(ResourceImporterCSVTranslation);
HashMap<StringName, Variant> options;
options["compress"] = false;
options["delimiter"] = 0;
List<String> gen_files;
Error result = import_csv_translation->import(0, TestUtils::get_data_path("translations.csv"),
"", options, nullptr, &gen_files);
CHECK(result == OK);
CHECK(gen_files.size() == 4);
TranslationServer *ts = TranslationServer::get_singleton();
for (const String &file : gen_files) {
Ref<Translation> translation = ResourceLoader::load(file);
CHECK(translation.is_valid());
ts->add_translation(translation);
}
ts->set_locale("en");
// `tr` can be called on any Object, we reuse TranslationServer for convenience.
CHECK(ts->tr("GOOD_MORNING") == "Good Morning");
CHECK(ts->tr("GOOD_EVENING") == "Good Evening");
ts->set_locale("de");
CHECK(ts->tr("GOOD_MORNING") == "Guten Morgen");
CHECK(ts->tr("GOOD_EVENING") == "Good Evening"); // Left blank in CSV, should source from 'en'.
ts->set_locale("ja");
CHECK(ts->tr("GOOD_MORNING") == String::utf8("おはよう"));
CHECK(ts->tr("GOOD_EVENING") == String::utf8("こんばんは"));
/* FIXME: This passes, but triggers a chain reaction that makes test_viewport
* and test_text_edit explode in a billion glittery Unicode particles.
ts->set_locale("fa");
CHECK(ts->tr("GOOD_MORNING") == String::utf8("صبح بخیر"));
CHECK(ts->tr("GOOD_EVENING") == String::utf8("عصر بخیر"));
*/
}
#endif // TOOLS_ENABLED
} // namespace TestTranslation

View File

@@ -0,0 +1,223 @@
/**************************************************************************/
/* test_translation_server.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/string/translation_server.h"
#include "tests/test_macros.h"
namespace TestTranslationServer {
TEST_CASE("[TranslationServer] Translation operations") {
Ref<Translation> t1 = memnew(Translation);
t1->set_locale("uk");
t1->add_message("Good Morning", String(U"Добрий ранок"));
Ref<Translation> t2 = memnew(Translation);
t2->set_locale("uk");
t2->add_message("Hello Godot", String(U"你好戈多"));
TranslationServer *ts = TranslationServer::get_singleton();
// Adds translation for UK locale for the first time.
int l_count_before = ts->get_loaded_locales().size();
ts->add_translation(t1);
int l_count_after = ts->get_loaded_locales().size();
CHECK(l_count_after > l_count_before);
// Adds translation for UK locale again.
ts->add_translation(t2);
CHECK_EQ(ts->get_loaded_locales().size(), l_count_after);
// Removing that translation.
ts->remove_translation(t2);
CHECK_EQ(ts->get_loaded_locales().size(), l_count_after);
CHECK(ts->get_translation_object("uk").is_valid());
ts->set_locale("uk");
CHECK(ts->translate("Good Morning") == String::utf8("Добрий ранок"));
ts->remove_translation(t1);
CHECK(ts->get_translation_object("uk").is_null());
// If no suitable Translation object has been found - the original message should be returned.
CHECK(ts->translate("Good Morning") == "Good Morning");
}
TEST_CASE("[TranslationServer] Locale operations") {
TranslationServer *ts = TranslationServer::get_singleton();
// Language variant test; we supplied the variant of Español and the result should be the same string.
String loc = "es_Hani_ES_tradnl";
String res = ts->standardize_locale(loc);
CHECK(res == loc);
// No such variant in variant_map; should return everything except the variant.
loc = "es_Hani_ES_missing";
res = ts->standardize_locale(loc);
CHECK(res == "es_Hani_ES");
// Non-ISO language name check (Windows issue).
loc = "iw_Hani_IL";
res = ts->standardize_locale(loc);
CHECK(res == "he_Hani_IL");
// Country rename check.
loc = "uk_Hani_UK";
res = ts->standardize_locale(loc);
CHECK(res == "uk_Hani_GB");
// Supplying a script name that is not in the list.
loc = "de_Wrong_DE";
res = ts->standardize_locale(loc);
CHECK(res == "de_DE");
// No added defaults.
loc = "es_ES";
res = ts->standardize_locale(loc, true);
CHECK(res == "es_ES");
// Add default script.
loc = "az_AZ";
res = ts->standardize_locale(loc, true);
CHECK(res == "az_Latn_AZ");
// Add default country.
loc = "pa_Arab";
res = ts->standardize_locale(loc, true);
CHECK(res == "pa_Arab_PK");
// Add default script and country.
loc = "zh";
res = ts->standardize_locale(loc, true);
CHECK(res == "zh_Hans_CN");
// Explicitly don't add defaults.
loc = "zh";
res = ts->standardize_locale(loc, false);
CHECK(res == "zh");
}
TEST_CASE("[TranslationServer] Comparing locales") {
TranslationServer *ts = TranslationServer::get_singleton();
String locale_a = "es";
String locale_b = "es";
// Exact match check.
int res = ts->compare_locales(locale_a, locale_b);
CHECK(res == 10);
locale_a = "sr-Latn-CS";
locale_b = "sr-Latn-RS";
// Script matches (+1) but country doesn't (-1).
res = ts->compare_locales(locale_a, locale_b);
CHECK(res == 5);
locale_a = "uz-Cyrl-UZ";
locale_b = "uz-Latn-UZ";
// Country matches (+1) but script doesn't (-1).
res = ts->compare_locales(locale_a, locale_b);
CHECK(res == 5);
locale_a = "aa-Latn-ER";
locale_b = "aa-Latn-ER-saaho";
// Script and country match (+2) with variant on one locale (+0).
res = ts->compare_locales(locale_a, locale_b);
CHECK(res == 7);
locale_a = "uz-Cyrl-UZ";
locale_b = "uz-Latn-KG";
// Both script and country mismatched (-2).
res = ts->compare_locales(locale_a, locale_b);
CHECK(res == 3);
locale_a = "es-ES";
locale_b = "es-AR";
// Mismatched country (-1).
res = ts->compare_locales(locale_a, locale_b);
CHECK(res == 4);
locale_a = "es";
locale_b = "es-AR";
// No country for one locale (+0).
res = ts->compare_locales(locale_a, locale_b);
CHECK(res == 5);
locale_a = "es-EC";
locale_b = "fr-LU";
// No match.
res = ts->compare_locales(locale_a, locale_b);
CHECK(res == 0);
locale_a = "zh-HK";
locale_b = "zh";
// In full standardization, zh-HK becomes zh_Hant_HK and zh becomes
// zh_Hans_CN. Both script and country mismatch (-2).
res = ts->compare_locales(locale_a, locale_b);
CHECK(res == 3);
locale_a = "zh-CN";
locale_b = "zh";
// In full standardization, zh and zh-CN both become zh_Hans_CN for an
// exact match.
res = ts->compare_locales(locale_a, locale_b);
CHECK(res == 10);
}
} // namespace TestTranslationServer

View File

@@ -0,0 +1,314 @@
/**************************************************************************/
/* test_a_hash_map.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/templates/a_hash_map.h"
#include "tests/test_macros.h"
namespace TestAHashMap {
TEST_CASE("[AHashMap] List initialization") {
AHashMap<int, String> map{ { 0, "A" }, { 1, "B" }, { 2, "C" }, { 3, "D" }, { 4, "E" } };
CHECK(map.size() == 5);
CHECK(map[0] == "A");
CHECK(map[1] == "B");
CHECK(map[2] == "C");
CHECK(map[3] == "D");
CHECK(map[4] == "E");
}
TEST_CASE("[AHashMap] List initialization with existing elements") {
AHashMap<int, String> map{ { 0, "A" }, { 0, "B" }, { 0, "C" }, { 0, "D" }, { 0, "E" } };
CHECK(map.size() == 1);
CHECK(map[0] == "E");
}
TEST_CASE("[AHashMap] Insert element") {
AHashMap<int, int> map;
AHashMap<int, int>::Iterator e = map.insert(42, 84);
CHECK(e);
CHECK(e->key == 42);
CHECK(e->value == 84);
CHECK(map[42] == 84);
CHECK(map.has(42));
CHECK(map.find(42));
}
TEST_CASE("[AHashMap] Overwrite element") {
AHashMap<int, int> map;
map.insert(42, 84);
map.insert(42, 1234);
CHECK(map[42] == 1234);
}
TEST_CASE("[AHashMap] Erase via element") {
AHashMap<int, int> map;
AHashMap<int, int>::Iterator e = map.insert(42, 84);
map.remove(e);
CHECK(!map.has(42));
CHECK(!map.find(42));
}
TEST_CASE("[AHashMap] Erase via key") {
AHashMap<int, int> map;
map.insert(42, 84);
map.erase(42);
CHECK(!map.has(42));
CHECK(!map.find(42));
}
TEST_CASE("[AHashMap] Size") {
AHashMap<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("[AHashMap] Iteration") {
AHashMap<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 (const KeyValue<int, int> &E : map) {
CHECK(expected[idx] == Pair<int, int>(E.key, E.value));
idx++;
}
idx--;
for (AHashMap<int, int>::Iterator it = map.last(); it; --it) {
CHECK(expected[idx] == Pair<int, int>(it->key, it->value));
idx--;
}
}
TEST_CASE("[AHashMap] Const iteration") {
AHashMap<int, int> map;
map.insert(42, 84);
map.insert(123, 12385);
map.insert(0, 12934);
map.insert(123485, 1238888);
map.insert(123, 111111);
const AHashMap<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));
expected.push_back(Pair<int, int>(123, 111111));
int idx = 0;
for (const KeyValue<int, int> &E : const_map) {
CHECK(expected[idx] == Pair<int, int>(E.key, E.value));
idx++;
}
idx--;
for (AHashMap<int, int>::ConstIterator it = const_map.last(); it; --it) {
CHECK(expected[idx] == Pair<int, int>(it->key, it->value));
idx--;
}
}
TEST_CASE("[AHashMap] Replace key") {
AHashMap<int, int> map;
map.insert(42, 84);
map.insert(0, 12934);
CHECK(map.replace_key(0, 1));
CHECK(map.has(1));
CHECK(map[1] == 12934);
}
TEST_CASE("[AHashMap] Clear") {
AHashMap<int, int> map;
map.insert(42, 84);
map.insert(123, 12385);
map.insert(0, 12934);
map.clear();
CHECK(!map.has(42));
CHECK(map.size() == 0);
CHECK(map.is_empty());
}
TEST_CASE("[AHashMap] Get") {
AHashMap<int, int> map;
map.insert(42, 84);
map.insert(123, 12385);
map.insert(0, 12934);
CHECK(map.get(123) == 12385);
map.get(123) = 10;
CHECK(map.get(123) == 10);
CHECK(*map.getptr(0) == 12934);
*map.getptr(0) = 1;
CHECK(*map.getptr(0) == 1);
CHECK(map.get(42) == 84);
CHECK(map.getptr(-10) == nullptr);
}
TEST_CASE("[AHashMap] Insert, iterate and remove many elements") {
const int elem_max = 1234;
AHashMap<int, int> map;
for (int i = 0; i < elem_max; i++) {
map.insert(i, i);
}
//insert order should have been kept
int idx = 0;
for (const KeyValue<int, int> &K : map) {
CHECK(idx == K.key);
CHECK(idx == K.value);
CHECK(map.has(idx));
idx++;
}
Vector<int> elems_still_valid;
for (int i = 0; i < elem_max; i++) {
if ((i % 5) == 0) {
map.erase(i);
} else {
elems_still_valid.push_back(i);
}
}
CHECK(elems_still_valid.size() == map.size());
for (int i = 0; i < elems_still_valid.size(); i++) {
CHECK(map.has(elems_still_valid[i]));
}
}
TEST_CASE("[AHashMap] Insert, iterate and remove many strings") {
const int elem_max = 432;
AHashMap<String, String> map;
// To not print WARNING: Excessive collision count (NN), is the right hash function being used?
ERR_PRINT_OFF;
for (int i = 0; i < elem_max; i++) {
map.insert(itos(i), itos(i));
}
ERR_PRINT_ON;
//insert order should have been kept
int idx = 0;
for (auto &K : map) {
CHECK(itos(idx) == K.key);
CHECK(itos(idx) == K.value);
CHECK(map.has(itos(idx)));
idx++;
}
Vector<String> elems_still_valid;
for (int i = 0; i < elem_max; i++) {
if ((i % 5) == 0) {
map.erase(itos(i));
} else {
elems_still_valid.push_back(itos(i));
}
}
CHECK(elems_still_valid.size() == map.size());
for (int i = 0; i < elems_still_valid.size(); i++) {
CHECK(map.has(elems_still_valid[i]));
}
elems_still_valid.clear();
}
TEST_CASE("[AHashMap] Copy constructor") {
AHashMap<int, int> map0;
const uint32_t count = 5;
for (uint32_t i = 0; i < count; i++) {
map0.insert(i, i);
}
AHashMap<int, int> map1(map0);
CHECK(map0.size() == map1.size());
CHECK(map0.get_capacity() == map1.get_capacity());
CHECK(*map0.getptr(0) == *map1.getptr(0));
}
TEST_CASE("[AHashMap] Operator =") {
AHashMap<int, int> map0;
AHashMap<int, int> map1;
const uint32_t count = 5;
map1.insert(1234, 1234);
for (uint32_t i = 0; i < count; i++) {
map0.insert(i, i);
}
map1 = map0;
CHECK(map0.size() == map1.size());
CHECK(map0.get_capacity() == map1.get_capacity());
CHECK(*map0.getptr(0) == *map1.getptr(0));
}
TEST_CASE("[AHashMap] Array methods") {
AHashMap<int, int> map;
for (int i = 0; i < 100; i++) {
map.insert(100 - i, i);
}
for (int i = 0; i < 100; i++) {
CHECK(map.get_by_index(i).value == i);
}
int index = map.get_index(1);
CHECK(map.get_by_index(index).value == 99);
CHECK(map.erase_by_index(index));
CHECK(!map.erase_by_index(index));
CHECK(map.get_index(1) == -1);
}
} // namespace TestAHashMap

View File

@@ -0,0 +1,563 @@
/**************************************************************************/
/* test_command_queue.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/config/project_settings.h"
#include "core/math/random_number_generator.h"
#include "core/object/worker_thread_pool.h"
#include "core/os/os.h"
#include "core/os/thread.h"
#include "core/templates/command_queue_mt.h"
#include "tests/test_macros.h"
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;
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;
WorkerThreadPool::TaskID reader_task_id = WorkerThreadPool::INVALID_TASK_ID;
Thread writer_thread;
int func1_count = 0;
void func1(Transform3D t) {
func1_count++;
}
void func2(Transform3D t, float f) {
func1_count++;
}
void func3(Transform3D t1, Transform3D t2, Transform3D t3, Transform3D t4, Transform3D t5, Transform3D t6) {
func1_count++;
}
Transform3D func1r(Transform3D t) {
func1_count++;
return t;
}
Transform3D func2r(Transform3D t, float f) {
func1_count++;
return t;
}
void add_msg_to_write(TestMsgType type) {
message_types_to_write.push_back(type);
}
void reader_thread_loop() {
reader_threadwork.thread_wait_for_work();
while (!exit_threads) {
if (reader_task_id == WorkerThreadPool::INVALID_TASK_ID) {
command_queue.flush_all();
} else {
if (message_count_to_read < 0) {
command_queue.flush_all();
}
for (int i = 0; i < message_count_to_read; i++) {
WorkerThreadPool::get_singleton()->yield();
command_queue.wait_and_flush();
}
}
message_count_to_read = 0;
reader_threadwork.thread_done_work();
reader_threadwork.thread_wait_for_work();
}
command_queue.flush_all();
reader_threadwork.thread_done_work();
}
static void static_reader_thread_loop(void *stsvoid) {
SharedThreadState *sts = static_cast<SharedThreadState *>(stsvoid);
sts->reader_thread_loop();
}
void writer_thread_loop() {
during_writing = false;
writer_threadwork.thread_wait_for_work();
while (!exit_threads) {
Transform3D tr;
Transform3D otr;
float f = 1;
during_writing = true;
for (int i = 0; i < message_types_to_write.size(); i++) {
TestMsgType msg_type = message_types_to_write[i];
switch (msg_type) {
case TEST_MSG_FUNC1_TRANSFORM:
command_queue.push(this, &SharedThreadState::func1, tr);
break;
case TEST_MSG_FUNC2_TRANSFORM_FLOAT:
command_queue.push(this, &SharedThreadState::func2, tr, f);
break;
case TEST_MSG_FUNC3_TRANSFORMx6:
command_queue.push(this, &SharedThreadState::func3, tr, tr, tr, tr, tr, tr);
break;
case TEST_MSGSYNC_FUNC1_TRANSFORM:
command_queue.push_and_sync(this, &SharedThreadState::func1, tr);
break;
case TEST_MSGSYNC_FUNC2_TRANSFORM_FLOAT:
command_queue.push_and_sync(this, &SharedThreadState::func2, tr, f);
break;
case TEST_MSGRET_FUNC1_TRANSFORM:
command_queue.push_and_ret(this, &SharedThreadState::func1r, &otr, tr);
break;
case TEST_MSGRET_FUNC2_TRANSFORM_FLOAT:
command_queue.push_and_ret(this, &SharedThreadState::func2r, &otr, tr, f);
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(bool p_use_thread_pool_sync = false) {
if (p_use_thread_pool_sync) {
reader_task_id = WorkerThreadPool::get_singleton()->add_native_task(&SharedThreadState::static_reader_thread_loop, this, true);
command_queue.set_pump_task_id(reader_task_id);
} else {
reader_thread.start(&SharedThreadState::static_reader_thread_loop, this);
}
writer_thread.start(&SharedThreadState::static_writer_thread_loop, this);
}
void destroy_threads() {
exit_threads = true;
reader_threadwork.main_start_work();
writer_threadwork.main_start_work();
if (reader_task_id != WorkerThreadPool::INVALID_TASK_ID) {
WorkerThreadPool::get_singleton()->wait_for_task_completion(reader_task_id);
} else {
reader_thread.wait_to_finish();
}
writer_thread.wait_to_finish();
}
struct CopyMoveTestType {
inline static int copy_count;
inline static int move_count;
int value = 0;
CopyMoveTestType(int p_value = 0) :
value(p_value) {}
CopyMoveTestType(const CopyMoveTestType &p_other) :
value(p_other.value) {
copy_count++;
}
CopyMoveTestType(CopyMoveTestType &&p_other) :
value(p_other.value) {
move_count++;
}
CopyMoveTestType &operator=(const CopyMoveTestType &p_other) {
value = p_other.value;
copy_count++;
return *this;
}
CopyMoveTestType &operator=(CopyMoveTestType &&p_other) {
value = p_other.value;
move_count++;
return *this;
}
};
void copy_move_test_copy(CopyMoveTestType p_test_type) {
}
void copy_move_test_ref(const CopyMoveTestType &p_test_type) {
}
void copy_move_test_move(CopyMoveTestType &&p_test_type) {
}
};
static void test_command_queue_basic(bool p_use_thread_pool_sync) {
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(p_use_thread_pool_sync);
sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC1_TRANSFORM);
sts.writer_threadwork.main_start_work();
sts.writer_threadwork.main_wait_for_done();
CHECK_MESSAGE(sts.func1_count == 0,
"Control: no messages read before reader has run.");
sts.message_count_to_read = 1;
sts.reader_threadwork.main_start_work();
sts.reader_threadwork.main_wait_for_done();
CHECK_MESSAGE(sts.func1_count == 1,
"Reader should have read one message");
sts.message_count_to_read = -1;
sts.reader_threadwork.main_start_work();
sts.reader_threadwork.main_wait_for_done();
CHECK_MESSAGE(sts.func1_count == 1,
"Reader should have read no additional messages from flush_all");
sts.add_msg_to_write(SharedThreadState::TEST_MSG_FUNC1_TRANSFORM);
sts.writer_threadwork.main_start_work();
sts.writer_threadwork.main_wait_for_done();
sts.message_count_to_read = -1;
sts.reader_threadwork.main_start_work();
sts.reader_threadwork.main_wait_for_done();
CHECK_MESSAGE(sts.func1_count == 2,
"Reader should have read one additional message from flush_all");
sts.destroy_threads();
CHECK_MESSAGE(sts.func1_count == 2,
"Reader should have read no additional messages after join");
ProjectSettings::get_singleton()->set_setting(COMMAND_QUEUE_SETTING,
ProjectSettings::get_singleton()->property_get_revert(COMMAND_QUEUE_SETTING));
}
TEST_CASE("[CommandQueue] Test Queue Basics") {
test_command_queue_basic(false);
}
TEST_CASE("[CommandQueue] Test Queue Basics with WorkerThreadPool sync.") {
test_command_queue_basic(true);
}
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));
}
TEST_CASE("[CommandQueue] Test Parameter Passing Semantics") {
SharedThreadState sts;
sts.init_threads();
SUBCASE("Testing with lvalue") {
SharedThreadState::CopyMoveTestType::copy_count = 0;
SharedThreadState::CopyMoveTestType::move_count = 0;
SharedThreadState::CopyMoveTestType lvalue(42);
SUBCASE("Pass by copy") {
sts.command_queue.push(&sts, &SharedThreadState::copy_move_test_copy, lvalue);
sts.message_count_to_read = -1;
sts.reader_threadwork.main_start_work();
sts.reader_threadwork.main_wait_for_done();
CHECK(SharedThreadState::CopyMoveTestType::copy_count == 1);
CHECK(SharedThreadState::CopyMoveTestType::move_count == 1);
}
SUBCASE("Pass by reference") {
sts.command_queue.push(&sts, &SharedThreadState::copy_move_test_ref, lvalue);
sts.message_count_to_read = -1;
sts.reader_threadwork.main_start_work();
sts.reader_threadwork.main_wait_for_done();
CHECK(SharedThreadState::CopyMoveTestType::copy_count == 1);
CHECK(SharedThreadState::CopyMoveTestType::move_count == 0);
}
}
SUBCASE("Testing with rvalue") {
SharedThreadState::CopyMoveTestType::copy_count = 0;
SharedThreadState::CopyMoveTestType::move_count = 0;
SUBCASE("Pass by copy") {
sts.command_queue.push(&sts, &SharedThreadState::copy_move_test_copy,
SharedThreadState::CopyMoveTestType(43));
sts.message_count_to_read = -1;
sts.reader_threadwork.main_start_work();
sts.reader_threadwork.main_wait_for_done();
CHECK(SharedThreadState::CopyMoveTestType::copy_count == 0);
CHECK(SharedThreadState::CopyMoveTestType::move_count == 2);
}
SUBCASE("Pass by reference") {
sts.command_queue.push(&sts, &SharedThreadState::copy_move_test_ref,
SharedThreadState::CopyMoveTestType(43));
sts.message_count_to_read = -1;
sts.reader_threadwork.main_start_work();
sts.reader_threadwork.main_wait_for_done();
CHECK(SharedThreadState::CopyMoveTestType::copy_count == 0);
CHECK(SharedThreadState::CopyMoveTestType::move_count == 1);
}
SUBCASE("Pass by rvalue reference") {
sts.command_queue.push(&sts, &SharedThreadState::copy_move_test_move,
SharedThreadState::CopyMoveTestType(43));
sts.message_count_to_read = -1;
sts.reader_threadwork.main_start_work();
sts.reader_threadwork.main_wait_for_done();
CHECK(SharedThreadState::CopyMoveTestType::copy_count == 0);
CHECK(SharedThreadState::CopyMoveTestType::move_count == 1);
}
}
sts.destroy_threads();
}
} // namespace TestCommandQueue

View File

@@ -0,0 +1,107 @@
/**************************************************************************/
/* test_fixed_vector.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/templates/fixed_vector.h"
#include "tests/test_macros.h"
namespace TestFixedVector {
TEST_CASE("[FixedVector] Basic Checks") {
FixedVector<uint16_t, 1> vector;
CHECK_EQ(vector.capacity(), 1);
CHECK_EQ(vector.size(), 0);
CHECK(vector.is_empty());
CHECK(!vector.is_full());
vector.push_back(5);
CHECK_EQ(vector.size(), 1);
CHECK_EQ(vector[0], 5);
CHECK_EQ(vector.ptr()[0], 5);
CHECK(!vector.is_empty());
CHECK(vector.is_full());
vector.pop_back();
CHECK_EQ(vector.size(), 0);
CHECK(vector.is_empty());
CHECK(!vector.is_full());
FixedVector<uint16_t, 2> vector1 = { 1, 2 };
CHECK_EQ(vector1.capacity(), 2);
CHECK_EQ(vector1.size(), 2);
CHECK_EQ(vector1[0], 1);
CHECK_EQ(vector1[1], 2);
FixedVector<uint16_t, 3> vector2(vector1);
CHECK_EQ(vector2.capacity(), 3);
CHECK_EQ(vector2.size(), 2);
CHECK_EQ(vector2[0], 1);
CHECK_EQ(vector2[1], 2);
FixedVector<Variant, 3> vector_variant;
CHECK_EQ(vector_variant.size(), 0);
CHECK_EQ(vector_variant.capacity(), 3);
vector_variant.resize_initialized(3);
vector_variant[0] = "Test";
vector_variant[1] = 1;
CHECK_EQ(vector_variant.capacity(), 3);
CHECK_EQ(vector_variant.size(), 3);
CHECK_EQ(vector_variant[0], "Test");
CHECK_EQ(vector_variant[1], Variant(1));
CHECK_EQ(vector_variant[2].get_type(), Variant::NIL);
}
TEST_CASE("[FixedVector] Alignment Checks") {
FixedVector<uint16_t, 4> vector_uint16;
vector_uint16.resize_uninitialized(4);
CHECK((size_t)&vector_uint16[0] % alignof(uint16_t) == 0);
CHECK((size_t)&vector_uint16[1] % alignof(uint16_t) == 0);
CHECK((size_t)&vector_uint16[2] % alignof(uint16_t) == 0);
CHECK((size_t)&vector_uint16[3] % alignof(uint16_t) == 0);
FixedVector<uint32_t, 4> vector_uint32;
vector_uint32.resize_uninitialized(4);
CHECK((size_t)&vector_uint32[0] % alignof(uint32_t) == 0);
CHECK((size_t)&vector_uint32[1] % alignof(uint32_t) == 0);
CHECK((size_t)&vector_uint32[2] % alignof(uint32_t) == 0);
CHECK((size_t)&vector_uint32[3] % alignof(uint32_t) == 0);
FixedVector<uint64_t, 4> vector_uint64;
vector_uint64.resize_uninitialized(4);
CHECK((size_t)&vector_uint64[0] % alignof(uint64_t) == 0);
CHECK((size_t)&vector_uint64[1] % alignof(uint64_t) == 0);
CHECK((size_t)&vector_uint64[2] % alignof(uint64_t) == 0);
CHECK((size_t)&vector_uint64[3] % alignof(uint64_t) == 0);
}
} //namespace TestFixedVector

View File

@@ -0,0 +1,176 @@
/**************************************************************************/
/* test_hash_map.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/templates/hash_map.h"
#include "tests/test_macros.h"
namespace TestHashMap {
TEST_CASE("[HashMap] List initialization") {
HashMap<int, String> map{ { 0, "A" }, { 1, "B" }, { 2, "C" }, { 3, "D" }, { 4, "E" } };
CHECK(map.size() == 5);
CHECK(map[0] == "A");
CHECK(map[1] == "B");
CHECK(map[2] == "C");
CHECK(map[3] == "D");
CHECK(map[4] == "E");
}
TEST_CASE("[HashMap] List initialization with existing elements") {
HashMap<int, String> map{ { 0, "A" }, { 0, "B" }, { 0, "C" }, { 0, "D" }, { 0, "E" } };
CHECK(map.size() == 1);
CHECK(map[0] == "E");
}
TEST_CASE("[HashMap] Insert element") {
HashMap<int, int> map;
HashMap<int, int>::Iterator e = map.insert(42, 84);
CHECK(e);
CHECK(e->key == 42);
CHECK(e->value == 84);
CHECK(map[42] == 84);
CHECK(map.has(42));
CHECK(map.find(42));
}
TEST_CASE("[HashMap] Overwrite element") {
HashMap<int, int> map;
map.insert(42, 84);
map.insert(42, 1234);
CHECK(map[42] == 1234);
}
TEST_CASE("[HashMap] Erase via element") {
HashMap<int, int> map;
HashMap<int, int>::Iterator e = map.insert(42, 84);
map.remove(e);
CHECK(!map.has(42));
CHECK(!map.find(42));
}
TEST_CASE("[HashMap] Erase via key") {
HashMap<int, int> map;
map.insert(42, 84);
map.erase(42);
CHECK(!map.has(42));
CHECK(!map.find(42));
}
TEST_CASE("[HashMap] Size") {
HashMap<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("[HashMap] Iteration") {
HashMap<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 (const KeyValue<int, int> &E : map) {
CHECK(expected[idx] == Pair<int, int>(E.key, E.value));
++idx;
}
}
TEST_CASE("[HashMap] Const iteration") {
HashMap<int, int> map;
map.insert(42, 84);
map.insert(123, 12385);
map.insert(0, 12934);
map.insert(123485, 1238888);
map.insert(123, 111111);
const HashMap<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));
expected.push_back(Pair<int, int>(123, 111111));
int idx = 0;
for (const KeyValue<int, int> &E : const_map) {
CHECK(expected[idx] == Pair<int, int>(E.key, E.value));
++idx;
}
}
TEST_CASE("[HashMap] Sort") {
HashMap<int, int> hashmap;
int shuffled_ints[]{ 6, 1, 9, 8, 3, 0, 4, 5, 7, 2 };
for (int i : shuffled_ints) {
hashmap[i] = i;
}
hashmap.sort();
int i = 0;
for (const KeyValue<int, int> &kv : hashmap) {
CHECK_EQ(kv.key, i);
i++;
}
struct ReverseSort {
bool operator()(const KeyValue<int, int> &p_a, const KeyValue<int, int> &p_b) {
return p_a.key > p_b.key;
}
};
hashmap.sort_custom<ReverseSort>();
for (const KeyValue<int, int> &kv : hashmap) {
i--;
CHECK_EQ(kv.key, i);
}
}
} // namespace TestHashMap

View File

@@ -0,0 +1,257 @@
/**************************************************************************/
/* test_hash_set.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/templates/hash_set.h"
#include "tests/test_macros.h"
namespace TestHashSet {
TEST_CASE("[HashSet] List initialization") {
HashSet<int> set{ 0, 1, 2, 3, 4 };
CHECK(set.size() == 5);
CHECK(set.has(0));
CHECK(set.has(1));
CHECK(set.has(2));
CHECK(set.has(3));
CHECK(set.has(4));
}
TEST_CASE("[HashSet] List initialization with existing elements") {
HashSet<int> set{ 0, 0, 0, 0, 0 };
CHECK(set.size() == 1);
CHECK(set.has(0));
}
TEST_CASE("[HashSet] Insert element") {
HashSet<int> set;
HashSet<int>::Iterator e = set.insert(42);
CHECK(e);
CHECK(*e == 42);
CHECK(set.has(42));
CHECK(set.find(42));
set.reset();
}
TEST_CASE("[HashSet] Insert existing element") {
HashSet<int> set;
set.insert(42);
set.insert(42);
CHECK(set.has(42));
CHECK(set.size() == 1);
}
TEST_CASE("[HashSet] Insert, iterate and remove many elements") {
const int elem_max = 12343;
HashSet<int> set;
for (int i = 0; i < elem_max; i++) {
set.insert(i);
}
//insert order should have been kept
int idx = 0;
for (const int &K : set) {
CHECK(idx == K);
CHECK(set.has(idx));
idx++;
}
Vector<int> elems_still_valid;
for (int i = 0; i < elem_max; i++) {
if ((i % 5) == 0) {
set.erase(i);
} else {
elems_still_valid.push_back(i);
}
}
CHECK(elems_still_valid.size() == set.size());
for (int i = 0; i < elems_still_valid.size(); i++) {
CHECK(set.has(elems_still_valid[i]));
}
}
TEST_CASE("[HashSet] Insert, iterate and remove many strings") {
// This tests a key that uses allocation, to see if any leaks occur
uint64_t pre_mem = Memory::get_mem_usage();
const int elem_max = 4018;
HashSet<String> set;
for (int i = 0; i < elem_max; i++) {
set.insert(itos(i));
}
//insert order should have been kept
int idx = 0;
for (const String &K : set) {
CHECK(itos(idx) == K);
CHECK(set.has(itos(idx)));
idx++;
}
Vector<String> elems_still_valid;
for (int i = 0; i < elem_max; i++) {
if ((i % 5) == 0) {
set.erase(itos(i));
} else {
elems_still_valid.push_back(itos(i));
}
}
CHECK(elems_still_valid.size() == set.size());
for (int i = 0; i < elems_still_valid.size(); i++) {
CHECK(set.has(elems_still_valid[i]));
}
elems_still_valid.clear();
set.reset();
CHECK(Memory::get_mem_usage() == pre_mem);
}
TEST_CASE("[HashSet] Erase via element") {
HashSet<int> set;
HashSet<int>::Iterator e = set.insert(42);
set.remove(e);
CHECK(!set.has(42));
CHECK(!set.find(42));
}
TEST_CASE("[HashSet] Erase via key") {
HashSet<int> set;
set.insert(42);
set.insert(49);
set.erase(42);
CHECK(!set.has(42));
CHECK(!set.find(42));
}
TEST_CASE("[HashSet] Insert and erase half elements") {
HashSet<int> set;
set.insert(1);
set.insert(2);
set.insert(3);
set.insert(4);
set.erase(1);
set.erase(3);
CHECK(set.size() == 2);
CHECK(set.has(2));
CHECK(set.has(4));
}
TEST_CASE("[HashSet] Size") {
HashSet<int> set;
set.insert(42);
set.insert(123);
set.insert(123);
set.insert(0);
set.insert(123485);
CHECK(set.size() == 4);
}
TEST_CASE("[HashSet] Iteration") {
HashSet<int> set;
set.insert(42);
set.insert(123);
set.insert(0);
set.insert(123485);
Vector<int> expected;
expected.push_back(42);
expected.push_back(123);
expected.push_back(0);
expected.push_back(123485);
int idx = 0;
for (const int &E : set) {
CHECK(expected[idx] == E);
++idx;
}
}
TEST_CASE("[HashSet] Copy") {
HashSet<int> set;
set.insert(42);
set.insert(123);
set.insert(0);
set.insert(123485);
Vector<int> expected;
expected.push_back(42);
expected.push_back(123);
expected.push_back(0);
expected.push_back(123485);
HashSet<int> copy_assign = set;
int idx = 0;
for (const int &E : copy_assign) {
CHECK(expected[idx] == E);
++idx;
}
HashSet<int> copy_construct(set);
idx = 0;
for (const int &E : copy_construct) {
CHECK(expected[idx] == E);
++idx;
}
}
TEST_CASE("[HashSet] Equality") {
// Empty sets.
CHECK(HashSet<int>{} == HashSet<int>{});
CHECK(HashSet<int>{} != HashSet<int>{ 1, 2, 3 });
CHECK(HashSet<int>{ 1, 2, 3 } != HashSet<int>{});
// Different length.
CHECK(HashSet<int>{ 1, 2, 3 } != HashSet<int>{ 1, 2, 3, 4 });
CHECK(HashSet<int>{ 1, 2, 3, 4 } != HashSet<int>{ 4, 3, 2 });
// Same length.
CHECK(HashSet<int>{ 1, 2, 3 } == HashSet<int>{ 1, 2, 3 });
CHECK(HashSet<int>{ 1, 2, 3 } == HashSet<int>{ 3, 2, 1 });
CHECK(HashSet<int>{ 1, 2, 3 } != HashSet<int>{ 1, 2, 8 });
}
} // namespace TestHashSet

View File

@@ -0,0 +1,601 @@
/**************************************************************************/
/* test_list.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#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] List initialization") {
List<int> list{ 0, 1, 2, 3, 4 };
CHECK(list.size() == 5);
CHECK(list.get(0) == 0);
CHECK(list.get(1) == 1);
CHECK(list.get(2) == 2);
CHECK(list.get(3) == 3);
CHECK(list.get(4) == 4);
}
TEST_CASE("[List] Push/pop back") {
List<String> list;
List<String>::Element *n;
n = list.push_back("A");
CHECK(n->get() == "A");
n = list.push_back("B");
CHECK(n->get() == "B");
n = list.push_back("C");
CHECK(n->get() == "C");
CHECK(list.size() == 3);
CHECK(!list.is_empty());
String v;
v = list.back()->get();
list.pop_back();
CHECK(v == "C");
v = list.back()->get();
list.pop_back();
CHECK(v == "B");
v = list.back()->get();
list.pop_back();
CHECK(v == "A");
CHECK(list.size() == 0);
CHECK(list.is_empty());
CHECK(list.back() == nullptr);
CHECK(list.front() == nullptr);
}
TEST_CASE("[List] Push/pop front") {
List<String> list;
List<String>::Element *n;
n = list.push_front("A");
CHECK(n->get() == "A");
n = list.push_front("B");
CHECK(n->get() == "B");
n = list.push_front("C");
CHECK(n->get() == "C");
CHECK(list.size() == 3);
CHECK(!list.is_empty());
String v;
v = list.front()->get();
list.pop_front();
CHECK(v == "C");
v = list.front()->get();
list.pop_front();
CHECK(v == "B");
v = list.front()->get();
list.pop_front();
CHECK(v == "A");
CHECK(list.size() == 0);
CHECK(list.is_empty());
CHECK(list.back() == nullptr);
CHECK(list.front() == nullptr);
}
TEST_CASE("[List] Set and get") {
List<String> list;
list.push_back("A");
List<String>::Element *n = list.front();
CHECK(n->get() == "A");
n->set("X");
CHECK(n->get() == "X");
}
TEST_CASE("[List] Insert before") {
List<String> list;
List<String>::Element *a = list.push_back("A");
List<String>::Element *b = list.push_back("B");
List<String>::Element *c = list.push_back("C");
list.insert_before(b, "I");
CHECK(a->next()->get() == "I");
CHECK(c->prev()->prev()->get() == "I");
CHECK(list.front()->next()->get() == "I");
CHECK(list.back()->prev()->prev()->get() == "I");
}
TEST_CASE("[List] Insert after") {
List<String> list;
List<String>::Element *a = list.push_back("A");
List<String>::Element *b = list.push_back("B");
List<String>::Element *c = list.push_back("C");
list.insert_after(b, "I");
CHECK(a->next()->next()->get() == "I");
CHECK(c->prev()->get() == "I");
CHECK(list.front()->next()->next()->get() == "I");
CHECK(list.back()->prev()->get() == "I");
}
TEST_CASE("[List] Insert before null") {
List<String> list;
List<String>::Element *a = list.push_back("A");
List<String>::Element *b = list.push_back("B");
List<String>::Element *c = list.push_back("C");
list.insert_before(nullptr, "I");
CHECK(a->next()->get() == "B");
CHECK(b->get() == "B");
CHECK(c->prev()->prev()->get() == "A");
CHECK(list.front()->next()->get() == "B");
CHECK(list.back()->prev()->prev()->get() == "B");
CHECK(list.back()->get() == "I");
}
TEST_CASE("[List] Insert after null") {
List<String> list;
List<String>::Element *a = list.push_back("A");
List<String>::Element *b = list.push_back("B");
List<String>::Element *c = list.push_back("C");
list.insert_after(nullptr, "I");
CHECK(a->next()->get() == "B");
CHECK(b->get() == "B");
CHECK(c->prev()->prev()->get() == "A");
CHECK(list.front()->next()->get() == "B");
CHECK(list.back()->prev()->prev()->get() == "B");
CHECK(list.back()->get() == "I");
}
TEST_CASE("[List] Find") {
List<int> list;
List<int>::Element *n[10];
// Indices match values.
populate_integers(list, n, 10);
for (int i = 0; i < 10; ++i) {
CHECK(n[i]->get() == list.find(i)->get());
}
}
TEST_CASE("[List] Erase (by value)") {
List<int> list;
List<int>::Element *n[4];
// Indices match values.
populate_integers(list, n, 4);
CHECK(list.front()->next()->next()->get() == 2);
bool erased = list.erase(2); // 0, 1, 3.
CHECK(erased);
CHECK(list.size() == 3);
// The pointer n[2] points to the freed memory which is not reset to zero,
// so the below assertion may pass, but this relies on undefined behavior.
// CHECK(n[2]->get() == 2);
CHECK(list.front()->get() == 0);
CHECK(list.front()->next()->next()->get() == 3);
CHECK(list.back()->get() == 3);
CHECK(list.back()->prev()->get() == 1);
CHECK(n[1]->next()->get() == 3);
CHECK(n[3]->prev()->get() == 1);
erased = list.erase(9000); // Doesn't exist.
CHECK(!erased);
}
TEST_CASE("[List] Erase (by element)") {
List<int> list;
List<int>::Element *n[4];
// Indices match values.
populate_integers(list, n, 4);
bool erased = list.erase(n[2]);
CHECK(erased);
CHECK(list.size() == 3);
CHECK(n[1]->next()->get() == 3);
CHECK(n[3]->prev()->get() == 1);
}
TEST_CASE("[List] Element erase") {
List<int> list;
List<int>::Element *n[4];
// Indices match values.
populate_integers(list, n, 4);
n[2]->erase();
CHECK(list.size() == 3);
CHECK(n[1]->next()->get() == 3);
CHECK(n[3]->prev()->get() == 1);
}
TEST_CASE("[List] Clear") {
List<int> list;
List<int>::Element *n[100];
populate_integers(list, n, 100);
list.clear();
CHECK(list.size() == 0);
CHECK(list.is_empty());
}
TEST_CASE("[List] Invert") {
List<int> list;
List<int>::Element *n[4];
populate_integers(list, n, 4);
list.reverse();
CHECK(list.front()->get() == 3);
CHECK(list.front()->next()->get() == 2);
CHECK(list.back()->prev()->get() == 1);
CHECK(list.back()->get() == 0);
}
TEST_CASE("[List] Move to front") {
List<int> list;
List<int>::Element *n[4];
populate_integers(list, n, 4);
list.move_to_front(n[3]);
CHECK(list.front()->get() == 3);
CHECK(list.back()->get() == 2);
}
TEST_CASE("[List] Move to back") {
List<int> list;
List<int>::Element *n[4];
populate_integers(list, n, 4);
list.move_to_back(n[0]);
CHECK(list.back()->get() == 0);
CHECK(list.front()->get() == 1);
}
TEST_CASE("[List] Move before") {
List<int> list;
List<int>::Element *n[4];
populate_integers(list, n, 4);
list.move_before(n[3], n[1]);
CHECK(list.front()->next()->get() == n[3]->get());
}
template <typename T>
static void compare_lists(const List<T> &p_result, const List<T> &p_expected) {
CHECK_EQ(p_result.size(), p_expected.size());
const typename List<T>::Element *result_it = p_result.front();
const typename List<T>::Element *expected_it = p_expected.front();
for (int i = 0; i < p_result.size(); i++) {
CHECK(result_it);
CHECK(expected_it);
CHECK_EQ(result_it->get(), expected_it->get());
result_it = result_it->next();
expected_it = expected_it->next();
}
CHECK(!result_it);
CHECK(!expected_it);
result_it = p_result.back();
expected_it = p_expected.back();
for (int i = 0; i < p_result.size(); i++) {
CHECK(result_it);
CHECK(expected_it);
CHECK_EQ(result_it->get(), expected_it->get());
result_it = result_it->prev();
expected_it = expected_it->prev();
}
CHECK(!result_it);
CHECK(!expected_it);
}
TEST_CASE("[List] Sort") {
List<String> result{ "D", "B", "A", "C" };
result.sort();
List<String> expected{ "A", "B", "C", "D" };
compare_lists(result, expected);
List<int> empty_result{};
empty_result.sort();
List<int> empty_expected{};
compare_lists(empty_result, empty_expected);
List<int> one_result{ 1 };
one_result.sort();
List<int> one_expected{ 1 };
compare_lists(one_result, one_expected);
List<float> reversed_result{ 2.0, 1.5, 1.0 };
reversed_result.sort();
List<float> reversed_expected{ 1.0, 1.5, 2.0 };
compare_lists(reversed_result, reversed_expected);
List<int> already_sorted_result{ 1, 2, 3, 4, 5 };
already_sorted_result.sort();
List<int> already_sorted_expected{ 1, 2, 3, 4, 5 };
compare_lists(already_sorted_result, already_sorted_expected);
List<int> with_duplicates_result{ 1, 2, 3, 1, 2, 3 };
with_duplicates_result.sort();
List<int> with_duplicates_expected{ 1, 1, 2, 2, 3, 3 };
compare_lists(with_duplicates_result, with_duplicates_expected);
}
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

View File

@@ -0,0 +1,263 @@
/**************************************************************************/
/* test_local_vector.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/templates/local_vector.h"
#include "tests/test_macros.h"
namespace TestLocalVector {
TEST_CASE("[LocalVector] List Initialization.") {
LocalVector<int> vector{ 0, 1, 2, 3, 4 };
CHECK(vector.size() == 5);
CHECK(vector[0] == 0);
CHECK(vector[1] == 1);
CHECK(vector[2] == 2);
CHECK(vector[3] == 3);
CHECK(vector[4] == 4);
}
TEST_CASE("[LocalVector] Push Back.") {
LocalVector<int> vector;
vector.push_back(0);
vector.push_back(1);
vector.push_back(2);
vector.push_back(3);
vector.push_back(4);
CHECK(vector[0] == 0);
CHECK(vector[1] == 1);
CHECK(vector[2] == 2);
CHECK(vector[3] == 3);
CHECK(vector[4] == 4);
}
TEST_CASE("[LocalVector] Find, has.") {
LocalVector<int> vector;
vector.push_back(3);
vector.push_back(1);
vector.push_back(4);
vector.push_back(0);
vector.push_back(2);
CHECK(vector[0] == 3);
CHECK(vector[1] == 1);
CHECK(vector[2] == 4);
CHECK(vector[3] == 0);
CHECK(vector[4] == 2);
CHECK(vector.find(0) == 3);
CHECK(vector.find(1) == 1);
CHECK(vector.find(2) == 4);
CHECK(vector.find(3) == 0);
CHECK(vector.find(4) == 2);
CHECK(vector.find(-1) == -1);
CHECK(vector.find(5) == -1);
CHECK(vector.has(0));
CHECK(vector.has(1));
CHECK(vector.has(2));
CHECK(vector.has(3));
CHECK(vector.has(4));
CHECK(!vector.has(-1));
CHECK(!vector.has(5));
}
TEST_CASE("[LocalVector] Remove.") {
LocalVector<int> vector;
vector.push_back(0);
vector.push_back(1);
vector.push_back(2);
vector.push_back(3);
vector.push_back(4);
vector.remove_at(0);
CHECK(vector[0] == 1);
CHECK(vector[1] == 2);
CHECK(vector[2] == 3);
CHECK(vector[3] == 4);
vector.remove_at(2);
CHECK(vector[0] == 1);
CHECK(vector[1] == 2);
CHECK(vector[2] == 4);
vector.remove_at(1);
CHECK(vector[0] == 1);
CHECK(vector[1] == 4);
vector.remove_at(0);
CHECK(vector[0] == 4);
}
TEST_CASE("[LocalVector] Remove Unordered.") {
LocalVector<int> vector;
vector.push_back(0);
vector.push_back(1);
vector.push_back(2);
vector.push_back(3);
vector.push_back(4);
CHECK(vector.size() == 5);
vector.remove_at_unordered(0);
CHECK(vector.size() == 4);
CHECK(vector.find(0) == -1);
CHECK(vector.find(1) != -1);
CHECK(vector.find(2) != -1);
CHECK(vector.find(3) != -1);
CHECK(vector.find(4) != -1);
// Now the vector is no more ordered.
vector.remove_at_unordered(vector.find(3));
CHECK(vector.size() == 3);
CHECK(vector.find(3) == -1);
CHECK(vector.find(1) != -1);
CHECK(vector.find(2) != -1);
CHECK(vector.find(4) != -1);
vector.remove_at_unordered(vector.find(2));
CHECK(vector.size() == 2);
CHECK(vector.find(2) == -1);
CHECK(vector.find(1) != -1);
CHECK(vector.find(4) != -1);
vector.remove_at_unordered(vector.find(4));
CHECK(vector.size() == 1);
CHECK(vector.find(4) == -1);
CHECK(vector.find(1) != -1);
// Remove the last one.
vector.remove_at_unordered(0);
CHECK(vector.is_empty());
CHECK(vector.size() == 0);
}
TEST_CASE("[LocalVector] Erase Unordered.") {
LocalVector<int> vector;
vector.push_back(1);
vector.push_back(3);
vector.push_back(0);
vector.push_back(2);
vector.push_back(4);
CHECK(vector.find(1) == 0);
vector.erase_unordered(1);
CHECK(vector.find(1) == -1);
CHECK(vector.size() == 4);
CHECK(vector[0] == 4);
}
TEST_CASE("[LocalVector] Erase.") {
LocalVector<int> vector;
vector.push_back(1);
vector.push_back(3);
vector.push_back(0);
vector.push_back(2);
vector.push_back(4);
CHECK(vector.find(2) == 3);
vector.erase(2);
CHECK(vector.find(2) == -1);
CHECK(vector.size() == 4);
}
TEST_CASE("[LocalVector] Size / Resize / Reserve.") {
LocalVector<int> vector;
CHECK(vector.is_empty());
CHECK(vector.size() == 0);
CHECK(vector.get_capacity() == 0);
vector.resize(10);
CHECK(vector.size() == 10);
CHECK(vector.get_capacity() >= 10);
vector.resize(5);
CHECK(vector.size() == 5);
// Capacity is supposed to change only when the size increase.
CHECK(vector.get_capacity() >= 10);
vector.remove_at(0);
vector.remove_at(0);
vector.remove_at(0);
CHECK(vector.size() == 2);
// Capacity is supposed to change only when the size increase.
CHECK(vector.get_capacity() >= 10);
vector.reset();
CHECK(vector.size() == 0);
CHECK(vector.get_capacity() == 0);
vector.reserve(3);
CHECK(vector.is_empty());
CHECK(vector.size() == 0);
CHECK(vector.get_capacity() >= 3);
vector.push_back(0);
vector.push_back(0);
vector.push_back(0);
CHECK(vector.size() == 3);
CHECK(vector.get_capacity() >= 3);
vector.push_back(0);
CHECK(vector.size() == 4);
CHECK(vector.get_capacity() >= 4);
}
} // namespace TestLocalVector

View File

@@ -0,0 +1,96 @@
/**************************************************************************/
/* test_lru.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/templates/lru.h"
#include "tests/test_macros.h"
namespace TestLRU {
TEST_CASE("[LRU] Store and read") {
LRUCache<int, int> lru;
lru.set_capacity(3);
lru.insert(1, 1);
lru.insert(50, 2);
lru.insert(100, 5);
CHECK(lru.has(1));
CHECK(lru.has(50));
CHECK(lru.has(100));
CHECK(!lru.has(200));
CHECK(lru.get(1) == 1);
CHECK(lru.get(50) == 2);
CHECK(lru.get(100) == 5);
CHECK(lru.getptr(1) != nullptr);
CHECK(lru.getptr(1000) == nullptr);
lru.insert(600, 600); // Erase <50>
CHECK(lru.has(600));
CHECK(!lru.has(50));
}
TEST_CASE("[LRU] Resize and clear") {
LRUCache<int, int> lru;
lru.set_capacity(3);
lru.insert(1, 1);
lru.insert(2, 2);
lru.insert(3, 3);
CHECK(lru.get_capacity() == 3);
lru.set_capacity(5);
CHECK(lru.get_capacity() == 5);
CHECK(lru.has(1));
CHECK(lru.has(2));
CHECK(lru.has(3));
CHECK(!lru.has(4));
lru.set_capacity(2);
CHECK(lru.get_capacity() == 2);
CHECK(!lru.has(1));
CHECK(lru.has(2));
CHECK(lru.has(3));
CHECK(!lru.has(4));
lru.clear();
CHECK(!lru.has(1));
CHECK(!lru.has(2));
CHECK(!lru.has(3));
CHECK(!lru.has(4));
}
} // namespace TestLRU

View File

@@ -0,0 +1,201 @@
/**************************************************************************/
/* test_paged_array.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/templates/paged_array.h"
#include "thirdparty/doctest/doctest.h"
namespace TestPagedArray {
// PagedArray
TEST_CASE("[PagedArray] Simple fill and refill") {
PagedArrayPool<uint32_t> pool;
PagedArray<uint32_t> array;
array.set_page_pool(&pool);
for (uint32_t i = 0; i < 123456; i++) {
array.push_back(i);
}
CHECK_MESSAGE(
array.size() == 123456,
"PagedArray should have 123456 elements.");
bool all_match = true;
for (uint32_t i = 0; i < 123456; i++) {
if (array[i] != i) {
all_match = false;
break;
}
}
CHECK_MESSAGE(
all_match,
"PagedArray elements should match from 0 to 123455.");
array.clear();
CHECK_MESSAGE(
array.size() == 0,
"PagedArray elements should be 0 after clear.");
for (uint32_t i = 0; i < 999; i++) {
array.push_back(i);
}
CHECK_MESSAGE(
array.size() == 999,
"PagedArray should have 999 elements.");
all_match = true;
for (uint32_t i = 0; i < 999; i++) {
if (array[i] != i) {
all_match = false;
}
}
CHECK_MESSAGE(
all_match,
"PagedArray elements should match from 0 to 998.");
array.reset(); //reset so pagepool can be reset
pool.reset();
}
TEST_CASE("[PagedArray] Shared pool fill, including merging") {
PagedArrayPool<uint32_t> pool;
PagedArray<uint32_t> array1;
PagedArray<uint32_t> array2;
array1.set_page_pool(&pool);
array2.set_page_pool(&pool);
for (uint32_t i = 0; i < 123456; i++) {
array1.push_back(i);
}
CHECK_MESSAGE(
array1.size() == 123456,
"PagedArray #1 should have 123456 elements.");
bool all_match = true;
for (uint32_t i = 0; i < 123456; i++) {
if (array1[i] != i) {
all_match = false;
}
}
CHECK_MESSAGE(
all_match,
"PagedArray #1 elements should match from 0 to 123455.");
for (uint32_t i = 0; i < 999; i++) {
array2.push_back(i);
}
CHECK_MESSAGE(
array2.size() == 999,
"PagedArray #2 should have 999 elements.");
all_match = true;
for (uint32_t i = 0; i < 999; i++) {
if (array2[i] != i) {
all_match = false;
}
}
CHECK_MESSAGE(
all_match,
"PagedArray #2 elements should match from 0 to 998.");
array1.merge_unordered(array2);
CHECK_MESSAGE(
array1.size() == 123456 + 999,
"PagedArray #1 should now be 123456 + 999 elements.");
CHECK_MESSAGE(
array2.size() == 0,
"PagedArray #2 should now be 0 elements.");
array1.reset(); //reset so pagepool can be reset
array2.reset(); //reset so pagepool can be reset
pool.reset();
}
TEST_CASE("[PagedArray] Extensive merge_unordered() test") {
for (int page_size = 1; page_size <= 128; page_size *= 2) {
PagedArrayPool<uint32_t> pool(page_size);
PagedArray<uint32_t> array1;
PagedArray<uint32_t> array2;
array1.set_page_pool(&pool);
array2.set_page_pool(&pool);
const int max_count = 123;
// Test merging arrays of lengths 0+123, 1+122, 2+121, ..., 123+0
for (uint32_t j = 0; j < max_count; j++) {
CHECK(array1.size() == 0);
CHECK(array2.size() == 0);
uint32_t sum = 12345;
for (uint32_t i = 0; i < j; i++) {
// Hashing the addend makes it extremely unlikely for any values
// other than the original inputs to produce a matching sum
uint32_t addend = hash_murmur3_one_32(i) + i;
array1.push_back(addend);
sum += addend;
}
for (uint32_t i = j; i < max_count; i++) {
// See above
uint32_t addend = hash_murmur3_one_32(i) + i;
array2.push_back(addend);
sum += addend;
}
CHECK(array1.size() == j);
CHECK(array2.size() == max_count - j);
array1.merge_unordered(array2);
CHECK_MESSAGE(array1.size() == max_count, "merge_unordered() added/dropped elements while merging");
// If any elements were altered during merging, the sum will not match up.
for (uint32_t i = 0; i < array1.size(); i++) {
sum -= array1[i];
}
CHECK_MESSAGE(sum == 12345, "merge_unordered() altered elements while merging");
array1.clear();
}
array1.reset();
array2.reset();
pool.reset();
}
}
} // namespace TestPagedArray

View File

@@ -0,0 +1,256 @@
/**************************************************************************/
/* test_rid.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/os/thread.h"
#include "core/templates/local_vector.h"
#include "core/templates/rid.h"
#include "core/templates/rid_owner.h"
#include "tests/test_macros.h"
#ifdef THREADS_ENABLED
#ifdef SANITIZERS_ENABLED
#ifdef __has_feature
#if __has_feature(thread_sanitizer)
#define TSAN_ENABLED
#endif
#elif defined(__SANITIZE_THREAD__)
#define TSAN_ENABLED
#endif
#endif // SANITIZERS_ENABLED
#endif // THREADS_ENABLED
#ifdef TSAN_ENABLED
#include <sanitizer/tsan_interface.h>
#endif
namespace TestRID {
TEST_CASE("[RID] Default Constructor") {
RID rid;
CHECK(rid.get_id() == 0);
}
TEST_CASE("[RID] Factory method") {
RID rid = RID::from_uint64(1);
CHECK(rid.get_id() == 1);
}
TEST_CASE("[RID] Operators") {
RID rid = RID::from_uint64(1);
RID rid_zero = RID::from_uint64(0);
RID rid_one = RID::from_uint64(1);
RID rid_two = RID::from_uint64(2);
CHECK_FALSE(rid == rid_zero);
CHECK(rid == rid_one);
CHECK_FALSE(rid == rid_two);
CHECK_FALSE(rid < rid_zero);
CHECK_FALSE(rid < rid_one);
CHECK(rid < rid_two);
CHECK_FALSE(rid <= rid_zero);
CHECK(rid <= rid_one);
CHECK(rid <= rid_two);
CHECK(rid > rid_zero);
CHECK_FALSE(rid > rid_one);
CHECK_FALSE(rid > rid_two);
CHECK(rid >= rid_zero);
CHECK(rid >= rid_one);
CHECK_FALSE(rid >= rid_two);
CHECK(rid != rid_zero);
CHECK_FALSE(rid != rid_one);
CHECK(rid != rid_two);
}
TEST_CASE("[RID] 'is_valid' & 'is_null'") {
RID rid_zero = RID::from_uint64(0);
RID rid_one = RID::from_uint64(1);
CHECK_FALSE(rid_zero.is_valid());
CHECK(rid_zero.is_null());
CHECK(rid_one.is_valid());
CHECK_FALSE(rid_one.is_null());
}
TEST_CASE("[RID] 'get_local_index'") {
CHECK(RID::from_uint64(1).get_local_index() == 1);
CHECK(RID::from_uint64(4'294'967'295).get_local_index() == 4'294'967'295);
CHECK(RID::from_uint64(4'294'967'297).get_local_index() == 1);
}
#ifdef THREADS_ENABLED
// This case would let sanitizers realize data races.
// Additionally, on purely weakly ordered architectures, it would detect synchronization issues
// if RID_Alloc failed to impose proper memory ordering and the test's threads are distributed
// among multiple L1 caches.
TEST_CASE("[RID_Owner] Thread safety") {
struct DataHolder {
char data[Thread::CACHE_LINE_BYTES];
};
struct RID_OwnerTester {
uint32_t thread_count = 0;
RID_Owner<DataHolder, true> rid_owner;
TightLocalVector<Thread> threads;
SafeNumeric<uint32_t> next_thread_idx;
// Using std::atomic directly since SafeNumeric doesn't support relaxed ordering.
TightLocalVector<std::atomic<uint64_t>> rids;
std::atomic<uint32_t> sync[2] = {};
std::atomic<uint32_t> correct = 0;
// A barrier that doesn't introduce memory ordering constraints, only compiler ones.
// The idea is not to cause any sync traffic that would make the code we want to test
// seem correct as a side effect.
void lockstep(uint32_t p_step) {
uint32_t buf_idx = p_step % 2;
uint32_t target = (p_step / 2 + 1) * threads.size();
sync[buf_idx].fetch_add(1, std::memory_order_relaxed);
do {
Thread::yield();
} while (sync[buf_idx].load(std::memory_order_relaxed) != target);
}
explicit RID_OwnerTester(bool p_chunk_for_all, bool p_chunks_preallocated) :
thread_count(OS::get_singleton()->get_processor_count()),
rid_owner(sizeof(DataHolder) * (p_chunk_for_all ? thread_count : 1)) {
threads.resize(thread_count);
rids.resize(threads.size());
if (p_chunks_preallocated) {
LocalVector<RID> prealloc_rids;
for (uint32_t i = 0; i < (p_chunk_for_all ? 1 : threads.size()); i++) {
prealloc_rids.push_back(rid_owner.make_rid());
}
for (uint32_t i = 0; i < prealloc_rids.size(); i++) {
rid_owner.free(prealloc_rids[i]);
}
}
}
~RID_OwnerTester() {
for (uint32_t i = 0; i < threads.size(); i++) {
rid_owner.free(RID::from_uint64(rids[i].load(std::memory_order_relaxed)));
}
}
void test() {
for (uint32_t i = 0; i < threads.size(); i++) {
threads[i].start(
[](void *p_data) {
RID_OwnerTester *rot = (RID_OwnerTester *)p_data;
auto _compute_thread_unique_byte = [](uint32_t p_idx) -> char {
return ((p_idx & 0xff) ^ (0b11111110 << (p_idx % 8)));
};
// 1. Each thread gets a zero-based index.
uint32_t self_th_idx = rot->next_thread_idx.postincrement();
rot->lockstep(0);
// 2. Each thread makes a RID holding unique data.
DataHolder initial_data;
memset(&initial_data, _compute_thread_unique_byte(self_th_idx), Thread::CACHE_LINE_BYTES);
RID my_rid = rot->rid_owner.make_rid(initial_data);
rot->rids[self_th_idx].store(my_rid.get_id(), std::memory_order_relaxed);
rot->lockstep(1);
// 3. Each thread verifies all the others.
uint32_t local_correct = 0;
for (uint32_t th_idx = 0; th_idx < rot->threads.size(); th_idx++) {
if (th_idx == self_th_idx) {
continue;
}
char expected_unique_byte = _compute_thread_unique_byte(th_idx);
RID rid = RID::from_uint64(rot->rids[th_idx].load(std::memory_order_relaxed));
DataHolder *data = rot->rid_owner.get_or_null(rid);
#ifdef TSAN_ENABLED
__tsan_acquire(data); // We know not a race in practice.
#endif
bool ok = true;
for (uint32_t j = 0; j < Thread::CACHE_LINE_BYTES; j++) {
if (data->data[j] != expected_unique_byte) {
ok = false;
break;
}
}
if (ok) {
local_correct++;
}
#ifdef TSAN_ENABLED
__tsan_release(data);
#endif
}
rot->lockstep(2);
rot->correct.fetch_add(local_correct, std::memory_order_acq_rel);
},
this);
}
for (uint32_t i = 0; i < threads.size(); i++) {
threads[i].wait_to_finish();
}
CHECK_EQ(correct.load(), threads.size() * (threads.size() - 1));
}
};
SUBCASE("All items in one chunk, pre-allocated") {
RID_OwnerTester tester(true, true);
tester.test();
}
SUBCASE("All items in one chunk, NOT pre-allocated") {
RID_OwnerTester tester(true, false);
tester.test();
}
SUBCASE("One item per chunk, pre-allocated") {
RID_OwnerTester tester(false, true);
tester.test();
}
SUBCASE("One item per chunk, NOT pre-allocated") {
RID_OwnerTester tester(false, false);
tester.test();
}
}
#endif // THREADS_ENABLED
} // namespace TestRID

View File

@@ -0,0 +1,72 @@
/**************************************************************************/
/* test_self_list.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/templates/self_list.h"
#include "tests/test_macros.h"
namespace TestSelfList {
TEST_CASE("[SelfList] Sort") {
const int SIZE = 5;
int numbers[SIZE]{ 3, 2, 5, 1, 4 };
SelfList<int> elements[SIZE]{
SelfList<int>(&numbers[0]),
SelfList<int>(&numbers[1]),
SelfList<int>(&numbers[2]),
SelfList<int>(&numbers[3]),
SelfList<int>(&numbers[4]),
};
SelfList<int>::List list;
for (int i = 0; i < SIZE; i++) {
list.add_last(&elements[i]);
}
SelfList<int> *it = list.first();
for (int i = 0; i < SIZE; i++) {
CHECK_EQ(numbers[i], *it->self());
it = it->next();
}
list.sort();
it = list.first();
for (int i = 1; i <= SIZE; i++) {
CHECK_EQ(i, *it->self());
it = it->next();
}
for (SelfList<int> &element : elements) {
element.remove_from_list();
}
}
} // namespace TestSelfList

View File

@@ -0,0 +1,70 @@
/**************************************************************************/
/* test_span.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/templates/span.h"
#include "tests/test_macros.h"
namespace TestSpan {
TEST_CASE("[Span] Constexpr Validators") {
constexpr Span<uint16_t> span_empty;
static_assert(span_empty.ptr() == nullptr);
static_assert(span_empty.size() == 0);
static_assert(span_empty.is_empty());
constexpr static uint16_t value = 5;
Span<uint16_t> span_value(&value, 1);
CHECK(span_value.ptr() == &value);
CHECK(span_value.size() == 1);
CHECK(!span_value.is_empty());
static constexpr int ints[] = { 0, 1, 2, 3, 4, 5 };
constexpr Span<int> span_array = ints;
static_assert(span_array.size() == 6);
static_assert(!span_array.is_empty());
static_assert(span_array[0] == 0);
static_assert(span_array[span_array.size() - 1] == 5);
constexpr Span<char32_t> span_string = U"122345";
static_assert(span_string.size() == 6);
static_assert(!span_string.is_empty());
static_assert(span_string[0] == U'1');
static_assert(span_string[span_string.size() - 1] == U'5');
int idx = 0;
for (const char32_t &chr : span_string) {
CHECK_EQ(chr, span_string[idx++]);
}
}
} // namespace TestSpan

View File

@@ -0,0 +1,707 @@
/**************************************************************************/
/* test_vector.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/templates/vector.h"
#include "tests/test_macros.h"
namespace TestVector {
TEST_CASE("[Vector] List initialization") {
Vector<int> vector{ 0, 1, 2, 3, 4 };
CHECK(vector.size() == 5);
CHECK(vector[0] == 0);
CHECK(vector[1] == 1);
CHECK(vector[2] == 2);
CHECK(vector[3] == 3);
CHECK(vector[4] == 4);
}
TEST_CASE("[Vector] Push back and append") {
Vector<int> vector;
vector.push_back(0);
vector.push_back(1);
vector.push_back(2);
vector.push_back(3);
// Alias for `push_back`.
vector.append(4);
CHECK(vector[0] == 0);
CHECK(vector[1] == 1);
CHECK(vector[2] == 2);
CHECK(vector[3] == 3);
CHECK(vector[4] == 4);
}
TEST_CASE("[Vector] Append array") {
Vector<int> vector;
vector.push_back(1);
vector.push_back(2);
Vector<int> vector_other;
vector_other.push_back(128);
vector_other.push_back(129);
vector.append_array(vector_other);
CHECK(vector.size() == 4);
CHECK(vector[0] == 1);
CHECK(vector[1] == 2);
CHECK(vector[2] == 128);
CHECK(vector[3] == 129);
}
TEST_CASE("[Vector] Insert") {
Vector<int> vector;
vector.insert(0, 2);
vector.insert(0, 8);
vector.insert(2, 5);
vector.insert(1, 5);
vector.insert(0, -2);
CHECK(vector.size() == 5);
CHECK(vector[0] == -2);
CHECK(vector[1] == 8);
CHECK(vector[2] == 5);
CHECK(vector[3] == 2);
CHECK(vector[4] == 5);
}
TEST_CASE("[Vector] Ordered insert") {
Vector<int> vector;
vector.ordered_insert(2);
vector.ordered_insert(8);
vector.ordered_insert(5);
vector.ordered_insert(5);
vector.ordered_insert(-2);
CHECK(vector.size() == 5);
CHECK(vector[0] == -2);
CHECK(vector[1] == 2);
CHECK(vector[2] == 5);
CHECK(vector[3] == 5);
CHECK(vector[4] == 8);
}
TEST_CASE("[Vector] Insert + Ordered insert") {
Vector<int> vector;
vector.ordered_insert(2);
vector.ordered_insert(8);
vector.insert(0, 5);
vector.ordered_insert(5);
vector.insert(1, -2);
CHECK(vector.size() == 5);
CHECK(vector[0] == 5);
CHECK(vector[1] == -2);
CHECK(vector[2] == 2);
CHECK(vector[3] == 5);
CHECK(vector[4] == 8);
}
TEST_CASE("[Vector] Fill large array and modify it") {
Vector<int> vector;
vector.resize(1'000'000);
vector.fill(0x60d07);
vector.write[200] = 0;
CHECK(vector.size() == 1'000'000);
CHECK(vector[0] == 0x60d07);
CHECK(vector[200] == 0);
CHECK(vector[499'999] == 0x60d07);
CHECK(vector[999'999] == 0x60d07);
vector.remove_at(200);
CHECK(vector[200] == 0x60d07);
vector.clear();
CHECK(vector.size() == 0);
}
TEST_CASE("[Vector] Copy creation") {
Vector<int> vector;
vector.push_back(0);
vector.push_back(1);
vector.push_back(2);
vector.push_back(3);
vector.push_back(4);
Vector<int> vector_other = Vector<int>(vector);
vector_other.remove_at(0);
CHECK(vector_other[0] == 1);
CHECK(vector_other[1] == 2);
CHECK(vector_other[2] == 3);
CHECK(vector_other[3] == 4);
// Make sure the original vector isn't modified.
CHECK(vector[0] == 0);
CHECK(vector[1] == 1);
CHECK(vector[2] == 2);
CHECK(vector[3] == 3);
CHECK(vector[4] == 4);
}
TEST_CASE("[Vector] Duplicate") {
Vector<int> vector;
vector.push_back(0);
vector.push_back(1);
vector.push_back(2);
vector.push_back(3);
vector.push_back(4);
Vector<int> vector_other = vector.duplicate();
vector_other.remove_at(0);
CHECK(vector_other[0] == 1);
CHECK(vector_other[1] == 2);
CHECK(vector_other[2] == 3);
CHECK(vector_other[3] == 4);
// Make sure the original vector isn't modified.
CHECK(vector[0] == 0);
CHECK(vector[1] == 1);
CHECK(vector[2] == 2);
CHECK(vector[3] == 3);
CHECK(vector[4] == 4);
}
TEST_CASE("[Vector] Get, set") {
Vector<int> vector;
vector.push_back(0);
vector.push_back(1);
vector.push_back(2);
vector.push_back(3);
vector.push_back(4);
CHECK(vector.get(0) == 0);
CHECK(vector.get(1) == 1);
vector.set(2, 256);
CHECK(vector.get(2) == 256);
CHECK(vector.get(3) == 3);
ERR_PRINT_OFF;
// Invalid (but should not crash): setting out of bounds.
vector.set(6, 500);
ERR_PRINT_ON;
CHECK(vector.get(4) == 4);
}
TEST_CASE("[Vector] To byte array (variant call)") {
// PackedInt32Array.
{
PackedInt32Array vector[] = { { 0, -1, 2008 }, {} };
PackedByteArray out[] = { { /* 0 */ 0x00, 0x00, 0x00, 0x00, /* -1 */ 0xFF, 0xFF, 0xFF, 0xFF, /* 2008 */ 0xD8, 0x07, 0x00, 0x00 }, {} };
for (size_t i = 0; i < std::size(vector); i++) {
Callable::CallError err;
Variant v_ret;
Variant v_vector = vector[i];
v_vector.callp("to_byte_array", nullptr, 0, v_ret, err);
CHECK(v_ret.get_type() == Variant::PACKED_BYTE_ARRAY);
CHECK(v_ret.operator PackedByteArray() == out[i]);
}
}
// PackedInt64Array.
{
PackedInt64Array vector[] = { { 0, -1, 2008 }, {} };
PackedByteArray out[] = { { /* 0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* -1 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, /* 2008 */ 0xD8, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, {} };
for (size_t i = 0; i < std::size(vector); i++) {
Callable::CallError err;
Variant v_ret;
Variant v_vector = vector[i];
v_vector.callp("to_byte_array", nullptr, 0, v_ret, err);
CHECK(v_ret.get_type() == Variant::PACKED_BYTE_ARRAY);
CHECK(v_ret.operator PackedByteArray() == out[i]);
}
}
// PackedFloat32Array.
{
PackedFloat32Array vector[] = { { 0.0, -1.0, 200e24 }, {} };
PackedByteArray out[] = { { /* 0.0 */ 0x00, 0x00, 0x00, 0x00, /* -1.0 */ 0x00, 0x00, 0x80, 0xBF, /* 200e24 */ 0xA6, 0x6F, 0x25, 0x6B }, {} };
for (size_t i = 0; i < std::size(vector); i++) {
Callable::CallError err;
Variant v_ret;
Variant v_vector = vector[i];
v_vector.callp("to_byte_array", nullptr, 0, v_ret, err);
CHECK(v_ret.get_type() == Variant::PACKED_BYTE_ARRAY);
CHECK(v_ret.operator PackedByteArray() == out[i]);
}
}
// PackedFloat64Array.
{
PackedFloat64Array vector[] = { { 0.0, -1.0, 200e24 }, {} };
PackedByteArray out[] = { { /* 0.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* -1.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xBF, /* 200e24 */ 0x35, 0x03, 0x32, 0xB7, 0xF4, 0xAD, 0x64, 0x45 }, {} };
for (size_t i = 0; i < std::size(vector); i++) {
Callable::CallError err;
Variant v_ret;
Variant v_vector = vector[i];
v_vector.callp("to_byte_array", nullptr, 0, v_ret, err);
CHECK(v_ret.get_type() == Variant::PACKED_BYTE_ARRAY);
CHECK(v_ret.operator PackedByteArray() == out[i]);
}
}
// PackedStringArray.
{
PackedStringArray vector[] = { { "test", "string" }, {}, { "", "test" } };
PackedByteArray out[] = { { /* test */ 0x74, 0x65, 0x73, 0x74, /* null */ 0x00, /* string */ 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, /* null */ 0x00 }, {}, { /* null */ 0x00, /* test */ 0x74, 0x65, 0x73, 0x74, /* null */ 0x00 } };
for (size_t i = 0; i < std::size(vector); i++) {
Callable::CallError err;
Variant v_ret;
Variant v_vector = vector[i];
v_vector.callp("to_byte_array", nullptr, 0, v_ret, err);
CHECK(v_ret.get_type() == Variant::PACKED_BYTE_ARRAY);
CHECK(v_ret.operator PackedByteArray() == out[i]);
}
}
// PackedVector2Array.
{
PackedVector2Array vector[] = { { Vector2(), Vector2(1, -1) }, {} };
#ifdef REAL_T_IS_DOUBLE
PackedByteArray out[] = { { /* X=0.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* Y=0.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* X=1.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, /* Y=-1.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xBF }, {} };
#else
PackedByteArray out[] = { { /* X=0.0 */ 0x00, 0x00, 0x00, 0x00, /* Y=0.0 */ 0x00, 0x00, 0x00, 0x00, /* X=1.0 */ 0x00, 0x00, 0x80, 0x3F, /* Y=-1.0 */ 0x00, 0x00, 0x80, 0xBF }, {} };
#endif
for (size_t i = 0; i < std::size(vector); i++) {
Callable::CallError err;
Variant v_ret;
Variant v_vector = vector[i];
v_vector.callp("to_byte_array", nullptr, 0, v_ret, err);
CHECK(v_ret.get_type() == Variant::PACKED_BYTE_ARRAY);
CHECK(v_ret.operator PackedByteArray() == out[i]);
}
}
// PackedVector3Array.
{
PackedVector3Array vector[] = { { Vector3(), Vector3(1, 1, -1) }, {} };
#ifdef REAL_T_IS_DOUBLE
PackedByteArray out[] = { { /* X=0.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* Y=0.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* Z=0.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* X=1.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, /* Y=1.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, /* Z=-1.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xBF }, {} };
#else
PackedByteArray out[] = { { /* X=0.0 */ 0x00, 0x00, 0x00, 0x00, /* Y=0.0 */ 0x00, 0x00, 0x00, 0x00, /* Z=0.0 */ 0x00, 0x00, 0x00, 0x00, /* X=1.0 */ 0x00, 0x00, 0x80, 0x3F, /* Y=1.0 */ 0x00, 0x00, 0x80, 0x3F, /* Z=-1.0 */ 0x00, 0x00, 0x80, 0xBF }, {} };
#endif
for (size_t i = 0; i < std::size(vector); i++) {
Callable::CallError err;
Variant v_ret;
Variant v_vector = vector[i];
v_vector.callp("to_byte_array", nullptr, 0, v_ret, err);
CHECK(v_ret.get_type() == Variant::PACKED_BYTE_ARRAY);
CHECK(v_ret.operator PackedByteArray() == out[i]);
}
}
// PackedColorArray.
{
PackedColorArray vector[] = { { Color(), Color(1, 1, 1) }, {} };
PackedByteArray out[] = { { /* R=0.0 */ 0x00, 0x00, 0x00, 0x00, /* G=0.0 */ 0x00, 0x00, 0x00, 0x00, /* B=0.0 */ 0x00, 0x00, 0x00, 0x00, /* A=1.0 */ 0x00, 0x00, 0x80, 0x3F, /* R=1.0 */ 0x00, 0x00, 0x80, 0x3F, /* G=1.0 */ 0x00, 0x00, 0x80, 0x3F, /* B=1.0 */ 0x00, 0x00, 0x80, 0x3F, /* A=1.0 */ 0x00, 0x00, 0x80, 0x3F }, {} };
for (size_t i = 0; i < std::size(vector); i++) {
Callable::CallError err;
Variant v_ret;
Variant v_vector = vector[i];
v_vector.callp("to_byte_array", nullptr, 0, v_ret, err);
CHECK(v_ret.get_type() == Variant::PACKED_BYTE_ARRAY);
CHECK(v_ret.operator PackedByteArray() == out[i]);
}
}
// PackedVector4Array.
{
PackedVector4Array vector[] = { { Vector4(), Vector4(1, -1, 1, -1) }, {} };
#ifdef REAL_T_IS_DOUBLE
PackedByteArray out[] = { { /* X=0.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* Y=0.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* Z 0.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* W=0.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* X=1.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, /* Y=-1.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xBF, /* Z=1.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, /* W=-1.0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xBF }, {} };
#else
PackedByteArray out[] = { { /* X=0.0 */ 0x00, 0x00, 0x00, 0x00, /* Y=0.0 */ 0x00, 0x00, 0x00, 0x00, /* Z=0.0 */ 0x00, 0x00, 0x00, 0x00, /* W 0.0 */ 0x00, 0x00, 0x00, 0x00, /* X 1.0 */ 0x00, 0x00, 0x80, 0x3F, /* Y=-1.0 */ 0x00, 0x00, 0x80, 0xBF, /* Z=1.0 */ 0x00, 0x00, 0x80, 0x3F, /* W=-1.0 */ 0x00, 0x00, 0x80, 0xBF }, {} };
#endif
for (size_t i = 0; i < std::size(vector); i++) {
Callable::CallError err;
Variant v_ret;
Variant v_vector = vector[i];
v_vector.callp("to_byte_array", nullptr, 0, v_ret, err);
CHECK(v_ret.get_type() == Variant::PACKED_BYTE_ARRAY);
CHECK(v_ret.operator PackedByteArray() == out[i]);
}
}
}
TEST_CASE("[Vector] To byte array") {
Vector<int> vector;
vector.push_back(0);
vector.push_back(-1);
vector.push_back(2008);
vector.push_back(999999999);
Vector<uint8_t> byte_array = vector.to_byte_array();
CHECK(byte_array.size() == 16);
// vector[0]
CHECK(byte_array[0] == 0);
CHECK(byte_array[1] == 0);
CHECK(byte_array[2] == 0);
CHECK(byte_array[3] == 0);
// vector[1]
CHECK(byte_array[4] == 255);
CHECK(byte_array[5] == 255);
CHECK(byte_array[6] == 255);
CHECK(byte_array[7] == 255);
// vector[2]
CHECK(byte_array[8] == 216);
CHECK(byte_array[9] == 7);
CHECK(byte_array[10] == 0);
CHECK(byte_array[11] == 0);
// vector[3]
CHECK(byte_array[12] == 255);
CHECK(byte_array[13] == 201);
CHECK(byte_array[14] == 154);
CHECK(byte_array[15] == 59);
}
TEST_CASE("[Vector] Slice") {
Vector<int> vector;
vector.push_back(0);
vector.push_back(1);
vector.push_back(2);
vector.push_back(3);
vector.push_back(4);
Vector<int> slice0 = vector.slice(0, 0);
CHECK(slice0.size() == 0);
Vector<int> slice1 = vector.slice(1, 3);
CHECK(slice1.size() == 2);
CHECK(slice1[0] == 1);
CHECK(slice1[1] == 2);
Vector<int> slice2 = vector.slice(1, -1);
CHECK(slice2.size() == 3);
CHECK(slice2[0] == 1);
CHECK(slice2[1] == 2);
CHECK(slice2[2] == 3);
Vector<int> slice3 = vector.slice(3);
CHECK(slice3.size() == 2);
CHECK(slice3[0] == 3);
CHECK(slice3[1] == 4);
Vector<int> slice4 = vector.slice(2, -2);
CHECK(slice4.size() == 1);
CHECK(slice4[0] == 2);
Vector<int> slice5 = vector.slice(-2);
CHECK(slice5.size() == 2);
CHECK(slice5[0] == 3);
CHECK(slice5[1] == 4);
Vector<int> slice6 = vector.slice(2, 42);
CHECK(slice6.size() == 3);
CHECK(slice6[0] == 2);
CHECK(slice6[1] == 3);
CHECK(slice6[2] == 4);
ERR_PRINT_OFF;
Vector<int> slice7 = vector.slice(5, 1);
CHECK(slice7.size() == 0); // Expected to fail.
ERR_PRINT_ON;
}
TEST_CASE("[Vector] Find, has") {
Vector<int> vector;
vector.push_back(3);
vector.push_back(1);
vector.push_back(4);
vector.push_back(0);
vector.push_back(2);
CHECK(vector[0] == 3);
CHECK(vector[1] == 1);
CHECK(vector[2] == 4);
CHECK(vector[3] == 0);
CHECK(vector[4] == 2);
CHECK(vector.find(0) == 3);
CHECK(vector.find(1) == 1);
CHECK(vector.find(2) == 4);
CHECK(vector.find(3) == 0);
CHECK(vector.find(4) == 2);
CHECK(vector.find(-1) == -1);
CHECK(vector.find(5) == -1);
CHECK(vector.has(0));
CHECK(vector.has(1));
CHECK(vector.has(2));
CHECK(vector.has(3));
CHECK(vector.has(4));
CHECK(!vector.has(-1));
CHECK(!vector.has(5));
}
TEST_CASE("[Vector] Remove at") {
Vector<int> vector;
vector.push_back(0);
vector.push_back(1);
vector.push_back(2);
vector.push_back(3);
vector.push_back(4);
vector.remove_at(0);
CHECK(vector[0] == 1);
CHECK(vector[1] == 2);
CHECK(vector[2] == 3);
CHECK(vector[3] == 4);
vector.remove_at(2);
CHECK(vector[0] == 1);
CHECK(vector[1] == 2);
CHECK(vector[2] == 4);
vector.remove_at(1);
CHECK(vector[0] == 1);
CHECK(vector[1] == 4);
vector.remove_at(0);
CHECK(vector[0] == 4);
}
TEST_CASE("[Vector] Remove at and find") {
Vector<int> vector;
vector.push_back(0);
vector.push_back(1);
vector.push_back(2);
vector.push_back(3);
vector.push_back(4);
CHECK(vector.size() == 5);
vector.remove_at(0);
CHECK(vector.size() == 4);
CHECK(vector.find(0) == -1);
CHECK(vector.find(1) != -1);
CHECK(vector.find(2) != -1);
CHECK(vector.find(3) != -1);
CHECK(vector.find(4) != -1);
vector.remove_at(vector.find(3));
CHECK(vector.size() == 3);
CHECK(vector.find(3) == -1);
CHECK(vector.find(1) != -1);
CHECK(vector.find(2) != -1);
CHECK(vector.find(4) != -1);
vector.remove_at(vector.find(2));
CHECK(vector.size() == 2);
CHECK(vector.find(2) == -1);
CHECK(vector.find(1) != -1);
CHECK(vector.find(4) != -1);
vector.remove_at(vector.find(4));
CHECK(vector.size() == 1);
CHECK(vector.find(4) == -1);
CHECK(vector.find(1) != -1);
vector.remove_at(0);
CHECK(vector.is_empty());
CHECK(vector.size() == 0);
}
TEST_CASE("[Vector] Erase") {
Vector<int> vector;
vector.push_back(1);
vector.push_back(3);
vector.push_back(0);
vector.push_back(2);
vector.push_back(4);
CHECK(vector.find(2) == 3);
vector.erase(2);
CHECK(vector.find(2) == -1);
CHECK(vector.size() == 4);
}
TEST_CASE("[Vector] Size, resize, reserve") {
Vector<int> vector;
CHECK(vector.is_empty());
CHECK(vector.size() == 0);
vector.resize(10);
CHECK(vector.size() == 10);
vector.resize(5);
CHECK(vector.size() == 5);
vector.remove_at(0);
vector.remove_at(0);
vector.remove_at(0);
CHECK(vector.size() == 2);
vector.clear();
CHECK(vector.size() == 0);
CHECK(vector.is_empty());
vector.push_back(0);
vector.push_back(0);
vector.push_back(0);
CHECK(vector.size() == 3);
vector.push_back(0);
CHECK(vector.size() == 4);
}
TEST_CASE("[Vector] Sort") {
Vector<int> vector;
vector.push_back(2);
vector.push_back(8);
vector.push_back(-4);
vector.push_back(5);
vector.sort();
CHECK(vector.size() == 4);
CHECK(vector[0] == -4);
CHECK(vector[1] == 2);
CHECK(vector[2] == 5);
CHECK(vector[3] == 8);
}
TEST_CASE("[Vector] Sort custom") {
Vector<String> vector;
vector.push_back("world");
vector.push_back("World");
vector.push_back("Hello");
vector.push_back("10Hello");
vector.push_back("12Hello");
vector.push_back("01Hello");
vector.push_back("1Hello");
vector.push_back(".Hello");
vector.sort_custom<NaturalNoCaseComparator>();
CHECK(vector.size() == 8);
CHECK(vector[0] == ".Hello");
CHECK(vector[1] == "01Hello");
CHECK(vector[2] == "1Hello");
CHECK(vector[3] == "10Hello");
CHECK(vector[4] == "12Hello");
CHECK(vector[5] == "Hello");
CHECK(vector[6] == "world");
CHECK(vector[7] == "World");
}
TEST_CASE("[Vector] Search") {
Vector<int> vector;
vector.push_back(1);
vector.push_back(2);
vector.push_back(3);
vector.push_back(5);
vector.push_back(8);
CHECK(vector.bsearch(2, true) == 1);
CHECK(vector.bsearch(2, false) == 2);
CHECK(vector.bsearch(5, true) == 3);
CHECK(vector.bsearch(5, false) == 4);
}
TEST_CASE("[Vector] Operators") {
Vector<int> vector;
vector.push_back(2);
vector.push_back(8);
vector.push_back(-4);
vector.push_back(5);
Vector<int> vector_other;
vector_other.push_back(2);
vector_other.push_back(8);
vector_other.push_back(-4);
vector_other.push_back(5);
CHECK(vector == vector_other);
vector_other.push_back(10);
CHECK(vector != vector_other);
}
struct CyclicVectorHolder {
Vector<CyclicVectorHolder> *vector = nullptr;
bool is_destructing = false;
~CyclicVectorHolder() {
if (is_destructing) {
// The vector must exist and not expose its backing array at this point.
CHECK_NE(vector, nullptr);
CHECK_EQ(vector->ptr(), nullptr);
}
}
};
TEST_CASE("[Vector] Cyclic Reference") {
// Create a stack-space vector.
Vector<CyclicVectorHolder> vector;
// Add a new (empty) element.
vector.resize(1);
// Expose the vector to its element through CyclicVectorHolder.
// This is questionable behavior, but should still behave graciously.
vector.ptrw()[0] = CyclicVectorHolder{ &vector };
vector.ptrw()[0].is_destructing = true;
// The vector goes out of scope and destructs, calling CyclicVectorHolder's destructor.
}
} // namespace TestVector

View File

@@ -0,0 +1,99 @@
/**************************************************************************/
/* test_vset.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/templates/vset.h"
#include "tests/test_macros.h"
namespace TestVSet {
template <typename T>
class TestClass : public VSet<T> {
public:
int _find(const T &p_val, bool &r_exact) const {
return VSet<T>::_find(p_val, r_exact);
}
};
TEST_CASE("[VSet] _find and _find_exact correctness.") {
TestClass<int> set;
// insert some values
set.insert(10);
set.insert(20);
set.insert(30);
set.insert(40);
set.insert(50);
// data should be sorted
CHECK(set.size() == 5);
CHECK(set[0] == 10);
CHECK(set[1] == 20);
CHECK(set[2] == 30);
CHECK(set[3] == 40);
CHECK(set[4] == 50);
// _find_exact return exact position for existing elements
CHECK(set.find(10) == 0);
CHECK(set.find(30) == 2);
CHECK(set.find(50) == 4);
// _find_exact return -1 for non-existing elements
CHECK(set.find(15) == -1);
CHECK(set.find(0) == -1);
CHECK(set.find(60) == -1);
// test _find
bool exact;
// existing elements
CHECK(set._find(10, exact) == 0);
CHECK(exact == true);
CHECK(set._find(30, exact) == 2);
CHECK(exact == true);
// non-existing elements
CHECK(set._find(25, exact) == 2);
CHECK(exact == false);
CHECK(set._find(35, exact) == 3);
CHECK(exact == false);
CHECK(set._find(5, exact) == 0);
CHECK(exact == false);
CHECK(set._find(60, exact) == 5);
CHECK(exact == false);
}
} // namespace TestVSet

70
tests/core/test_crypto.h Normal file
View File

@@ -0,0 +1,70 @@
/**************************************************************************/
/* test_crypto.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/crypto/crypto.h"
#include "tests/test_macros.h"
namespace TestCrypto {
class _MockCrypto : public Crypto {
virtual PackedByteArray generate_random_bytes(int p_bytes) { return PackedByteArray(); }
virtual Ref<CryptoKey> generate_rsa(int p_bytes) { return nullptr; }
virtual Ref<X509Certificate> generate_self_signed_certificate(Ref<CryptoKey> p_key, const String &p_issuer_name, const String &p_not_before, const String &p_not_after) { return nullptr; }
virtual Vector<uint8_t> sign(HashingContext::HashType p_hash_type, const Vector<uint8_t> &p_hash, Ref<CryptoKey> p_key) { return Vector<uint8_t>(); }
virtual bool verify(HashingContext::HashType p_hash_type, const Vector<uint8_t> &p_hash, const Vector<uint8_t> &p_signature, Ref<CryptoKey> p_key) { return false; }
virtual Vector<uint8_t> encrypt(Ref<CryptoKey> p_key, const Vector<uint8_t> &p_plaintext) { return Vector<uint8_t>(); }
virtual Vector<uint8_t> decrypt(Ref<CryptoKey> p_key, const Vector<uint8_t> &p_ciphertext) { return Vector<uint8_t>(); }
virtual PackedByteArray hmac_digest(HashingContext::HashType p_hash_type, const PackedByteArray &p_key, const PackedByteArray &p_msg) { return PackedByteArray(); }
};
PackedByteArray raw_to_pba(const uint8_t *arr, size_t len) {
PackedByteArray pba;
pba.resize(len);
for (size_t i = 0; i < len; i++) {
pba.set(i, arr[i]);
}
return pba;
}
TEST_CASE("[Crypto] PackedByteArray constant time compare") {
const uint8_t hm1[] = { 144, 140, 176, 38, 88, 113, 101, 45, 71, 105, 10, 91, 248, 16, 117, 244, 189, 30, 238, 29, 219, 134, 82, 130, 212, 114, 161, 166, 188, 169, 200, 106 };
const uint8_t hm2[] = { 80, 30, 144, 228, 108, 38, 188, 125, 150, 64, 165, 127, 221, 118, 144, 232, 45, 100, 15, 248, 193, 244, 245, 34, 116, 147, 132, 200, 110, 27, 38, 75 };
PackedByteArray p1 = raw_to_pba(hm1, std::size(hm1));
PackedByteArray p2 = raw_to_pba(hm2, std::size(hm2));
_MockCrypto crypto;
bool equal = crypto.constant_time_compare(p1, p1);
CHECK(equal);
equal = crypto.constant_time_compare(p1, p2);
CHECK(!equal);
}
} // namespace TestCrypto

View File

@@ -0,0 +1,162 @@
/**************************************************************************/
/* test_hashing_context.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/crypto/hashing_context.h"
#include "tests/test_macros.h"
namespace TestHashingContext {
TEST_CASE("[HashingContext] Default - MD5/SHA1/SHA256") {
HashingContext ctx;
static const uint8_t md5_expected[] = {
0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8, 0x42, 0x7e
};
static const uint8_t sha1_expected[] = {
0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60, 0x18, 0x90,
0xaf, 0xd8, 0x07, 0x09
};
static const uint8_t sha256_expected[] = {
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55
};
CHECK(ctx.start(HashingContext::HASH_MD5) == OK);
PackedByteArray result = ctx.finish();
REQUIRE(result.size() == 16);
CHECK(memcmp(result.ptr(), md5_expected, 16) == 0);
CHECK(ctx.start(HashingContext::HASH_SHA1) == OK);
result = ctx.finish();
REQUIRE(result.size() == 20);
CHECK(memcmp(result.ptr(), sha1_expected, 20) == 0);
CHECK(ctx.start(HashingContext::HASH_SHA256) == OK);
result = ctx.finish();
REQUIRE(result.size() == 32);
CHECK(memcmp(result.ptr(), sha256_expected, 32) == 0);
}
TEST_CASE("[HashingContext] Multiple updates - MD5/SHA1/SHA256") {
HashingContext ctx;
const String s = "xyz";
const PackedByteArray s_byte_parts[] = {
String("x").to_ascii_buffer(),
String("y").to_ascii_buffer(),
String("z").to_ascii_buffer()
};
static const uint8_t md5_expected[] = {
0xd1, 0x6f, 0xb3, 0x6f, 0x09, 0x11, 0xf8, 0x78, 0x99, 0x8c, 0x13, 0x61, 0x91, 0xaf, 0x70, 0x5e
};
static const uint8_t sha1_expected[] = {
0x66, 0xb2, 0x74, 0x17, 0xd3, 0x7e, 0x02, 0x4c, 0x46, 0x52, 0x6c, 0x2f, 0x6d, 0x35, 0x8a, 0x75,
0x4f, 0xc5, 0x52, 0xf3
};
static const uint8_t sha256_expected[] = {
0x36, 0x08, 0xbc, 0xa1, 0xe4, 0x4e, 0xa6, 0xc4, 0xd2, 0x68, 0xeb, 0x6d, 0xb0, 0x22, 0x60, 0x26,
0x98, 0x92, 0xc0, 0xb4, 0x2b, 0x86, 0xbb, 0xf1, 0xe7, 0x7a, 0x6f, 0xa1, 0x6c, 0x3c, 0x92, 0x82
};
CHECK(ctx.start(HashingContext::HASH_MD5) == OK);
CHECK(ctx.update(s_byte_parts[0]) == OK);
CHECK(ctx.update(s_byte_parts[1]) == OK);
CHECK(ctx.update(s_byte_parts[2]) == OK);
PackedByteArray result = ctx.finish();
REQUIRE(result.size() == 16);
CHECK(memcmp(result.ptr(), md5_expected, 16) == 0);
CHECK(ctx.start(HashingContext::HASH_SHA1) == OK);
CHECK(ctx.update(s_byte_parts[0]) == OK);
CHECK(ctx.update(s_byte_parts[1]) == OK);
CHECK(ctx.update(s_byte_parts[2]) == OK);
result = ctx.finish();
REQUIRE(result.size() == 20);
CHECK(memcmp(result.ptr(), sha1_expected, 20) == 0);
CHECK(ctx.start(HashingContext::HASH_SHA256) == OK);
CHECK(ctx.update(s_byte_parts[0]) == OK);
CHECK(ctx.update(s_byte_parts[1]) == OK);
CHECK(ctx.update(s_byte_parts[2]) == OK);
result = ctx.finish();
REQUIRE(result.size() == 32);
CHECK(memcmp(result.ptr(), sha256_expected, 32) == 0);
}
TEST_CASE("[HashingContext] Invalid use of start") {
HashingContext ctx;
ERR_PRINT_OFF;
CHECK_MESSAGE(
ctx.start(static_cast<HashingContext::HashType>(-1)) == ERR_UNAVAILABLE,
"Using invalid hash types should fail.");
ERR_PRINT_ON;
REQUIRE(ctx.start(HashingContext::HASH_MD5) == OK);
ERR_PRINT_OFF;
CHECK_MESSAGE(
ctx.start(HashingContext::HASH_MD5) == ERR_ALREADY_IN_USE,
"Calling 'start' twice before 'finish' should fail.");
ERR_PRINT_ON;
}
TEST_CASE("[HashingContext] Invalid use of update") {
HashingContext ctx;
ERR_PRINT_OFF;
CHECK_MESSAGE(
ctx.update(PackedByteArray()) == ERR_UNCONFIGURED,
"Calling 'update' before 'start' should fail.");
ERR_PRINT_ON;
REQUIRE(ctx.start(HashingContext::HASH_MD5) == OK);
ERR_PRINT_OFF;
CHECK_MESSAGE(
ctx.update(PackedByteArray()) == FAILED,
"Calling 'update' with an empty byte array should fail.");
ERR_PRINT_ON;
}
TEST_CASE("[HashingContext] Invalid use of finish") {
HashingContext ctx;
ERR_PRINT_OFF;
CHECK_MESSAGE(
ctx.finish() == PackedByteArray(),
"Calling 'finish' before 'start' should return an empty byte array.");
ERR_PRINT_ON;
}
} // namespace TestHashingContext

145
tests/core/test_time.h Normal file
View File

@@ -0,0 +1,145 @@
/**************************************************************************/
/* test_time.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/os/time.h"
#include "thirdparty/doctest/doctest.h"
#define YEAR_KEY "year"
#define MONTH_KEY "month"
#define DAY_KEY "day"
#define WEEKDAY_KEY "weekday"
#define HOUR_KEY "hour"
#define MINUTE_KEY "minute"
#define SECOND_KEY "second"
#define DST_KEY "dst"
namespace TestTime {
TEST_CASE("[Time] Unix time conversion to/from datetime string") {
const Time *time = Time::get_singleton();
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1970-01-01T00:00:00") == 0, "Time get_unix_time_from_datetime_string: The timestamp for Unix epoch is zero.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1970-01-01 00:00:00") == 0, "Time get_unix_time_from_datetime_string: The timestamp for Unix epoch with space is zero.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1970-01-01") == 0, "Time get_unix_time_from_datetime_string: The timestamp for Unix epoch without time is zero.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("00:00:00") == 0, "Time get_unix_time_from_datetime_string: The timestamp for zero time without date is zero.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1969-12-31T23:59:59") == -1, "Time get_unix_time_from_datetime_string: The timestamp for just before Unix epoch is negative one.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1234-05-06T07:08:09") == -23215049511, "Time get_unix_time_from_datetime_string: The timestamp for an arbitrary datetime is as expected.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1234-05-06 07:08:09") == -23215049511, "Time get_unix_time_from_datetime_string: The timestamp for an arbitrary datetime with space is as expected.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1234-05-06") == -23215075200, "Time get_unix_time_from_datetime_string: The timestamp for an arbitrary date without time is as expected.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("07:08:09") == 25689, "Time get_unix_time_from_datetime_string: The timestamp for an arbitrary time without date is as expected.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("2014-02-09T22:10:30") == 1391983830, "Time get_unix_time_from_datetime_string: The timestamp for GODOT IS OPEN SOURCE is as expected.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("2014-02-09 22:10:30") == 1391983830, "Time get_unix_time_from_datetime_string: The timestamp for GODOT IS OPEN SOURCE with space is as expected.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("2014-02-09") == 1391904000, "Time get_unix_time_from_datetime_string: The date for GODOT IS OPEN SOURCE without time is as expected.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("22:10:30") == 79830, "Time get_unix_time_from_datetime_string: The time for GODOT IS OPEN SOURCE without date is as expected.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("-1000000000-01-01T00:00:00") == -31557014167219200, "Time get_unix_time_from_datetime_string: In the year negative a billion, Japan might not have been here.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string("1000000-01-01T00:00:00") == 31494784780800, "Time get_unix_time_from_datetime_string: The timestamp for the year a million is as expected.");
CHECK_MESSAGE(time->get_datetime_string_from_unix_time(0) == "1970-01-01T00:00:00", "Time get_datetime_string_from_unix_time: The timestamp string for Unix epoch is zero.");
CHECK_MESSAGE(time->get_datetime_string_from_unix_time(0, true) == "1970-01-01 00:00:00", "Time get_datetime_string_from_unix_time: The timestamp string for Unix epoch with space is zero.");
CHECK_MESSAGE(time->get_date_string_from_unix_time(0) == "1970-01-01", "Time get_date_string_from_unix_time: The date string for zero is Unix epoch date.");
CHECK_MESSAGE(time->get_time_string_from_unix_time(0) == "00:00:00", "Time get_time_string_from_unix_time: The date for zero zero is Unix epoch date.");
CHECK_MESSAGE(time->get_datetime_string_from_unix_time(-1) == "1969-12-31T23:59:59", "Time get_time_string_from_unix_time: The timestamp string for just before Unix epoch is as expected.");
CHECK_MESSAGE(time->get_datetime_string_from_unix_time(-23215049511) == "1234-05-06T07:08:09", "Time get_datetime_string_from_unix_time: The timestamp for an arbitrary datetime is as expected.");
CHECK_MESSAGE(time->get_datetime_string_from_unix_time(-23215049511, true) == "1234-05-06 07:08:09", "Time get_datetime_string_from_unix_time: The timestamp for an arbitrary datetime with space is as expected.");
CHECK_MESSAGE(time->get_date_string_from_unix_time(-23215075200) == "1234-05-06", "Time get_date_string_from_unix_time: The timestamp for an arbitrary date without time is as expected.");
CHECK_MESSAGE(time->get_time_string_from_unix_time(25689) == "07:08:09", "Time get_time_string_from_unix_time: The timestamp for an arbitrary time without date is as expected.");
CHECK_MESSAGE(time->get_datetime_string_from_unix_time(1391983830) == "2014-02-09T22:10:30", "Time get_datetime_string_from_unix_time: The timestamp for GODOT IS OPEN SOURCE is as expected.");
CHECK_MESSAGE(time->get_datetime_string_from_unix_time(1391983830, true) == "2014-02-09 22:10:30", "Time get_datetime_string_from_unix_time: The timestamp for GODOT IS OPEN SOURCE with space is as expected.");
CHECK_MESSAGE(time->get_date_string_from_unix_time(1391904000) == "2014-02-09", "Time get_date_string_from_unix_time: The date for GODOT IS OPEN SOURCE without time is as expected.");
CHECK_MESSAGE(time->get_time_string_from_unix_time(79830) == "22:10:30", "Time get_time_string_from_unix_time: The time for GODOT IS OPEN SOURCE without date is as expected.");
CHECK_MESSAGE(time->get_datetime_string_from_unix_time(31494784780800) == "1000000-01-01T00:00:00", "Time get_datetime_string_from_unix_time: The timestamp for the year a million is as expected.");
CHECK_MESSAGE(time->get_offset_string_from_offset_minutes(0) == "+00:00", "Time get_offset_string_from_offset_minutes: The offset string is as expected.");
CHECK_MESSAGE(time->get_offset_string_from_offset_minutes(-600) == "-10:00", "Time get_offset_string_from_offset_minutes: The offset string is as expected.");
CHECK_MESSAGE(time->get_offset_string_from_offset_minutes(345) == "+05:45", "Time get_offset_string_from_offset_minutes: The offset string is as expected.");
}
TEST_CASE("[Time] Datetime dictionary conversion methods") {
const Time *time = Time::get_singleton();
Dictionary datetime;
datetime[YEAR_KEY] = 2014;
datetime[MONTH_KEY] = 2;
datetime[DAY_KEY] = 9;
datetime[WEEKDAY_KEY] = (int64_t)Weekday::WEEKDAY_SUNDAY;
datetime[HOUR_KEY] = 22;
datetime[MINUTE_KEY] = 10;
datetime[SECOND_KEY] = 30;
Dictionary date_only;
date_only[YEAR_KEY] = 2014;
date_only[MONTH_KEY] = 2;
date_only[DAY_KEY] = 9;
date_only[WEEKDAY_KEY] = (int64_t)Weekday::WEEKDAY_SUNDAY;
Dictionary time_only;
time_only[HOUR_KEY] = 22;
time_only[MINUTE_KEY] = 10;
time_only[SECOND_KEY] = 30;
CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(datetime) == 1391983830, "Time get_unix_time_from_datetime_dict: The datetime dictionary for GODOT IS OPEN SOURCE is converted to a timestamp as expected.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(date_only) == 1391904000, "Time get_unix_time_from_datetime_dict: The date dictionary for GODOT IS OPEN SOURCE is converted to a timestamp as expected.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(time_only) == 79830, "Time get_unix_time_from_datetime_dict: The time dictionary for GODOT IS OPEN SOURCE is converted to a timestamp as expected.");
CHECK_MESSAGE(time->get_datetime_dict_from_unix_time(1391983830).hash() == datetime.hash(), "Time get_datetime_dict_from_unix_time: The datetime timestamp for GODOT IS OPEN SOURCE is converted to a dictionary as expected.");
CHECK_MESSAGE(time->get_date_dict_from_unix_time(1391904000).hash() == date_only.hash(), "Time get_date_dict_from_unix_time: The date timestamp for GODOT IS OPEN SOURCE is converted to a dictionary as expected.");
CHECK_MESSAGE(time->get_time_dict_from_unix_time(79830).hash() == time_only.hash(), "Time get_time_dict_from_unix_time: The time timestamp for GODOT IS OPEN SOURCE is converted to a dictionary as expected.");
CHECK_MESSAGE((Weekday)(int)time->get_datetime_dict_from_unix_time(0)[WEEKDAY_KEY] == Weekday::WEEKDAY_THURSDAY, "Time get_datetime_dict_from_unix_time: The weekday for the Unix epoch is a Thursday as expected.");
CHECK_MESSAGE((Weekday)(int)time->get_datetime_dict_from_unix_time(1391983830)[WEEKDAY_KEY] == Weekday::WEEKDAY_SUNDAY, "Time get_datetime_dict_from_unix_time: The weekday for GODOT IS OPEN SOURCE is a Sunday as expected.");
CHECK_MESSAGE(time->get_datetime_dict_from_datetime_string("2014-02-09T22:10:30").hash() == datetime.hash(), "Time get_datetime_dict_from_string: The dictionary from string for GODOT IS OPEN SOURCE works as expected.");
CHECK_MESSAGE(!time->get_datetime_dict_from_datetime_string("2014-02-09T22:10:30", false).has(WEEKDAY_KEY), "Time get_datetime_dict_from_string: The dictionary from string for GODOT IS OPEN SOURCE without weekday doesn't contain the weekday key as expected.");
CHECK_MESSAGE(time->get_datetime_string_from_datetime_dict(datetime) == "2014-02-09T22:10:30", "Time get_datetime_string_from_dict: The string from dictionary for GODOT IS OPEN SOURCE works as expected.");
CHECK_MESSAGE(time->get_datetime_string_from_datetime_dict(time->get_datetime_dict_from_datetime_string("2014-02-09T22:10:30")) == "2014-02-09T22:10:30", "Time get_datetime_string_from_dict: The round-trip string to dict to string GODOT IS OPEN SOURCE works as expected.");
CHECK_MESSAGE(time->get_datetime_string_from_datetime_dict(time->get_datetime_dict_from_datetime_string("2014-02-09 22:10:30"), true) == "2014-02-09 22:10:30", "Time get_datetime_string_from_dict: The round-trip string to dict to string GODOT IS OPEN SOURCE with spaces works as expected.");
}
TEST_CASE("[Time] System time methods") {
const Time *time = Time::get_singleton();
const uint64_t ticks_msec = time->get_ticks_msec();
const uint64_t ticks_usec = time->get_ticks_usec();
CHECK_MESSAGE(time->get_unix_time_from_system() > 1000000000, "Time get_unix_time_from_system: The timestamp from system time doesn't fail and is very positive.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(time->get_datetime_dict_from_system()) > 1000000000, "Time get_datetime_string_from_system: The timestamp from system time doesn't fail and is very positive.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(time->get_date_dict_from_system()) > 1000000000, "Time get_datetime_string_from_system: The date from system time doesn't fail and is very positive.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_dict(time->get_time_dict_from_system()) < 86400, "Time get_datetime_string_from_system: The time from system time doesn't fail and is within the acceptable range.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string(time->get_datetime_string_from_system()) > 1000000000, "Time get_datetime_string_from_system: The timestamp from system time doesn't fail and is very positive.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string(time->get_date_string_from_system()) > 1000000000, "Time get_datetime_string_from_system: The date from system time doesn't fail and is very positive.");
CHECK_MESSAGE(time->get_unix_time_from_datetime_string(time->get_time_string_from_system()) < 86400, "Time get_datetime_string_from_system: The time from system time doesn't fail and is within the acceptable range.");
CHECK_MESSAGE(time->get_ticks_msec() >= ticks_msec, "Time get_ticks_msec: The value has not decreased.");
CHECK_MESSAGE(time->get_ticks_usec() > ticks_usec, "Time get_ticks_usec: The value has increased.");
}
} // namespace TestTime

View File

@@ -0,0 +1,175 @@
/**************************************************************************/
/* test_worker_thread_pool.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/object/worker_thread_pool.h"
#include "tests/test_macros.h"
namespace TestWorkerThreadPool {
static LocalVector<SafeNumeric<int>> counter;
static SafeFlag exit;
static void static_test(void *p_arg) {
counter[(uint64_t)p_arg].increment();
counter[0].add(2);
}
static void static_callable_test() {
counter[0].sub(2);
}
TEST_CASE("[WorkerThreadPool] Process threads using individual tasks") {
for (int iterations = 0; iterations < 500; iterations++) {
const int count = Math::pow(2.0f, Math::random(0.0f, 5.0f));
const bool low_priority = Math::rand() % 2;
LocalVector<WorkerThreadPool::TaskID> tasks1;
LocalVector<WorkerThreadPool::TaskID> tasks2;
tasks1.resize(count);
tasks2.resize(count);
counter.clear();
counter.resize(count);
for (int i = 0; i < count; i++) {
tasks1[i] = WorkerThreadPool::get_singleton()->add_native_task(static_test, (void *)(uintptr_t)i, low_priority);
tasks2[i] = WorkerThreadPool::get_singleton()->add_task(callable_mp_static(static_callable_test), !low_priority);
}
for (int i = 0; i < count; i++) {
WorkerThreadPool::get_singleton()->wait_for_task_completion(tasks1[i]);
WorkerThreadPool::get_singleton()->wait_for_task_completion(tasks2[i]);
}
bool all_run_once = true;
for (int i = 0; i < count; i++) {
//Reduce number of check messages
all_run_once &= counter[i].get() == 1;
}
CHECK(all_run_once);
}
}
static void static_group_test(void *p_arg, uint32_t p_index) {
counter[p_index].increment();
counter[0].add((uintptr_t)p_arg);
}
static void static_callable_group_test(uint32_t p_index) {
counter[p_index].increment();
counter[0].sub(2);
}
TEST_CASE("[WorkerThreadPool] Process elements using group tasks") {
for (int iterations = 0; iterations < 500; iterations++) {
const int count = Math::pow(2.0f, Math::random(0.0f, 5.0f));
const int tasks = Math::pow(2.0f, Math::random(0.0f, 5.0f));
const bool low_priority = Math::rand() % 2;
counter.clear();
counter.resize(count);
WorkerThreadPool::GroupID group1 = WorkerThreadPool::get_singleton()->add_native_group_task(static_group_test, (void *)2, count, tasks, !low_priority);
WorkerThreadPool::GroupID group2 = WorkerThreadPool::get_singleton()->add_group_task(callable_mp_static(static_callable_group_test), count, tasks, low_priority);
WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group1);
WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group2);
bool all_run_once = true;
for (int i = 0; i < count; i++) {
//Reduce number of check messages
all_run_once &= counter[i].get() == 2;
}
CHECK(all_run_once);
}
}
static void static_test_daemon(void *p_arg) {
while (!exit.is_set()) {
counter[0].add(1);
WorkerThreadPool::get_singleton()->yield();
}
}
static void static_busy_task(void *p_arg) {
while (!exit.is_set()) {
OS::get_singleton()->delay_usec(1);
}
}
static void static_legit_task(void *p_arg) {
*((bool *)p_arg) = counter[0].get() > 0;
counter[1].add(1);
}
TEST_CASE("[WorkerThreadPool] Run a yielding daemon as the only hope for other tasks to run") {
exit.clear();
counter.clear();
counter.resize(2);
WorkerThreadPool::TaskID daemon_task_id = WorkerThreadPool::get_singleton()->add_native_task(static_test_daemon, nullptr, true);
int num_threads = WorkerThreadPool::get_singleton()->get_thread_count();
// Keep all the other threads busy.
LocalVector<WorkerThreadPool::TaskID> task_ids;
for (int i = 0; i < num_threads - 1; i++) {
task_ids.push_back(WorkerThreadPool::get_singleton()->add_native_task(static_busy_task, nullptr, true));
}
LocalVector<WorkerThreadPool::TaskID> legit_task_ids;
LocalVector<bool> legit_task_needed_yield;
int legit_tasks_count = num_threads * 4;
legit_task_needed_yield.resize(legit_tasks_count);
for (int i = 0; i < legit_tasks_count; i++) {
legit_task_needed_yield[i] = false;
task_ids.push_back(WorkerThreadPool::get_singleton()->add_native_task(static_legit_task, &legit_task_needed_yield[i], i >= legit_tasks_count / 2));
}
while (counter[1].get() != legit_tasks_count) {
OS::get_singleton()->delay_usec(1);
}
exit.set();
for (uint32_t i = 0; i < task_ids.size(); i++) {
WorkerThreadPool::get_singleton()->wait_for_task_completion(task_ids[i]);
}
WorkerThreadPool::get_singleton()->notify_yield_over(daemon_task_id);
WorkerThreadPool::get_singleton()->wait_for_task_completion(daemon_task_id);
CHECK_MESSAGE(counter[0].get() > 0, "Daemon task should have looped at least once.");
CHECK_MESSAGE(counter[1].get() == legit_tasks_count, "All legit tasks should have been able to run.");
bool all_needed_yield = true;
for (int i = 0; i < legit_tasks_count; i++) {
if (!legit_task_needed_yield[i]) {
all_needed_yield = false;
break;
}
}
CHECK_MESSAGE(all_needed_yield, "All legit tasks should have needed the daemon yielding to run.");
}
} // namespace TestWorkerThreadPool

View File

@@ -0,0 +1,647 @@
/**************************************************************************/
/* test_array.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/variant/array.h"
#include "tests/test_macros.h"
#include "tests/test_tools.h"
namespace TestArray {
TEST_CASE("[Array] initializer list") {
Array arr = { 0, 1, "test", true, { 0.0, 1.0 } };
CHECK(arr.size() == 5);
CHECK(arr[0] == Variant(0));
CHECK(arr[1] == Variant(1));
CHECK(arr[2] == Variant("test"));
CHECK(arr[3] == Variant(true));
CHECK(arr[4] == Variant({ 0.0, 1.0 }));
arr = { "reassign" };
CHECK(arr.size() == 1);
CHECK(arr[0] == Variant("reassign"));
TypedArray<int> typed_arr = { 0, 1, 2 };
CHECK(typed_arr.size() == 3);
CHECK(typed_arr[0] == Variant(0));
CHECK(typed_arr[1] == Variant(1));
CHECK(typed_arr[2] == Variant(2));
}
TEST_CASE("[Array] size(), clear(), and is_empty()") {
Array arr;
CHECK(arr.size() == 0);
CHECK(arr.is_empty());
arr.push_back(1);
CHECK(arr.size() == 1);
arr.clear();
CHECK(arr.is_empty());
CHECK(arr.size() == 0);
}
TEST_CASE("[Array] Assignment and comparison operators") {
Array arr1;
Array arr2;
arr1.push_back(1);
CHECK(arr1 != arr2);
CHECK(arr1 > arr2);
CHECK(arr1 >= arr2);
arr2.push_back(2);
CHECK(arr1 != arr2);
CHECK(arr1 < arr2);
CHECK(arr1 <= arr2);
CHECK(arr2 > arr1);
CHECK(arr2 >= arr1);
Array arr3 = arr2;
CHECK(arr3 == arr2);
}
TEST_CASE("[Array] append_array()") {
Array arr1;
Array arr2;
arr1.push_back(1);
arr1.append_array(arr2);
CHECK(arr1.size() == 1);
arr2.push_back(2);
arr1.append_array(arr2);
CHECK(arr1.size() == 2);
CHECK(int(arr1[0]) == 1);
CHECK(int(arr1[1]) == 2);
}
TEST_CASE("[Array] resize(), insert(), and erase()") {
Array arr;
arr.resize(2);
CHECK(arr.size() == 2);
arr.insert(0, 1);
CHECK(int(arr[0]) == 1);
arr.insert(0, 2);
CHECK(int(arr[0]) == 2);
arr.erase(2);
CHECK(int(arr[0]) == 1);
arr.resize(0);
CHECK(arr.size() == 0);
arr.insert(0, 8);
CHECK(arr.size() == 1);
arr.insert(1, 16);
CHECK(int(arr[1]) == 16);
arr.insert(-1, 3);
CHECK(int(arr[1]) == 3);
}
TEST_CASE("[Array] front() and back()") {
Array arr;
arr.push_back(1);
CHECK(int(arr.front()) == 1);
CHECK(int(arr.back()) == 1);
arr.push_back(3);
CHECK(int(arr.front()) == 1);
CHECK(int(arr.back()) == 3);
}
TEST_CASE("[Array] has() and count()") {
Array arr = { 1, 1 };
CHECK(arr.has(1));
CHECK(!arr.has(2));
CHECK(arr.count(1) == 2);
CHECK(arr.count(2) == 0);
}
TEST_CASE("[Array] remove_at()") {
Array arr = { 1, 2 };
arr.remove_at(0);
CHECK(arr.size() == 1);
CHECK(int(arr[0]) == 2);
arr.remove_at(0);
CHECK(arr.size() == 0);
// Negative index.
arr.push_back(3);
arr.push_back(4);
arr.remove_at(-1);
CHECK(arr.size() == 1);
CHECK(int(arr[0]) == 3);
arr.remove_at(-1);
CHECK(arr.size() == 0);
// The array is now empty; try to use `remove_at()` again.
// Normally, this prints an error message so we silence it.
ERR_PRINT_OFF;
arr.remove_at(0);
ERR_PRINT_ON;
CHECK(arr.size() == 0);
}
TEST_CASE("[Array] get()") {
Array arr = { 1 };
CHECK(int(arr.get(0)) == 1);
}
TEST_CASE("[Array] sort()") {
Array arr = { 3, 4, 2, 1 };
arr.sort();
int val = 1;
for (int i = 0; i < arr.size(); i++) {
CHECK(int(arr[i]) == val);
val++;
}
}
TEST_CASE("[Array] push_front(), pop_front(), pop_back()") {
Array arr;
arr.push_front(1);
arr.push_front(2);
CHECK(int(arr[0]) == 2);
arr.pop_front();
CHECK(int(arr[0]) == 1);
CHECK(arr.size() == 1);
arr.push_front(2);
arr.push_front(3);
arr.pop_back();
CHECK(int(arr[1]) == 2);
CHECK(arr.size() == 2);
}
TEST_CASE("[Array] pop_at()") {
ErrorDetector ed;
Array arr = { 2, 4, 6, 8, 10 };
REQUIRE(int(arr.pop_at(2)) == 6);
REQUIRE(arr.size() == 4);
CHECK(int(arr[0]) == 2);
CHECK(int(arr[1]) == 4);
CHECK(int(arr[2]) == 8);
CHECK(int(arr[3]) == 10);
REQUIRE(int(arr.pop_at(2)) == 8);
REQUIRE(arr.size() == 3);
CHECK(int(arr[0]) == 2);
CHECK(int(arr[1]) == 4);
CHECK(int(arr[2]) == 10);
// Negative index.
REQUIRE(int(arr.pop_at(-1)) == 10);
REQUIRE(arr.size() == 2);
CHECK(int(arr[0]) == 2);
CHECK(int(arr[1]) == 4);
// Invalid pop.
ed.clear();
ERR_PRINT_OFF;
const Variant ret = arr.pop_at(-15);
ERR_PRINT_ON;
REQUIRE(ret.is_null());
CHECK(ed.has_error);
REQUIRE(int(arr.pop_at(0)) == 2);
REQUIRE(arr.size() == 1);
CHECK(int(arr[0]) == 4);
REQUIRE(int(arr.pop_at(0)) == 4);
REQUIRE(arr.is_empty());
// Pop from empty array.
ed.clear();
REQUIRE(arr.pop_at(24).is_null());
CHECK_FALSE(ed.has_error);
}
TEST_CASE("[Array] max() and min()") {
Array arr;
arr.push_back(3);
arr.push_front(4);
arr.push_back(5);
arr.push_back(2);
int max = int(arr.max());
int min = int(arr.min());
CHECK(max == 5);
CHECK(min == 2);
}
TEST_CASE("[Array] slice()") {
Array array = { 0, 1, 2, 3, 4, 5 };
Array slice0 = array.slice(0, 0);
CHECK(slice0.size() == 0);
Array slice1 = array.slice(1, 3);
CHECK(slice1.size() == 2);
CHECK(slice1[0] == Variant(1));
CHECK(slice1[1] == Variant(2));
Array slice2 = array.slice(1, -1);
CHECK(slice2.size() == 4);
CHECK(slice2[0] == Variant(1));
CHECK(slice2[1] == Variant(2));
CHECK(slice2[2] == Variant(3));
CHECK(slice2[3] == Variant(4));
Array slice3 = array.slice(3);
CHECK(slice3.size() == 3);
CHECK(slice3[0] == Variant(3));
CHECK(slice3[1] == Variant(4));
CHECK(slice3[2] == Variant(5));
Array slice4 = array.slice(2, -2);
CHECK(slice4.size() == 2);
CHECK(slice4[0] == Variant(2));
CHECK(slice4[1] == Variant(3));
Array slice5 = array.slice(-2);
CHECK(slice5.size() == 2);
CHECK(slice5[0] == Variant(4));
CHECK(slice5[1] == Variant(5));
Array slice6 = array.slice(2, 42);
CHECK(slice6.size() == 4);
CHECK(slice6[0] == Variant(2));
CHECK(slice6[1] == Variant(3));
CHECK(slice6[2] == Variant(4));
CHECK(slice6[3] == Variant(5));
Array slice7 = array.slice(4, 0, -2);
CHECK(slice7.size() == 2);
CHECK(slice7[0] == Variant(4));
CHECK(slice7[1] == Variant(2));
Array slice8 = array.slice(5, 0, -2);
CHECK(slice8.size() == 3);
CHECK(slice8[0] == Variant(5));
CHECK(slice8[1] == Variant(3));
CHECK(slice8[2] == Variant(1));
Array slice9 = array.slice(10, 0, -2);
CHECK(slice9.size() == 3);
CHECK(slice9[0] == Variant(5));
CHECK(slice9[1] == Variant(3));
CHECK(slice9[2] == Variant(1));
Array slice10 = array.slice(2, -10, -1);
CHECK(slice10.size() == 3);
CHECK(slice10[0] == Variant(2));
CHECK(slice10[1] == Variant(1));
CHECK(slice10[2] == Variant(0));
ERR_PRINT_OFF;
Array slice11 = array.slice(4, 1);
CHECK(slice11.size() == 0);
Array slice12 = array.slice(3, -4);
CHECK(slice12.size() == 0);
ERR_PRINT_ON;
Array slice13 = Array().slice(1);
CHECK(slice13.size() == 0);
Array slice14 = array.slice(6);
CHECK(slice14.size() == 0);
}
TEST_CASE("[Array] Duplicate array") {
// a = [1, [2, 2], {3: 3}]
Array a = { 1, { 2, 2 }, Dictionary({ { 3, 3 } }) };
// Deep copy
Array deep_a = a.duplicate(true);
CHECK_MESSAGE(deep_a.id() != a.id(), "Should create a new array");
CHECK_MESSAGE(Array(deep_a[1]).id() != Array(a[1]).id(), "Should clone nested array");
CHECK_MESSAGE(Dictionary(deep_a[2]).id() != Dictionary(a[2]).id(), "Should clone nested dictionary");
CHECK_EQ(deep_a, a);
deep_a.push_back(1);
CHECK_NE(deep_a, a);
deep_a.pop_back();
Array(deep_a[1]).push_back(1);
CHECK_NE(deep_a, a);
Array(deep_a[1]).pop_back();
CHECK_EQ(deep_a, a);
// Shallow copy
Array shallow_a = a.duplicate(false);
CHECK_MESSAGE(shallow_a.id() != a.id(), "Should create a new array");
CHECK_MESSAGE(Array(shallow_a[1]).id() == Array(a[1]).id(), "Should keep nested array");
CHECK_MESSAGE(Dictionary(shallow_a[2]).id() == Dictionary(a[2]).id(), "Should keep nested dictionary");
CHECK_EQ(shallow_a, a);
Array(shallow_a).push_back(1);
CHECK_NE(shallow_a, a);
}
TEST_CASE("[Array] Duplicate recursive array") {
// Self recursive
Array a;
a.push_back(a);
Array a_shallow = a.duplicate(false);
CHECK_EQ(a, a_shallow);
// Deep copy of recursive array ends up with recursion limit and return
// an invalid result (multiple nested arrays), the point is we should
// not end up with a segfault and an error log should be printed
ERR_PRINT_OFF;
a.duplicate(true);
ERR_PRINT_ON;
// Nested recursive
Array a1;
Array a2;
a2.push_back(a1);
a1.push_back(a2);
Array a1_shallow = a1.duplicate(false);
CHECK_EQ(a1, a1_shallow);
// Same deep copy issue as above
ERR_PRINT_OFF;
a1.duplicate(true);
ERR_PRINT_ON;
// Break the recursivity otherwise Array teardown will leak memory
a.clear();
a1.clear();
a2.clear();
}
TEST_CASE("[Array] Hash array") {
// a = [1, [2, 2], {3: 3}]
Array a = { 1, { 2, 2 }, Dictionary({ { 3, 3 } }) };
uint32_t original_hash = a.hash();
a.push_back(1);
CHECK_NE(a.hash(), original_hash);
a.pop_back();
CHECK_EQ(a.hash(), original_hash);
Array(a[1]).push_back(1);
CHECK_NE(a.hash(), original_hash);
Array(a[1]).pop_back();
CHECK_EQ(a.hash(), original_hash);
(Dictionary(a[2]))[1] = 1;
CHECK_NE(a.hash(), original_hash);
Dictionary(a[2]).erase(1);
CHECK_EQ(a.hash(), original_hash);
Array a2 = a.duplicate(true);
CHECK_EQ(a2.hash(), a.hash());
}
TEST_CASE("[Array] Hash recursive array") {
Array a1;
a1.push_back(a1);
Array a2;
a2.push_back(a2);
// Hash should reach recursion limit
ERR_PRINT_OFF;
CHECK_EQ(a1.hash(), a2.hash());
ERR_PRINT_ON;
// Break the recursivity otherwise Array teardown will leak memory
a1.clear();
a2.clear();
}
TEST_CASE("[Array] Empty comparison") {
Array a1;
Array a2;
// test both operator== and operator!=
CHECK_EQ(a1, a2);
CHECK_FALSE(a1 != a2);
}
TEST_CASE("[Array] Flat comparison") {
Array a1 = { 1 };
Array a2 = { 1 };
Array other_a = { 2 };
// test both operator== and operator!=
CHECK_EQ(a1, a1); // compare self
CHECK_FALSE(a1 != a1);
CHECK_EQ(a1, a2); // different equivalent arrays
CHECK_FALSE(a1 != a2);
CHECK_NE(a1, other_a); // different arrays with different content
CHECK_FALSE(a1 == other_a);
}
TEST_CASE("[Array] Nested array comparison") {
// a1 = [[[1], 2], 3]
Array a1 = { { { 1 }, 2 }, 3 };
Array a2 = a1.duplicate(true);
// other_a = [[[1, 0], 2], 3]
Array other_a = { { { 1, 0 }, 2 }, 3 };
// test both operator== and operator!=
CHECK_EQ(a1, a1); // compare self
CHECK_FALSE(a1 != a1);
CHECK_EQ(a1, a2); // different equivalent arrays
CHECK_FALSE(a1 != a2);
CHECK_NE(a1, other_a); // different arrays with different content
CHECK_FALSE(a1 == other_a);
}
TEST_CASE("[Array] Nested dictionary comparison") {
// a1 = [{1: 2}, 3]
Array a1 = { Dictionary({ { 1, 2 } }), 3 };
Array a2 = a1.duplicate(true);
// other_a = [{1: 0}, 3]
Array other_a = { Dictionary({ { 1, 0 } }), 3 };
// test both operator== and operator!=
CHECK_EQ(a1, a1); // compare self
CHECK_FALSE(a1 != a1);
CHECK_EQ(a1, a2); // different equivalent arrays
CHECK_FALSE(a1 != a2);
CHECK_NE(a1, other_a); // different arrays with different content
CHECK_FALSE(a1 == other_a);
}
TEST_CASE("[Array] Recursive comparison") {
Array a1;
a1.push_back(a1);
Array a2;
a2.push_back(a2);
// Comparison should reach recursion limit
ERR_PRINT_OFF;
CHECK_EQ(a1, a2);
CHECK_FALSE(a1 != a2);
ERR_PRINT_ON;
a1.push_back(1);
a2.push_back(1);
// Comparison should reach recursion limit
ERR_PRINT_OFF;
CHECK_EQ(a1, a2);
CHECK_FALSE(a1 != a2);
ERR_PRINT_ON;
a1.push_back(1);
a2.push_back(2);
// Comparison should reach recursion limit
ERR_PRINT_OFF;
CHECK_NE(a1, a2);
CHECK_FALSE(a1 == a2);
ERR_PRINT_ON;
// Break the recursivity otherwise Array tearndown will leak memory
a1.clear();
a2.clear();
}
TEST_CASE("[Array] Recursive self comparison") {
Array a1;
Array a2;
a2.push_back(a1);
a1.push_back(a2);
CHECK_EQ(a1, a1);
CHECK_FALSE(a1 != a1);
// Break the recursivity otherwise Array tearndown will leak memory
a1.clear();
a2.clear();
}
TEST_CASE("[Array] Iteration") {
Array a1 = { 1, 2, 3 };
Array a2 = { 1, 2, 3 };
int idx = 0;
for (Variant &E : a1) {
CHECK_EQ(int(a2[idx]), int(E));
idx++;
}
CHECK_EQ(idx, a1.size());
idx = 0;
for (const Variant &E : (const Array &)a1) {
CHECK_EQ(int(a2[idx]), int(E));
idx++;
}
CHECK_EQ(idx, a1.size());
a1.clear();
}
TEST_CASE("[Array] Iteration and modification") {
Array a1 = { 1, 2, 3 };
Array a2 = { 2, 3, 4 };
Array a3 = { 1, 2, 3 };
Array a4 = { 1, 2, 3 };
a3.make_read_only();
int idx = 0;
for (Variant &E : a1) {
E = a2[idx];
idx++;
}
CHECK_EQ(a1, a2);
// Ensure read-only is respected.
idx = 0;
for (Variant &E : a3) {
E = a2[idx];
}
CHECK_EQ(a3, a4);
a1.clear();
a2.clear();
a4.clear();
}
TEST_CASE("[Array] Typed copying") {
TypedArray<int> a1 = { 1 };
TypedArray<double> a2 = { 1.0 };
Array a3 = a1;
TypedArray<int> a4 = a3;
Array a5 = a2;
TypedArray<int> a6 = a5;
a3[0] = 2;
a4[0] = 3;
// Same typed TypedArray should be shared.
CHECK_EQ(a1[0], Variant(3));
CHECK_EQ(a3[0], Variant(3));
CHECK_EQ(a4[0], Variant(3));
a5[0] = 2.0;
a6[0] = 3.0;
// Different typed TypedArray should not be shared.
CHECK_EQ(a2[0], Variant(2.0));
CHECK_EQ(a5[0], Variant(2.0));
CHECK_EQ(a6[0], Variant(3.0));
a1.clear();
a2.clear();
a3.clear();
a4.clear();
a5.clear();
a6.clear();
}
static bool _find_custom_callable(const Variant &p_val) {
return (int)p_val % 2 == 0;
}
TEST_CASE("[Array] Test find_custom") {
Array a1 = { 1, 3, 4, 5, 8, 9 };
// Find first even number.
int index = a1.find_custom(callable_mp_static(_find_custom_callable));
CHECK_EQ(index, 2);
}
TEST_CASE("[Array] Test rfind_custom") {
Array a1 = { 1, 3, 4, 5, 8, 9 };
// Find last even number.
int index = a1.rfind_custom(callable_mp_static(_find_custom_callable));
CHECK_EQ(index, 4);
}
} // namespace TestArray

View File

@@ -0,0 +1,197 @@
/**************************************************************************/
/* test_callable.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/object/class_db.h"
#include "core/object/object.h"
#include "tests/test_macros.h"
namespace TestCallable {
class TestClass : public Object {
GDCLASS(TestClass, Object);
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("test_func_1", "foo", "bar"), &TestClass::test_func_1);
ClassDB::bind_method(D_METHOD("test_func_2", "foo", "bar", "baz"), &TestClass::test_func_2);
ClassDB::bind_static_method("TestClass", D_METHOD("test_func_5", "foo", "bar"), &TestClass::test_func_5);
ClassDB::bind_static_method("TestClass", D_METHOD("test_func_6", "foo", "bar", "baz"), &TestClass::test_func_6);
{
MethodInfo mi;
mi.name = "test_func_7";
mi.arguments.push_back(PropertyInfo(Variant::INT, "foo"));
mi.arguments.push_back(PropertyInfo(Variant::INT, "bar"));
ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "test_func_7", &TestClass::test_func_7, mi, varray(), false);
}
{
MethodInfo mi;
mi.name = "test_func_8";
mi.arguments.push_back(PropertyInfo(Variant::INT, "foo"));
mi.arguments.push_back(PropertyInfo(Variant::INT, "bar"));
mi.arguments.push_back(PropertyInfo(Variant::INT, "baz"));
ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "test_func_8", &TestClass::test_func_8, mi, varray(), false);
}
}
public:
void test_func_1(int p_foo, int p_bar) {}
void test_func_2(int p_foo, int p_bar, int p_baz) {}
int test_func_3(int p_foo, int p_bar) const { return 0; }
int test_func_4(int p_foo, int p_bar, int p_baz) const { return 0; }
static void test_func_5(int p_foo, int p_bar) {}
static void test_func_6(int p_foo, int p_bar, int p_baz) {}
void test_func_7(const Variant **p_args, int p_argcount, Callable::CallError &r_error) {}
void test_func_8(const Variant **p_args, int p_argcount, Callable::CallError &r_error) {}
};
TEST_CASE("[Callable] Argument count") {
TestClass *my_test = memnew(TestClass);
// Bound methods tests.
// Test simple methods.
Callable callable_1 = Callable(my_test, "test_func_1");
CHECK_EQ(callable_1.get_argument_count(), 2);
Callable callable_2 = Callable(my_test, "test_func_2");
CHECK_EQ(callable_2.get_argument_count(), 3);
Callable callable_3 = Callable(my_test, "test_func_5");
CHECK_EQ(callable_3.get_argument_count(), 2);
Callable callable_4 = Callable(my_test, "test_func_6");
CHECK_EQ(callable_4.get_argument_count(), 3);
// Test vararg methods.
Callable callable_vararg_1 = Callable(my_test, "test_func_7");
CHECK_MESSAGE(callable_vararg_1.get_argument_count() == 2, "vararg Callable should return the number of declared arguments");
Callable callable_vararg_2 = Callable(my_test, "test_func_8");
CHECK_MESSAGE(callable_vararg_2.get_argument_count() == 3, "vararg Callable should return the number of declared arguments");
// Callable MP tests.
// Test simple methods.
Callable callable_mp_1 = callable_mp(my_test, &TestClass::test_func_1);
CHECK_EQ(callable_mp_1.get_argument_count(), 2);
Callable callable_mp_2 = callable_mp(my_test, &TestClass::test_func_2);
CHECK_EQ(callable_mp_2.get_argument_count(), 3);
Callable callable_mp_3 = callable_mp(my_test, &TestClass::test_func_3);
CHECK_EQ(callable_mp_3.get_argument_count(), 2);
Callable callable_mp_4 = callable_mp(my_test, &TestClass::test_func_4);
CHECK_EQ(callable_mp_4.get_argument_count(), 3);
// Test static methods.
Callable callable_mp_static_1 = callable_mp_static(&TestClass::test_func_5);
CHECK_EQ(callable_mp_static_1.get_argument_count(), 2);
Callable callable_mp_static_2 = callable_mp_static(&TestClass::test_func_6);
CHECK_EQ(callable_mp_static_2.get_argument_count(), 3);
// Test bind.
Callable callable_mp_bind_1 = callable_mp_2.bind(1);
CHECK_MESSAGE(callable_mp_bind_1.get_argument_count() == 2, "bind should subtract from the argument count");
Callable callable_mp_bind_2 = callable_mp_2.bind(1, 2);
CHECK_MESSAGE(callable_mp_bind_2.get_argument_count() == 1, "bind should subtract from the argument count");
// Test unbind.
Callable callable_mp_unbind_1 = callable_mp_2.unbind(1);
CHECK_MESSAGE(callable_mp_unbind_1.get_argument_count() == 4, "unbind should add to the argument count");
Callable callable_mp_unbind_2 = callable_mp_2.unbind(2);
CHECK_MESSAGE(callable_mp_unbind_2.get_argument_count() == 5, "unbind should add to the argument count");
memdelete(my_test);
}
class TestBoundUnboundArgumentCount : public Object {
GDCLASS(TestBoundUnboundArgumentCount, Object);
protected:
static void _bind_methods() {
ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "test_func", &TestBoundUnboundArgumentCount::test_func, MethodInfo("test_func"));
}
public:
Variant test_func(const Variant **p_args, int p_argcount, Callable::CallError &r_error) {
Array result;
result.resize(p_argcount);
for (int i = 0; i < p_argcount; i++) {
result[i] = *p_args[i];
}
return result;
}
static String get_output(const Callable &p_callable) {
Array effective_args = { 7, 8, 9 };
effective_args.resize(3 - p_callable.get_unbound_arguments_count());
effective_args.append_array(p_callable.get_bound_arguments());
return vformat(
"%d %d %s %s %s",
p_callable.get_unbound_arguments_count(),
p_callable.get_bound_arguments_count(),
p_callable.get_bound_arguments(),
p_callable.call(7, 8, 9),
effective_args);
}
};
TEST_CASE("[Callable] Bound and unbound argument count") {
String (*get_output)(const Callable &) = TestBoundUnboundArgumentCount::get_output;
TestBoundUnboundArgumentCount *test_instance = memnew(TestBoundUnboundArgumentCount);
Callable test_func = Callable(test_instance, "test_func");
CHECK(get_output(test_func) == "0 0 [] [7, 8, 9] [7, 8, 9]");
CHECK(get_output(test_func.bind(1, 2)) == "0 2 [1, 2] [7, 8, 9, 1, 2] [7, 8, 9, 1, 2]");
CHECK(get_output(test_func.bind(1, 2).unbind(1)) == "1 2 [1, 2] [7, 8, 1, 2] [7, 8, 1, 2]");
CHECK(get_output(test_func.bind(1, 2).unbind(1).bind(3, 4)) == "0 3 [3, 1, 2] [7, 8, 9, 3, 1, 2] [7, 8, 9, 3, 1, 2]");
CHECK(get_output(test_func.bind(1, 2).unbind(1).bind(3, 4).unbind(1)) == "1 3 [3, 1, 2] [7, 8, 3, 1, 2] [7, 8, 3, 1, 2]");
CHECK(get_output(test_func.bind(1).bind(2).bind(3).unbind(1)) == "1 3 [3, 2, 1] [7, 8, 3, 2, 1] [7, 8, 3, 2, 1]");
CHECK(get_output(test_func.bind(1).bind(2).unbind(1).bind(3)) == "0 2 [2, 1] [7, 8, 9, 2, 1] [7, 8, 9, 2, 1]");
CHECK(get_output(test_func.bind(1).unbind(1).bind(2).bind(3)) == "0 2 [3, 1] [7, 8, 9, 3, 1] [7, 8, 9, 3, 1]");
CHECK(get_output(test_func.unbind(1).bind(1).bind(2).bind(3)) == "0 2 [3, 2] [7, 8, 9, 3, 2] [7, 8, 9, 3, 2]");
CHECK(get_output(test_func.unbind(1).unbind(1).unbind(1).bind(1, 2, 3)) == "0 0 [] [7, 8, 9] [7, 8, 9]");
CHECK(get_output(test_func.unbind(1).unbind(1).bind(1, 2, 3).unbind(1)) == "1 1 [1] [7, 8, 1] [7, 8, 1]");
CHECK(get_output(test_func.unbind(1).bind(1, 2, 3).unbind(1).unbind(1)) == "2 2 [1, 2] [7, 1, 2] [7, 1, 2]");
CHECK(get_output(test_func.bind(1, 2, 3).unbind(1).unbind(1).unbind(1)) == "3 3 [1, 2, 3] [1, 2, 3] [1, 2, 3]");
memdelete(test_instance);
}
} // namespace TestCallable

View File

@@ -0,0 +1,620 @@
/**************************************************************************/
/* test_dictionary.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/variant/typed_dictionary.h"
#include "tests/test_macros.h"
namespace TestDictionary {
TEST_CASE("[Dictionary] Assignment using bracket notation ([])") {
Dictionary map;
map["Hello"] = 0;
CHECK(int(map["Hello"]) == 0);
map["Hello"] = 3;
CHECK(int(map["Hello"]) == 3);
map["World!"] = 4;
CHECK(int(map["World!"]) == 4);
map[StringName("HelloName")] = 6;
CHECK(int(map[StringName("HelloName")]) == 6);
CHECK(int(map.find_key(6).get_type()) == Variant::STRING_NAME);
map[StringName("HelloName")] = 7;
CHECK(int(map[StringName("HelloName")]) == 7);
// Test String and StringName are equivalent.
map[StringName("Hello")] = 8;
CHECK(int(map["Hello"]) == 8);
map["Hello"] = 9;
CHECK(int(map[StringName("Hello")]) == 9);
// Test non-string keys, since keys can be of any Variant type.
map[12345] = -5;
CHECK(int(map[12345]) == -5);
map[false] = 128;
CHECK(int(map[false]) == 128);
map[Vector2(10, 20)] = 30;
CHECK(int(map[Vector2(10, 20)]) == 30);
map[0] = 400;
CHECK(int(map[0]) == 400);
// Check that assigning 0 doesn't overwrite the value for `false`.
CHECK(int(map[false]) == 128);
// Ensure read-only maps aren't modified by non-existing keys.
const int length = map.size();
map.make_read_only();
CHECK(int(map["This key does not exist"].get_type()) == Variant::NIL);
CHECK(map.size() == length);
}
TEST_CASE("[Dictionary] List init") {
Dictionary dict{
{ 0, "int" },
{ "packed_string_array", PackedStringArray({ "array", "of", "values" }) },
{ "key", Dictionary({ { "nested", 200 } }) },
{ Vector2(), "v2" },
};
CHECK(dict.size() == 4);
CHECK(dict[0] == "int");
CHECK(PackedStringArray(dict["packed_string_array"])[2] == "values");
CHECK(Dictionary(dict["key"])["nested"] == Variant(200));
CHECK(dict[Vector2()] == "v2");
TypedDictionary<double, double> tdict{
{ 0.0, 1.0 },
{ 5.0, 2.0 },
};
CHECK_EQ(tdict[0.0], Variant(1.0));
CHECK_EQ(tdict[5.0], Variant(2.0));
}
TEST_CASE("[Dictionary] get_key_list()") {
Dictionary map;
LocalVector<Variant> keys;
keys = map.get_key_list();
CHECK(keys.is_empty());
map[1] = 3;
keys = map.get_key_list();
CHECK(keys.size() == 1);
CHECK(int(keys[0]) == 1);
map[2] = 4;
keys = map.get_key_list();
CHECK(keys.size() == 2);
}
TEST_CASE("[Dictionary] get_key_at_index()") {
Dictionary map;
map[4] = 3;
Variant val = map.get_key_at_index(0);
CHECK(int(val) == 4);
map[3] = 1;
val = map.get_key_at_index(0);
CHECK(int(val) == 4);
val = map.get_key_at_index(1);
CHECK(int(val) == 3);
}
TEST_CASE("[Dictionary] getptr()") {
Dictionary map;
map[1] = 3;
Variant *key = map.getptr(1);
CHECK(int(*key) == 3);
key = map.getptr(2);
CHECK(key == nullptr);
}
TEST_CASE("[Dictionary] get_valid()") {
Dictionary map;
map[1] = 3;
Variant val = map.get_valid(1);
CHECK(int(val) == 3);
}
TEST_CASE("[Dictionary] get()") {
Dictionary map;
map[1] = 3;
Variant val = map.get(1, -1);
CHECK(int(val) == 3);
}
TEST_CASE("[Dictionary] size(), empty() and clear()") {
Dictionary map;
CHECK(map.size() == 0);
CHECK(map.is_empty());
map[1] = 3;
CHECK(map.size() == 1);
CHECK(!map.is_empty());
map.clear();
CHECK(map.size() == 0);
CHECK(map.is_empty());
}
TEST_CASE("[Dictionary] has() and has_all()") {
Dictionary map;
CHECK(map.has(1) == false);
map[1] = 3;
CHECK(map.has(1));
Array keys;
keys.push_back(1);
CHECK(map.has_all(keys));
keys.push_back(2);
CHECK(map.has_all(keys) == false);
}
TEST_CASE("[Dictionary] keys() and values()") {
Dictionary map;
Array keys = map.keys();
Array values = map.values();
CHECK(keys.is_empty());
CHECK(values.is_empty());
map[1] = 3;
keys = map.keys();
values = map.values();
CHECK(int(keys[0]) == 1);
CHECK(int(values[0]) == 3);
}
TEST_CASE("[Dictionary] Duplicate dictionary") {
// d = {1: {1: 1}, {2: 2}: [2], [3]: 3}
Dictionary k2 = { { 2, 2 } };
Array k3 = { 3 };
Dictionary d = {
{ 1, Dictionary({ { 1, 1 } }) },
{ k2, Array({ 2 }) },
{ k3, 3 }
};
// Deep copy
Dictionary deep_d = d.duplicate(true);
CHECK_MESSAGE(deep_d.id() != d.id(), "Should create a new dictionary");
CHECK_MESSAGE(Dictionary(deep_d[1]).id() != Dictionary(d[1]).id(), "Should clone nested dictionary");
CHECK_MESSAGE(Array(deep_d[k2]).id() != Array(d[k2]).id(), "Should clone nested array");
CHECK_EQ(deep_d, d);
deep_d[0] = 0;
CHECK_NE(deep_d, d);
deep_d.erase(0);
Dictionary(deep_d[1]).operator[](0) = 0;
CHECK_NE(deep_d, d);
Dictionary(deep_d[1]).erase(0);
CHECK_EQ(deep_d, d);
// Keys should also be copied
k2[0] = 0;
CHECK_NE(deep_d, d);
k2.erase(0);
CHECK_EQ(deep_d, d);
k3.push_back(0);
CHECK_NE(deep_d, d);
k3.pop_back();
CHECK_EQ(deep_d, d);
// Shallow copy
Dictionary shallow_d = d.duplicate(false);
CHECK_MESSAGE(shallow_d.id() != d.id(), "Should create a new array");
CHECK_MESSAGE(Dictionary(shallow_d[1]).id() == Dictionary(d[1]).id(), "Should keep nested dictionary");
CHECK_MESSAGE(Array(shallow_d[k2]).id() == Array(d[k2]).id(), "Should keep nested array");
CHECK_EQ(shallow_d, d);
shallow_d[0] = 0;
CHECK_NE(shallow_d, d);
shallow_d.erase(0);
#if 0 // TODO: recursion in dict key currently is buggy
// Keys should also be shallowed
k2[0] = 0;
CHECK_EQ(shallow_d, d);
k2.erase(0);
k3.push_back(0);
CHECK_EQ(shallow_d, d);
#endif
}
TEST_CASE("[Dictionary] Duplicate recursive dictionary") {
// Self recursive
Dictionary d;
d[1] = d;
Dictionary d_shallow = d.duplicate(false);
CHECK_EQ(d, d_shallow);
// Deep copy of recursive dictionary endup with recursion limit and return
// an invalid result (multiple nested dictionaries), the point is we should
// not end up with a segfault and an error log should be printed
ERR_PRINT_OFF;
d.duplicate(true);
ERR_PRINT_ON;
// Nested recursive
Dictionary d1;
Dictionary d2;
d1[2] = d2;
d2[1] = d1;
Dictionary d1_shallow = d1.duplicate(false);
CHECK_EQ(d1, d1_shallow);
// Same deep copy issue as above
ERR_PRINT_OFF;
d1.duplicate(true);
ERR_PRINT_ON;
// Break the recursivity otherwise Dictionary teardown will leak memory
d.clear();
d1.clear();
d2.clear();
}
#if 0 // TODO: duplicate recursion in dict key is currently buggy
TEST_CASE("[Dictionary] Duplicate recursive dictionary on keys") {
// Self recursive
Dictionary d;
d[d] = d;
Dictionary d_shallow = d.duplicate(false);
CHECK_EQ(d, d_shallow);
// Deep copy of recursive dictionary endup with recursion limit and return
// an invalid result (multiple nested dictionaries), the point is we should
// not end up with a segfault and an error log should be printed
ERR_PRINT_OFF;
d.duplicate(true);
ERR_PRINT_ON;
// Nested recursive
Dictionary d1;
Dictionary d2;
d1[d2] = d2;
d2[d1] = d1;
Dictionary d1_shallow = d1.duplicate(false);
CHECK_EQ(d1, d1_shallow);
// Same deep copy issue as above
ERR_PRINT_OFF;
d1.duplicate(true);
ERR_PRINT_ON;
// Break the recursivity otherwise Dictionary teardown will leak memory
d.clear();
d1.clear();
d2.clear();
}
#endif
TEST_CASE("[Dictionary] Hash dictionary") {
// d = {1: {1: 1}, {2: 2}: [2], [3]: 3}
Dictionary k2 = { { 2, 2 } };
Array k3 = { 3 };
Dictionary d = {
{ 1, Dictionary({ { 1, 1 } }) },
{ k2, Array({ 2 }) },
{ k3, 3 }
};
uint32_t original_hash = d.hash();
// Modify dict change the hash
d[0] = 0;
CHECK_NE(d.hash(), original_hash);
d.erase(0);
CHECK_EQ(d.hash(), original_hash);
// Modify nested item change the hash
Dictionary(d[1]).operator[](0) = 0;
CHECK_NE(d.hash(), original_hash);
Dictionary(d[1]).erase(0);
Array(d[k2]).push_back(0);
CHECK_NE(d.hash(), original_hash);
Array(d[k2]).pop_back();
// Modify a key change the hash
k2[0] = 0;
CHECK_NE(d.hash(), original_hash);
k2.erase(0);
CHECK_EQ(d.hash(), original_hash);
k3.push_back(0);
CHECK_NE(d.hash(), original_hash);
k3.pop_back();
CHECK_EQ(d.hash(), original_hash);
// Duplication doesn't change the hash
Dictionary d2 = d.duplicate(true);
CHECK_EQ(d2.hash(), original_hash);
}
TEST_CASE("[Dictionary] Hash recursive dictionary") {
Dictionary d;
d[1] = d;
// Hash should reach recursion limit, we just make sure this doesn't blow up
ERR_PRINT_OFF;
d.hash();
ERR_PRINT_ON;
// Break the recursivity otherwise Dictionary teardown will leak memory
d.clear();
}
#if 0 // TODO: recursion in dict key is currently buggy
TEST_CASE("[Dictionary] Hash recursive dictionary on keys") {
Dictionary d;
d[d] = 1;
// Hash should reach recursion limit, we just make sure this doesn't blow up
ERR_PRINT_OFF;
d.hash();
ERR_PRINT_ON;
// Break the recursivity otherwise Dictionary teardown will leak memory
d.clear();
}
#endif
TEST_CASE("[Dictionary] Empty comparison") {
Dictionary d1;
Dictionary d2;
// test both operator== and operator!=
CHECK_EQ(d1, d2);
CHECK_FALSE(d1 != d2);
}
TEST_CASE("[Dictionary] Flat comparison") {
Dictionary d1 = { { 1, 1 } };
Dictionary d2 = { { 1, 1 } };
Dictionary other_d = { { 2, 1 } };
// test both operator== and operator!=
CHECK_EQ(d1, d1); // compare self
CHECK_FALSE(d1 != d1);
CHECK_EQ(d1, d2); // different equivalent arrays
CHECK_FALSE(d1 != d2);
CHECK_NE(d1, other_d); // different arrays with different content
CHECK_FALSE(d1 == other_d);
}
TEST_CASE("[Dictionary] Nested dictionary comparison") {
// d1 = {1: {2: {3: 4}}}
Dictionary d1 = { { 1, Dictionary({ { 2, Dictionary({ { 3, 4 } }) } }) } };
Dictionary d2 = d1.duplicate(true);
// other_d = {1: {2: {3: 0}}}
Dictionary other_d = { { 1, Dictionary({ { 2, Dictionary({ { 3, 0 } }) } }) } };
// test both operator== and operator!=
CHECK_EQ(d1, d1); // compare self
CHECK_FALSE(d1 != d1);
CHECK_EQ(d1, d2); // different equivalent arrays
CHECK_FALSE(d1 != d2);
CHECK_NE(d1, other_d); // different arrays with different content
CHECK_FALSE(d1 == other_d);
}
TEST_CASE("[Dictionary] Nested array comparison") {
// d1 = {1: [2, 3]}
Dictionary d1 = { { 1, { 2, 3 } } };
Dictionary d2 = d1.duplicate(true);
// other_d = {1: [2, 0]}
Dictionary other_d = { { 1, { 2, 0 } } };
// test both operator== and operator!=
CHECK_EQ(d1, d1); // compare self
CHECK_FALSE(d1 != d1);
CHECK_EQ(d1, d2); // different equivalent arrays
CHECK_FALSE(d1 != d2);
CHECK_NE(d1, other_d); // different arrays with different content
CHECK_FALSE(d1 == other_d);
}
TEST_CASE("[Dictionary] Recursive comparison") {
Dictionary d1;
d1[1] = d1;
Dictionary d2;
d2[1] = d2;
// Comparison should reach recursion limit
ERR_PRINT_OFF;
CHECK_EQ(d1, d2);
CHECK_FALSE(d1 != d2);
ERR_PRINT_ON;
d1[2] = 2;
d2[2] = 2;
// Comparison should reach recursion limit
ERR_PRINT_OFF;
CHECK_EQ(d1, d2);
CHECK_FALSE(d1 != d2);
ERR_PRINT_ON;
d1[3] = 3;
d2[3] = 0;
// Comparison should reach recursion limit
ERR_PRINT_OFF;
CHECK_NE(d1, d2);
CHECK_FALSE(d1 == d2);
ERR_PRINT_ON;
// Break the recursivity otherwise Dictionary teardown will leak memory
d1.clear();
d2.clear();
}
#if 0 // TODO: recursion in dict key is currently buggy
TEST_CASE("[Dictionary] Recursive comparison on keys") {
Dictionary d1;
// Hash computation should reach recursion limit
ERR_PRINT_OFF;
d1[d1] = 1;
ERR_PRINT_ON;
Dictionary d2;
// Hash computation should reach recursion limit
ERR_PRINT_OFF;
d2[d2] = 1;
ERR_PRINT_ON;
// Comparison should reach recursion limit
ERR_PRINT_OFF;
CHECK_EQ(d1, d2);
CHECK_FALSE(d1 != d2);
ERR_PRINT_ON;
d1[2] = 2;
d2[2] = 2;
// Comparison should reach recursion limit
ERR_PRINT_OFF;
CHECK_EQ(d1, d2);
CHECK_FALSE(d1 != d2);
ERR_PRINT_ON;
d1[3] = 3;
d2[3] = 0;
// Comparison should reach recursion limit
ERR_PRINT_OFF;
CHECK_NE(d1, d2);
CHECK_FALSE(d1 == d2);
ERR_PRINT_ON;
// Break the recursivity otherwise Dictionary teardown will leak memory
d1.clear();
d2.clear();
}
#endif
TEST_CASE("[Dictionary] Recursive self comparison") {
Dictionary d1;
Dictionary d2;
d1[1] = d2;
d2[1] = d1;
CHECK_EQ(d1, d1);
CHECK_FALSE(d1 != d1);
// Break the recursivity otherwise Dictionary teardown will leak memory
d1.clear();
d2.clear();
}
TEST_CASE("[Dictionary] Order and find") {
Dictionary d;
d[4] = "four";
d[8] = "eight";
d[12] = "twelve";
d["4"] = "four";
Array keys = { 4, 8, 12, "4" };
CHECK_EQ(d.keys(), keys);
CHECK_EQ(d.find_key("four"), Variant(4));
CHECK_EQ(d.find_key("does not exist"), Variant());
}
TEST_CASE("[Dictionary] Typed copying") {
TypedDictionary<int, int> d1;
d1[0] = 1;
TypedDictionary<double, double> d2;
d2[0] = 1.0;
Dictionary d3 = d1;
TypedDictionary<int, int> d4 = d3;
Dictionary d5 = d2;
TypedDictionary<int, int> d6 = d5;
d3[0] = 2;
d4[0] = 3;
// Same typed TypedDictionary should be shared.
CHECK_EQ(d1[0], Variant(3));
CHECK_EQ(d3[0], Variant(3));
CHECK_EQ(d4[0], Variant(3));
d5[0] = 2.0;
d6[0] = 3.0;
// Different typed TypedDictionary should not be shared.
CHECK_EQ(d2[0], Variant(2.0));
CHECK_EQ(d5[0], Variant(2.0));
CHECK_EQ(d6[0], Variant(3.0));
d1.clear();
d2.clear();
d3.clear();
d4.clear();
d5.clear();
d6.clear();
}
TEST_CASE("[Dictionary] Iteration") {
Dictionary a1 = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
Dictionary a2 = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
int idx = 0;
for (const KeyValue<Variant, Variant> &kv : (const Dictionary &)a1) {
CHECK_EQ(int(a2[kv.key]), int(kv.value));
idx++;
}
CHECK_EQ(idx, a1.size());
a1.clear();
a2.clear();
}
TEST_CASE("[Dictionary] Object value init") {
Object *a = memnew(Object);
Object *b = memnew(Object);
TypedDictionary<double, Object *> tdict = {
{ 0.0, a },
{ 5.0, b },
};
CHECK_EQ(tdict[0.0], Variant(a));
CHECK_EQ(tdict[5.0], Variant(b));
memdelete(a);
memdelete(b);
}
TEST_CASE("[Dictionary] RefCounted value init") {
Ref<RefCounted> a = memnew(RefCounted);
Ref<RefCounted> b = memnew(RefCounted);
TypedDictionary<double, Ref<RefCounted>> tdict = {
{ 0.0, a },
{ 5.0, b },
};
CHECK_EQ(tdict[0.0], Variant(a));
CHECK_EQ(tdict[5.0], Variant(b));
}
} // namespace TestDictionary

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,131 @@
/**************************************************************************/
/* test_variant_utility.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/variant/variant_utility.h"
#include "tests/test_macros.h"
namespace TestVariantUtility {
TEST_CASE("[VariantUtility] Type conversion") {
Variant converted;
converted = VariantUtilityFunctions::type_convert("Hi!", Variant::Type::NIL);
CHECK(converted.get_type() == Variant::Type::NIL);
CHECK(converted == Variant());
converted = VariantUtilityFunctions::type_convert("Hi!", Variant::Type::INT);
CHECK(converted.get_type() == Variant::Type::INT);
CHECK(converted == Variant(0));
converted = VariantUtilityFunctions::type_convert("123", Variant::Type::INT);
CHECK(converted.get_type() == Variant::Type::INT);
CHECK(converted == Variant(123));
converted = VariantUtilityFunctions::type_convert(123, Variant::Type::STRING);
CHECK(converted.get_type() == Variant::Type::STRING);
CHECK(converted == Variant("123"));
converted = VariantUtilityFunctions::type_convert(123.4, Variant::Type::INT);
CHECK(converted.get_type() == Variant::Type::INT);
CHECK(converted == Variant(123));
converted = VariantUtilityFunctions::type_convert(5, Variant::Type::VECTOR2);
CHECK(converted.get_type() == Variant::Type::VECTOR2);
CHECK(converted == Variant(Vector2(0, 0)));
converted = VariantUtilityFunctions::type_convert(Vector3(1, 2, 3), Variant::Type::VECTOR2);
CHECK(converted.get_type() == Variant::Type::VECTOR2);
CHECK(converted == Variant(Vector2(1, 2)));
converted = VariantUtilityFunctions::type_convert(Vector2(1, 2), Variant::Type::VECTOR4);
CHECK(converted.get_type() == Variant::Type::VECTOR4);
CHECK(converted == Variant(Vector4(1, 2, 0, 0)));
converted = VariantUtilityFunctions::type_convert(Vector4(1.2, 3.4, 5.6, 7.8), Variant::Type::VECTOR3I);
CHECK(converted.get_type() == Variant::Type::VECTOR3I);
CHECK(converted == Variant(Vector3i(1, 3, 5)));
{
Basis basis = Basis::from_scale(Vector3(1.2, 3.4, 5.6));
Transform3D transform = Transform3D(basis, Vector3());
converted = VariantUtilityFunctions::type_convert(transform, Variant::Type::BASIS);
CHECK(converted.get_type() == Variant::Type::BASIS);
CHECK(converted == basis);
converted = VariantUtilityFunctions::type_convert(basis, Variant::Type::TRANSFORM3D);
CHECK(converted.get_type() == Variant::Type::TRANSFORM3D);
CHECK(converted == transform);
converted = VariantUtilityFunctions::type_convert(basis, Variant::Type::STRING);
CHECK(converted.get_type() == Variant::Type::STRING);
CHECK(converted == Variant("[X: (1.2, 0.0, 0.0), Y: (0.0, 3.4, 0.0), Z: (0.0, 0.0, 5.6)]"));
}
{
Array arr = { 1.2, 3.4, 5.6 };
PackedFloat64Array packed = { 1.2, 3.4, 5.6 };
converted = VariantUtilityFunctions::type_convert(arr, Variant::Type::PACKED_FLOAT64_ARRAY);
CHECK(converted.get_type() == Variant::Type::PACKED_FLOAT64_ARRAY);
CHECK(converted == packed);
converted = VariantUtilityFunctions::type_convert(packed, Variant::Type::ARRAY);
CHECK(converted.get_type() == Variant::Type::ARRAY);
CHECK(converted == arr);
}
{
// Check that using Variant::call_utility_function also works.
Vector<const Variant *> args;
Variant data_arg = "Hi!";
args.push_back(&data_arg);
Variant type_arg = Variant::Type::NIL;
args.push_back(&type_arg);
Callable::CallError call_error;
Variant::call_utility_function("type_convert", &converted, (const Variant **)args.ptr(), 2, call_error);
CHECK(converted.get_type() == Variant::Type::NIL);
CHECK(converted == Variant());
type_arg = Variant::Type::INT;
Variant::call_utility_function("type_convert", &converted, (const Variant **)args.ptr(), 2, call_error);
CHECK(converted.get_type() == Variant::Type::INT);
CHECK(converted == Variant(0));
data_arg = "123";
Variant::call_utility_function("type_convert", &converted, (const Variant **)args.ptr(), 2, call_error);
CHECK(converted.get_type() == Variant::Type::INT);
CHECK(converted == Variant(123));
}
}
} // namespace TestVariantUtility

136
tests/create_test.py Normal file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
import argparse
import os
import re
import sys
from subprocess import call
def main():
# Change to the directory where the script is located,
# so that the script can be run from any location.
os.chdir(os.path.dirname(os.path.realpath(__file__)))
parser = argparse.ArgumentParser(description="Creates a new unit test file.")
parser.add_argument(
"name",
type=str,
help="Specifies the class or component name to be tested, in PascalCase (e.g., MeshInstance3D). The name will be prefixed with 'test_' for the header file and 'Test' for the namespace.",
)
parser.add_argument(
"path",
type=str,
nargs="?",
help="The path to the unit test file relative to the tests folder (e.g. core). This should correspond to the relative path of the class or component being tested. (default: .)",
default=".",
)
parser.add_argument(
"-i",
"--invasive",
action="store_true",
help="if set, the script will automatically insert the include directive in test_main.cpp. Use with caution!",
)
args = parser.parse_args()
snake_case_regex = re.compile(r"(?<!^)(?=[A-Z, 0-9])")
# Replace 2D, 3D, and 4D with 2d, 3d, and 4d, respectively. This avoids undesired splits like node_3_d.
prefiltered_name = re.sub(r"([234])D", lambda match: match.group(1).lower() + "d", args.name)
name_snake_case = snake_case_regex.sub("_", prefiltered_name).lower()
file_path = os.path.normpath(os.path.join(args.path, f"test_{name_snake_case}.h"))
# Ensure the directory exists.
os.makedirs(os.path.dirname(file_path), exist_ok=True)
print(file_path)
if os.path.isfile(file_path):
print(f'ERROR: The file "{file_path}" already exists.')
sys.exit(1)
with open(file_path, "w", encoding="utf-8", newline="\n") as file:
file.write(
"""/**************************************************************************/
/* test_{name_snake_case}.h {padding} */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "tests/test_macros.h"
namespace Test{name_pascal_case} {{
TEST_CASE("[{name_pascal_case}] Example test case") {{
// TODO: Remove this comment and write your test code here.
}}
}} // namespace Test{name_pascal_case}
""".format(
name_snake_case=name_snake_case,
# Capitalize the first letter but keep capitalization for the rest of the string.
# This is done in case the user passes a camelCase string instead of PascalCase.
name_pascal_case=args.name[0].upper() + args.name[1:],
# The padding length depends on the test name length.
padding=" " * (61 - len(name_snake_case)),
)
)
# Print an absolute path so it can be Ctrl + clicked in some IDEs and terminal emulators.
print("Test header file created:")
print(os.path.abspath(file_path))
if args.invasive:
print("Trying to insert include directive in test_main.cpp...")
with open("test_main.cpp", "r", encoding="utf-8") as file:
contents = file.read()
match = re.search(r'#include "tests.*\n', contents)
if match:
new_string = contents[: match.start()] + f'#include "tests/{file_path}"\n' + contents[match.start() :]
with open("test_main.cpp", "w", encoding="utf-8", newline="\n") as file:
file.write(new_string)
print("Done.")
# Use clang format to sort include directives afster insertion.
clang_format_args = ["clang-format", "test_main.cpp", "-i"]
retcode = call(clang_format_args)
if retcode != 0:
print(
"Include directives in test_main.cpp could not be sorted automatically using clang-format. Please sort them manually."
)
else:
print("Could not find a valid position in test_main.cpp to insert the include directive.")
else:
print("\nRemember to #include the new test header in this file (following alphabetical order):")
print(os.path.abspath("test_main.cpp"))
print("Insert the following line in the appropriate place:")
print(f'#include "tests/{file_path}"')
if __name__ == "__main__":
main()

51
tests/data/crypto/in.key Normal file
View File

@@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEAp+sWMepqN+dXhmZkS45W0FTLX2f0zRb+kUhXngfiEprgA2Uu
ZP5gGv7/EjOBDniYalGZqYPJum08Px++17P6Hyopr05aPhZ6Ocnt+LAU/WRAFspN
3xXTZglqnmGMsdNcqBl3loVNmC1H/zX53oBeP/L/i4lva3DjCRIpHtGjfy3ufSn7
fTrlBqKIEHYgQZyzos9dxbk1NJcWVxlNmvFRtgPW6DQ/P3g0ahLJeq/hO3ykUFfi
KdhHkS+s9dC9qjZwSxCCEF1o6zkEO6ynxk0sxjOpPTJFw7fQ+2KeJWJQfdZeJ6u+
DpbwK4g9tMUMNxf3QMk1KhAnwwUrKXfZcviS3zweD8Tl5zEj45H1ghoUQfuW7Y3w
gwHMu8lF8iGA87cf/fhqr0V9piCcwWkfVP/athpMoUfyj9Sa3ag0dvDSo9PAet0u
rXmdKTyhMg4lQL6f9BmMuuB/KwWzCuG/5VY9ONxno3OVX6juHTpng5UglbkiDsD3
tivl1gCbvIryoGdt+xI0JmAC5eXfg79Nio/BayDR9Npm1m460p3GeRaawyYysBo/
L5/YZ/S3bYBRoJ7lq6GkTA+22lWAb04IgtS8wxO4Ma8EOtKD+AoR3C+WLivcp9LN
TxbQOMKGL+8imQGBEz3XTR4lrE02QDQy0DIBKy7p7dhlyBdwhTmBX3P2mx0CAwEA
AQKCAgBv7edUjIITE5UnFHeEWbQKmIsb5GqsjshPxV4KDA0pA62Q9dAQJ/Od6x3R
Xx2GrOJD9HKuKRe9ufSvyxRmKiTuwycYIO1Md6UvgierXowPP9TsnBt+Ock5Ocul
GTc0jcQ0lQ0++0p2xrA4MR2GsCCjFfI7a/gmMRBVSpK4ZVtLei1/pw1pM2nYm1yB
RIxJ0A951ioWk1cg4BlXI5m0T2l9H2AQVktWnmSp1C4TJsvG4FWS7JHn/K/v2ky7
alIS9MizcKSSDgHS0aW9tV/8chMHZwZHsYwJYyzddKYgG0G2L7+BSByfEwOysNUY
+0QiMUpyF+zlRfGLMJXNxYLf/UvAhu2xbNi6+A1/xm4FerFF0arMMUzY1Lwwa02t
yBmflhZ+s2ngT4grj3waShC14H6idL2j5HFcyBs/UOA+HkV1I5SoGihNziMP8gfX
IDSb4WBzckPZD6kUAojNFqhx+W7XpWWE5QnWam76b8Mzdg9Xf9pKo6ULt6kwdC8Z
ufbOTRXO08jkb1F64Fmb4F7EAvXLyhFtclY4CuPYSA68Sad8A41ipCsQ5bwvUTMd
o2l7kYplk4f/Bvz2yOhZZVdWGanmKvnGUMehJ+B4zi8HFOIRd21bXkeBwwKjqNni
3kqVairo3O2HWrAJwRvhCZam14HGkr+HQPEGLn48fstquizoQQKCAQEA3slWrItS
wdwciouUbEDbftmWEL+eXAQyVxbIkVuvugLyeZ1lHIk7K5lZ8hecWLNJXPeqyi7P
i2AI8O+7VEoJmePwGmZ19rJHNBB4thgZq114sbkHcQNXXhdFnen8HkdZRdjusViC
BHXCtG8TzxCrpQl/6dLDUkG7D3DiYGaV1IHQ+BNYjJQfh6grkcqRrOq7JH2CnKV0
VCxoSobKgB1zEdc7gKyeRp9SSs5rJe5qTmEMXptrQUeXeElsHfmOV8RUO9b1BBrN
bZhMR3zbE/8Oq3umro7WBaSg7XZMSwCeI0FR8uUAy/FZTrWRPeb86ywmP+pQqcdp
R0OQL0vSWWaLcQKCAQEAwPOzV9p0dQtERy3Z0hh6A3pj+fV5+l5QsA+R4vDoOH/l
GCoEJwSh21qOspSy66fulJR3Jml385/x8I3MC87u8h33aKsdmeVYOdmuiB+lj3sc
DRY/J9w9WbL3mdF3H1rU0ldxfKr1HdfK4xcSdJKBE1FYfO9tB6NFvgdSnvMbmSa1
LVtc8N5hSB1h+d5LWHzh4TC4SG89TivQi0oEacErVJT9OEdGwAtgEU3K8UGKOcTr
OKos0vts281DuKpkfLBstH8l+VOdBD4E+I+MB1Y50oJ/D3h4bl+WDcR0DqlWzay3
3WCSjzZC5T9lEyQ/0TKv7GPgAiH5/41nQnSM6ar8bQKCAQEAvSf9q2pvzaFxqkBw
uKkotD9SJs5LSp1VkJQLnz9VqH2wGooEu4HY91+w+tgJK1auR30RSbENDq1vagJh
72MdW8goqIGuTtN3mUES/KjhwpoOS/dp1g6cM4tW1IlCQwMZTTCvGWyol9jUhBZ7
nyfsVKgILyOAK2sbxDR4QJlZRaEjKD5kxJdPXgLvW02++i4izwyxxQbGCmHZ+s0P
Sk+2z8MLBmmJyTSkzlcMqpwPLpU/x2P2YOrENKFCZwDoVqSfUF9mkSGgohjZSyk7
aXL5pafLEhK8rPXmnTf/9v6DRjPDvJOrZX158lY/B2wD+jj2EPaFnmFthdBbr4yV
AMsMQQKCAQEAn8nxroKRyNAAxjV5Wly8xp6XpsucLTPn/DWYqei5VvjLPxykfa9/
Xsl6vPcZyMA0esUMezoChTXixUSYQvsmtEkOt5ZlmCnuy1GzELWshMr96vSObrMb
92mXVMG7tbKh5mNV71kgTouDUFauCO2+iMHn1ubsUtPqkLk9ubY4F7ePeLVdnXd7
9p2moqdtnCUnZjbTleDRUyhDtuYgC3hWKuCLZwzX0XhaIVpcAzk0gCzMYwvCvSJL
/ybYu1gYiY4NJ9jYGMcelAHMWg9+diD5F5TMJoKssTLlcBdNyUqBQSiUx3cPSBw2
f+TlDloJo3QnbkszmnCKuRBgAA/HFkdsbQKCAQBokUYEzTy3iOwcv9xe2DZoeIeG
Y0B/ri8FxQ+Wt5t/LtGKIwKL5BEpgjXVLL1l/is4jncLkUdGrdqglbjgkJ/fUB/5
/354BjX9a1EBw95XTrUFcdX8uglYkPZkIR9GWY7m87NvLZUdrsmrfl6HA4I4kpAt
1N0dcn/8GIm9vm7COPGDjjPzv+xlMuMjdeTuBxe8tddOwwtXjzkTZmcOdZpianxF
R3zY1LCHk9vPulkAs2o+qCTBGT0qHJp04AamY3KWPW4Cf5GzPousOB4p7Hu8UFyv
FISkNe48bzSYveJ+yZ3myG2fBCGFQmSRJVcauIokPAl6lKF/Vw5DdWp1LYWa
-----END RSA PRIVATE KEY-----

14
tests/data/crypto/in.pub Normal file
View File

@@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp+sWMepqN+dXhmZkS45W
0FTLX2f0zRb+kUhXngfiEprgA2UuZP5gGv7/EjOBDniYalGZqYPJum08Px++17P6
Hyopr05aPhZ6Ocnt+LAU/WRAFspN3xXTZglqnmGMsdNcqBl3loVNmC1H/zX53oBe
P/L/i4lva3DjCRIpHtGjfy3ufSn7fTrlBqKIEHYgQZyzos9dxbk1NJcWVxlNmvFR
tgPW6DQ/P3g0ahLJeq/hO3ykUFfiKdhHkS+s9dC9qjZwSxCCEF1o6zkEO6ynxk0s
xjOpPTJFw7fQ+2KeJWJQfdZeJ6u+DpbwK4g9tMUMNxf3QMk1KhAnwwUrKXfZcviS
3zweD8Tl5zEj45H1ghoUQfuW7Y3wgwHMu8lF8iGA87cf/fhqr0V9piCcwWkfVP/a
thpMoUfyj9Sa3ag0dvDSo9PAet0urXmdKTyhMg4lQL6f9BmMuuB/KwWzCuG/5VY9
ONxno3OVX6juHTpng5UglbkiDsD3tivl1gCbvIryoGdt+xI0JmAC5eXfg79Nio/B
ayDR9Npm1m460p3GeRaawyYysBo/L5/YZ/S3bYBRoJ7lq6GkTA+22lWAb04IgtS8
wxO4Ma8EOtKD+AoR3C+WLivcp9LNTxbQOMKGL+8imQGBEz3XTR4lrE02QDQy0DIB
Ky7p7dhlyBdwhTmBX3P2mx0CAwEAAQ==
-----END PUBLIC KEY-----

Binary file not shown.

View File

@@ -0,0 +1 @@
@IV

View File

@@ -0,0 +1 @@
VI@

View File

@@ -0,0 +1,999 @@
./menu/home/home_menu.tscn
./menu/tooltips/tooltip_server.tscn
./menu/tooltips/tooltip_server.gd
./menu/tooltips/tooltip.gd
./menu/menu/characters/smoker/4.wav
./menu/menu/characters/smoker/6.wav
./menu/menu/characters/smoker/10.wav
./menu/menu/characters/smoker/smoker.tscn
./menu/menu/characters/smoker/8.wav
./menu/menu/characters/smoker/type.gd
./menu/menu/characters/smoker/9.wav
./menu/menu/characters/smoker/5.wav
./menu/menu/characters/smoker/0.wav
./menu/menu/characters/smoker/back_light.png
./menu/menu/characters/smoker/glasses.png
./menu/menu/characters/smoker/smoker.gd
./menu/menu/characters/smoker/cig.gd
./menu/menu/characters/smoker/eyes.png
./menu/menu/characters/smoker/3.wav
./menu/menu/characters/smoker/to_pixelate.gd
./menu/menu/characters/smoker/7.wav
./menu/menu/characters/smoker/cig.png
./menu/menu/characters/smoker/2.wav
./menu/menu/characters/smoker/1.wav
./menu/menu/characters/smoke.png
./menu/menu/characters/space_bandit.tres
./menu/menu/characters/dead_guy/blood_texture.png
./menu/menu/characters/dead_guy/head_gibbed.png
./menu/menu/characters/dead_guy/back_light.png
./menu/menu/characters/dead_guy/smoker.gd
./menu/menu/characters/dead_guy/eyes.png
./menu/menu/characters/dead_guy/to_pixelate.gd
./menu/menu/characters/dead_guy/dead_guy.gd
./menu/menu/characters/dead_guy/eyes.gd
./menu/menu/characters/dead_guy/x.png
./menu/menu/characters/dead_guy/dead_guy.tscn
./menu/menu/characters/dead_guy/mouth.png
./menu/menu/characters/dead_guy/dead_guy.tres
./menu/menu/characters/Label.gd
./menu/menu/characters/guns2.png
./menu/menu/characters/c.gd
./menu/menu/characters/smoke.gd
./menu/menu/characters/character.gd
./menu/menu/characters/space_bandit/eyes.tres
./menu/menu/characters/space_bandit/space_bandit_face_happy.png
./menu/menu/characters/space_bandit/space_bandit.gd
./menu/menu/characters/space_bandit/space_bandit.tscn
./menu/menu/characters/boss/smoker.tscn
./menu/menu/characters/boss/back_light.png
./menu/menu/characters/boss/glasses.png
./menu/menu/characters/boss/smoker.gd
./menu/menu/characters/boss/cig.gd
./menu/menu/characters/boss/eyes.png
./menu/menu/characters/boss/to_pixelate.gd
./menu/menu/characters/boss/x.png
./menu/menu/characters/boss/cig.png
./menu/menu/characters/eye.gd
./menu/menu/characters/space_bandit_face_happy.png
./menu/menu/characters/face.gd
./menu/menu/characters/color.tres
./menu/menu/characters/space_bandit.tscn
./menu/menu/characters/space_bandit_face_bloody.png
./menu/menu/characters/guns.png
./menu/menu/characters/eyes2.tres
./menu/options/controls/use.tres
./menu/options/controls/input_map_button.gd
./menu/options/controls/swap.tres
./menu/options/controls/teleport.tres
./menu/options/controls/joy_controls.tscn
./menu/options/controls/mouse_and_keyboard_controls.tscn
./menu/options/controls/input_map_button.tscn
./menu/options/controls/special.tres
./menu/options/controls/throw.tres
./menu/options/controls/center.tres
./menu/options/controls/input_action.gd
./menu/options/controls/move.tres
./menu/options/controls/melee.tres
./menu/options/controls/controls.gd
./menu/options/options.gd
./menu/options/options.tscn
./menu/options/graphics/graphics.tscn
./menu/options/graphics/graphics.gd
./menu/options/audio/audio.gd
./menu/options/audio/audio.tscn
./menu/options/game/game.gd
./menu/options/game/game.tscn
./menu/circle.tres
./menu/fonts/keys.png
./menu/fonts/rainbow_font.tres
./menu/fonts/fallback_font.tres
./menu/fonts/taxi_Driver.png
./menu/fonts/NotoSansJP-Regular.ttf
./menu/fonts/taxi_Driver_noise.png
./menu/fonts/rainbow_font_shader.tres
./menu/fonts/m5x7.ttf
./menu/colors.gd
./menu/toast_enter.wav
./menu/ui_colors.tres
./menu/pause/pause.gd
./menu/pause/rainbow.tres
./menu/pause/Label.gd
./menu/pause/label.tscn
./menu/pause/pause.tscn
./menu/hoola.wav
./menu/in_game_fallback.tres
./menu/widgets/next_unlock.gd
./menu/widgets/slider.gd
./menu/widgets/fade.tscn
./menu/widgets/background_hint.gd
./menu/widgets/panel_container_smoke.gd
./menu/widgets/wishlist_sticker.gd
./menu/widgets/smoke.tres
./menu/widgets/color_grade.gd
./menu/widgets/rich_text_button.gd
./menu/widgets/panel_container_smok2.tscn
./menu/widgets/slider.tscn
./menu/widgets/rich_text_heading.gd
./menu/widgets/background_hint.tscn
./menu/widgets/tip.tscn
./menu/widgets/rich_text_button.tscn
./menu/widgets/toggle.tscn
./menu/widgets/heading.tscn
./menu/widgets/hover.tscn
./menu/widgets/toggle.gd
./menu/widgets/smoke_panel_material.tres
./menu/widgets/confirm.gd
./menu/widgets/tip.gd
./menu/widgets/panel.gd
./menu/widgets/modal.gd
./menu/widgets/NinePatchRect.gd
./menu/widgets/smoke.shader
./menu/widgets/9patch.png
./menu/widgets/big_hint.gd
./menu/widgets/TDVB1i.png
./menu/widgets/color_grade.tscn
./menu/widgets/text.gd
./menu/widgets/panel_container_smoke.tscn
./menu/widgets/1x1.png
./menu/widgets/confirm.tscn
./menu/widgets/RichTextPanel.tscn
./menu/hud/cursor.png
./menu/hud/inventory/draggable.gd
./menu/hud/inventory/menu/characters/color.tres
./menu/hud/inventory/drop_zone.tscn
./menu/hud/inventory/RichTextLabel.gd
./menu/hud/inventory/hud_icon_mutation.tscn
./menu/hud/inventory/use_count.gd
./menu/hud/inventory/draggable.tscn
./menu/hud/inventory/black_shadow_font.tres
./menu/hud/inventory/x.png
./menu/hud/inventory/hud_icon_mutation.gd
./menu/hud/inventory/flash_parent.gd
./menu/hud/inventory/TextureRect4.gd
./menu/hud/cursor.tscn
./menu/hud/hud.tscn
./menu/hud/cursor.gd
./menu/hud/hud.gd
./menu/metal_text.tres
./menu/rich_text_effects/RichTextType.gd
./menu/rich_text_effects/RichTextPanel.gd
./menu/rich_text_effects/RichTextFlash.gd
./menu/rich_text_effects/RichTextTranslate.gd
./menu/in_game.tres
./menu/lcd_screen_font.tres
./menu/toast_exit.wav
./menu/stack/ahses_material.tres
./menu/stack/home.kra
./menu/stack/fade.gd
./menu/stack/stack.tscn
./menu/stack/stack.gd
./menu/stack/version.gd
./menu/stack/art.kra
./entity/unlock_skin_classic/icon.png
./entity/use.gd
./entity/chair/entity.tscn
./entity/chair/icon.png
./entity/chair/data.gd
./entity/man_desert/entity.tscn
./entity/man_desert/icon.png
./entity/man_desert/teleprompts/need_medbay.wav
./entity/man_desert/teleprompts/me_too.wav
./entity/man_desert/teleprompts/get_up_alt.wav
./entity/man_desert/teleprompts/getting_a_medpack.wav
./entity/man_desert/teleprompts/firstaid-incoming.wav
./entity/man_desert/teleprompts/batch_name.py
./entity/man_desert/teleprompts/what.wav
./entity/man_desert/teleprompts/oo.wav
./entity/man_desert/teleprompts/yell.wav
./entity/man_desert/teleprompts/rushing.wav
./entity/man_desert/teleprompts/ooo.wav
./entity/man_desert/teleprompts/coming_to_heal_ya.wav
./entity/man_desert/teleprompts/where_is_the_medpack.wav
./entity/man_desert/teleprompts/ah.wav
./entity/man_desert/teleprompts/no.wav
./entity/man_desert/teleprompts/going_to_camp_medbay.wav
./entity/man_desert/teleprompts/aa.wav
./entity/man_desert/teleprompts/pirate_alt.wav
./entity/man_desert/teleprompts/take_morphine.wav
./entity/man_desert/teleprompts/ee.wav
./entity/man_desert/teleprompts/get_up.wav
./entity/man_desert/teleprompts/aw.wav
./entity/man_desert/teleprompts/easy.wav
./entity/man_desert/teleprompts/intruder.wav
./entity/man_desert/teleprompts/amateur.wav
./entity/man_desert/teleprompts/hes_not_moving.wav
./entity/man_desert/teleprompts/pirate.wav
./entity/man_desert/teleprompts/i_dont_know.wav
./entity/man_desert/teleprompts/index.txt
./entity/man_desert/teleprompts/move.wav
./entity/man_desert/teleprompts/hes_stuck.wav
./entity/man_desert/teleprompts/how.wav
./entity/man_desert/teleprompts/uu.wav
./entity/man_desert/teleprompts/where_is_the_gun.wav
./entity/man_desert/teleprompts/getting_a_gun.wav
./entity/man_desert/data.gd
./entity/man_desert/hand.png
./entity/barrel_side_smoke/entity.tscn
./entity/barrel_side_smoke/icon.png
./entity/barrel_side_smoke/data.gd
./entity/barrel_smoke/entity.tscn
./entity/barrel_smoke/icon.png
./entity/barrel_smoke/data.gd
./entity/project_box/entity.tscn
./entity/project_box/icon.png
./entity/project_box/data.gd
./entity/mutation_saw/entity.tscn
./entity/mutation_saw/icon.png
./entity/mutation_saw/special.gd
./entity/mutation_saw/data.gd
./entity/lift_entrance/entity.tscn
./entity/lift_entrance/icon.png
./entity/lift_entrance/special.gd
./entity/lift_entrance/data.gd
./entity/mutation_accuracy_boost_DELETE/entity.tscn
./entity/mutation_accuracy_boost_DELETE/icon.png
./entity/mutation_accuracy_boost_DELETE/special.gd
./entity/mutation_accuracy_boost_DELETE/data.gd
./entity/skin_ruffle/entity.tscn
./entity/skin_ruffle/icon.png
./entity/skin_ruffle/carried.png
./entity/skin_ruffle/data.gd
./entity/editor_only_icon.gd
./entity/console_dark/entity.tscn
./entity/console_dark/icon.png
./entity/console_dark/data.gd
./entity/console_dark/animation.png
./entity/smg2/entity.tscn
./entity/smg2/used.wav
./entity/smg2/icon.png
./entity/smg2/data.gd
./entity/smg2/debug.gd
./entity/grenade_launcher/entity.tscn
./entity/grenade_launcher/used.wav
./entity/grenade_launcher/icon.png
./entity/grenade_launcher/special.gd
./entity/grenade_launcher/data.gd
./entity/floor_tile_full_square/entity.tscn
./entity/floor_tile_full_square/icon.png
./entity/floor_tile_full_square/data.gd
./entity/grate_1/entity.tscn
./entity/grate_1/icon.png
./entity/grate_1/data.gd
./entity/bed_bunk_corner/entity.tscn
./entity/bed_bunk_corner/icon.png
./entity/bed_bunk_corner/data.gd
./entity/kill_streak_rail_gun_level_3/entity.tscn
./entity/kill_streak_rail_gun_level_3/data.gd
./entity/teleporter_random_weak/entity.tscn
./entity/teleporter_random_weak/teleporter_model.gd
./entity/teleporter_random_weak/used.wav
./entity/teleporter_random_weak/icon.png
./entity/teleporter_random_weak/special.gd
./entity/teleporter_random_weak/ray.gd
./entity/teleporter_random_weak/data.gd
./entity/teleporter_random_weak/flap.png
./entity/entities.kra
./entity/jerry_can/entity.tscn
./entity/jerry_can/icon.png
./entity/jerry_can/data.gd
./entity/kill_streak_helmet_full/entity.tscn
./entity/kill_streak_helmet_full/data.gd
./entity/background_derelict/background2.gd
./entity/background_derelict/entity.tscn
./entity/background_derelict/icon.png
./entity/background_derelict/background/space.png
./entity/background_derelict/background/line.png
./entity/background_derelict/background/overlay.png
./entity/background_derelict/background/background2.png
./entity/background_derelict/background/background.png
./entity/background_derelict/background/engine_glow.tscn
./entity/background_derelict/background/lines3.png
./entity/background_derelict/background/background.tscn
./entity/background_derelict/background/lines.tres
./entity/background_derelict/background/xx.gd
./entity/background_derelict/background/background.gd
./entity/background_derelict/background/bayer16tile2.png
./entity/background_derelict/background/push.png
./entity/background_derelict/background/palette_mono.png
./entity/background_derelict/background/stars.gd
./entity/background_derelict/background/lines2.png
./entity/background_derelict/background/lines.shader
./entity/background_derelict/background/ambience.gd
./entity/background_derelict/background/space_ship_ambience.ogg
./entity/background_derelict/background/stars.png
./entity/background_derelict/data.gd
./entity/smoker/entity.tscn
./entity/smoker/right_hand.png
./entity/smoker/eyes.png
./entity/smoker/data.gd
./entity/smoker/animate.gd
./entity/smoker/left_hand.png
./entity/EntityStatic.gd
./entity/level_model.gd
./entity/class_teleporter_drop_chance/entity.tscn
./entity/class_teleporter_drop_chance/icon.png
./entity/class_teleporter_drop_chance/special.gd
./entity/class_teleporter_drop_chance/data.gd
./entity/smg4/entity.tscn
./entity/smg4/used.wav
./entity/smg4/icon.png
./entity/smg4/data.gd
./entity/medpack/entity.tscn
./entity/medpack/icon.png
./entity/medpack/dead.png
./entity/medpack/data.gd
./entity/model.gd
./entity/doom_transition/entity.tscn
./entity/doom_transition/icon.png
./entity/doom_transition/special.gd
./entity/doom_transition/Screenshot from 2021-12-08 18-25-03.png
./entity/doom_transition/data.gd
./entity/glass_block_exploding/entity.tscn
./entity/glass_block_exploding/icon.png
./entity/glass_block_exploding/special.gd
./entity/glass_block_exploding/dead.png
./entity/glass_block_exploding/data.gd
./entity/floor_ting/entity.tscn
./entity/floor_ting/icon.png
./entity/floor_ting/data.gd
./entity/background_crashed_ship/entity.tscn
./entity/background_crashed_ship/icon.png
./entity/background_crashed_ship/background/background2.kra
./entity/background_crashed_ship/background/dust_storm_negative.png
./entity/background_crashed_ship/background/background2.png
./entity/background_crashed_ship/background/background2 (copy 1).png
./entity/background_crashed_ship/background/dust_bowl.ogg
./entity/background_crashed_ship/background/background.tscn
./entity/background_crashed_ship/background/background.kra
./entity/background_crashed_ship/data.gd
./entity/game_aim_hack_boss/entity.tscn
./entity/game_aim_hack_boss/icon.png
./entity/game_aim_hack_boss/special.gd
./entity/game_aim_hack_boss/give_my_arm_back.wav
./entity/game_aim_hack_boss/my_arm_came_off.wav
./entity/game_aim_hack_boss/data.gd
./entity/sink/entity.tscn
./entity/sink/icon.png
./entity/sink/data.gd
./entity/grate_2/entity.tscn
./entity/grate_2/icon.png
./entity/grate_2/data.gd
./entity/barrel_side/entity.tscn
./entity/barrel_side/icon.png
./entity/barrel_side/data.gd
./entity/oxygen/entity.tscn
./entity/oxygen/icon.png
./entity/oxygen/shadow.png
./entity/oxygen/data.gd
./entity/oxygen/normal.png
./entity/unlock_skin_robo/entity.tscn
./entity/unlock_skin_robo/icon.png
./entity/unlock_skin_robo/special.gd
./entity/unlock_skin_robo/data.gd
./entity/entity_agency_model.gd
./entity/floor_tile_wood/entity.tscn
./entity/floor_tile_wood/icon.png
./entity/floor_tile_wood/data.gd
./entity/qr_code/entity.tscn
./entity/qr_code/icon.png
./entity/qr_code/data.gd
./entity/background_sun/overlay.png
./entity/background_sun/entity.tscn
./entity/background_sun/c.gd
./entity/background_sun/kill.tscn
./entity/background_sun/icon.png
./entity/background_sun/special.gd
./entity/background_sun/wtf.tres
./entity/background_sun/background/background2.png
./entity/background_sun/background/background.tscn
./entity/background_sun/background/color2s.tres
./entity/background_sun/background/background_glow.png
./entity/background_sun/data.gd
./entity/background_sun/kill.gd
./entity/background_sun/stars.png
./entity/background_zone_intro/overlay.png
./entity/background_zone_intro/entity.tscn
./entity/background_zone_intro/icon.png
./entity/background_zone_intro/special.gd
./entity/background_zone_intro/background/space.png
./entity/background_zone_intro/background/line.png
./entity/background_zone_intro/background/background2.png
./entity/background_zone_intro/background/background.png
./entity/background_zone_intro/background/engine_glow.tscn
./entity/background_zone_intro/background/lines3.png
./entity/background_zone_intro/background/background.tscn
./entity/background_zone_intro/background/lines.tres
./entity/background_zone_intro/background/background.gd
./entity/background_zone_intro/background/bayer16tile2.png
./entity/background_zone_intro/background/push.png
./entity/background_zone_intro/background/palette_mono.png
./entity/background_zone_intro/background/stars.gd
./entity/background_zone_intro/background/lines2.png
./entity/background_zone_intro/background/lines.shader
./entity/background_zone_intro/background/ambience.gd
./entity/background_zone_intro/background/space_ship_ambience.ogg
./entity/background_zone_intro/background/stars.png
./entity/background_zone_intro/background_end.png
./entity/background_zone_intro/data.gd
./entity/background_zone_intro/tinge.png
./entity/closet_alt/entity.tscn
./entity/closet_alt/icon.png
./entity/closet_alt/data.gd
./entity/meta_random_sound/entity.tscn
./entity/meta_random_sound/giberish.wav
./entity/meta_random_sound/icon.png
./entity/meta_random_sound/special.gd
./entity/meta_random_sound/who.wav
./entity/meta_random_sound/data.gd
./entity/meta_random_sound/hoola_boola.wav
./entity/meta_random_sound/space_bandit.wav
./entity/lines/entity.tscn
./entity/lines/icon.png
./entity/lines/data.gd
./entity/teleporter_random_avoid_ray/entity.tscn
./entity/teleporter_random_avoid_ray/used.wav
./entity/teleporter_random_avoid_ray/icon.png
./entity/teleporter_random_avoid_ray/ray.gd
./entity/teleporter_random_avoid_ray/data.gd
./entity/teleporter_random_avoid_ray/flap.png
./entity/teleporter_random_avoid_ray/RayCast2D.gd
./entity/teleporter_random_avoid_ray/area.gd
./entity/teleporter_random_avoid_ray/flap.gd
./entity/saw/blades.gd
./entity/saw/entity.tscn
./entity/saw/used.wav
./entity/saw/icon.png
./entity/saw/special.gd
./entity/saw/carried.png
./entity/saw/data.gd
./entity/saw/used (copy 1).wav
./entity/saw/saw.wav
./entity/saw/carried_blades.png
./entity/floor_tile_checkerdboard/damage.png
./entity/floor_tile_checkerdboard/entity.tscn
./entity/floor_tile_checkerdboard/icon.png
./entity/floor_tile_checkerdboard/entity.tres
./entity/floor_tile_checkerdboard/data.gd
./entity/mutation_smoke_grenade_upgrade/entity.tscn
./entity/mutation_smoke_grenade_upgrade/icon.png
./entity/mutation_smoke_grenade_upgrade/special.gd
./entity/mutation_smoke_grenade_upgrade/data.gd
./entity/mutation_smoke_grenade_upgrade/mutation_model.gd
./entity/helmet_full/entity.tscn
./entity/helmet_full/pick_up.wav
./entity/helmet_full/icon.png
./entity/helmet_full/data.gd
./entity/helmet_full/helmet-ping.wav
./entity/barrel_explosive/entity.tscn
./entity/barrel_explosive/icon.png
./entity/barrel_explosive/data.gd
./entity/bank/entity.tscn
./entity/bank/icon.png
./entity/bank/special.gd
./entity/bank/data.gd
./entity/kick/entity.tscn
./entity/kick/swipe.png
./entity/kick/used.wav
./entity/kick/icon.png
./entity/kick/AnimatedSprite.gd
./entity/kick/data.gd
./entity/battery/entity.tscn
./entity/battery/icon.png
./entity/battery/data.gd
./entity/lift/entity.tscn
./entity/lift/opening.wav
./entity/lift/doors_open.png
./entity/lift/RichTextLabel.gd
./entity/lift/icon.png
./entity/lift/open.wav
./entity/lift/elevator_end.wav
./entity/lift/lift_model.gd
./entity/lift/label.tscn
./entity/lift/rumble.gd
./entity/lift/level_portal_model.gd
./entity/lift/data.gd
./entity/lift/doors.png
./entity/lift/area.gd
./entity/snes/entity.tscn
./entity/snes/icon.png
./entity/snes/data.gd
./entity/passive_disarm/entity.tscn
./entity/passive_disarm/icon.png
./entity/passive_disarm/special.gd
./entity/passive_disarm/data.gd
./entity/mutation_lots_of_shot/entity.tscn
./entity/mutation_lots_of_shot/icon.png
./entity/mutation_lots_of_shot/special.gd
./entity/mutation_lots_of_shot/data.gd
./entity/pallet2/entity.tscn
./entity/pallet2/icon.png
./entity/pallet2/data.gd
./entity/kill_streak_sword/entity.tscn
./entity/kill_streak_sword/data.gd
./entity/rain/entity.tscn
./entity/rain/icon.png
./entity/rain/special.gd
./entity/rain/rain.png
./entity/rain/rain.tscn
./entity/rain/data.gd
./entity/rain/rain.gd
./entity/white_line/entity.tscn
./entity/white_line/icon.png
./entity/white_line/data.gd
./entity/game_break_sword/entity.tscn
./entity/game_break_sword/icon.png
./entity/game_break_sword/special.gd
./entity/game_break_sword/data.gd
./entity/background_zone1/overlay.png
./entity/background_zone1/entity.tscn
./entity/background_zone1/icon.png
./entity/background_zone1/special.gd
./entity/background_zone1/background/space.png
./entity/background_zone1/background/line.png
./entity/background_zone1/background/background2.png
./entity/background_zone1/background/background.png
./entity/background_zone1/background/engine_glow.tscn
./entity/background_zone1/background/lines3.png
./entity/background_zone1/background/background.tscn
./entity/background_zone1/background/lines.tres
./entity/background_zone1/background/background.gd
./entity/background_zone1/background/bayer16tile2.png
./entity/background_zone1/background/push.png
./entity/background_zone1/background/palette_mono.png
./entity/background_zone1/background/stars.gd
./entity/background_zone1/background/lines2.png
./entity/background_zone1/background/lines.shader
./entity/background_zone1/background/ambience.gd
./entity/background_zone1/background/space_ship_ambience.ogg
./entity/background_zone1/background/stars.png
./entity/background_zone1/data.gd
./entity/background_zone1/tinge.png
./entity/mutation_throw_trap_DELETE/entity.tscn
./entity/mutation_throw_trap_DELETE/icon.png
./entity/mutation_throw_trap_DELETE/special.gd
./entity/mutation_throw_trap_DELETE/data.gd
./entity/agency.gd
./entity/skin_cheese/entity.tscn
./entity/skin_cheese/icon.png
./entity/skin_cheese/carried.png
./entity/skin_cheese/data.gd
./entity/toilet/entity.tscn
./entity/toilet/icon.png
./entity/toilet/special.gd
./entity/toilet/water.png
./entity/toilet/drink.wav
./entity/toilet/data.gd
./entity/smg3/entity.tscn
./entity/smg3/used.wav
./entity/smg3/icon.png
./entity/smg3/dead.png
./entity/smg3/data.gd
./entity/smg3/debug.gd
./entity/teleporter_super/entity.tscn
./entity/teleporter_super/icon.png
./entity/teleporter_super/data.gd
./entity/background_zone_end/overlay.png
./entity/background_zone_end/entity.tscn
./entity/background_zone_end/icon.png
./entity/background_zone_end/special.gd
./entity/background_zone_end/stars2.png
./entity/background_zone_end/background_end.png
./entity/background_zone_end/data.gd
./entity/background_zone_end/tinge.png
./entity/kill_streak_barricade/entity.tscn
./entity/kill_streak_barricade/data.gd
./entity/game_zone_4_boss_1/entity.tscn
./entity/game_zone_4_boss_1/icon.png
./entity/game_zone_4_boss_1/special.gd
./entity/game_zone_4_boss_1/data.gd
./entity/game_zone_4_boss_1/kill_me_and_explode_ship.wav
./entity/mutation_remove_melee/entity.tscn
./entity/mutation_remove_melee/icon.png
./entity/mutation_remove_melee/special.gd
./entity/mutation_remove_melee/data.gd
./entity/he_grenade_level_2/entity.tscn
./entity/he_grenade_level_2/icon.png
./entity/he_grenade_level_2/data.gd
./entity/background_zone_2/entity.tscn
./entity/background_zone_2/icon.png
./entity/background_zone_2/background/background2.kra
./entity/background_zone_2/background/grad.png
./entity/background_zone_2/background/background2.png
./entity/background_zone_2/background/background.png
./entity/background_zone_2/background/background2 (copy 1).png
./entity/background_zone_2/background/backgrounds.gd
./entity/background_zone_2/background/wall_overlay.png
./entity/background_zone_2/background/background.tscn
./entity/background_zone_2/background/Screenshot from 2022-07-07 10-58-48.png
./entity/background_zone_2/background/background.gd
./entity/background_zone_2/background/shadow.png
./entity/background_zone_2/background/engine smoke.png
./entity/background_zone_2/background/background.kra
./entity/background_zone_2/background/sea.ogg
./entity/background_zone_2/background/background2blur.png
./entity/background_zone_2/background/test.gd
./entity/background_zone_2/background/grad3.png
./entity/background_zone_2/background/lines2.png
./entity/background_zone_2/background/smoke.tscn
./entity/background_zone_2/background/left_water.tscn
./entity/background_zone_2/background/grad2.png
./entity/background_zone_2/background/para.png
./entity/background_zone_2/data.gd
./entity/pipe_corner/entity.tscn
./entity/pipe_corner/icon.png
./entity/pipe_corner/data.gd
./entity/floor_tile_metal_cow_trap/entity.tscn
./entity/floor_tile_metal_cow_trap/icon.png
./entity/floor_tile_metal_cow_trap/data.gd
./entity/skin_naked/entity.tscn
./entity/skin_naked/icon.png
./entity/skin_naked/carried.png
./entity/skin_naked/data.gd
./entity/valve/entity.tscn
./entity/valve/icon.png
./entity/valve/.icon.png-autosave.kra
./entity/valve/data.gd
./entity/bed/entity.tscn
./entity/bed/icon.png
./entity/bed/data.gd
./entity/game_invisible_guy/entity.tscn
./entity/game_invisible_guy/icon.png
./entity/game_invisible_guy/special.gd
./entity/game_invisible_guy/data.gd
./entity/smg/entity.tscn
./entity/smg/used.wav
./entity/smg/icon.png
./entity/smg/data.gd
./entity/skin_robo/entity.tscn
./entity/skin_robo/icon.png
./entity/skin_robo/carried.png
./entity/skin_robo/data.gd
./entity/bandana/entity.tscn
./entity/bandana/bob.gd
./entity/bandana/icon.png
./entity/bandana/special.gd
./entity/bandana/carried.png
./entity/bandana/data.gd
./entity/bandana/pixel.png
./entity/floor_plug/entity.tscn
./entity/floor_plug/icon.png
./entity/floor_plug/data.gd
./entity/bench/entity.tscn
./entity/bench/icon.png
./entity/bench/data.gd
./entity/meta_strip_items/entity.tscn
./entity/meta_strip_items/special.gd
./entity/meta_strip_items/meta_strip_items_model.gd
./entity/meta_strip_items/data.gd
./entity/crate_teleporter/entity.tscn
./entity/crate_teleporter/icon.png
./entity/crate_teleporter/data.gd
./entity/crate_teleporter/satellite.kra
./entity/crate_garbage/entity.tscn
./entity/crate_garbage/icon.png
./entity/crate_garbage/data.gd
./entity/crate_garbage/gibbed.png
./entity/meta_stats/entity.tscn
./entity/meta_stats/letters.tres
./entity/meta_stats/icon.png
./entity/meta_stats/special.gd
./entity/meta_stats/data.gd
./entity/meta_stats/meta_stats_model.gd
./entity/rail_gun/entity.tscn
./entity/rail_gun/used.wav
./entity/rail_gun/icon.png
./entity/rail_gun/special.gd
./entity/rail_gun/carried.png
./entity/rail_gun/data.gd
./entity/drop_ship_door/entity.tscn
./entity/drop_ship_door/icon.png
./entity/drop_ship_door/data.gd
./entity/floor_lines/entity.tscn
./entity/floor_lines/icon.png
./entity/floor_lines/data.gd
./entity/game_trap/entity.tscn
./entity/game_trap/you_blew_up_my_force_field.wav
./entity/game_trap/droped_my_grenade_2.wav
./entity/game_trap/icon.png
./entity/game_trap/special.gd
./entity/game_trap/droped_my_grenade_0.wav
./entity/game_trap/shock.wav
./entity/game_trap/uh_my_helmet.wav
./entity/game_trap/ha_missed_me.wav
./entity/game_trap/data.gd
./entity/game_trap/try_beat_this_force_field.wav
./entity/game_trap/droped_my_grenade_1.wav
./entity/blood_sword/entity.tscn
./entity/blood_sword/pick_up.wav
./entity/blood_sword/used.wav
./entity/blood_sword/sam2.png
./entity/blood_sword/icon.png
./entity/blood_sword/special.gd
./entity/blood_sword/hit_bar.gd
./entity/blood_sword/data.gd
./entity/blood_sword/sam.png
./entity/blood_sword/dead.wav
./entity/blood_sword/animation.png
./entity/auto_cables_thick/entity.tscn
./entity/auto_cables_thick/data.gd
./entity/auto_cables_thick/wires2.png
./entity/shield/entity.tscn
./entity/shield/pick_up.wav
./entity/shield/icon.png
./entity/shield/carried.png
./entity/shield/data.gd
./entity/shield/helmet-ping.wav
./entity/game_teleport_in/entity.tscn
./entity/game_teleport_in/icon.png
./entity/game_teleport_in/special.gd
./entity/game_teleport_in/data.gd
./entity/shotgun_super/entity.tscn
./entity/shotgun_super/icon.png
./entity/shotgun_super/data.gd
./entity/bottle/entity.tscn
./entity/bottle/icon.png
./entity/bottle/data.gd
./entity/bottle/normal.png
./entity/bottle/icon_shadow.png
./entity/kill_streak_p90/entity.tscn
./entity/kill_streak_p90/data.gd
./entity/drain/entity.tscn
./entity/drain/icon.png
./entity/drain/data.gd
./entity/auto_wires_three/entity.tscn
./entity/auto_wires_three/data.gd
./entity/light/entity.tscn
./entity/light/icon.png
./entity/light/special.gd
./entity/light/light.wav
./entity/light/data.gd
./entity/debris/entity.tscn
./entity/debris/icon.png
./entity/debris/data.gd
./entity/debris/gibbed.png
./entity/mutation_rail_gun_upgrade/entity.tscn
./entity/mutation_rail_gun_upgrade/icon.png
./entity/mutation_rail_gun_upgrade/special.gd
./entity/mutation_rail_gun_upgrade/data.gd
./entity/mutation_rail_gun_upgrade/mutation_model.gd
./entity/auto_cables/entity.tscn
./entity/auto_cables/data.gd
./entity/auto_cables/wires2.png
./entity/stealth_camo/entity.tscn
./entity/stealth_camo/special.gd
./entity/stealth_camo/data.gd
./entity/colt_45/entity.tscn
./entity/colt_45/used.wav
./entity/colt_45/icon.png
./entity/colt_45/dead.png
./entity/colt_45/data.gd
./entity/quantum_suicide_drive/entity.tscn
./entity/quantum_suicide_drive/heart.ogg
./entity/quantum_suicide_drive/icon.png
./entity/quantum_suicide_drive/special.gd
./entity/quantum_suicide_drive/qsd_model.gd
./entity/quantum_suicide_drive/multi.gd
./entity/quantum_suicide_drive/multi.tscn
./entity/quantum_suicide_drive/CenterContainer.gd
./entity/quantum_suicide_drive/carried.png
./entity/quantum_suicide_drive/data.gd
./entity/helmet/entity.tscn
./entity/helmet/pick_up.wav
./entity/helmet/icon.png
./entity/helmet/special.gd
./entity/helmet/die.wav
./entity/helmet/carried.png
./entity/helmet/data.gd
./entity/helmet/helmet-ping.wav
./entity/ammo_box/entity.tscn
./entity/ammo_box/icon.png
./entity/ammo_box/data.gd
./entity/rail_gun_level_2/entity.tscn
./entity/rail_gun_level_2/icon.png
./entity/rail_gun_level_2/data.gd
./entity/glass_block_backup/entity.tscn
./entity/glass_block_backup/icon.png
./entity/glass_block_backup/data.gd
./entity/closet/entity.tscn
./entity/closet/icon.png
./entity/closet/data.gd
./entity/little_boxes/entity.tscn
./entity/little_boxes/icon.png
./entity/little_boxes/data.gd
./entity/meta_health_bar/entity.tscn
./entity/meta_health_bar/health_bar_model.gd
./entity/meta_health_bar/icon.png
./entity/meta_health_bar/special.gd
./entity/meta_health_bar/invunerable.png
./entity/meta_health_bar/data.gd
./entity/night_stand/entity.tscn
./entity/night_stand/icon_normal.png
./entity/night_stand/icon.png
./entity/night_stand/shadow.png
./entity/night_stand/data.gd
./entity/fan/entity.tscn
./entity/fan/flap2.png
./entity/fan/flaps.gd
./entity/fan/icon.png
./entity/fan/data.gd
./entity/fan/flap.png
./entity/fan/icon_shadow.png
./entity/fan/animation.png
./entity/fan/gibbed.png
./entity/game_tutorial_end/entity.tscn
./entity/game_tutorial_end/icon.png
./entity/game_tutorial_end/special.gd
./entity/game_tutorial_end/data.gd
./entity/mutation_disarmament/entity.tscn
./entity/mutation_disarmament/icon.png
./entity/mutation_disarmament/special.gd
./entity/mutation_disarmament/data.gd
./entity/air_lock/icon_open.png
./entity/air_lock/entity.tscn
./entity/air_lock/door_close.wav
./entity/air_lock/icon.png
./entity/air_lock/special.gd
./entity/air_lock/air_lock_model.gd
./entity/air_lock/data.gd
./entity/scorpion/entity.tscn
./entity/scorpion/used.wav
./entity/scorpion/laser.gd
./entity/scorpion/icon.png
./entity/scorpion/data.gd
./entity/kill_streak_aim_hack/entity.tscn
./entity/kill_streak_aim_hack/data.gd
./entity/dungeon_proc_debug/entity.tscn
./entity/dungeon_proc_debug/icon.png
./entity/dungeon_proc_debug/data.gd
./entity/dungeon_proc_debug/debug.gd
./entity/dungeon_proc_debug/debug.tscn
./entity/tarp/entity.tscn
./entity/tarp/icon.png
./entity/tarp/data.gd
./entity/hit_indicator/entity.tscn
./entity/hit_indicator/data.gd
./entity/console_corner/entity.tscn
./entity/console_corner/animation2.tscn
./entity/console_corner/icon.png
./entity/console_corner/data.gd
./entity/console_corner/animation.tscn
./entity/icon.png
./entity/couch_corner/entity.tscn
./entity/couch_corner/icon.png
./entity/couch_corner/data.gd
./entity/m4/entity.tscn
./entity/m4/used.wav
./entity/m4/icon.png
./entity/m4/data.gd
./entity/game_hud/entity.tscn
./entity/game_hud/icon.png
./entity/game_hud/data.gd
./entity/game_hud/inventory_game.tscn
./entity/prototypes.gd
./entity/agent_chicken/emotes.png
./entity/agent_chicken/entity.tscn
./entity/agent_chicken/sound_board.gd
./entity/agent_chicken/bones.tscn
./entity/agent_chicken/bones.gd
./entity/agent_chicken/barks.gd
./entity/agent_chicken/emote.gd
./entity/agent_chicken/icon.png
./entity/agent_chicken/special.gd
./entity/agent_chicken/bark.gd
./entity/agent_chicken/deaad.png
./entity/agent_chicken/icon.gd
./entity/agent_chicken/data.gd
./entity/agent_chicken/animation.tscn
./entity/agent_chicken/emote.tscn
./entity/agent_chicken/hand.png
./entity/velocity/entity.tscn
./entity/velocity/icon.png
./entity/velocity/special.gd
./entity/velocity/data.gd
./entity/aircon/entity.tscn
./entity/aircon/grate.png
./entity/aircon/icon.png
./entity/aircon/data.gd
./entity/aircon/animation.png
./entity/floor_tile_bricks/entity.tscn
./entity/floor_tile_bricks/icon.png
./entity/floor_tile_bricks/data.gd
./entity/pallet/entity.tscn
./entity/pallet/icon.png
./entity/pallet/data.gd
./entity/barricade_deployed/debug.png
./entity/barricade_deployed/field.tscn
./entity/barricade_deployed/entity.tscn
./entity/barricade_deployed/ambience.ogg
./entity/barricade_deployed/icon.png
./entity/barricade_deployed/field.gd
./entity/barricade_deployed/field_material.tres
./entity/barricade_deployed/debug2.png
./entity/barricade_deployed/data.gd
./entity/barricade_deployed/field_material_invert.tres
./entity/barricade_deployed/field_material.gd
./entity/barricade_deployed/gibbed.png
./entity/helmet_nv/entity.tscn
./entity/helmet_nv/pick_up.wav
./entity/helmet_nv/icon.png
./entity/helmet_nv/special.gd
./entity/helmet_nv/carried.png
./entity/helmet_nv/eyes.png
./entity/helmet_nv/data.gd
./entity/helmet_nv/helmet-ping.wav
./entity/helmet_nv/eyes.gd
./entity/mutation_sword/entity.tscn
./entity/mutation_sword/icon.png
./entity/mutation_sword/special.gd
./entity/mutation_sword/data.gd
./entity/field_full_super/entity.tscn
./entity/field_full_super/icon.png
./entity/field_full_super/special.gd
./entity/field_full_super/carried.png
./entity/field_full_super/data.gd
./entity/entity_man.gd
./entity/couch/entity.tscn
./entity/couch/icon.png
./entity/couch/data.gd
./entity/teleporter_lil_hunter/entity.tscn
./entity/teleporter_lil_hunter/icon.png
./entity/teleporter_lil_hunter/tubes.png
./entity/teleporter_lil_hunter/osc_shader.tres
./entity/teleporter_lil_hunter/eyes.png
./entity/teleporter_lil_hunter/data.gd
./entity/teleporter_lil_hunter/osc.tres
./entity/game_tutorial_melee_zone/entity.tscn
./entity/game_tutorial_melee_zone/icon.png
./entity/game_tutorial_melee_zone/special.gd
./entity/game_tutorial_melee_zone/data.gd
./entity/kill_streak_glock/entity.tscn
./entity/kill_streak_glock/data.gd
./entity/skin_mime/entity.tscn
./entity/skin_mime/icon.png
./entity/skin_mime/special.gd
./entity/skin_mime/carried.png
./entity/skin_mime/data.gd
./entity/medpack_hard/entity.tscn
./entity/medpack_hard/icon.png
./entity/medpack_hard/data.gd
./entity/teleporter_overload/entity.tscn
./entity/teleporter_overload/icon.png
./entity/teleporter_overload/special.gd
./entity/teleporter_overload/carried.png
./entity/teleporter_overload/data.gd
./entity/background_freighter/overlay.png
./entity/background_freighter/entity.tscn
./entity/background_freighter/icon.png
./entity/background_freighter/Master.ogg
./entity/background_freighter/background/space.png
./entity/background_freighter/background/line.png
./entity/background_freighter/background/background2.gd
./entity/background_freighter/background/good create.png
./entity/background_freighter/background/backgip.png
./entity/background_freighter/background/background2.png
./entity/background_freighter/background/background.png
./entity/background_freighter/background/engine_glow.tscn
./entity/background_freighter/background/gra2d.png
./entity/background_freighter/background/lines3.png
./entity/background_freighter/background/background.tscn
./entity/background_freighter/background/lines.tres
./entity/background_freighter/background/background.gd
./entity/background_freighter/background/bayer16tile2.png
./entity/background_freighter/background/goodcrate.png
./entity/background_freighter/background/push.png
./entity/background_freighter/background/background_floor.png
./entity/background_freighter/background/palette_mono.png
./entity/background_freighter/background/stars.gd
./entity/background_freighter/background/lines2.png
./entity/background_freighter/background/lines.shader
./entity/background_freighter/background/ambience.gd
./entity/background_freighter/background/bacsdas.png
./entity/background_freighter/background/space_ship_ambience.ogg
./entity/background_freighter/background/stars.png
./entity/background_freighter/data.gd
./entity/auto_wires/entity.tscn
./entity/auto_wires/data.gd
./entity/kill_streak/entity.tscn
./entity/kill_streak/kill_streak_toast.tscn
./entity/kill_streak/icon.png

View File

@@ -0,0 +1 @@
5U

View File

@@ -0,0 +1 @@
U5

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

BIN
tests/data/images/icon.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
tests/data/images/icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
tests/data/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
tests/data/images/icon.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
tests/data/images/icon.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1 @@
Hello darkness

View File

@@ -0,0 +1,4 @@
Hello darkness
My old friend
I've come to talk
With you again

View File

@@ -0,0 +1,4 @@
Hello darkness
My old friend
I've come to talk
With you again

Some files were not shown because too many files have changed in this diff Show More