diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/data/translations.csv | 5 | ||||
-rw-r--r-- | tests/test_aabb.h | 3 | ||||
-rw-r--r-- | tests/test_array.h | 51 | ||||
-rw-r--r-- | tests/test_astar.h | 2 | ||||
-rw-r--r-- | tests/test_class_db.h | 64 | ||||
-rw-r--r-- | tests/test_code_edit.h | 3190 | ||||
-rw-r--r-- | tests/test_config_file.h | 11 | ||||
-rw-r--r-- | tests/test_curve.h | 35 | ||||
-rw-r--r-- | tests/test_file_access.h | 29 | ||||
-rw-r--r-- | tests/test_geometry_2d.h | 33 | ||||
-rw-r--r-- | tests/test_geometry_3d.h | 36 | ||||
-rw-r--r-- | tests/test_macros.h | 219 | ||||
-rw-r--r-- | tests/test_main.cpp | 145 | ||||
-rw-r--r-- | tests/test_math.cpp | 12 | ||||
-rw-r--r-- | tests/test_object.h | 8 | ||||
-rw-r--r-- | tests/test_physics_2d.cpp | 18 | ||||
-rw-r--r-- | tests/test_physics_3d.cpp | 19 | ||||
-rw-r--r-- | tests/test_rect2.h | 12 | ||||
-rw-r--r-- | tests/test_render.cpp | 14 | ||||
-rw-r--r-- | tests/test_shader_lang.cpp | 57 | ||||
-rw-r--r-- | tests/test_string.h | 127 | ||||
-rw-r--r-- | tests/test_text_server.h | 107 | ||||
-rw-r--r-- | tests/test_tools.h | 61 | ||||
-rw-r--r-- | tests/test_translation.h | 31 | ||||
-rw-r--r-- | tests/test_validate_testing.h | 12 | ||||
-rw-r--r-- | tests/test_variant.h | 39 | ||||
-rw-r--r-- | tests/test_vector.h | 13 |
27 files changed, 4163 insertions, 190 deletions
diff --git a/tests/data/translations.csv b/tests/data/translations.csv index 4c9ad4996a..8cb7b800c5 100644 --- a/tests/data/translations.csv +++ b/tests/data/translations.csv @@ -1,3 +1,8 @@ keys,en,de GOOD_MORNING,"Good Morning","Guten Morgen" GOOD_EVENING,"Good Evening","" +Without quotes,"With, comma","With ""inner"" quotes","With ""inner"", quotes"","" and comma","With ""inner +split"" quotes and +line breaks","With \nnewline chars" +Some other~delimiter~should still work, shouldn't it? +What about tab separated lines, good? diff --git a/tests/test_aabb.h b/tests/test_aabb.h index c4daa56e5a..2724d9481a 100644 --- a/tests/test_aabb.h +++ b/tests/test_aabb.h @@ -65,6 +65,9 @@ TEST_CASE("[AABB] Basic getters") { 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") { diff --git a/tests/test_array.h b/tests/test_array.h index 52da256860..3bd476fd27 100644 --- a/tests/test_array.h +++ b/tests/test_array.h @@ -39,6 +39,7 @@ #include "core/variant/container_type_validate.h" #include "core/variant/variant.h" #include "tests/test_macros.h" +#include "tests/test_tools.h" namespace TestArray { @@ -170,6 +171,56 @@ TEST_CASE("[Array] push_front(), pop_front(), pop_back()") { CHECK(arr.size() == 2); } +TEST_CASE("[Array] pop_at()") { + ErrorDetector ed; + + Array arr; + arr.push_back(2); + arr.push_back(4); + arr.push_back(6); + arr.push_back(8); + arr.push_back(10); + + REQUIRE(int(arr.pop_at(2)) == 6); + REQUIRE(arr.size() == 4); + CHECK(int(arr[0]) == 2); + CHECK(int(arr[1]) == 4); + CHECK(int(arr[2]) == 8); + CHECK(int(arr[3]) == 10); + + REQUIRE(int(arr.pop_at(2)) == 8); + REQUIRE(arr.size() == 3); + CHECK(int(arr[0]) == 2); + CHECK(int(arr[1]) == 4); + CHECK(int(arr[2]) == 10); + + // Negative index. + REQUIRE(int(arr.pop_at(-1)) == 10); + REQUIRE(arr.size() == 2); + CHECK(int(arr[0]) == 2); + CHECK(int(arr[1]) == 4); + + // Invalid pop. + ed.clear(); + ERR_PRINT_OFF; + const Variant ret = arr.pop_at(-15); + ERR_PRINT_ON; + REQUIRE(ret.is_null()); + CHECK(ed.has_error); + + REQUIRE(int(arr.pop_at(0)) == 2); + REQUIRE(arr.size() == 1); + CHECK(int(arr[0]) == 4); + + REQUIRE(int(arr.pop_at(0)) == 4); + REQUIRE(arr.is_empty()); + + // Pop from empty array. + ed.clear(); + REQUIRE(arr.pop_at(24).is_null()); + CHECK_FALSE(ed.has_error); +} + TEST_CASE("[Array] max() and min()") { Array arr; arr.push_back(3); diff --git a/tests/test_astar.h b/tests/test_astar.h index 12664a5ff1..137c477946 100644 --- a/tests/test_astar.h +++ b/tests/test_astar.h @@ -63,7 +63,7 @@ public: } // Disable heuristic completely. - float _compute_cost(int p_from, int p_to) { + real_t _compute_cost(int p_from, int p_to) { if (p_from == A && p_to == C) { return 1000; } diff --git a/tests/test_class_db.h b/tests/test_class_db.h index 29edf5a4a0..20397bb144 100644 --- a/tests/test_class_db.h +++ b/tests/test_class_db.h @@ -108,9 +108,9 @@ struct ExposedClass { List<SignalData> signals_; const PropertyData *find_property_by_name(const StringName &p_name) const { - for (const List<PropertyData>::Element *E = properties.front(); E; E = E->next()) { - if (E->get().name == p_name) { - return &E->get(); + for (const PropertyData &E : properties) { + if (E.name == p_name) { + return &E; } } @@ -118,9 +118,9 @@ struct ExposedClass { } const MethodData *find_method_by_name(const StringName &p_name) const { - for (const List<MethodData>::Element *E = methods.front(); E; E = E->next()) { - if (E->get().name == p_name) { - return &E->get(); + for (const MethodData &E : methods) { + if (E.name == p_name) { + return &E; } } @@ -395,8 +395,8 @@ void validate_method(const Context &p_context, const ExposedClass &p_class, cons } } - for (const List<ArgumentData>::Element *F = p_method.arguments.front(); F; F = F->next()) { - const ArgumentData &arg = F->get(); + for (const ArgumentData &F : p_method.arguments) { + const ArgumentData &arg = F; const ExposedClass *arg_class = p_context.find_exposed_class(arg.type); if (arg_class) { @@ -427,8 +427,8 @@ void validate_method(const Context &p_context, const ExposedClass &p_class, cons } void validate_signal(const Context &p_context, const ExposedClass &p_class, const SignalData &p_signal) { - for (const List<ArgumentData>::Element *F = p_signal.arguments.front(); F; F = F->next()) { - const ArgumentData &arg = F->get(); + for (const ArgumentData &F : p_signal.arguments) { + const ArgumentData &arg = F; const ExposedClass *arg_class = p_context.find_exposed_class(arg.type); if (arg_class) { @@ -469,16 +469,16 @@ void validate_class(const Context &p_context, const ExposedClass &p_exposed_clas TEST_FAIL_COND((is_derived_type && !p_context.exposed_classes.has(p_exposed_class.base)), "Base type '", p_exposed_class.base.operator String(), "' does not exist, for class '", p_exposed_class.name, "'."); - for (const List<PropertyData>::Element *F = p_exposed_class.properties.front(); F; F = F->next()) { - validate_property(p_context, p_exposed_class, F->get()); + for (const PropertyData &F : p_exposed_class.properties) { + validate_property(p_context, p_exposed_class, F); } - for (const List<MethodData>::Element *F = p_exposed_class.methods.front(); F; F = F->next()) { - validate_method(p_context, p_exposed_class, F->get()); + for (const MethodData &F : p_exposed_class.methods) { + validate_method(p_context, p_exposed_class, F); } - for (const List<SignalData>::Element *F = p_exposed_class.signals_.front(); F; F = F->next()) { - validate_signal(p_context, p_exposed_class, F->get()); + for (const SignalData &F : p_exposed_class.signals_) { + validate_signal(p_context, p_exposed_class, F); } } @@ -526,10 +526,8 @@ void add_exposed_classes(Context &r_context) { Map<StringName, StringName> accessor_methods; - for (const List<PropertyInfo>::Element *E = property_list.front(); E; E = E->next()) { - const PropertyInfo &property = E->get(); - - if (property.usage & PROPERTY_USAGE_GROUP || property.usage & PROPERTY_USAGE_SUBGROUP || property.usage & PROPERTY_USAGE_CATEGORY) { + 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; } @@ -561,8 +559,8 @@ void add_exposed_classes(Context &r_context) { ClassDB::get_method_list(class_name, &method_list, true); method_list.sort(); - for (List<MethodInfo>::Element *E = method_list.front(); E; E = E->next()) { - const MethodInfo &method_info = E->get(); + for (const MethodInfo &E : method_list) { + const MethodInfo &method_info = E; int argc = method_info.arguments.size(); @@ -667,8 +665,8 @@ void add_exposed_classes(Context &r_context) { // 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 List<PropertyData>::Element *F = exposed_class.properties.front(); F; F = F->next()) { - const PropertyData &prop = F->get(); + 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); @@ -752,8 +750,8 @@ void add_exposed_classes(Context &r_context) { enum_.name = *k; const List<StringName> &enum_constants = enum_map.get(*k); - for (const List<StringName>::Element *E = enum_constants.front(); E; E = E->next()) { - const StringName &constant_name = E->get(); + for (const StringName &E : enum_constants) { + const StringName &constant_name = E; TEST_FAIL_COND(String(constant_name).find("::") != -1, "Enum constant contains '::', check bindings to remove the scope: '", String(class_name), ".", String(enum_.name), ".", String(constant_name), "'."); @@ -774,12 +772,12 @@ void add_exposed_classes(Context &r_context) { r_context.enum_types.push_back(String(class_name) + "." + String(*k)); } - for (const List<String>::Element *E = constants.front(); E; E = E->next()) { - const String &constant_name = E->get(); + for (const String &E : constants) { + const String &constant_name = E; TEST_FAIL_COND(constant_name.find("::") != -1, "Constant contains '::', check bindings to remove the scope: '", String(class_name), ".", constant_name, "'."); - int *value = class_info->constant_map.getptr(StringName(E->get())); + int *value = class_info->constant_map.getptr(StringName(E)); TEST_FAIL_COND(!value, "Missing constant value: '", String(class_name), ".", String(constant_name), "'."); ConstantData constant; @@ -829,8 +827,8 @@ void add_global_enums(Context &r_context) { } } - for (List<EnumData>::Element *E = r_context.global_enums.front(); E; E = E->next()) { - r_context.enum_types.push_back(E->get().name); + for (const EnumData &E : r_context.global_enums) { + r_context.enum_types.push_back(E.name); } } @@ -840,10 +838,10 @@ void add_global_enums(Context &r_context) { hardcoded_enums.push_back("Vector2i.Axis"); hardcoded_enums.push_back("Vector3.Axis"); hardcoded_enums.push_back("Vector3i.Axis"); - for (List<StringName>::Element *E = hardcoded_enums.front(); E; E = E->next()) { + for (const StringName &E : hardcoded_enums) { // These enums are not generated and must be written manually (e.g.: Vector3.Axis) // Here, we assume core types do not begin with underscore - r_context.enum_types.push_back(E->get()); + r_context.enum_types.push_back(E); } } diff --git a/tests/test_code_edit.h b/tests/test_code_edit.h new file mode 100644 index 0000000000..62235ed0ae --- /dev/null +++ b/tests/test_code_edit.h @@ -0,0 +1,3190 @@ +/*************************************************************************/ +/* test_code_edit.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_CODE_EDIT_H +#define TEST_CODE_EDIT_H + +#include "core/input/input_map.h" +#include "core/object/message_queue.h" +#include "core/os/keyboard.h" +#include "core/string/string_builder.h" +#include "scene/gui/code_edit.h" +#include "scene/resources/default_theme/default_theme.h" + +#include "tests/test_macros.h" + +namespace TestCodeEdit { + +TEST_CASE("[SceneTree][CodeEdit] line gutters") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + SUBCASE("[CodeEdit] breakpoints") { + SIGNAL_WATCH(code_edit, "breakpoint_toggled"); + + SUBCASE("[CodeEdit] draw breakpoints gutter") { + code_edit->set_draw_breakpoints_gutter(false); + CHECK_FALSE(code_edit->is_drawing_breakpoints_gutter()); + + code_edit->set_draw_breakpoints_gutter(true); + CHECK(code_edit->is_drawing_breakpoints_gutter()); + } + + SUBCASE("[CodeEdit] set line as breakpoint") { + /* Out of bounds. */ + ERR_PRINT_OFF; + + code_edit->set_line_as_breakpoint(-1, true); + CHECK_FALSE(code_edit->is_line_breakpointed(-1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + code_edit->set_line_as_breakpoint(1, true); + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + ERR_PRINT_ON; + + Array arg1; + arg1.push_back(0); + Array args; + args.push_back(arg1); + + code_edit->set_line_as_breakpoint(0, true); + CHECK(code_edit->is_line_breakpointed(0)); + CHECK(code_edit->get_breakpointed_lines()[0] == Variant(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->set_line_as_breakpoint(0, false); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] clear breakpointed lines") { + code_edit->clear_breakpointed_lines(); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + Array arg1; + arg1.push_back(0); + Array args; + args.push_back(arg1); + + code_edit->set_line_as_breakpoint(0, true); + CHECK(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->clear_breakpointed_lines(); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and set text") { + Array arg1; + arg1.push_back(0); + Array args; + args.push_back(arg1); + + code_edit->set_text("test\nline"); + code_edit->set_line_as_breakpoint(0, true); + CHECK(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* breakpoint on lines that still exist are kept. */ + code_edit->set_text(""); + MessageQueue::get_singleton()->flush(); + CHECK(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + /* breakpoint on lines that are removed should also be removed. */ + code_edit->clear_breakpointed_lines(); + SIGNAL_DISCARD("breakpoint_toggled") + + ((Array)args[0])[0] = 1; + code_edit->set_text("test\nline"); + code_edit->set_line_as_breakpoint(1, true); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->set_text(""); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + ERR_PRINT_ON; + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and clear") { + Array arg1; + arg1.push_back(0); + Array args; + args.push_back(arg1); + + code_edit->set_text("test\nline"); + code_edit->set_line_as_breakpoint(0, true); + CHECK(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* breakpoint on lines that still exist are removed. */ + code_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* breakpoint on lines that are removed should also be removed. */ + code_edit->clear_breakpointed_lines(); + SIGNAL_DISCARD("breakpoint_toggled") + + ((Array)args[0])[0] = 1; + code_edit->set_text("test\nline"); + code_edit->set_line_as_breakpoint(1, true); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + ERR_PRINT_ON; + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and new lines no text") { + Array arg1; + arg1.push_back(0); + Array args; + args.push_back(arg1); + + /* No text moves breakpoint. */ + code_edit->set_line_as_breakpoint(0, true); + CHECK(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Normal. */ + ((Array)args[0])[0] = 0; + Array arg2; + arg2.push_back(1); + args.push_back(arg2); + + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line_count() == 2); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Non-Breaking. */ + ((Array)args[0])[0] = 1; + ((Array)args[1])[0] = 2; + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line_count() == 3); + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + CHECK(code_edit->is_line_breakpointed(2)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Above. */ + ((Array)args[0])[0] = 2; + ((Array)args[1])[0] = 3; + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line_count() == 4); + CHECK_FALSE(code_edit->is_line_breakpointed(2)); + CHECK(code_edit->is_line_breakpointed(3)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and new lines with text") { + Array arg1; + arg1.push_back(0); + Array args; + args.push_back(arg1); + + /* Having text does not move breakpoint. */ + code_edit->insert_text_at_caret("text"); + code_edit->set_line_as_breakpoint(0, true); + CHECK(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Normal. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line_count() == 2); + CHECK(code_edit->is_line_breakpointed(0)); + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + /* Non-Breaking. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line_count() == 3); + CHECK(code_edit->is_line_breakpointed(0)); + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + /* Above does move. */ + ((Array)args[0])[0] = 0; + Array arg2; + arg2.push_back(1); + args.push_back(arg2); + + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line_count() == 4); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and backspace") { + Array arg1; + arg1.push_back(1); + Array args; + args.push_back(arg1); + + code_edit->set_text("\n\n"); + code_edit->set_line_as_breakpoint(1, true); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->set_caret_line(2); + + /* backspace onto line does not remove breakpoint */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + /* backspace on breakpointed line removes it */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + ERR_PRINT_ON; + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Backspace above breakpointed line moves it. */ + ((Array)args[0])[0] = 2; + + code_edit->set_text("\n\n"); + code_edit->set_line_as_breakpoint(2, true); + CHECK(code_edit->is_line_breakpointed(2)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->set_caret_line(1); + + Array arg2; + arg2.push_back(1); + args.push_back(arg2); + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(2)); + ERR_PRINT_ON; + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and delete") { + Array arg1; + arg1.push_back(1); + Array args; + args.push_back(arg1); + + code_edit->set_text("\n\n"); + code_edit->set_line_as_breakpoint(1, true); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + code_edit->set_caret_line(1); + + /* Delete onto breakpointed lines does not remove it. */ + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + CHECK(code_edit->get_line_count() == 2); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + /* Delete moving breakpointed line up removes it. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + CHECK(code_edit->get_line_count() == 1); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + ERR_PRINT_ON; + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Delete above breakpointed line moves it. */ + ((Array)args[0])[0] = 2; + + code_edit->set_text("\n\n"); + code_edit->set_line_as_breakpoint(2, true); + CHECK(code_edit->is_line_breakpointed(2)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->set_caret_line(0); + + Array arg2; + arg2.push_back(1); + args.push_back(arg2); + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(2)); + ERR_PRINT_ON; + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and delete selection") { + Array arg1; + arg1.push_back(1); + Array args; + args.push_back(arg1); + + code_edit->set_text("\n\n"); + code_edit->set_line_as_breakpoint(1, true); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->select(0, 0, 2, 0); + code_edit->delete_selection(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Should handle breakpoint move when deleting selection by adding less text then removed. */ + ((Array)args[0])[0] = 9; + + code_edit->set_text("\n\n\n\n\n\n\n\n\n"); + code_edit->set_line_as_breakpoint(9, true); + CHECK(code_edit->is_line_breakpointed(9)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->select(0, 0, 6, 0); + + Array arg2; + arg2.push_back(4); + args.push_back(arg2); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(9)); + ERR_PRINT_ON; + CHECK(code_edit->is_line_breakpointed(4)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Should handle breakpoint move when deleting selection by adding more text then removed. */ + ((Array)args[0])[0] = 9; + ((Array)args[1])[0] = 14; + + code_edit->insert_text_at_caret("\n\n\n\n\n"); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("breakpoint_toggled") + CHECK(code_edit->is_line_breakpointed(9)); + + code_edit->select(0, 0, 6, 0); + code_edit->insert_text_at_caret("\n\n\n\n\n\n\n\n\n\n\n"); + MessageQueue::get_singleton()->flush(); + CHECK(code_edit->is_line_breakpointed(14)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and undo") { + Array arg1; + arg1.push_back(1); + Array args; + args.push_back(arg1); + + code_edit->set_text("\n\n"); + code_edit->set_line_as_breakpoint(1, true); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->select(0, 0, 2, 0); + code_edit->delete_selection(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Undo does not restore breakpoint. */ + code_edit->undo(); + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + } + + SIGNAL_UNWATCH(code_edit, "breakpoint_toggled"); + } + + SUBCASE("[CodeEdit] bookmarks") { + SUBCASE("[CodeEdit] draw bookmarks gutter") { + code_edit->set_draw_bookmarks_gutter(false); + CHECK_FALSE(code_edit->is_drawing_bookmarks_gutter()); + + code_edit->set_draw_bookmarks_gutter(true); + CHECK(code_edit->is_drawing_bookmarks_gutter()); + } + + SUBCASE("[CodeEdit] set line as bookmarks") { + /* Out of bounds. */ + ERR_PRINT_OFF; + + code_edit->set_line_as_bookmarked(-1, true); + CHECK_FALSE(code_edit->is_line_bookmarked(-1)); + + code_edit->set_line_as_bookmarked(1, true); + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + + ERR_PRINT_ON; + + code_edit->set_line_as_bookmarked(0, true); + CHECK(code_edit->get_bookmarked_lines()[0] == Variant(0)); + CHECK(code_edit->is_line_bookmarked(0)); + + code_edit->set_line_as_bookmarked(0, false); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + } + + SUBCASE("[CodeEdit] clear bookmarked lines") { + code_edit->clear_bookmarked_lines(); + + code_edit->set_line_as_bookmarked(0, true); + CHECK(code_edit->is_line_bookmarked(0)); + + code_edit->clear_bookmarked_lines(); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + } + + SUBCASE("[CodeEdit] bookmarks and set text") { + code_edit->set_text("test\nline"); + code_edit->set_line_as_bookmarked(0, true); + CHECK(code_edit->is_line_bookmarked(0)); + + /* bookmarks on lines that still exist are kept. */ + code_edit->set_text(""); + MessageQueue::get_singleton()->flush(); + CHECK(code_edit->is_line_bookmarked(0)); + + /* bookmarks on lines that are removed should also be removed. */ + code_edit->clear_bookmarked_lines(); + + code_edit->set_text("test\nline"); + code_edit->set_line_as_bookmarked(1, true); + CHECK(code_edit->is_line_bookmarked(1)); + + code_edit->set_text(""); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] bookmarks and clear") { + code_edit->set_text("test\nline"); + code_edit->set_line_as_bookmarked(0, true); + CHECK(code_edit->is_line_bookmarked(0)); + + /* bookmarks on lines that still exist are removed. */ + code_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + + /* bookmarks on lines that are removed should also be removed. */ + code_edit->clear_bookmarked_lines(); + + code_edit->set_text("test\nline"); + code_edit->set_line_as_bookmarked(1, true); + CHECK(code_edit->is_line_bookmarked(1)); + + code_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] bookmarks and new lines no text") { + /* No text moves bookmarks. */ + code_edit->set_line_as_bookmarked(0, true); + CHECK(code_edit->is_line_bookmarked(0)); + + /* Normal. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line_count() == 2); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + CHECK(code_edit->is_line_bookmarked(1)); + + /* Non-Breaking. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line_count() == 3); + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + CHECK(code_edit->is_line_bookmarked(2)); + + /* Above. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line_count() == 4); + CHECK_FALSE(code_edit->is_line_bookmarked(2)); + CHECK(code_edit->is_line_bookmarked(3)); + } + + SUBCASE("[CodeEdit] bookmarks and new lines with text") { + /* Having text does not move bookmark. */ + code_edit->insert_text_at_caret("text"); + code_edit->set_line_as_bookmarked(0, true); + CHECK(code_edit->is_line_bookmarked(0)); + + /* Normal. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line_count() == 2); + CHECK(code_edit->is_line_bookmarked(0)); + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + + /* Non-Breaking. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line_count() == 3); + CHECK(code_edit->is_line_bookmarked(0)); + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + + /* Above does move. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line_count() == 4); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + CHECK(code_edit->is_line_bookmarked(1)); + } + + SUBCASE("[CodeEdit] bookmarks and backspace") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_bookmarked(1, true); + CHECK(code_edit->is_line_bookmarked(1)); + + code_edit->set_caret_line(2); + + /* backspace onto line does not remove bookmark */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK(code_edit->is_line_bookmarked(1)); + + /* backspace on bookmarked line removes it */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] bookmarks and delete") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_bookmarked(1, true); + CHECK(code_edit->is_line_bookmarked(1)); + code_edit->set_caret_line(1); + + /* Delete onto bookmarked lines does not remove it. */ + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + CHECK(code_edit->get_line_count() == 2); + CHECK(code_edit->is_line_bookmarked(1)); + + /* Delete moving bookmarked line up removes it. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + CHECK(code_edit->get_line_count() == 1); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] bookmarks and delete selection") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_bookmarked(1, true); + CHECK(code_edit->is_line_bookmarked(1)); + + code_edit->select(0, 0, 2, 0); + code_edit->delete_selection(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + } + + SUBCASE("[CodeEdit] bookmarks and undo") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_bookmarked(1, true); + CHECK(code_edit->is_line_bookmarked(1)); + + code_edit->select(0, 0, 2, 0); + code_edit->delete_selection(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + + /* Undo does not restore bookmark. */ + code_edit->undo(); + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + } + } + + SUBCASE("[CodeEdit] executing lines") { + SUBCASE("[CodeEdit] draw executing lines gutter") { + code_edit->set_draw_executing_lines_gutter(false); + CHECK_FALSE(code_edit->is_drawing_executing_lines_gutter()); + + code_edit->set_draw_executing_lines_gutter(true); + CHECK(code_edit->is_drawing_executing_lines_gutter()); + } + + SUBCASE("[CodeEdit] set line as executing lines") { + /* Out of bounds. */ + ERR_PRINT_OFF; + + code_edit->set_line_as_executing(-1, true); + CHECK_FALSE(code_edit->is_line_executing(-1)); + + code_edit->set_line_as_executing(1, true); + CHECK_FALSE(code_edit->is_line_executing(1)); + + ERR_PRINT_ON; + + code_edit->set_line_as_executing(0, true); + CHECK(code_edit->get_executing_lines()[0] == Variant(0)); + CHECK(code_edit->is_line_executing(0)); + + code_edit->set_line_as_executing(0, false); + CHECK_FALSE(code_edit->is_line_executing(0)); + } + + SUBCASE("[CodeEdit] clear executing lines lines") { + code_edit->clear_executing_lines(); + + code_edit->set_line_as_executing(0, true); + CHECK(code_edit->is_line_executing(0)); + + code_edit->clear_executing_lines(); + CHECK_FALSE(code_edit->is_line_executing(0)); + } + + SUBCASE("[CodeEdit] executing lines and set text") { + code_edit->set_text("test\nline"); + code_edit->set_line_as_executing(0, true); + CHECK(code_edit->is_line_executing(0)); + + /* executing on lines that still exist are kept. */ + code_edit->set_text(""); + MessageQueue::get_singleton()->flush(); + CHECK(code_edit->is_line_executing(0)); + + /* executing on lines that are removed should also be removed. */ + code_edit->clear_executing_lines(); + + code_edit->set_text("test\nline"); + code_edit->set_line_as_executing(1, true); + CHECK(code_edit->is_line_executing(1)); + + code_edit->set_text(""); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_executing(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_executing(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] executing lines and clear") { + code_edit->set_text("test\nline"); + code_edit->set_line_as_executing(0, true); + CHECK(code_edit->is_line_executing(0)); + + /* executing on lines that still exist are removed. */ + code_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_executing(0)); + + /* executing on lines that are removed should also be removed. */ + code_edit->clear_executing_lines(); + + code_edit->set_text("test\nline"); + code_edit->set_line_as_executing(1, true); + CHECK(code_edit->is_line_executing(1)); + + code_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_executing(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_executing(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] executing lines and new lines no text") { + /* No text moves executing lines. */ + code_edit->set_line_as_executing(0, true); + CHECK(code_edit->is_line_executing(0)); + + /* Normal. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line_count() == 2); + CHECK_FALSE(code_edit->is_line_executing(0)); + CHECK(code_edit->is_line_executing(1)); + + /* Non-Breaking. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line_count() == 3); + CHECK_FALSE(code_edit->is_line_executing(1)); + CHECK(code_edit->is_line_executing(2)); + + /* Above. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line_count() == 4); + CHECK_FALSE(code_edit->is_line_executing(2)); + CHECK(code_edit->is_line_executing(3)); + } + + SUBCASE("[CodeEdit] executing lines and new lines with text") { + /* Having text does not move executing lines. */ + code_edit->insert_text_at_caret("text"); + code_edit->set_line_as_executing(0, true); + CHECK(code_edit->is_line_executing(0)); + + /* Normal. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line_count() == 2); + CHECK(code_edit->is_line_executing(0)); + CHECK_FALSE(code_edit->is_line_executing(1)); + + /* Non-Breaking. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line_count() == 3); + CHECK(code_edit->is_line_executing(0)); + CHECK_FALSE(code_edit->is_line_executing(1)); + + /* Above does move. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line_count() == 4); + CHECK_FALSE(code_edit->is_line_executing(0)); + CHECK(code_edit->is_line_executing(1)); + } + + SUBCASE("[CodeEdit] executing lines and backspace") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_executing(1, true); + CHECK(code_edit->is_line_executing(1)); + + code_edit->set_caret_line(2); + + /* backspace onto line does not remove executing lines. */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK(code_edit->is_line_executing(1)); + + /* backspace on executing line removes it */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK_FALSE(code_edit->is_line_executing(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_executing(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] executing lines and delete") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_executing(1, true); + CHECK(code_edit->is_line_executing(1)); + code_edit->set_caret_line(1); + + /* Delete onto executing lines does not remove it. */ + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + CHECK(code_edit->get_line_count() == 2); + CHECK(code_edit->is_line_executing(1)); + + /* Delete moving executing line up removes it. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + CHECK(code_edit->get_line_count() == 1); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_executing(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] executing lines and delete selection") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_executing(1, true); + CHECK(code_edit->is_line_executing(1)); + + code_edit->select(0, 0, 2, 0); + code_edit->delete_selection(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_executing(0)); + } + + SUBCASE("[CodeEdit] executing lines and undo") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_executing(1, true); + CHECK(code_edit->is_line_executing(1)); + + code_edit->select(0, 0, 2, 0); + code_edit->delete_selection(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_executing(0)); + + /* Undo does not restore executing lines. */ + code_edit->undo(); + CHECK_FALSE(code_edit->is_line_executing(1)); + } + } + + SUBCASE("[CodeEdit] line numbers") { + SUBCASE("[CodeEdit] draw line numbers gutter and padding") { + code_edit->set_draw_line_numbers(false); + CHECK_FALSE(code_edit->is_draw_line_numbers_enabled()); + + code_edit->set_draw_line_numbers(true); + CHECK(code_edit->is_draw_line_numbers_enabled()); + + code_edit->set_line_numbers_zero_padded(false); + CHECK_FALSE(code_edit->is_line_numbers_zero_padded()); + + code_edit->set_line_numbers_zero_padded(true); + CHECK(code_edit->is_line_numbers_zero_padded()); + + code_edit->set_line_numbers_zero_padded(false); + CHECK_FALSE(code_edit->is_line_numbers_zero_padded()); + + code_edit->set_draw_line_numbers(false); + CHECK_FALSE(code_edit->is_draw_line_numbers_enabled()); + + code_edit->set_line_numbers_zero_padded(true); + CHECK(code_edit->is_line_numbers_zero_padded()); + } + } + + SUBCASE("[CodeEdit] line folding") { + SUBCASE("[CodeEdit] draw line folding gutter") { + code_edit->set_draw_fold_gutter(false); + CHECK_FALSE(code_edit->is_drawing_fold_gutter()); + + code_edit->set_draw_fold_gutter(true); + CHECK(code_edit->is_drawing_fold_gutter()); + } + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] delimiters") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + const Point2 OUTSIDE_DELIMETER = Point2(-1, -1); + + code_edit->clear_string_delimiters(); + code_edit->clear_comment_delimiters(); + + SUBCASE("[CodeEdit] add and remove delimiters") { + SUBCASE("[CodeEdit] add and remove string delimiters") { + /* Add a delimiter.*/ + code_edit->add_string_delimiter("\"", "\"", false); + CHECK(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + ERR_PRINT_OFF; + + /* Adding a duplicate start key is not allowed. */ + code_edit->add_string_delimiter("\"", "\'", false); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Adding a duplicate end key is allowed. */ + code_edit->add_string_delimiter("'", "\"", false); + CHECK(code_edit->has_string_delimiter("'")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + /* Both start and end keys have to be symbols. */ + code_edit->add_string_delimiter("f", "\"", false); + CHECK_FALSE(code_edit->has_string_delimiter("f")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + code_edit->add_string_delimiter("f", "\"", false); + CHECK_FALSE(code_edit->has_string_delimiter("f")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + code_edit->add_string_delimiter("@", "f", false); + CHECK_FALSE(code_edit->has_string_delimiter("@")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + code_edit->add_string_delimiter("f", "f", false); + CHECK_FALSE(code_edit->has_string_delimiter("f")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + /* Blank start keys are not allowed */ + code_edit->add_string_delimiter("", "#", false); + CHECK_FALSE(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + ERR_PRINT_ON; + + /* Blank end keys are allowed. */ + code_edit->add_string_delimiter("#", "", false); + CHECK(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 3); + + /* Remove a delimiter. */ + code_edit->remove_string_delimiter("#"); + CHECK_FALSE(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + /* Set should override existing, and test multiline */ + TypedArray<String> delimiters; + delimiters.push_back("^^ ^^"); + + code_edit->set_string_delimiters(delimiters); + CHECK_FALSE(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->has_string_delimiter("^^")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* clear should remove all. */ + code_edit->clear_string_delimiters(); + CHECK_FALSE(code_edit->has_string_delimiter("^^")); + CHECK(code_edit->get_string_delimiters().size() == 0); + } + + SUBCASE("[CodeEdit] add and remove comment delimiters") { + /* Add a delimiter.*/ + code_edit->add_comment_delimiter("\"", "\"", false); + CHECK(code_edit->has_comment_delimiter("\"")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + ERR_PRINT_OFF; + + /* Adding a duplicate start key is not allowed. */ + code_edit->add_comment_delimiter("\"", "\'", false); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Adding a duplicate end key is allowed. */ + code_edit->add_comment_delimiter("'", "\"", false); + CHECK(code_edit->has_comment_delimiter("'")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + /* Both start and end keys have to be symbols. */ + code_edit->add_comment_delimiter("f", "\"", false); + CHECK_FALSE(code_edit->has_comment_delimiter("f")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + code_edit->add_comment_delimiter("f", "\"", false); + CHECK_FALSE(code_edit->has_comment_delimiter("f")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + code_edit->add_comment_delimiter("@", "f", false); + CHECK_FALSE(code_edit->has_comment_delimiter("@")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + code_edit->add_comment_delimiter("f", "f", false); + CHECK_FALSE(code_edit->has_comment_delimiter("f")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + /* Blank start keys are not allowed. */ + code_edit->add_comment_delimiter("", "#", false); + CHECK_FALSE(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + ERR_PRINT_ON; + + /* Blank end keys are allowed. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 3); + + /* Remove a delimiter. */ + code_edit->remove_comment_delimiter("#"); + CHECK_FALSE(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + /* Set should override existing, and test multiline. */ + TypedArray<String> delimiters; + delimiters.push_back("^^ ^^"); + + code_edit->set_comment_delimiters(delimiters); + CHECK_FALSE(code_edit->has_comment_delimiter("\"")); + CHECK(code_edit->has_comment_delimiter("^^")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* clear should remove all. */ + code_edit->clear_comment_delimiters(); + CHECK_FALSE(code_edit->has_comment_delimiter("^^")); + CHECK(code_edit->get_comment_delimiters().size() == 0); + } + + SUBCASE("[CodeEdit] add and remove mixed delimiters") { + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + ERR_PRINT_OFF; + + /* Disallow adding a string with the same start key as comment. */ + code_edit->add_string_delimiter("#", "", false); + CHECK_FALSE(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 0); + + code_edit->add_string_delimiter("\"", "\"", false); + CHECK(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Disallow adding a comment with the same start key as string. */ + code_edit->add_comment_delimiter("\"", "", false); + CHECK_FALSE(code_edit->has_comment_delimiter("\"")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + ERR_PRINT_ON; + + /* Cannot remove string with remove comment. */ + code_edit->remove_comment_delimiter("\""); + CHECK(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Cannot remove comment with remove string. */ + code_edit->remove_string_delimiter("#"); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Clear comments leave strings. */ + code_edit->clear_comment_delimiters(); + CHECK(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Clear string leave comments. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + code_edit->clear_string_delimiters(); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + } + } + + SUBCASE("[CodeEdit] single line delimiters") { + SUBCASE("[CodeEdit] single line string delimiters") { + /* Blank end key should set lineonly to true. */ + code_edit->add_string_delimiter("#", "", false); + CHECK(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Insert line above, line with string then line below. */ + code_edit->insert_text_at_caret(" \n#\n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after start key is in string and start / end positions are correct. */ + CHECK(code_edit->is_in_string(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(2, 1)); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check region metadata. */ + int idx = code_edit->is_in_string(1, 1); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == ""); + + /* Check nested strings are handeled correctly. */ + code_edit->set_text(" \n# # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before first start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after the first start key is in string and start / end positions are correct. */ + CHECK(code_edit->is_in_string(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1)); + + /* Check column after the second start key returns data for the first. */ + CHECK(code_edit->is_in_string(1, 5) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1)); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check is in string with no column retruns true if entire line is comment excluding whitespace. */ + code_edit->set_text(" \n # # \n "); + CHECK(code_edit->is_in_string(1) != -1); + + code_edit->set_text(" \n text # # \n "); + CHECK(code_edit->is_in_string(1) == -1); + + /* Removing delimiter should update. */ + code_edit->set_text(" \n # # \n "); + + code_edit->remove_string_delimiter("#"); + CHECK_FALSE(code_edit->has_string_delimiter("$")); + CHECK(code_edit->get_string_delimiters().size() == 0); + + CHECK(code_edit->is_in_string(1) == -1); + + /* Adding and clear should update. */ + code_edit->add_string_delimiter("#", "", false); + CHECK(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 1); + CHECK(code_edit->is_in_string(1) != -1); + + code_edit->clear_string_delimiters(); + CHECK_FALSE(code_edit->has_string_delimiter("$")); + CHECK(code_edit->get_string_delimiters().size() == 0); + + CHECK(code_edit->is_in_string(1) == -1); + } + + SUBCASE("[CodeEdit] single line comment delimiters") { + /* Blank end key should set lineonly to true. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Insert line above, line with comment then line below. */ + code_edit->insert_text_at_caret(" \n#\n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after start key is in comment and start / end positions are correct. */ + CHECK(code_edit->is_in_comment(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(2, 1)); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check region metadata. */ + int idx = code_edit->is_in_comment(1, 1); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == ""); + + /* Check nested comments are handeled correctly. */ + code_edit->set_text(" \n# # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before first start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after the first start key is in comment and start / end positions are correct. */ + CHECK(code_edit->is_in_comment(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1)); + + /* Check column after the second start key returns data for the first. */ + CHECK(code_edit->is_in_comment(1, 5) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1)); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check is in comment with no column retruns true if entire line is comment excluding whitespace. */ + code_edit->set_text(" \n # # \n "); + CHECK(code_edit->is_in_comment(1) != -1); + + code_edit->set_text(" \n text # # \n "); + CHECK(code_edit->is_in_comment(1) == -1); + + /* Removing delimiter should update. */ + code_edit->set_text(" \n # # \n "); + + code_edit->remove_comment_delimiter("#"); + CHECK_FALSE(code_edit->has_comment_delimiter("$")); + CHECK(code_edit->get_comment_delimiters().size() == 0); + + CHECK(code_edit->is_in_comment(1) == -1); + + /* Adding and clear should update. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + CHECK(code_edit->is_in_comment(1) != -1); + + code_edit->clear_comment_delimiters(); + CHECK_FALSE(code_edit->has_comment_delimiter("$")); + CHECK(code_edit->get_comment_delimiters().size() == 0); + + CHECK(code_edit->is_in_comment(1) == -1); + } + + SUBCASE("[CodeEdit] single line mixed delimiters") { + /* Blank end key should set lineonly to true. */ + /* Add string delimiter. */ + code_edit->add_string_delimiter("&", "", false); + CHECK(code_edit->has_string_delimiter("&")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Add comment delimiter. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Nest a string delimiter inside a comment. */ + code_edit->set_text(" \n# & \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before first start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after the first start key is in comment and start / end positions are correct. */ + CHECK(code_edit->is_in_comment(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1)); + + /* Check column after the second start key returns data for the first, and does not state string. */ + CHECK(code_edit->is_in_comment(1, 5) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1)); + CHECK(code_edit->is_in_string(1, 5) == -1); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Remove the comment delimiter. */ + code_edit->remove_comment_delimiter("#"); + CHECK_FALSE(code_edit->has_comment_delimiter("$")); + CHECK(code_edit->get_comment_delimiters().size() == 0); + + /* The "first" comment region is no longer valid. */ + CHECK(code_edit->is_in_comment(1, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 1) == OUTSIDE_DELIMETER); + + /* The "second" region as string is now valid. */ + CHECK(code_edit->is_in_string(1, 5) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(4, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1)); + } + } + + SUBCASE("[CodeEdit] multiline delimiters") { + SUBCASE("[CodeEdit] multiline string delimiters") { + code_edit->clear_string_delimiters(); + code_edit->clear_comment_delimiters(); + + /* Add string delimiter. */ + code_edit->add_string_delimiter("#", "#", false); + CHECK(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* First test over a single line. */ + code_edit->set_text(" \n # # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column before closing delimiter is in string. */ + CHECK(code_edit->is_in_string(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(5, 1)); + + /* Check column after end key is not in string. */ + CHECK(code_edit->is_in_string(1, 6) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 6) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 6) == OUTSIDE_DELIMETER); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check the region metadata. */ + int idx = code_edit->is_in_string(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test over a multiple blank lines. */ + code_edit->set_text(" \n # \n\n # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in string. */ + CHECK(code_edit->is_in_string(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3)); + + /* Check blank middle line. */ + CHECK(code_edit->is_in_string(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3)); + + /* Check column just before end key is in string. */ + CHECK(code_edit->is_in_string(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3)); + + /* Check column after end key is not in string. */ + CHECK(code_edit->is_in_string(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* Next test over a multiple non-blank lines. */ + code_edit->set_text(" \n # \n \n # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in string. */ + CHECK(code_edit->is_in_string(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_string(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3)); + + /* Check column just before end key is in string. */ + CHECK(code_edit->is_in_string(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3)); + + /* Check column after end key is not in string. */ + CHECK(code_edit->is_in_string(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + idx = code_edit->is_in_string(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test nested strings. */ + code_edit->add_string_delimiter("^", "^", false); + CHECK(code_edit->has_string_delimiter("^")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + code_edit->set_text(" \n # ^\n \n^ # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in string. */ + CHECK(code_edit->is_in_string(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_string(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3)); + + /* Check column just before end key is in string. */ + CHECK(code_edit->is_in_string(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3)); + + /* Check column after end key is not in string. */ + CHECK(code_edit->is_in_string(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + idx = code_edit->is_in_string(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test no end key. */ + code_edit->set_text(" \n # \n "); + + /* check the region metadata. */ + idx = code_edit->is_in_string(1, 2); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(-1, -1)); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Check is in string with no column retruns true if entire line is string excluding whitespace. */ + code_edit->set_text(" \n # \n\n #\n "); + CHECK(code_edit->is_in_string(1) != -1); + CHECK(code_edit->is_in_string(2) != -1); + CHECK(code_edit->is_in_string(3) != -1); + + code_edit->set_text(" \n test # \n\n # test \n "); + CHECK(code_edit->is_in_string(1) == -1); + CHECK(code_edit->is_in_string(2) != -1); + CHECK(code_edit->is_in_string(3) == -1); + } + + SUBCASE("[CodeEdit] multiline comment delimiters") { + /* Add comment delimiter. */ + code_edit->add_comment_delimiter("#", "#", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* First test over a single line. */ + code_edit->set_text(" \n # # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column before closing delimiter is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(5, 1)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 6) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 6) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 6) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check the region metadata. */ + int idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test over a multiple blank lines. */ + code_edit->set_text(" \n # \n\n # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3)); + + /* Check blank middle line. */ + CHECK(code_edit->is_in_comment(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3)); + + /* Check column just before end key is in comment. */ + CHECK(code_edit->is_in_comment(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* Next test over a multiple non-blank lines. */ + code_edit->set_text(" \n # \n \n # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_comment(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3)); + + /* Check column just before end key is in comment. */ + CHECK(code_edit->is_in_comment(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test nested comments. */ + code_edit->add_comment_delimiter("^", "^", false); + CHECK(code_edit->has_comment_delimiter("^")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + code_edit->set_text(" \n # ^\n \n^ # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_comment(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3)); + + /* Check column just before end key is in comment. */ + CHECK(code_edit->is_in_comment(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test no end key. */ + code_edit->set_text(" \n # \n "); + + /* check the region metadata. */ + idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(-1, -1)); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Check is in comment with no column retruns true if entire line is comment excluding whitespace. */ + code_edit->set_text(" \n # \n\n #\n "); + CHECK(code_edit->is_in_comment(1) != -1); + CHECK(code_edit->is_in_comment(2) != -1); + CHECK(code_edit->is_in_comment(3) != -1); + + code_edit->set_text(" \n test # \n\n # test \n "); + CHECK(code_edit->is_in_comment(1) == -1); + CHECK(code_edit->is_in_comment(2) != -1); + CHECK(code_edit->is_in_comment(3) == -1); + } + + SUBCASE("[CodeEdit] multiline mixed delimiters") { + /* Add comment delimiter. */ + code_edit->add_comment_delimiter("#", "#", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Add string delimiter. */ + code_edit->add_string_delimiter("^", "^", false); + CHECK(code_edit->has_string_delimiter("^")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Nest a string inside a comment. */ + code_edit->set_text(" \n # ^\n \n^ # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_comment(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3)); + + /* Check column just before end key is in comment. */ + CHECK(code_edit->is_in_comment(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + int idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Check is in comment with no column retruns true as inner delimiter should not be counted. */ + CHECK(code_edit->is_in_comment(1) != -1); + CHECK(code_edit->is_in_comment(2) != -1); + CHECK(code_edit->is_in_comment(3) != -1); + } + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] indent") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + SUBCASE("[CodeEdit] indent settings") { + code_edit->set_indent_size(10); + CHECK(code_edit->get_indent_size() == 10); + CHECK(code_edit->get_tab_size() == 10); + + code_edit->set_auto_indent_enabled(false); + CHECK_FALSE(code_edit->is_auto_indent_enabled()); + + code_edit->set_auto_indent_enabled(true); + CHECK(code_edit->is_auto_indent_enabled()); + + code_edit->set_indent_using_spaces(false); + CHECK_FALSE(code_edit->is_indent_using_spaces()); + + code_edit->set_indent_using_spaces(true); + CHECK(code_edit->is_indent_using_spaces()); + + /* Only the first char is registered. */ + TypedArray<String> auto_indent_prefixes; + auto_indent_prefixes.push_back("::"); + auto_indent_prefixes.push_back("s"); + auto_indent_prefixes.push_back("1"); + code_edit->set_auto_indent_prefixes(auto_indent_prefixes); + + auto_indent_prefixes = code_edit->get_auto_indent_prefixes(); + CHECK(auto_indent_prefixes.has(":")); + CHECK(auto_indent_prefixes.has("s")); + CHECK(auto_indent_prefixes.has("1")); + } + + SUBCASE("[CodeEdit] indent tabs") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(false); + + /* Do nothing if not editable. */ + code_edit->set_editable(false); + + code_edit->do_indent(); + CHECK(code_edit->get_line(0).is_empty()); + + code_edit->indent_lines(); + CHECK(code_edit->get_line(0).is_empty()); + + code_edit->set_editable(true); + + /* Simple indent. */ + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\t"); + + /* Check input action. */ + SEND_GUI_ACTION(code_edit, "ui_text_indent"); + CHECK(code_edit->get_line(0) == "\t\t"); + + /* Insert in place. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test"); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "test\t"); + + /* Indent lines does entire line and works without selection. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test"); + code_edit->indent_lines(); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Selection does entire line. */ + code_edit->set_text("test"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Handles multiple lines. */ + code_edit->set_text("test\ntext"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "\ttext"); + + /* Do not indent line if last col is zero. */ + code_edit->set_text("test\ntext"); + code_edit->select(0, 0, 1, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "text"); + + /* Indent even if last column of first line. */ + code_edit->set_text("test\ntext"); + code_edit->select(0, 4, 1, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "text"); + + /* Check selection is adjusted. */ + code_edit->set_text("test"); + code_edit->select(0, 1, 0, 2); + code_edit->do_indent(); + CHECK(code_edit->get_selection_from_column() == 2); + CHECK(code_edit->get_selection_to_column() == 3); + CHECK(code_edit->get_line(0) == "\ttest"); + code_edit->undo(); + } + + SUBCASE("[CodeEdit] indent spaces") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(true); + + /* Do nothing if not editable. */ + code_edit->set_editable(false); + + code_edit->do_indent(); + CHECK(code_edit->get_line(0).is_empty()); + + code_edit->indent_lines(); + CHECK(code_edit->get_line(0).is_empty()); + + code_edit->set_editable(true); + + /* Simple indent. */ + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " "); + + /* Check input action. */ + SEND_GUI_ACTION(code_edit, "ui_text_indent"); + CHECK(code_edit->get_line(0) == " "); + + /* Insert in place. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test"); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "test "); + + /* Indent lines does entire line and works without selection. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test"); + code_edit->indent_lines(); + CHECK(code_edit->get_line(0) == " test"); + + /* Selection does entire line. */ + code_edit->set_text("test"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + + /* single indent only add required spaces. */ + code_edit->set_text(" test"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + + /* Handles multiple lines. */ + code_edit->set_text("test\ntext"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == " text"); + + /* Do not indent line if last col is zero. */ + code_edit->set_text("test\ntext"); + code_edit->select(0, 0, 1, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Indent even if last column of first line. */ + code_edit->set_text("test\ntext"); + code_edit->select(0, 4, 1, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Check selection is adjusted. */ + code_edit->set_text("test"); + code_edit->select(0, 1, 0, 2); + code_edit->do_indent(); + CHECK(code_edit->get_selection_from_column() == 5); + CHECK(code_edit->get_selection_to_column() == 6); + CHECK(code_edit->get_line(0) == " test"); + } + + SUBCASE("[CodeEdit] unindent tabs") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(false); + + /* Do nothing if not editable. */ + code_edit->set_text("\t"); + + code_edit->set_editable(false); + + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "\t"); + + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "\t"); + + code_edit->set_editable(true); + + /* Simple unindent. */ + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == ""); + + /* Should inindent inplace. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test\t"); + + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + + /* Backspace does a simple unindent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("\t"); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == ""); + + /* Unindent lines does entire line and works without selection. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("\ttest"); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + + /* Caret on col zero unindent line. */ + code_edit->set_text("\t\ttest"); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Check input action. */ + code_edit->set_text("\t\ttest"); + SEND_GUI_ACTION(code_edit, "ui_text_dedent"); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Selection does entire line. */ + code_edit->set_text("\t\ttest"); + code_edit->select_all(); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Handles multiple lines. */ + code_edit->set_text("\ttest\n\ttext"); + code_edit->select_all(); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Do not unindent line if last col is zero. */ + code_edit->set_text("\ttest\n\ttext"); + code_edit->select(0, 0, 1, 0); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "\ttext"); + + /* Unindent even if last column of first line. */ + code_edit->set_text("\ttest\n\ttext"); + code_edit->select(0, 5, 1, 1); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Check selection is adjusted. */ + code_edit->set_text("\ttest"); + code_edit->select(0, 1, 0, 2); + code_edit->do_unindent(); + CHECK(code_edit->get_selection_from_column() == 0); + CHECK(code_edit->get_selection_to_column() == 1); + CHECK(code_edit->get_line(0) == "test"); + } + + SUBCASE("[CodeEdit] unindent spaces") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(true); + + /* Do nothing if not editable. */ + code_edit->set_text(" "); + + code_edit->set_editable(false); + + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == " "); + + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == " "); + + code_edit->set_editable(true); + + /* Simple unindent. */ + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == ""); + + /* Should inindent inplace. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test "); + + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + + /* Backspace does a simple unindent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret(" "); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == ""); + + /* Backspace with letter. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret(" a"); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == " "); + + /* Unindent lines does entire line and works without selection. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret(" test"); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + + /* Caret on col zero unindent line. */ + code_edit->set_text(" test"); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == " test"); + + /* Only as far as needed */ + code_edit->set_text(" test"); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == " test"); + + /* Check input action. */ + code_edit->set_text(" test"); + SEND_GUI_ACTION(code_edit, "ui_text_dedent"); + CHECK(code_edit->get_line(0) == " test"); + + /* Selection does entire line. */ + code_edit->set_text(" test"); + code_edit->select_all(); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == " test"); + + /* Handles multiple lines. */ + code_edit->set_text(" test\n text"); + code_edit->select_all(); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Do not unindent line if last col is zero. */ + code_edit->set_text(" test\n text"); + code_edit->select(0, 0, 1, 0); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == " text"); + + /* Unindent even if last column of first line. */ + code_edit->set_text(" test\n text"); + code_edit->select(0, 5, 1, 1); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Check selection is adjusted. */ + code_edit->set_text(" test"); + code_edit->select(0, 4, 0, 5); + code_edit->do_unindent(); + CHECK(code_edit->get_selection_from_column() == 0); + CHECK(code_edit->get_selection_to_column() == 1); + CHECK(code_edit->get_line(0) == "test"); + } + + SUBCASE("[CodeEdit] auto indent") { + SUBCASE("[CodeEdit] auto indent tabs") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(false); + + /* Simple indent on new line. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test:"); + CHECK(code_edit->get_line(1) == "\t"); + + /* new blank line should still indent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test:"); + CHECK(code_edit->get_line(1) == "\t"); + + /* new line above should not indent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test:"); + + /* Whitespace between symbol and caret is okay. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: "); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: "); + CHECK(code_edit->get_line(1) == "\t"); + + /* Comment between symbol and caret is okay. */ + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: # comment"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: # comment"); + CHECK(code_edit->get_line(1) == "\t"); + code_edit->remove_comment_delimiter("#"); + + /* Strings between symbol and caret are not okay. */ + code_edit->add_string_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: # string"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: # string"); + CHECK(code_edit->get_line(1) == ""); + code_edit->remove_comment_delimiter("#"); + + /* If between brace pairs an extra line is added. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test{"); + CHECK(code_edit->get_line(1) == "\t"); + CHECK(code_edit->get_line(2) == "}"); + + /* Except when we are going above. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test{}"); + + /* or below. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test{}"); + CHECK(code_edit->get_line(1) == ""); + } + + SUBCASE("[CodeEdit] auto indent spaces") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(true); + + /* Simple indent on new line. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test:"); + CHECK(code_edit->get_line(1) == " "); + + /* new blank line should still indent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test:"); + CHECK(code_edit->get_line(1) == " "); + + /* new line above should not indent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test:"); + + /* Whitespace between symbol and caret is okay. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: "); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: "); + CHECK(code_edit->get_line(1) == " "); + + /* Comment between symbol and caret is okay. */ + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: # comment"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: # comment"); + CHECK(code_edit->get_line(1) == " "); + code_edit->remove_comment_delimiter("#"); + + /* Strings between symbol and caret are not okay. */ + code_edit->add_string_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: # string"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: # string"); + CHECK(code_edit->get_line(1) == ""); + code_edit->remove_comment_delimiter("#"); + + /* If between brace pairs an extra line is added. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test{"); + CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_line(2) == "}"); + + /* Except when we are going above. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test{}"); + + /* or below. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test{}"); + CHECK(code_edit->get_line(1) == ""); + } + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] folding") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + SUBCASE("[CodeEdit] folding settings") { + code_edit->set_line_folding_enabled(true); + CHECK(code_edit->is_line_folding_enabled()); + + code_edit->set_line_folding_enabled(false); + CHECK_FALSE(code_edit->is_line_folding_enabled()); + } + + SUBCASE("[CodeEdit] folding") { + code_edit->set_line_folding_enabled(true); + + // No indent. + code_edit->set_text("line1\nline2\nline3"); + for (int i = 0; i < 2; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Indented lines. + code_edit->set_text("\tline1\n\tline2\n\tline3"); + for (int i = 0; i < 2; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Indent. + code_edit->set_text("line1\n\tline2\nline3"); + CHECK(code_edit->can_fold_line(0)); + for (int i = 1; i < 2; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2); + + // Nested indents. + code_edit->set_text("line1\n\tline2\n\t\tline3\nline4"); + CHECK(code_edit->can_fold_line(0)); + CHECK(code_edit->can_fold_line(1)); + for (int i = 2; i < 3; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK(code_edit->is_line_folded(1)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK_FALSE(code_edit->is_line_folded(3)); + CHECK(code_edit->get_next_visible_line_offset_from(2, 1) == 2); + + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK_FALSE(code_edit->is_line_folded(3)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3); + + // Check metadata. + CHECK(code_edit->get_folded_lines().size() == 1); + CHECK((int)code_edit->get_folded_lines()[0] == 0); + + // Cannot unfold nested. + code_edit->unfold_line(1); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // (un)Fold all / toggle. + code_edit->unfold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Check metadata. + CHECK(code_edit->get_folded_lines().size() == 0); + + code_edit->fold_all_lines(); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3); + + code_edit->unfold_all_lines(); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->toggle_foldable_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3); + + // Can also unfold from hidden line. + code_edit->unfold_line(1); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Blank lines. + code_edit->set_text("line1\n\tline2\n\n\n\ttest\n\nline3"); + CHECK(code_edit->can_fold_line(0)); + for (int i = 1; i < code_edit->get_line_count(); i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + for (int i = 1; i < code_edit->get_line_count(); i++) { + CHECK_FALSE(code_edit->is_line_folded(i)); + } + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 6); + + // End of file. + code_edit->set_text("line1\n\tline2"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Comment & string blocks. + // Single line block + code_edit->add_comment_delimiter("#", "", true); + code_edit->set_text("#line1\n#\tline2"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Has to be full line. + code_edit->set_text("test #line1\n#\tline2"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->set_text("#line1\ntest #\tline2"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // String. + code_edit->add_string_delimiter("^", "", true); + code_edit->set_text("^line1\n^\tline2"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Has to be full line. + code_edit->set_text("test ^line1\n^\tline2"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->set_text("^line1\ntest ^\tline2"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Multiline blocks. + code_edit->add_comment_delimiter("&", "&", false); + code_edit->set_text("&line1\n\tline2&"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Has to be full line. + code_edit->set_text("test &line1\n\tline2&"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->set_text("&line1\n\tline2& test"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Strings. + code_edit->add_string_delimiter("$", "$", false); + code_edit->set_text("$line1\n\tline2$"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Has to be full line. + code_edit->set_text("test $line1\n\tline2$"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->set_text("$line1\n\tline2$ test"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Non-indented comments/ strings. + // Single line + code_edit->set_text("test\n\tline1\n#line1\n#line2\n\ttest"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + + code_edit->set_text("test\n\tline1\n^line1\n^line2\n\ttest"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + + // Multiline + code_edit->set_text("test\n\tline1\n&line1\nline2&\n\ttest"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + + code_edit->set_text("test\n\tline1\n$line1\nline2$\n\ttest"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] completion") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + SUBCASE("[CodeEdit] auto brace completion") { + code_edit->set_auto_brace_completion_enabled(true); + CHECK(code_edit->is_auto_brace_completion_enabled()); + + code_edit->set_highlight_matching_braces_enabled(true); + CHECK(code_edit->is_highlight_matching_braces_enabled()); + + /* Try setters, any length. */ + Dictionary auto_brace_completion_pairs; + auto_brace_completion_pairs["["] = "]"; + auto_brace_completion_pairs["'"] = "'"; + auto_brace_completion_pairs[";"] = "'"; + auto_brace_completion_pairs["'''"] = "'''"; + code_edit->set_auto_brace_completion_pairs(auto_brace_completion_pairs); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + CHECK(code_edit->get_auto_brace_completion_pairs()["["] == "]"); + CHECK(code_edit->get_auto_brace_completion_pairs()["'"] == "'"); + CHECK(code_edit->get_auto_brace_completion_pairs()[";"] == "'"); + CHECK(code_edit->get_auto_brace_completion_pairs()["'''"] == "'''"); + + ERR_PRINT_OFF; + + /* No duplicate start keys. */ + code_edit->add_auto_brace_completion_pair("[", "]"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + /* No empty keys. */ + code_edit->add_auto_brace_completion_pair("[", ""); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + code_edit->add_auto_brace_completion_pair("", "]"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + code_edit->add_auto_brace_completion_pair("", ""); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + /* Must be a symbol. */ + code_edit->add_auto_brace_completion_pair("a", "]"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + code_edit->add_auto_brace_completion_pair("[", "a"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + code_edit->add_auto_brace_completion_pair("a", "a"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + ERR_PRINT_ON; + + /* Check metadata. */ + CHECK(code_edit->has_auto_brace_completion_open_key("[")); + CHECK(code_edit->has_auto_brace_completion_open_key("'")); + CHECK(code_edit->has_auto_brace_completion_open_key(";")); + CHECK(code_edit->has_auto_brace_completion_open_key("'''")); + CHECK_FALSE(code_edit->has_auto_brace_completion_open_key("(")); + + CHECK(code_edit->has_auto_brace_completion_close_key("]")); + CHECK(code_edit->has_auto_brace_completion_close_key("'")); + CHECK(code_edit->has_auto_brace_completion_close_key("'''")); + CHECK_FALSE(code_edit->has_auto_brace_completion_close_key(")")); + + CHECK(code_edit->get_auto_brace_completion_close_key("[") == "]"); + CHECK(code_edit->get_auto_brace_completion_close_key("'") == "'"); + CHECK(code_edit->get_auto_brace_completion_close_key(";") == "'"); + CHECK(code_edit->get_auto_brace_completion_close_key("'''") == "'''"); + CHECK(code_edit->get_auto_brace_completion_close_key("(").is_empty()); + + /* Check typing inserts closing pair. */ + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, KEY_BRACKETLEFT); + CHECK(code_edit->get_line(0) == "[]"); + + /* Should first match and insert smaller key. */ + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE); + CHECK(code_edit->get_line(0) == "''"); + CHECK(code_edit->get_caret_column() == 1); + + /* Move out from centre, Should match and insert larger key. */ + SEND_GUI_ACTION(code_edit, "ui_text_caret_right"); + SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE); + CHECK(code_edit->get_line(0) == "''''''"); + CHECK(code_edit->get_caret_column() == 3); + + /* Backspace should remove all. */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK(code_edit->get_line(0).is_empty()); + + /* If in between and typing close key should "skip". */ + SEND_GUI_KEY_EVENT(code_edit, KEY_BRACKETLEFT); + CHECK(code_edit->get_line(0) == "[]"); + CHECK(code_edit->get_caret_column() == 1); + SEND_GUI_KEY_EVENT(code_edit, KEY_BRACKETRIGHT); + CHECK(code_edit->get_line(0) == "[]"); + CHECK(code_edit->get_caret_column() == 2); + + /* If current is char and inserting a string, do not autocomplete. */ + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, KEY_A); + SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE); + CHECK(code_edit->get_line(0) == "A'"); + + /* If in comment, do not complete. */ + code_edit->add_comment_delimiter("#", ""); + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, KEY_NUMBERSIGN); + SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE); + CHECK(code_edit->get_line(0) == "#'"); + + /* If in string, and inserting string do not complete. */ + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE); + SEND_GUI_KEY_EVENT(code_edit, KEY_QUOTEDBL); + CHECK(code_edit->get_line(0) == "'\"'"); + } + + SUBCASE("[CodeEdit] autocomplete") { + code_edit->set_code_completion_enabled(true); + CHECK(code_edit->is_code_completion_enabled()); + + /* Set prefixes, single char only, disallow empty. */ + TypedArray<String> completion_prefixes; + completion_prefixes.push_back(""); + completion_prefixes.push_back("."); + completion_prefixes.push_back("."); + completion_prefixes.push_back(",,"); + + ERR_PRINT_OFF; + code_edit->set_code_completion_prefixes(completion_prefixes); + ERR_PRINT_ON; + completion_prefixes = code_edit->get_code_completion_prefixes(); + CHECK(completion_prefixes.size() == 2); + CHECK(completion_prefixes.has(".")); + CHECK(completion_prefixes.has(",")); + + code_edit->set_text("test\ntest"); + CHECK(code_edit->get_text_for_code_completion() == String::chr(0xFFFF) + "test\ntest"); + } + + SUBCASE("[CodeEdit] autocomplete request") { + SIGNAL_WATCH(code_edit, "request_code_completion"); + code_edit->set_code_completion_enabled(true); + + Array signal_args; + signal_args.push_back(Array()); + + /* Force request. */ + code_edit->request_code_completion(); + SIGNAL_CHECK_FALSE("request_code_completion"); + code_edit->request_code_completion(true); + SIGNAL_CHECK("request_code_completion", signal_args); + + /* Manual request should force. */ + SEND_GUI_ACTION(code_edit, "ui_text_completion_query"); + SIGNAL_CHECK("request_code_completion", signal_args); + + /* Insert prefix. */ + TypedArray<String> completion_prefixes; + completion_prefixes.push_back("."); + code_edit->set_code_completion_prefixes(completion_prefixes); + + code_edit->insert_text_at_caret("."); + code_edit->request_code_completion(); + SIGNAL_CHECK("request_code_completion", signal_args); + + /* Should work with space too. */ + code_edit->insert_text_at_caret(" "); + code_edit->request_code_completion(); + SIGNAL_CHECK("request_code_completion", signal_args); + + /* Should work when complete ends with prefix. */ + code_edit->clear(); + code_edit->insert_text_at_caret("t"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "test.", "test."); + code_edit->update_code_completion_options(); + code_edit->confirm_code_completion(); + CHECK(code_edit->get_line(0) == "test."); + SIGNAL_CHECK("request_code_completion", signal_args); + + SIGNAL_UNWATCH(code_edit, "request_code_completion"); + } + + SUBCASE("[CodeEdit] autocomplete completion") { + CHECK(code_edit->get_code_completion_selected_index() == -1); + code_edit->set_code_completion_enabled(true); + CHECK(code_edit->get_code_completion_selected_index() == -1); + + code_edit->update_code_completion_options(); + code_edit->set_code_completion_selected_index(1); + CHECK(code_edit->get_code_completion_selected_index() == -1); + CHECK(code_edit->get_code_completion_option(0).size() == 0); + CHECK(code_edit->get_code_completion_options().size() == 0); + + /* Adding does not update the list. */ + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_0.", "item_0"); + + code_edit->set_code_completion_selected_index(1); + CHECK(code_edit->get_code_completion_selected_index() == -1); + CHECK(code_edit->get_code_completion_option(0).size() == 0); + CHECK(code_edit->get_code_completion_options().size() == 0); + + /* After update, pending add should not be counted, */ + /* also does not work on col 0 */ + code_edit->insert_text_at_caret("i"); + code_edit->update_code_completion_options(); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0", Color(1, 0, 0), RES(), Color(1, 0, 0)); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_1.", "item_1"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_2.", "item_2"); + + ERR_PRINT_OFF; + code_edit->set_code_completion_selected_index(1); + ERR_PRINT_ON; + CHECK(code_edit->get_code_completion_selected_index() == 0); + CHECK(code_edit->get_code_completion_option(0).size() == 6); + CHECK(code_edit->get_code_completion_options().size() == 1); + + /* Check cancel closes completion. */ + SEND_GUI_ACTION(code_edit, "ui_cancel"); + CHECK(code_edit->get_code_completion_selected_index() == -1); + + code_edit->update_code_completion_options(); + CHECK(code_edit->get_code_completion_selected_index() == 0); + code_edit->set_code_completion_selected_index(1); + CHECK(code_edit->get_code_completion_selected_index() == 1); + CHECK(code_edit->get_code_completion_option(0).size() == 6); + CHECK(code_edit->get_code_completion_options().size() == 3); + + /* Check data. */ + Dictionary option = code_edit->get_code_completion_option(0); + CHECK((int)option["kind"] == (int)CodeEdit::CodeCompletionKind::KIND_CLASS); + CHECK(option["display_text"] == "item_0."); + CHECK(option["insert_text"] == "item_0"); + CHECK(option["font_color"] == Color(1, 0, 0)); + CHECK(option["icon"] == RES()); + CHECK(option["default_value"] == Color(1, 0, 0)); + + /* Set size for mouse input. */ + code_edit->set_size(Size2(100, 100)); + + /* Check input. */ + SEND_GUI_ACTION(code_edit, "ui_end"); + CHECK(code_edit->get_code_completion_selected_index() == 2); + + SEND_GUI_ACTION(code_edit, "ui_home"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_page_down"); + CHECK(code_edit->get_code_completion_selected_index() == 2); + + SEND_GUI_ACTION(code_edit, "ui_page_up"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_up"); + CHECK(code_edit->get_code_completion_selected_index() == 2); + + SEND_GUI_ACTION(code_edit, "ui_down"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_KEY_EVENT(code_edit, KEY_T); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_left"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_right"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + Point2 caret_pos = code_edit->get_caret_draw_pos(); + caret_pos.y -= code_edit->get_line_height(); + SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MOUSE_BUTTON_WHEEL_DOWN, MOUSE_BUTTON_NONE); + CHECK(code_edit->get_code_completion_selected_index() == 1); + + SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_NONE); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + /* Single click selects. */ + SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MASK_LEFT); + CHECK(code_edit->get_code_completion_selected_index() == 2); + + /* Double click inserts. */ + SEND_GUI_DOUBLE_CLICK(code_edit, caret_pos); + CHECK(code_edit->get_code_completion_selected_index() == -1); + CHECK(code_edit->get_line(0) == "item_2"); + + code_edit->set_auto_brace_completion_enabled(false); + + /* Does nothing in readonly. */ + code_edit->undo(); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + code_edit->set_editable(false); + code_edit->confirm_code_completion(); + code_edit->set_editable(true); + CHECK(code_edit->get_line(0) == "i"); + + /* Replace */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0 test"); + + /* Replace string. */ + code_edit->clear(); + code_edit->insert_text_at_caret("\"item_1 test\""); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "\"item_0\""); + + /* Normal replace if no end is given. */ + code_edit->clear(); + code_edit->insert_text_at_caret("\"item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "\"item_0\" test"); + + /* Insert at completion. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_accept"); + CHECK(code_edit->get_line(0) == "item_01 test"); + + /* Insert at completion with string should have same output. */ + code_edit->clear(); + code_edit->insert_text_at_caret("\"item_1 test\""); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_accept"); + CHECK(code_edit->get_line(0) == "\"item_0\"1 test\""); + + /* Merge symbol at end on insert text. */ + /* End on completion entry. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0("); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 7); + + /* End of text*/ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1( test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 6); + + /* End of both. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1( test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0("); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 7); + + /* Full set. */ + /* End on completion entry. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 8); + + /* End of text*/ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1() test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 6); + + /* End of both. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1() test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 8); + + /* Autobrace completion. */ + code_edit->set_auto_brace_completion_enabled(true); + + /* End on completion entry. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0("); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 7); + + /* End of text*/ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1( test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 6); + + /* End of both. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1( test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0("); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 7); + + /* Full set. */ + /* End on completion entry. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 8); + + /* End of text*/ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1() test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 6); + + /* End of both. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1() test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 8); + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] symbol lookup") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + code_edit->set_symbol_lookup_on_click_enabled(true); + CHECK(code_edit->is_symbol_lookup_on_click_enabled()); + + /* Set size for mouse input. */ + code_edit->set_size(Size2(100, 100)); + + code_edit->set_text("this is some text"); + + Point2 caret_pos = code_edit->get_caret_draw_pos(); + caret_pos.x += 55; + SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE); + CHECK(code_edit->get_text_for_symbol_lookup() == "this is s" + String::chr(0xFFFF) + "ome text"); + + SIGNAL_WATCH(code_edit, "symbol_validate"); + +#ifdef OSX_ENABLED + SEND_GUI_KEY_EVENT(code_edit, KEY_META); +#else + SEND_GUI_KEY_EVENT(code_edit, KEY_CTRL); +#endif + + Array signal_args; + Array arg; + arg.push_back("some"); + signal_args.push_back(arg); + SIGNAL_CHECK("symbol_validate", signal_args); + + SIGNAL_UNWATCH(code_edit, "symbol_validate"); + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] line length guidelines") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + TypedArray<int> guide_lines; + + code_edit->set_line_length_guidelines(guide_lines); + CHECK(code_edit->get_line_length_guidelines().size() == 0); + + guide_lines.push_back(80); + guide_lines.push_back(120); + + /* Order should be preserved. */ + code_edit->set_line_length_guidelines(guide_lines); + CHECK((int)code_edit->get_line_length_guidelines()[0] == 80); + CHECK((int)code_edit->get_line_length_guidelines()[1] == 120); + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] Backspace delete") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + + /* Backspace with selection on first line. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test backspace"); + code_edit->select(0, 0, 0, 5); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == "backspace"); + + /* Backspace with selection on first line and caret at the beginning of file. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test backspace"); + code_edit->select(0, 0, 0, 5); + code_edit->set_caret_column(0); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == "backspace"); + + /* Move caret up to the previous line on backspace if carret is at the first column. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("line 1\nline 2"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(0); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == "line 1line 2"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 6); + + /* Backspace delete all text if all text is selected. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("line 1\nline 2\nline 3"); + code_edit->select_all(); + code_edit->backspace(); + CHECK(code_edit->get_text() == ""); + + /* Backspace at the beginning without selection has no effect. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("line 1\nline 2\nline 3"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(0); + code_edit->backspace(); + CHECK(code_edit->get_text() == "line 1\nline 2\nline 3"); + + memdelete(code_edit); +} + +} // namespace TestCodeEdit + +#endif // TEST_CODE_EDIT_H diff --git a/tests/test_config_file.h b/tests/test_config_file.h index 33fd30ffa1..e3f40e16fd 100644 --- a/tests/test_config_file.h +++ b/tests/test_config_file.h @@ -122,6 +122,8 @@ TEST_CASE("[ConfigFile] Saving file") { config_file.set_value("player", "position", Vector2(3, 4)); config_file.set_value("graphics", "antialiasing", true); config_file.set_value("graphics", "antiAliasing", false); + config_file.set_value("quoted", String::utf8("静音"), 42); + config_file.set_value("quoted", "a=b", 7); #ifdef WINDOWS_ENABLED const String config_path = OS::get_singleton()->get_environment("TEMP").plus_file("config.ini"); @@ -132,7 +134,7 @@ TEST_CASE("[ConfigFile] Saving file") { config_file.save(config_path); // Expected contents of the saved ConfigFile. - const String contents = R"([player] + const String contents = String::utf8(R"([player] name="Unnamed Player" tagline="Waiting @@ -145,7 +147,12 @@ position=Vector2(3, 4) antialiasing=true antiAliasing=false -)"; + +[quoted] + +"静音"=42 +"a=b"=7 +)"); FileAccessRef file = FileAccess::open(config_path, FileAccess::READ); CHECK_MESSAGE(file->get_as_utf8_string() == contents, diff --git a/tests/test_curve.h b/tests/test_curve.h index 7eeee86f32..e079905e35 100644 --- a/tests/test_curve.h +++ b/tests/test_curve.h @@ -216,6 +216,41 @@ TEST_CASE("[Curve] Custom curve with linear tangents") { Math::is_equal_approx(curve->interpolate_baked(0.7), (real_t)0.8), "Custom free curve should return the expected baked value at offset 0.7 after removing point at invalid index 10."); } + +TEST_CASE("[Curve2D] Linear sampling should return exact value") { + Ref<Curve2D> curve = memnew(Curve2D); + int len = 2048; + + curve->add_point(Vector2(0, 0)); + curve->add_point(Vector2((float)len, 0)); + + float baked_length = curve->get_baked_length(); + CHECK((float)len == baked_length); + + for (int i = 0; i < len; i++) { + float expected = (float)i; + Vector2 pos = curve->interpolate_baked(expected); + CHECK_MESSAGE(pos.x == expected, "interpolate_baked should return exact value"); + } +} + +TEST_CASE("[Curve3D] Linear sampling should return exact value") { + Ref<Curve3D> curve = memnew(Curve3D); + int len = 2048; + + curve->add_point(Vector3(0, 0, 0)); + curve->add_point(Vector3((float)len, 0, 0)); + + float baked_length = curve->get_baked_length(); + CHECK((float)len == baked_length); + + for (int i = 0; i < len; i++) { + float expected = (float)i; + Vector3 pos = curve->interpolate_baked(expected); + CHECK_MESSAGE(pos.x == expected, "interpolate_baked should return exact value"); + } +} + } // namespace TestCurve #endif // TEST_CURVE_H diff --git a/tests/test_file_access.h b/tests/test_file_access.h index cb74e08a0d..b3da16c1d1 100644 --- a/tests/test_file_access.h +++ b/tests/test_file_access.h @@ -37,12 +37,12 @@ namespace TestFileAccess { TEST_CASE("[FileAccess] CSV read") { - FileAccess *f = FileAccess::open(TestUtils::get_data_path("translations.csv"), FileAccess::READ); + FileAccessRef f = FileAccess::open(TestUtils::get_data_path("translations.csv"), FileAccess::READ); - Vector<String> header = f->get_csv_line(); // Default delimiter: "," + Vector<String> header = f->get_csv_line(); // Default delimiter: ",". REQUIRE(header.size() == 3); - Vector<String> row1 = f->get_csv_line(","); + Vector<String> row1 = f->get_csv_line(","); // Explicit delimiter, should be the same. REQUIRE(row1.size() == 3); CHECK(row1[0] == "GOOD_MORNING"); CHECK(row1[1] == "Good Morning"); @@ -53,12 +53,31 @@ TEST_CASE("[FileAccess] CSV read") { CHECK(row2[0] == "GOOD_EVENING"); CHECK(row2[1] == "Good Evening"); CHECK(row2[2] == ""); // Use case: not yet translated! - // https://github.com/godotengine/godot/issues/44269 CHECK_MESSAGE(row2[2] != "\"", "Should not parse empty string as a single double quote."); + Vector<String> row3 = f->get_csv_line(); + REQUIRE(row3.size() == 6); + CHECK(row3[0] == "Without quotes"); + CHECK(row3[1] == "With, comma"); + CHECK(row3[2] == "With \"inner\" quotes"); + CHECK(row3[3] == "With \"inner\", quotes\",\" and comma"); + CHECK(row3[4] == "With \"inner\nsplit\" quotes and\nline breaks"); + CHECK(row3[5] == "With \\nnewline chars"); // Escaped, not an actual newline. + + Vector<String> row4 = f->get_csv_line("~"); // Custom delimiter, makes inline commas easier. + REQUIRE(row4.size() == 3); + CHECK(row4[0] == "Some other"); + CHECK(row4[1] == "delimiter"); + CHECK(row4[2] == "should still work, shouldn't it?"); + + Vector<String> row5 = f->get_csv_line("\t"); // Tab separated variables. + REQUIRE(row5.size() == 3); + CHECK(row5[0] == "What about"); + CHECK(row5[1] == "tab separated"); + CHECK(row5[2] == "lines, good?"); + f->close(); - memdelete(f); } } // namespace TestFileAccess diff --git a/tests/test_geometry_2d.h b/tests/test_geometry_2d.h index 32d4114a1c..25af8c355e 100644 --- a/tests/test_geometry_2d.h +++ b/tests/test_geometry_2d.h @@ -51,8 +51,6 @@ TEST_CASE("[Geometry2D] Point in circle") { 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. - // In `is_point_in_triangle` and `is_point_in_polygon` they are treated as being outside, so in order the make - // the behaviour consistent this may change in the future (see issue #44717 and PR #44274). 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)); } @@ -66,7 +64,7 @@ TEST_CASE("[Geometry2D] Point in triangle") { 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` they are treated as being inside, so in order the make + // In `is_point_in_circle` and `is_point_in_polygon` they are treated as being inside, so in order the make // the behaviour consistent this may change in the future (see issue #44717 and PR #44274). CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(1, 1), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1))); CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(0, 1), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1))); @@ -95,11 +93,16 @@ TEST_CASE("[Geometry2D] Point in polygon") { 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 outside the polygon. - // In `is_point_in_circle` they are treated as being inside, so in order the make - // the behaviour consistent this may change in the future (see issue #44717 and PR #44274). - CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(68, 112), p)); - CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-88, 120), 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") { @@ -140,9 +143,21 @@ TEST_CASE("[Geometry2D] Segment intersection.") { 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(1, -1), &r), + Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(0, 1), Vector2(2, -1), &r), "Parallel segments should not intersect."); + + CHECK_MESSAGE( + Geometry2D::segment_intersects_segment(Vector2(0, 0), Vector2(0, 1), Vector2(0, 0), Vector2(1, 0), &r), + "Touching segments should intersect."); + CHECK(r.is_equal_approx(Vector2(0, 0))); + + CHECK_MESSAGE( + Geometry2D::segment_intersects_segment(Vector2(0, 1), Vector2(0, 0), Vector2(0, 0), Vector2(1, 0), &r), + "Touching segments should intersect."); + CHECK(r.is_equal_approx(Vector2(0, 0))); } TEST_CASE("[Geometry2D] Closest point to segment") { diff --git a/tests/test_geometry_3d.h b/tests/test_geometry_3d.h index 2b2a424b2b..ae30737fe2 100644 --- a/tests/test_geometry_3d.h +++ b/tests/test_geometry_3d.h @@ -28,8 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef TEST_3D_GEOMETRY_H -#define TEST_3D_GEOMETRY_H +#ifndef TEST_GEOMETRY_3D_H +#define TEST_GEOMETRY_3D_H #include "core/math/geometry_3d.h" #include "core/math/plane.h" @@ -38,7 +38,7 @@ #include "tests/test_macros.h" #include "vector" -namespace Test3DGeometry { +namespace TestGeometry3D { TEST_CASE("[Geometry3D] Closest Points Between Segments") { struct Case { Vector3 p_1, p_2, p_3, p_4; @@ -57,6 +57,7 @@ TEST_CASE("[Geometry3D] Closest Points Between Segments") { CHECK(current_case.got_2.is_equal_approx(current_case.want_2)); } } + TEST_CASE("[Geometry3D] Closest Distance Between Segments") { struct Case { Vector3 p_1, p_2, p_3, p_4; @@ -73,6 +74,7 @@ TEST_CASE("[Geometry3D] Closest Distance Between Segments") { CHECK(out == current_case.want); } } + TEST_CASE("[Geometry3D] Build Box Planes") { const Vector3 extents = Vector3(5, 5, 20); Vector<Plane> box = Geometry3D::build_box_planes(extents); @@ -90,6 +92,7 @@ TEST_CASE("[Geometry3D] Build Box Planes") { CHECK(extents.z == box[5].d); CHECK(box[5].normal == Vector3(0, 0, -1)); } + TEST_CASE("[Geometry3D] Build Capsule Planes") { struct Case { real_t radius, height; @@ -109,6 +112,7 @@ TEST_CASE("[Geometry3D] Build Capsule Planes") { CHECK(capsule.size() == current_case.want_size); } } + TEST_CASE("[Geometry3D] Build Cylinder Planes") { struct Case { real_t radius, height; @@ -127,6 +131,7 @@ TEST_CASE("[Geometry3D] Build Cylinder Planes") { CHECK(planes.size() == current_case.want_size); } } + TEST_CASE("[Geometry3D] Build Sphere Planes") { struct Case { real_t radius; @@ -145,6 +150,11 @@ TEST_CASE("[Geometry3D] Build Sphere Planes") { 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; @@ -166,6 +176,8 @@ TEST_CASE("[Geometry3D] Build Convex Mesh") { CHECK(mesh.vertices.size() == current_case.want_vertices); } } +#endif + TEST_CASE("[Geometry3D] Clip Polygon") { struct Case { Plane clipping_plane; @@ -179,7 +191,7 @@ 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()); tt.push_back(Case(Plane(), box, true)); - tt.push_back(Case(Plane(Vector3(0, 3, 0), Vector3(0, 1, 0)), box, false)); + tt.push_back(Case(Plane(Vector3(0, 1, 0), Vector3(0, 3, 0)), box, false)); for (int i = 0; i < tt.size(); ++i) { Case current_case = tt[i]; Vector<Vector3> output = Geometry3D::clip_polygon(current_case.polygon, current_case.clipping_plane); @@ -190,6 +202,7 @@ TEST_CASE("[Geometry3D] Clip Polygon") { } } } + TEST_CASE("[Geometry3D] Compute Convex Mesh Points") { struct Case { Vector<Plane> mesh; @@ -215,6 +228,7 @@ TEST_CASE("[Geometry3D] Compute Convex Mesh Points") { CHECK(vectors == current_case.want); } } + TEST_CASE("[Geometry3D] Get Closest Point To Segment") { struct Case { Vector3 point; @@ -235,6 +249,7 @@ TEST_CASE("[Geometry3D] Get Closest Point To Segment") { CHECK(output.is_equal_approx(current_case.want)); } } + TEST_CASE("[Geometry3D] Plane and Box Overlap") { struct Case { Vector3 normal, max_box; @@ -254,6 +269,7 @@ TEST_CASE("[Geometry3D] Plane and Box Overlap") { CHECK(overlap == current_case.want); } } + TEST_CASE("[Geometry3D] Is Point in Projected Triangle") { struct Case { Vector3 point, v_1, v_2, v_3; @@ -272,6 +288,7 @@ TEST_CASE("[Geometry3D] Is Point in Projected Triangle") { CHECK(output == current_case.want); } } + TEST_CASE("[Geometry3D] Does Ray Intersect Triangle") { struct Case { Vector3 from, direction, v_1, v_2, v_3; @@ -291,6 +308,7 @@ TEST_CASE("[Geometry3D] Does Ray Intersect Triangle") { CHECK(output == current_case.want); } } + TEST_CASE("[Geometry3D] Does Segment Intersect Convex") { struct Case { Vector3 from, to; @@ -311,6 +329,7 @@ TEST_CASE("[Geometry3D] Does Segment Intersect Convex") { CHECK(output == current_case.want); } } + TEST_CASE("[Geometry3D] Segment Intersects Cylinder") { struct Case { Vector3 from, to; @@ -330,6 +349,7 @@ TEST_CASE("[Geometry3D] Segment Intersects Cylinder") { CHECK(output == current_case.want); } } + TEST_CASE("[Geometry3D] Segment Intersects Cylinder") { struct Case { Vector3 from, to, sphere_pos; @@ -350,6 +370,7 @@ TEST_CASE("[Geometry3D] Segment Intersects Cylinder") { CHECK(output == current_case.want); } } + TEST_CASE("[Geometry3D] Segment Intersects Triangle") { struct Case { Vector3 from, to, v_1, v_2, v_3, *result; @@ -368,6 +389,7 @@ TEST_CASE("[Geometry3D] Segment Intersects Triangle") { CHECK(output == current_case.want); } } + TEST_CASE("[Geometry3D] Triangle and Box Overlap") { struct Case { Vector3 box_centre; @@ -389,6 +411,7 @@ TEST_CASE("[Geometry3D] Triangle and Box Overlap") { CHECK(output == current_case.want); } } + TEST_CASE("[Geometry3D] Triangle and Sphere Intersect") { struct Case { Vector<Vector3> triangle; @@ -413,5 +436,6 @@ TEST_CASE("[Geometry3D] Triangle and Sphere Intersect") { CHECK(output == current_case.want); } } -} // namespace Test3DGeometry -#endif +} // namespace TestGeometry3D + +#endif // TEST_GEOMETRY_3D_H diff --git a/tests/test_macros.h b/tests/test_macros.h index a1f1932db4..2f0bc6dcfa 100644 --- a/tests/test_macros.h +++ b/tests/test_macros.h @@ -31,6 +31,8 @@ #ifndef TEST_MACROS_H #define TEST_MACROS_H +#include "core/object/callable_method_pointer.h" +#include "core/object/class_db.h" #include "core/templates/map.h" #include "core/variant/variant.h" @@ -129,4 +131,221 @@ int register_test_command(String p_command, TestFunc p_function); register_test_command(m_command, m_function); \ DOCTEST_GLOBAL_NO_WARNINGS_END() +// Utility macros to send an event actions to a given object +// Requires Message Queue and InputMap to be setup. +// SEND_GUI_ACTION - takes an object and a input map key. e.g SEND_GUI_ACTION(code_edit, "ui_text_newline"). +// SEND_GUI_KEY_EVENT - takes an object and a keycode set. e.g SEND_GUI_KEY_EVENT(code_edit, KEY_A | KEY_MASK_CMD). +// SEND_GUI_MOUSE_EVENT - takes an object, position, mouse button and mouse mask e.g SEND_GUI_MOUSE_EVENT(code_edit, Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE); +// SEND_GUI_DOUBLE_CLICK - takes an object and a postion. e.g SEND_GUI_DOUBLE_CLICK(code_edit, Vector2(50, 50)); + +#define SEND_GUI_ACTION(m_object, m_action) \ + { \ + const List<Ref<InputEvent>> *events = InputMap::get_singleton()->action_get_events(m_action); \ + const List<Ref<InputEvent>>::Element *first_event = events->front(); \ + Ref<InputEventKey> event = first_event->get(); \ + event->set_pressed(true); \ + m_object->gui_input(event); \ + MessageQueue::get_singleton()->flush(); \ + } + +#define SEND_GUI_KEY_EVENT(m_object, m_input) \ + { \ + Ref<InputEventKey> event = InputEventKey::create_reference(m_input); \ + event->set_pressed(true); \ + m_object->gui_input(event); \ + MessageQueue::get_singleton()->flush(); \ + } + +#define _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask) \ + Ref<InputEventMouseButton> event; \ + event.instantiate(); \ + event->set_position(m_local_pos); \ + event->set_button_index(m_input); \ + event->set_button_mask(m_mask); \ + event->set_pressed(true); + +#define SEND_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask) \ + { \ + _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask); \ + m_object->get_viewport()->push_input(event); \ + MessageQueue::get_singleton()->flush(); \ + } + +#define SEND_GUI_DOUBLE_CLICK(m_object, m_local_pos) \ + { \ + _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_LEFT); \ + event->set_double_click(true); \ + m_object->get_viewport()->push_input(event); \ + MessageQueue::get_singleton()->flush(); \ + } + +// Utility class / macros for testing signals +// +// Use SIGNAL_WATCH(*object, "signal_name") to start watching +// Makes sure to call SIGNAL_UNWATCH(*object, "signal_name") to stop watching in cleanup, this is not done automatically. +// +// The SignalWatcher will capture all signals and their args sent between checks. +// +// Use SIGNAL_CHECK("signal_name"), Vector<Vector<Variant>>), to check the arguments of all fired signals. +// The outer vector is each fired signal, the inner vector the list of arguments for that signal. Order does matter. +// +// Use SIGNAL_CHECK_FALSE("signal_name") to check if a signal was not fired. +// +// Use SIGNAL_DISCARD("signal_name") to discard records all of the given signal, use only in placed you don't need to check. +// +// All signals are automaticaly discared between test/sub test cases. + +class SignalWatcher : public Object { +private: + inline static SignalWatcher *singleton; + + /* Equal to: Map<String, Vector<Vector<Variant>>> */ + Map<String, Array> _signals; + void _add_signal_entry(const Array &p_args, const String &p_name) { + if (!_signals.has(p_name)) { + _signals[p_name] = Array(); + } + _signals[p_name].push_back(p_args); + } + + void _signal_callback_zero(const String &p_name) { + Array args; + _add_signal_entry(args, p_name); + } + + void _signal_callback_one(Variant p_arg1, const String &p_name) { + Array args; + args.push_back(p_arg1); + _add_signal_entry(args, p_name); + } + + void _signal_callback_two(Variant p_arg1, Variant p_arg2, const String &p_name) { + Array args; + args.push_back(p_arg1); + args.push_back(p_arg2); + _add_signal_entry(args, p_name); + } + + void _signal_callback_three(Variant p_arg1, Variant p_arg2, Variant p_arg3, const String &p_name) { + Array args; + args.push_back(p_arg1); + args.push_back(p_arg2); + args.push_back(p_arg3); + _add_signal_entry(args, p_name); + } + +public: + static SignalWatcher *get_singleton() { return singleton; } + + void watch_signal(Object *p_object, const String &p_signal) { + Vector<Variant> args; + args.push_back(p_signal); + MethodInfo method_info; + ClassDB::get_signal(p_object->get_class(), p_signal, &method_info); + switch (method_info.arguments.size()) { + case 0: { + p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_zero), args); + } break; + case 1: { + p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_one), args); + } break; + case 2: { + p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_two), args); + } break; + case 3: { + p_object->connect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_three), args); + } break; + default: { + MESSAGE("Signal ", p_signal, " arg count not supported."); + } break; + } + } + + void unwatch_signal(Object *p_object, const String &p_signal) { + MethodInfo method_info; + ClassDB::get_signal(p_object->get_class(), p_signal, &method_info); + switch (method_info.arguments.size()) { + case 0: { + p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_zero)); + } break; + case 1: { + p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_one)); + } break; + case 2: { + p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_two)); + } break; + case 3: { + p_object->disconnect(p_signal, callable_mp(this, &SignalWatcher::_signal_callback_three)); + } break; + default: { + MESSAGE("Signal ", p_signal, " arg count not supported."); + } break; + } + } + + bool check(const String &p_name, const Array &p_args) { + if (!_signals.has(p_name)) { + MESSAGE("Signal ", p_name, " not emitted"); + return false; + } + + if (p_args.size() != _signals[p_name].size()) { + MESSAGE("Signal has " << _signals[p_name] << " expected " << p_args); + discard_signal(p_name); + return false; + } + + bool match = true; + for (int i = 0; i < p_args.size(); i++) { + if (((Array)p_args[i]).size() != ((Array)_signals[p_name][i]).size()) { + MESSAGE("Signal has " << _signals[p_name][i] << " expected " << p_args[i]); + match = false; + continue; + } + + for (int j = 0; j < ((Array)p_args[i]).size(); j++) { + if (((Array)p_args[i])[j] != ((Array)_signals[p_name][i])[j]) { + MESSAGE("Signal has " << _signals[p_name][i] << " expected " << p_args[i]); + match = false; + break; + } + } + } + + discard_signal(p_name); + return match; + } + + bool check_false(const String &p_name) { + bool has = _signals.has(p_name); + discard_signal(p_name); + return !has; + } + + void discard_signal(const String &p_name) { + if (_signals.has(p_name)) { + _signals.erase(p_name); + } + } + + void _clear_signals() { + _signals.clear(); + } + + SignalWatcher() { + singleton = this; + } + + ~SignalWatcher() { + singleton = nullptr; + } +}; + +#define SIGNAL_WATCH(m_object, m_signal) SignalWatcher::get_singleton()->watch_signal(m_object, m_signal); +#define SIGNAL_UNWATCH(m_object, m_signal) SignalWatcher::get_singleton()->unwatch_signal(m_object, m_signal); + +#define SIGNAL_CHECK(m_signal, m_args) CHECK(SignalWatcher::get_singleton()->check(m_signal, m_args)); +#define SIGNAL_CHECK_FALSE(m_signal) CHECK(SignalWatcher::get_singleton()->check_false(m_signal)); +#define SIGNAL_DISCARD(m_signal) SignalWatcher::get_singleton()->discard_signal(m_signal); + #endif // TEST_MACROS_H diff --git a/tests/test_main.cpp b/tests/test_main.cpp index d0466d1e2d..341fb1af61 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -37,6 +37,7 @@ #include "test_astar.h" #include "test_basis.h" #include "test_class_db.h" +#include "test_code_edit.h" #include "test_color.h" #include "test_command_queue.h" #include "test_config_file.h" @@ -146,3 +147,147 @@ int test_main(int argc, char *argv[]) { return test_context.run(); } + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "servers/navigation_server_2d.h" +#include "servers/navigation_server_3d.h" +#include "servers/rendering/rendering_server_default.h" + +struct GodotTestCaseListener : public doctest::IReporter { + GodotTestCaseListener(const doctest::ContextOptions &p_in) {} + + SignalWatcher *signal_watcher = nullptr; + + PhysicsServer3D *physics_3d_server = nullptr; + PhysicsServer2D *physics_2d_server = nullptr; + NavigationServer3D *navigation_3d_server = nullptr; + NavigationServer2D *navigation_2d_server = nullptr; + + void test_case_start(const doctest::TestCaseData &p_in) override { + SignalWatcher::get_singleton()->_clear_signals(); + + String name = String(p_in.m_name); + + if (name.find("[SceneTree]") != -1) { + GLOBAL_DEF("memory/limits/multithreaded_server/rid_pool_prealloc", 60); + memnew(MessageQueue); + + GLOBAL_DEF("internationalization/rendering/force_right_to_left_layout_direction", false); + + Error err = OK; + OS::get_singleton()->set_has_server_feature_callback(nullptr); + for (int i = 0; i < DisplayServer::get_create_function_count(); i++) { + if (String("headless") == DisplayServer::get_create_function_name(i)) { + DisplayServer::create(i, "", DisplayServer::WindowMode::WINDOW_MODE_MINIMIZED, DisplayServer::VSyncMode::VSYNC_ENABLED, 0, Vector2i(0, 0), err); + break; + } + } + memnew(RenderingServerDefault()); + RenderingServerDefault::get_singleton()->init(); + RenderingServerDefault::get_singleton()->set_render_loop_enabled(false); + + physics_3d_server = PhysicsServer3DManager::new_default_server(); + physics_3d_server->init(); + + physics_2d_server = PhysicsServer2DManager::new_default_server(); + physics_2d_server->init(); + + navigation_3d_server = NavigationServer3DManager::new_default_server(); + navigation_2d_server = memnew(NavigationServer2D); + + memnew(InputMap); + InputMap::get_singleton()->load_default(); + + make_default_theme(false, Ref<Font>()); + + memnew(SceneTree); + SceneTree::get_singleton()->initialize(); + return; + } + } + + void test_case_end(const doctest::CurrentTestCaseStats &) override { + if (SceneTree::get_singleton()) { + SceneTree::get_singleton()->finalize(); + } + + if (MessageQueue::get_singleton()) { + MessageQueue::get_singleton()->flush(); + } + + if (SceneTree::get_singleton()) { + memdelete(SceneTree::get_singleton()); + } + + clear_default_theme(); + + if (navigation_3d_server) { + memdelete(navigation_3d_server); + navigation_3d_server = nullptr; + } + + if (navigation_2d_server) { + memdelete(navigation_2d_server); + navigation_2d_server = nullptr; + } + + if (physics_3d_server) { + physics_3d_server->finish(); + memdelete(physics_3d_server); + physics_3d_server = nullptr; + } + + if (physics_2d_server) { + physics_2d_server->finish(); + memdelete(physics_2d_server); + physics_2d_server = nullptr; + } + + if (RenderingServer::get_singleton()) { + RenderingServer::get_singleton()->sync(); + RenderingServer::get_singleton()->global_variables_clear(); + RenderingServer::get_singleton()->finish(); + memdelete(RenderingServer::get_singleton()); + } + + if (DisplayServer::get_singleton()) { + memdelete(DisplayServer::get_singleton()); + } + + if (InputMap::get_singleton()) { + memdelete(InputMap::get_singleton()); + } + + if (MessageQueue::get_singleton()) { + MessageQueue::get_singleton()->flush(); + memdelete(MessageQueue::get_singleton()); + } + } + + void test_run_start() override { + signal_watcher = memnew(SignalWatcher); + } + + void test_run_end(const doctest::TestRunStats &) override { + memdelete(signal_watcher); + } + + void test_case_reenter(const doctest::TestCaseData &) override { + SignalWatcher::get_singleton()->_clear_signals(); + } + + void subcase_start(const doctest::SubcaseSignature &) override { + SignalWatcher::get_singleton()->_clear_signals(); + } + + void report_query(const doctest::QueryData &) override {} + void test_case_exception(const doctest::TestCaseException &) override {} + void subcase_end() override {} + + void log_assert(const doctest::AssertData &in) override {} + void log_message(const doctest::MessageData &) override {} + void test_case_skipped(const doctest::TestCaseData &) override {} +}; + +REGISTER_LISTENER("GodotTestCaseListener", 1, GodotTestCaseListener); diff --git a/tests/test_math.cpp b/tests/test_math.cpp index 67d9a52539..72272382ce 100644 --- a/tests/test_math.cpp +++ b/tests/test_math.cpp @@ -296,8 +296,8 @@ public: if (tk == TK_IDENTIFIER) { String name = value; if (use_next_class || p_known_class_name == name) { - for (Map<int, String>::Element *E = namespace_stack.front(); E; E = E->next()) { - class_name += E->get() + "."; + for (const KeyValue<int, String> &E : namespace_stack) { + class_name += E.value + "."; } class_name += String(value); break; @@ -549,8 +549,8 @@ MainLoop *test() { List<StringName> tl; ClassDB::get_class_list(&tl); - for (List<StringName>::Element *E = tl.front(); E; E = E->next()) { - Vector<uint8_t> m5b = E->get().operator String().md5_buffer(); + for (const StringName &E : tl) { + Vector<uint8_t> m5b = E.operator String().md5_buffer(); hashes.push_back(hashes.size()); } @@ -589,13 +589,13 @@ MainLoop *test() { { Vector3 v(1, 2, 3); v.normalize(); - float a = 0.3; + real_t a = 0.3; Basis m(v, a); Vector3 v2(7, 3, 1); v2.normalize(); - float a2 = 0.8; + real_t a2 = 0.8; Basis m2(v2, a2); diff --git a/tests/test_object.h b/tests/test_object.h index 36f9ef2a51..8cb7116a20 100644 --- a/tests/test_object.h +++ b/tests/test_object.h @@ -93,8 +93,8 @@ public: Ref<Script> get_script() const override { return Ref<Script>(); } - const Vector<MultiplayerAPI::RPCConfig> get_rpc_methods() const override { - return Vector<MultiplayerAPI::RPCConfig>(); + const Vector<Multiplayer::RPCConfig> get_rpc_methods() const override { + return Vector<Multiplayer::RPCConfig>(); } ScriptLanguage *get_language() override { return nullptr; @@ -139,7 +139,7 @@ TEST_CASE("[Object] Metadata") { Color(object.get_meta(meta_path)).is_equal_approx(Color(0, 1, 0)), "The returned object metadata after setting should match the expected value."); - List<String> meta_list; + List<StringName> meta_list; object.get_meta_list(&meta_list); CHECK_MESSAGE( meta_list.size() == 1, @@ -154,7 +154,7 @@ TEST_CASE("[Object] Metadata") { "The returned object metadata after removing should match the expected value."); ERR_PRINT_ON; - List<String> meta_list2; + List<StringName> meta_list2; object.get_meta_list(&meta_list2); CHECK_MESSAGE( meta_list2.size() == 0, diff --git a/tests/test_physics_2d.cpp b/tests/test_physics_2d.cpp index a9e2e92b34..f6619db8fd 100644 --- a/tests/test_physics_2d.cpp +++ b/tests/test_physics_2d.cpp @@ -199,7 +199,7 @@ protected: if (mb.is_valid()) { if (mb->is_pressed()) { - Point2 p(mb->get_position().x, mb->get_position().y); + Point2 p = mb->get_position(); if (mb->get_button_index() == 1) { ray_to = p; @@ -248,20 +248,20 @@ protected: return body; } - void _add_plane(const Vector2 &p_normal, real_t p_d) { + void _add_world_boundary(const Vector2 &p_normal, real_t p_d) { PhysicsServer2D *ps = PhysicsServer2D::get_singleton(); Array arr; arr.push_back(p_normal); arr.push_back(p_d); - RID plane = ps->line_shape_create(); - ps->shape_set_data(plane, arr); + RID world_boundary = ps->world_boundary_shape_create(); + ps->shape_set_data(world_boundary, arr); RID plane_body = ps->body_create(); ps->body_set_mode(plane_body, PhysicsServer2D::BODY_MODE_STATIC); ps->body_set_space(plane_body, space); - ps->body_add_shape(plane_body, plane); + ps->body_add_shape(plane_body, world_boundary); } void _add_concave(const Vector<Vector2> &p_points, const Transform2D &p_xform = Transform2D()) { @@ -381,12 +381,12 @@ public: } _add_concave(parr); - //_add_plane(Vector2(0.0,-1).normalized(),-300); - //_add_plane(Vector2(1,0).normalized(),50); - //_add_plane(Vector2(-1,0).normalized(),-600); + //_add_world_boundary(Vector2(0.0,-1).normalized(),-300); + //_add_world_boundary(Vector2(1,0).normalized(),50); + //_add_world_boundary(Vector2(-1,0).normalized(),-600); } - virtual bool process(float p_time) override { + virtual bool process(double p_time) override { return false; } virtual void finalize() override { diff --git a/tests/test_physics_3d.cpp b/tests/test_physics_3d.cpp index 4488e4bf64..d839ae26b7 100644 --- a/tests/test_physics_3d.cpp +++ b/tests/test_physics_3d.cpp @@ -100,18 +100,17 @@ protected: return body; } - RID create_static_plane(const Plane &p_plane) { + RID create_world_boundary(const Plane &p_plane) { PhysicsServer3D *ps = PhysicsServer3D::get_singleton(); - RID world_margin_shape = ps->shape_create(PhysicsServer3D::SHAPE_PLANE); - ps->shape_set_data(world_margin_shape, p_plane); + RID world_boundary_shape = ps->shape_create(PhysicsServer3D::SHAPE_WORLD_BOUNDARY); + ps->shape_set_data(world_boundary_shape, p_plane); RID b = ps->body_create(); ps->body_set_mode(b, PhysicsServer3D::BODY_MODE_STATIC); ps->body_set_space(b, space); - //todo set space - ps->body_add_shape(b, world_margin_shape); + ps->body_add_shape(b, world_boundary_shape); return b; } @@ -313,7 +312,7 @@ public: test_fall(); quit = false; } - virtual bool physics_process(float p_time) override { + virtual bool physics_process(double p_time) override { if (mover.is_valid()) { static real_t joy_speed = 10; PhysicsServer3D *ps = PhysicsServer3D::get_singleton(); @@ -362,7 +361,7 @@ public: RID mesh_instance = vs->instance_create2(capsule_mesh, scenario); character = ps->body_create(); - ps->body_set_mode(character, PhysicsServer3D::BODY_MODE_DYNAMIC_LOCKED); + ps->body_set_mode(character, PhysicsServer3D::BODY_MODE_DYNAMIC_LINEAR); ps->body_set_space(character, space); //todo add space ps->body_add_shape(character, capsule_shape); @@ -391,15 +390,15 @@ public: create_body(type, PhysicsServer3D::BODY_MODE_DYNAMIC, t); } - create_static_plane(Plane(Vector3(0, 1, 0), -1)); + create_world_boundary(Plane(Vector3(0, 1, 0), -1)); } void test_activate() { create_body(PhysicsServer3D::SHAPE_BOX, PhysicsServer3D::BODY_MODE_DYNAMIC, Transform3D(Basis(), Vector3(0, 2, 0)), true); - create_static_plane(Plane(Vector3(0, 1, 0), -1)); + create_world_boundary(Plane(Vector3(0, 1, 0), -1)); } - virtual bool process(float p_time) override { + virtual bool process(double p_time) override { return false; } diff --git a/tests/test_rect2.h b/tests/test_rect2.h index c5740167db..3d9fe5a32e 100644 --- a/tests/test_rect2.h +++ b/tests/test_rect2.h @@ -76,6 +76,12 @@ TEST_CASE("[Rect2] Basic getters") { 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") { @@ -288,6 +294,12 @@ TEST_CASE("[Rect2i] Basic getters") { 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") { diff --git a/tests/test_render.cpp b/tests/test_render.cpp index fe223ca258..21b4da9b3b 100644 --- a/tests/test_render.cpp +++ b/tests/test_render.cpp @@ -184,7 +184,7 @@ public: light = vs->instance_create2(lightaux, scenario); Transform3D lla; //lla.set_look_at(Vector3(),Vector3(1, -1, 1)); - lla.set_look_at(Vector3(), Vector3(0.0, -0.836026, -0.548690)); + lla.basis = Basis::looking_at(Vector3(0.0, -0.836026, -0.548690)); vs->instance_set_transform(light, lla); @@ -199,7 +199,7 @@ public: ofs = 0; quit = false; } - virtual bool iteration(float p_time) { + virtual bool iteration(double p_time) { RenderingServer *vs = RenderingServer::get_singleton(); //Transform3D t; //t.rotate(Vector3(0, 1, 0), ofs); @@ -210,12 +210,12 @@ public: //return quit; - for (List<InstanceInfo>::Element *E = instances.front(); E; E = E->next()) { - Transform3D pre(Basis(E->get().rot_axis, ofs), Vector3()); - vs->instance_set_transform(E->get().instance, pre * E->get().base); + for (const InstanceInfo &E : instances) { + Transform3D pre(Basis(E.rot_axis, ofs), Vector3()); + vs->instance_set_transform(E.instance, pre * E.base); /* if( !E->next() ) { - vs->free( E->get().instance ); + vs->free( E.instance ); instances.erase(E ); }*/ } @@ -223,7 +223,7 @@ public: return quit; } - virtual bool idle(float p_time) { + virtual bool idle(double p_time) { return quit; } diff --git a/tests/test_shader_lang.cpp b/tests/test_shader_lang.cpp index ad763b344e..5598852f29 100644 --- a/tests/test_shader_lang.cpp +++ b/tests/test_shader_lang.cpp @@ -120,38 +120,43 @@ static String dump_node_code(SL::Node *p_node, int p_level) { case SL::Node::TYPE_SHADER: { SL::ShaderNode *pnode = (SL::ShaderNode *)p_node; - for (Map<StringName, SL::ShaderNode::Uniform>::Element *E = pnode->uniforms.front(); E; E = E->next()) { + for (const KeyValue<StringName, SL::ShaderNode::Uniform> &E : pnode->uniforms) { String ucode = "uniform "; - ucode += _prestr(E->get().precision); - ucode += _typestr(E->get().type); - ucode += " " + String(E->key()); - - if (E->get().default_value.size()) { - ucode += " = " + get_constant_text(E->get().type, E->get().default_value); - } + ucode += _prestr(E.value.precision); + ucode += _typestr(E.value.type); + ucode += " " + String(E.key); + if (E.value.array_size > 0) { + ucode += "["; + ucode += itos(E.value.array_size); + ucode += "]"; + } else { + if (E.value.default_value.size()) { + ucode += " = " + get_constant_text(E.value.type, E.value.default_value); + } - static const char *hint_name[SL::ShaderNode::Uniform::HINT_MAX] = { - "", - "color", - "range", - "albedo", - "normal", - "black", - "white" - }; - - if (E->get().hint) { - ucode += " : " + String(hint_name[E->get().hint]); + static const char *hint_name[SL::ShaderNode::Uniform::HINT_MAX] = { + "", + "color", + "range", + "albedo", + "normal", + "black", + "white" + }; + + if (E.value.hint) { + ucode += " : " + String(hint_name[E.value.hint]); + } } code += ucode + "\n"; } - for (Map<StringName, SL::ShaderNode::Varying>::Element *E = pnode->varyings.front(); E; E = E->next()) { + for (const KeyValue<StringName, SL::ShaderNode::Varying> &E : pnode->varyings) { String vcode = "varying "; - vcode += _prestr(E->get().precision); - vcode += _typestr(E->get().type); - vcode += " " + String(E->key()); + vcode += _prestr(E.value.precision); + vcode += _typestr(E.value.type); + vcode += " " + String(E.key); code += vcode + "\n"; } @@ -183,8 +188,8 @@ static String dump_node_code(SL::Node *p_node, int p_level) { //variables code += _mktab(p_level - 1) + "{\n"; - for (Map<StringName, SL::BlockNode::Variable>::Element *E = bnode->variables.front(); E; E = E->next()) { - code += _mktab(p_level) + _prestr(E->get().precision) + _typestr(E->get().type) + " " + E->key() + ";\n"; + for (const KeyValue<StringName, SL::BlockNode::Variable> &E : bnode->variables) { + code += _mktab(p_level) + _prestr(E.value.precision) + _typestr(E.value.type) + " " + E.key + ";\n"; } for (int i = 0; i < bnode->statements.size(); i++) { diff --git a/tests/test_string.h b/tests/test_string.h index 1982d8de60..28d1089d2f 100644 --- a/tests/test_string.h +++ b/tests/test_string.h @@ -39,6 +39,7 @@ #include "core/os/main_loop.h" #include "core/os/os.h" #include "core/string/ustring.h" +#include "core/variant/variant.h" #include "tests/test_macros.h" @@ -350,13 +351,47 @@ TEST_CASE("[String] Insertion") { } TEST_CASE("[String] Number to string") { + CHECK(String::num(0) == "0"); + CHECK(String::num(0.0) == "0"); // No trailing zeros. + CHECK(String::num(-0.0) == "-0"); // Includes sign even for zero. CHECK(String::num(3.141593) == "3.141593"); CHECK(String::num(3.141593, 3) == "3.142"); - CHECK(String::num_real(3.141593) == "3.141593"); CHECK(String::num_scientific(30000000) == "3e+07"); CHECK(String::num_int64(3141593) == "3141593"); CHECK(String::num_int64(0xA141593, 16) == "a141593"); CHECK(String::num_int64(0xA141593, 16, true) == "A141593"); + CHECK(String::num(42.100023, 4) == "42.1"); // No trailing zeros. + + // String::num_real tests. + CHECK(String::num_real(3.141593) == "3.141593"); + CHECK(String::num_real(3.141) == "3.141"); // No trailing zeros. +#ifdef REAL_T_IS_DOUBLE + CHECK_MESSAGE(String::num_real(Math_PI) == "3.14159265358979", "Prints the appropriate amount of digits for real_t = double."); + CHECK_MESSAGE(String::num_real(3.1415f) == "3.14149999618530", "Prints more digits of 32-bit float when real_t = double (ones that would be reliable for double)."); +#else + CHECK_MESSAGE(String::num_real(Math_PI) == "3.141593", "Prints the appropriate amount of digits for real_t = float."); + CHECK_MESSAGE(String::num_real(3.1415f) == "3.1415", "Prints only reliable digits of 32-bit float when real_t = float."); +#endif // REAL_T_IS_DOUBLE + + // Checks doubles with many decimal places. + CHECK(String::num(0.0000012345432123454321, -1) == "0.00000123454321"); // -1 uses 14 as sane default. + CHECK(String::num(0.0000012345432123454321) == "0.00000123454321"); // -1 is the default value. + CHECK(String::num(-0.0000012345432123454321) == "-0.00000123454321"); + CHECK(String::num(-10000.0000012345432123454321) == "-10000.0000012345"); + CHECK(String::num(0.0000000000012345432123454321) == "0.00000000000123"); + CHECK(String::num(0.0000000000012345432123454321, 3) == "0"); + + // Note: When relevant (remainder > 0.5), the last digit gets rounded up, + // which can also lead to not include a trailing zero, e.g. "...89" -> "...9". + CHECK(String::num(0.0000056789876567898765) == "0.00000567898766"); // Should round last digit. + CHECK(String::num(10000.000005678999999999) == "10000.000005679"); // We cut at ...789|99 which is rounded to ...79, so only 13 decimals. + CHECK(String::num(42.12999999, 6) == "42.13"); // Also happens with lower decimals count. + + // 32 is MAX_DECIMALS. We can't reliably store that many so we can't compare against a string, + // but we can check that the string length is 34 (32 + 2 for "0."). + CHECK(String::num(0.00000123456789987654321123456789987654321, 32).length() == 34); + CHECK(String::num(0.00000123456789987654321123456789987654321, 42).length() == 34); // Should enforce MAX_DECIMALS. + CHECK(String::num(10000.00000123456789987654321123456789987654321, 42).length() == 38); // 32 decimals + "10000.". } TEST_CASE("[String] String to integer") { @@ -1118,20 +1153,20 @@ TEST_CASE("[String] dedent") { } TEST_CASE("[String] Path functions") { - static const char *path[4] = { "C:\\Godot\\project\\test.tscn", "/Godot/project/test.xscn", "../Godot/project/test.scn", "Godot\\test.doc" }; - static const char *base_dir[4] = { "C:\\Godot\\project", "/Godot/project", "../Godot/project", "Godot" }; - static const char *base_name[4] = { "C:\\Godot\\project\\test", "/Godot/project/test", "../Godot/project/test", "Godot\\test" }; - static const char *ext[4] = { "tscn", "xscn", "scn", "doc" }; - static const char *file[4] = { "test.tscn", "test.xscn", "test.scn", "test.doc" }; - static const bool abs[4] = { true, true, false, false }; - - for (int i = 0; i < 4; i++) { + static const char *path[7] = { "C:\\Godot\\project\\test.tscn", "/Godot/project/test.xscn", "../Godot/project/test.scn", "Godot\\test.doc", "C:\\test.", "res://test", "/.test" }; + static const char *base_dir[7] = { "C:\\Godot\\project", "/Godot/project", "../Godot/project", "Godot", "C:\\", "res://", "/" }; + static const char *base_name[7] = { "C:\\Godot\\project\\test", "/Godot/project/test", "../Godot/project/test", "Godot\\test", "C:\\test", "res://test", "/" }; + static const char *ext[7] = { "tscn", "xscn", "scn", "doc", "", "", "test" }; + static const char *file[7] = { "test.tscn", "test.xscn", "test.scn", "test.doc", "test.", "test", ".test" }; + static const bool abs[7] = { true, true, false, false, true, true, true }; + + for (int i = 0; i < 7; i++) { CHECK(String(path[i]).get_base_dir() == base_dir[i]); CHECK(String(path[i]).get_basename() == base_name[i]); CHECK(String(path[i]).get_extension() == ext[i]); CHECK(String(path[i]).get_file() == file[i]); CHECK(String(path[i]).is_absolute_path() == abs[i]); - CHECK(String(path[i]).is_rel_path() != abs[i]); + CHECK(String(path[i]).is_relative_path() != abs[i]); CHECK(String(path[i]).simplify_path().get_base_dir().plus_file(file[i]) == String(path[i]).simplify_path()); } @@ -1346,6 +1381,78 @@ TEST_CASE("[String] validate_node_name") { String name_with_invalid_chars = "Name with invalid characters :.@removed!"; CHECK(name_with_invalid_chars.validate_node_name() == "Name with invalid characters removed!"); } + +TEST_CASE("[String] Variant indexed get") { + Variant s = String("abcd"); + bool valid = false; + bool oob = true; + + String r = s.get_indexed(1, valid, oob); + + CHECK(valid); + CHECK_FALSE(oob); + CHECK_EQ(r, String("b")); +} + +TEST_CASE("[String] Variant validated indexed get") { + Variant s = String("abcd"); + + Variant::ValidatedIndexedGetter getter = Variant::get_member_validated_indexed_getter(Variant::STRING); + + Variant r; + bool oob = true; + getter(&s, 1, &r, &oob); + + CHECK_FALSE(oob); + CHECK_EQ(r, String("b")); +} + +TEST_CASE("[String] Variant ptr indexed get") { + String s("abcd"); + + Variant::PTRIndexedGetter getter = Variant::get_member_ptr_indexed_getter(Variant::STRING); + + String r; + getter(&s, 1, &r); + + CHECK_EQ(r, String("b")); +} + +TEST_CASE("[String] Variant indexed set") { + Variant s = String("abcd"); + bool valid = false; + bool oob = true; + + s.set_indexed(1, String("z"), valid, oob); + + CHECK(valid); + CHECK_FALSE(oob); + CHECK_EQ(s, String("azcd")); +} + +TEST_CASE("[String] Variant validated indexed set") { + Variant s = String("abcd"); + + Variant::ValidatedIndexedSetter setter = Variant::get_member_validated_indexed_setter(Variant::STRING); + + Variant v = String("z"); + bool oob = true; + setter(&s, 1, &v, &oob); + + CHECK_FALSE(oob); + CHECK_EQ(s, String("azcd")); +} + +TEST_CASE("[String] Variant ptr indexed set") { + String s("abcd"); + + Variant::PTRIndexedSetter setter = Variant::get_member_ptr_indexed_setter(Variant::STRING); + + String v("z"); + setter(&s, 1, &v); + + CHECK_EQ(s, String("azcd")); +} } // namespace TestString #endif // TEST_STRING_H diff --git a/tests/test_text_server.h b/tests/test_text_server.h index cac022e33f..af3df7bc79 100644 --- a/tests/test_text_server.h +++ b/tests/test_text_server.h @@ -41,33 +41,31 @@ namespace TestTextServer { TEST_SUITE("[[TextServer]") { TEST_CASE("[TextServer] Init, font loading and shaping") { - TextServerManager *tsman = memnew(TextServerManager); - Error err = OK; - - SUBCASE("[TextServer] Init") { - for (int i = 0; i < TextServerManager::get_interface_count(); i++) { - TextServer *ts = TextServerManager::initialize(i, err); - TEST_FAIL_COND((err != OK || ts == nullptr), "Text server ", TextServerManager::get_interface_name(i), " init failed."); - } - } - SUBCASE("[TextServer] Loading fonts") { - for (int i = 0; i < TextServerManager::get_interface_count(); i++) { - TextServer *ts = TextServerManager::initialize(i, err); + for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) { + Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i); + TEST_FAIL_COND(ts.is_null(), "Invalid TS interface."); - RID font = ts->create_font_memory(_font_NotoSans_Regular, _font_NotoSans_Regular_size, "ttf"); + RID font = ts->create_font(); + ts->font_set_data_ptr(font, _font_NotoSans_Regular, _font_NotoSans_Regular_size); TEST_FAIL_COND(font == RID(), "Loading font failed."); ts->free(font); } } SUBCASE("[TextServer] Text layout: Font fallback") { - for (int i = 0; i < TextServerManager::get_interface_count(); i++) { - TextServer *ts = TextServerManager::initialize(i, err); + for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) { + Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i); + TEST_FAIL_COND(ts.is_null(), "Invalid TS interface."); + + RID font1 = ts->create_font(); + ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size); + RID font2 = ts->create_font(); + ts->font_set_data_ptr(font2, _font_NotoSansThaiUI_Regular, _font_NotoSansThaiUI_Regular_size); Vector<RID> font; - font.push_back(ts->create_font_memory(_font_NotoSans_Regular, _font_NotoSans_Regular_size, "ttf")); - font.push_back(ts->create_font_memory(_font_NotoSansThaiUI_Regular, _font_NotoSansThaiUI_Regular_size, "ttf")); + font.push_back(font1); + font.push_back(font2); String test = U"คนอ้วน khon uan ראה"; // 6^ 17^ @@ -77,9 +75,10 @@ TEST_SUITE("[[TextServer]") { bool ok = ts->shaped_text_add_string(ctx, test, font, 16); TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); - Vector<TextServer::Glyph> glyphs = ts->shaped_text_get_glyphs(ctx); - TEST_FAIL_COND(glyphs.size() == 0, "Shaping failed"); - for (int j = 0; j < glyphs.size(); j++) { + const Glyph *glyphs = ts->shaped_text_get_glyphs(ctx); + int gl_size = ts->shaped_text_get_glyph_count(ctx); + TEST_FAIL_COND(gl_size == 0, "Shaping failed"); + for (int j = 0; j < gl_size; j++) { if (glyphs[j].start < 6) { TEST_FAIL_COND(glyphs[j].font_rid != font[1], "Incorrect font selected."); } @@ -104,16 +103,22 @@ TEST_SUITE("[[TextServer]") { } SUBCASE("[TextServer] Text layout: BiDi") { - for (int i = 0; i < TextServerManager::get_interface_count(); i++) { - TextServer *ts = TextServerManager::initialize(i, err); + for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) { + Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i); + TEST_FAIL_COND(ts.is_null(), "Invalid TS interface."); if (!ts->has_feature(TextServer::FEATURE_BIDI_LAYOUT)) { continue; } + RID font1 = ts->create_font(); + ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size); + RID font2 = ts->create_font(); + ts->font_set_data_ptr(font2, _font_NotoNaskhArabicUI_Regular, _font_NotoNaskhArabicUI_Regular_size); + Vector<RID> font; - font.push_back(ts->create_font_memory(_font_NotoSans_Regular, _font_NotoSans_Regular_size, "ttf")); - font.push_back(ts->create_font_memory(_font_NotoNaskhArabicUI_Regular, _font_NotoNaskhArabicUI_Regular_size, "ttf")); + font.push_back(font1); + font.push_back(font2); String test = U"Arabic (اَلْعَرَبِيَّةُ, al-ʿarabiyyah)"; // 7^ 26^ @@ -123,9 +128,10 @@ TEST_SUITE("[[TextServer]") { bool ok = ts->shaped_text_add_string(ctx, test, font, 16); TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); - Vector<TextServer::Glyph> glyphs = ts->shaped_text_get_glyphs(ctx); - TEST_FAIL_COND(glyphs.size() == 0, "Shaping failed"); - for (int j = 0; j < glyphs.size(); j++) { + const Glyph *glyphs = ts->shaped_text_get_glyphs(ctx); + int gl_size = ts->shaped_text_get_glyph_count(ctx); + TEST_FAIL_COND(gl_size == 0, "Shaping failed"); + for (int j = 0; j < gl_size; j++) { if (glyphs[j].count > 0) { if (glyphs[j].start < 7) { TEST_FAIL_COND(((glyphs[j].flags & TextServer::GRAPHEME_IS_RTL) == TextServer::GRAPHEME_IS_RTL), "Incorrect direction."); @@ -149,27 +155,38 @@ TEST_SUITE("[[TextServer]") { } SUBCASE("[TextServer] Text layout: Line breaking") { - for (int i = 0; i < TextServerManager::get_interface_count(); i++) { - TextServer *ts = TextServerManager::initialize(i, err); + for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) { + Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i); + TEST_FAIL_COND(ts.is_null(), "Invalid TS interface."); String test_1 = U"test test test"; // 5^ 10^ + RID font1 = ts->create_font(); + ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size); + RID font2 = ts->create_font(); + ts->font_set_data_ptr(font2, _font_NotoSansThaiUI_Regular, _font_NotoSansThaiUI_Regular_size); + Vector<RID> font; - font.push_back(ts->create_font_memory(_font_NotoSans_Regular, _font_NotoSans_Regular_size, "ttf")); - font.push_back(ts->create_font_memory(_font_NotoSansThaiUI_Regular, _font_NotoSansThaiUI_Regular_size, "ttf")); + font.push_back(font1); + font.push_back(font2); RID ctx = ts->create_shaped_text(); TEST_FAIL_COND(ctx == RID(), "Creating text buffer failed."); bool ok = ts->shaped_text_add_string(ctx, test_1, font, 16); TEST_FAIL_COND(!ok, "Adding text to the buffer failed."); - Vector<Vector2i> brks = ts->shaped_text_get_line_breaks(ctx, 1); - TEST_FAIL_COND(brks.size() != 3, "Invalid line breaks number."); - if (brks.size() == 3) { - TEST_FAIL_COND(brks[0] != Vector2i(0, 5), "Invalid line break position."); - TEST_FAIL_COND(brks[1] != Vector2i(5, 10), "Invalid line break position."); - TEST_FAIL_COND(brks[2] != Vector2i(10, 14), "Invalid line break position."); + PackedInt32Array brks = ts->shaped_text_get_line_breaks(ctx, 1); + TEST_FAIL_COND(brks.size() != 6, "Invalid line breaks number."); + if (brks.size() == 6) { + TEST_FAIL_COND(brks[0] != 0, "Invalid line break position."); + TEST_FAIL_COND(brks[1] != 5, "Invalid line break position."); + + TEST_FAIL_COND(brks[2] != 5, "Invalid line break position."); + TEST_FAIL_COND(brks[3] != 10, "Invalid line break position."); + + TEST_FAIL_COND(brks[4] != 10, "Invalid line break position."); + TEST_FAIL_COND(brks[5] != 14, "Invalid line break position."); } ts->free(ctx); @@ -182,12 +199,18 @@ TEST_SUITE("[[TextServer]") { } SUBCASE("[TextServer] Text layout: Justification") { - for (int i = 0; i < TextServerManager::get_interface_count(); i++) { - TextServer *ts = TextServerManager::initialize(i, err); + for (int i = 0; i < TextServerManager::get_singleton()->get_interface_count(); i++) { + Ref<TextServer> ts = TextServerManager::get_singleton()->get_interface(i); + TEST_FAIL_COND(ts.is_null(), "Invalid TS interface."); + + RID font1 = ts->create_font(); + ts->font_set_data_ptr(font1, _font_NotoSans_Regular, _font_NotoSans_Regular_size); + RID font2 = ts->create_font(); + ts->font_set_data_ptr(font2, _font_NotoNaskhArabicUI_Regular, _font_NotoNaskhArabicUI_Regular_size); Vector<RID> font; - font.push_back(ts->create_font_memory(_font_NotoSans_Regular, _font_NotoSans_Regular_size, "ttf")); - font.push_back(ts->create_font_memory(_font_NotoNaskhArabicUI_Regular, _font_NotoNaskhArabicUI_Regular_size, "ttf")); + font.push_back(font1); + font.push_back(font2); String test_1 = U"الحمد"; String test_2 = U"الحمد test"; @@ -242,8 +265,6 @@ TEST_SUITE("[[TextServer]") { font.clear(); } } - - memdelete(tsman); } } }; // namespace TestTextServer diff --git a/tests/test_tools.h b/tests/test_tools.h new file mode 100644 index 0000000000..ec18610f04 --- /dev/null +++ b/tests/test_tools.h @@ -0,0 +1,61 @@ +/*************************************************************************/ +/* test_tools.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_TOOLS_H +#define TEST_TOOLS_H + +#include "core/error/error_macros.h" + +struct ErrorDetector { + ErrorDetector() { + eh.errfunc = _detect_error; + eh.userdata = this; + + add_error_handler(&eh); + } + + ~ErrorDetector() { + remove_error_handler(&eh); + } + + void clear() { + has_error = false; + } + + static void _detect_error(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type) { + ErrorDetector *self = (ErrorDetector *)p_self; + self->has_error = true; + } + + ErrorHandlerList eh; + bool has_error = false; +}; + +#endif // TEST_TOOLS_H diff --git a/tests/test_translation.h b/tests/test_translation.h index 52ff49bf9b..93c53bbbc9 100644 --- a/tests/test_translation.h +++ b/tests/test_translation.h @@ -35,6 +35,10 @@ #include "core/string/translation.h" #include "core/string/translation_po.h" +#ifdef TOOLS_ENABLED +#include "editor/import/resource_importer_csv_translation.h" +#endif + #include "thirdparty/doctest/doctest.h" namespace TestTranslation { @@ -145,6 +149,33 @@ TEST_CASE("[OptimizedTranslation] Generate from Translation and read messages") CHECK(messages.size() == 0); } +#ifdef TOOLS_ENABLED +TEST_CASE("[Translation] CSV import") { + Ref<ResourceImporterCSVTranslation> import_csv_translation = memnew(ResourceImporterCSVTranslation); + + Map<StringName, Variant> options; + options["compress"] = false; + options["delimiter"] = 0; + + List<String> gen_files; + + Error result = import_csv_translation->import(TestUtils::get_data_path("translations.csv"), + "", options, nullptr, &gen_files); + CHECK(result == OK); + CHECK(gen_files.size() == 2); + + for (const String &file : gen_files) { + Ref<Translation> translation = ResourceLoader::load(file); + CHECK(translation.is_valid()); + TranslationServer::get_singleton()->add_translation(translation); + } + + TranslationServer::get_singleton()->set_locale("de"); + + CHECK(Object().tr("GOOD_MORNING", "") == "Guten Morgen"); +} +#endif // TOOLS_ENABLED + } // namespace TestTranslation #endif // TEST_TRANSLATION_H diff --git a/tests/test_validate_testing.h b/tests/test_validate_testing.h index f301047509..40b255e18a 100644 --- a/tests/test_validate_testing.h +++ b/tests/test_validate_testing.h @@ -34,6 +34,7 @@ #include "core/os/os.h" #include "tests/test_macros.h" +#include "tests/test_tools.h" TEST_SUITE("Validate tests") { TEST_CASE("Always pass") { @@ -182,6 +183,17 @@ TEST_SUITE("Validate tests") { // doctest string concatenation. CHECK_MESSAGE(true, var, " ", vec2, " ", rect2, " ", color); } + TEST_CASE("Detect error messages") { + ErrorDetector ed; + + REQUIRE_FALSE(ed.has_error); + + ERR_PRINT_OFF; + ERR_PRINT("Still waiting for Godot!"); + ERR_PRINT_ON; + + REQUIRE(ed.has_error); + } } #endif // TEST_VALIDATE_TESTING_H diff --git a/tests/test_variant.h b/tests/test_variant.h index dfc72b512c..598fe488d7 100644 --- a/tests/test_variant.h +++ b/tests/test_variant.h @@ -39,7 +39,7 @@ namespace TestVariant { TEST_CASE("[Variant] Writer and parser integer") { - int64_t a32 = 2147483648; // 2^31, so out of bounds for 32-bit signed int [-2^31,-2^31-1]. + int64_t a32 = 2147483648; // 2^31, so out of bounds for 32-bit signed int [-2^31, +2^31-1]. String a32_str; VariantWriter::write_to_string(a32, a32_str); @@ -76,34 +76,35 @@ TEST_CASE("[Variant] Writer and parser integer") { CHECK_MESSAGE(b64_int_parsed == 9223372036854775807, "The result should be clamped to max value."); } -TEST_CASE("[Variant] Writer and parser float") { - // Assuming real_t is double. - real_t a64 = 340282346638528859811704183484516925440.0; // std::numeric_limits<real_t>::max() +TEST_CASE("[Variant] Writer and parser Variant::FLOAT") { + // Variant::FLOAT is always 64-bit (C++ double). + // This is the maximum non-infinity double-precision float. + double a64 = 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0; String a64_str; VariantWriter::write_to_string(a64, a64_str); - CHECK_MESSAGE(a64_str == "3.40282e+38", "Writes in scientific notation."); + CHECK_MESSAGE(a64_str == "1.79769e+308", "Writes in scientific notation."); CHECK_MESSAGE(a64_str != "inf", "Should not overflow."); CHECK_MESSAGE(a64_str != "nan", "The result should be defined."); - VariantParser::StreamString ss; String errs; int line; - Variant b64_parsed; - real_t b64_float_parsed; - - ss.s = a64_str; - VariantParser::parse(&ss, b64_parsed, errs, line); - b64_float_parsed = b64_parsed; + Variant variant_parsed; + double float_parsed; - CHECK_MESSAGE(b64_float_parsed == 340282001837565597733306976381245063168.0, "Should parse back."); + VariantParser::StreamString bss; + bss.s = a64_str; + VariantParser::parse(&bss, variant_parsed, errs, line); + float_parsed = variant_parsed; // Loses precision, but that's alright. - - ss.s = "1.0e+100"; // Float version of Googol! - VariantParser::parse(&ss, b64_parsed, errs, line); - b64_float_parsed = b64_parsed; - - CHECK_MESSAGE(b64_float_parsed == 340282001837565597733306976381245063168.0, "Should not overflow."); + CHECK_MESSAGE(float_parsed == 1.79769e+308, "Should parse back."); + + // Approximation of Googol with a double-precision float. + VariantParser::StreamString css; + css.s = "1.0e+100"; + VariantParser::parse(&css, variant_parsed, errs, line); + float_parsed = variant_parsed; + CHECK_MESSAGE(float_parsed == 1.0e+100, "Should match the double literal."); } TEST_CASE("[Variant] Assignment To Bool from Int,Float,String,Vec2,Vec2i,Vec3,Vec3i and Color") { diff --git a/tests/test_vector.h b/tests/test_vector.h index 02c56e59f6..bfdf389aa7 100644 --- a/tests/test_vector.h +++ b/tests/test_vector.h @@ -472,6 +472,19 @@ TEST_CASE("[Vector] Sort custom") { 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); |