diff --git a/core/variant/dictionary.cpp b/core/variant/dictionary.cpp index 5f2ff1219bb..ed6693daa38 100644 --- a/core/variant/dictionary.cpp +++ b/core/variant/dictionary.cpp @@ -654,6 +654,10 @@ bool Dictionary::is_typed_value() const { return _p->typed_value.type != Variant::NIL; } +bool Dictionary::is_same_instance(const Dictionary &p_other) const { + return _p == p_other._p; +} + bool Dictionary::is_same_typed(const Dictionary &p_other) const { return is_same_typed_key(p_other) && is_same_typed_value(p_other); } diff --git a/core/variant/dictionary.h b/core/variant/dictionary.h index 45005d2b2b8..86c50b1c037 100644 --- a/core/variant/dictionary.h +++ b/core/variant/dictionary.h @@ -108,6 +108,7 @@ public: bool is_typed() const; bool is_typed_key() const; bool is_typed_value() const; + bool is_same_instance(const Dictionary &p_other) const; bool is_same_typed(const Dictionary &p_other) const; bool is_same_typed_key(const Dictionary &p_other) const; bool is_same_typed_value(const Dictionary &p_other) const; diff --git a/doc/classes/Array.xml b/doc/classes/Array.xml index d21bd3ac186..35415b3b3ac 100644 --- a/doc/classes/Array.xml +++ b/doc/classes/Array.xml @@ -332,7 +332,7 @@ Returns a new copy of the array. - By default, a [b]shallow[/b] copy is returned: all nested [Array], [Dictionary], and [Resource] elements are shared with the original array. Modifying them in one array will also affect them in the other. + By default, a [b]shallow[/b] copy is returned: all nested [Array], [Dictionary], and [Resource] elements are shared with the original array. Modifying any of those in one array will also affect them in the other. If [param deep] is [code]true[/code], a [b]deep[/b] copy is returned: all nested arrays and dictionaries are also duplicated (recursively). Any [Resource] is still shared with the original array, though. diff --git a/doc/classes/Dictionary.xml b/doc/classes/Dictionary.xml index 031ad415e75..efaeefd151b 100644 --- a/doc/classes/Dictionary.xml +++ b/doc/classes/Dictionary.xml @@ -188,7 +188,7 @@ Returns a new copy of the dictionary. - By default, a [b]shallow[/b] copy is returned: all nested [Array], [Dictionary], and [Resource] keys and values are shared with the original dictionary. Modifying them in one dictionary will also affect them in the other. + By default, a [b]shallow[/b] copy is returned: all nested [Array], [Dictionary], and [Resource] keys and values are shared with the original dictionary. Modifying any of those in one dictionary will also affect them in the other. If [param deep] is [code]true[/code], a [b]deep[/b] copy is returned: all nested arrays and dictionaries are also duplicated (recursively). Any [Resource] is still shared with the original dictionary, though. diff --git a/modules/gdscript/tests/scripts/runtime/features/duplicate_resource.gd b/modules/gdscript/tests/scripts/runtime/features/duplicate_resource.gd new file mode 100644 index 00000000000..507797d6cda --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/duplicate_resource.gd @@ -0,0 +1,40 @@ +# We could use @export_custom to really test every property usage, but we know for good +# that duplicating scripted properties flows through the same code already thoroughly tested +# in the [Resource] test cases. The same goes for all the potential deep duplicate modes. +# Therefore, it's enough to ensure the exported scriped properties are copied when invoking +# duplication by each entry point. +class TestResource: + extends Resource + @export var text: String = "holaaa" + @export var arr: Array = [1, 2, 3] + @export var dict: Dictionary = { "a": 1, "b": 2 } + +func test(): + # Via Resource type. + var res := TestResource.new() + var dupe: TestResource + + dupe = res.duplicate() + print(dupe.text) + print(dupe.arr) + print(dupe.dict) + + dupe = res.duplicate_deep() + print(dupe.text) + print(dupe.arr) + print(dupe.dict) + + # Via Variant type. + + var res_var = TestResource.new() + var dupe_var + + dupe_var = res_var.duplicate() + print(dupe_var.text) + print(dupe_var.arr) + print(dupe_var.dict) + + dupe_var = res_var.duplicate_deep() + print(dupe_var.text) + print(dupe_var.arr) + print(dupe_var.dict) diff --git a/modules/gdscript/tests/scripts/runtime/features/duplicate_resource.out b/modules/gdscript/tests/scripts/runtime/features/duplicate_resource.out new file mode 100644 index 00000000000..a8e8dc2f9d7 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/duplicate_resource.out @@ -0,0 +1,13 @@ +GDTEST_OK +holaaa +[1, 2, 3] +{ "a": 1, "b": 2 } +holaaa +[1, 2, 3] +{ "a": 1, "b": 2 } +holaaa +[1, 2, 3] +{ "a": 1, "b": 2 } +holaaa +[1, 2, 3] +{ "a": 1, "b": 2 } diff --git a/tests/core/io/test_resource.h b/tests/core/io/test_resource.h index ef98a8c0138..83b21762cef 100644 --- a/tests/core/io/test_resource.h +++ b/tests/core/io/test_resource.h @@ -34,37 +34,505 @@ #include "core/io/resource_loader.h" #include "core/io/resource_saver.h" #include "core/os/os.h" +#include "scene/main/node.h" #include "thirdparty/doctest/doctest.h" #include "tests/test_macros.h" +#include + namespace TestResource { +enum TestDuplicateMode { + TEST_MODE_RESOURCE_DUPLICATE_SHALLOW, + TEST_MODE_RESOURCE_DUPLICATE_DEEP, + TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE, + TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE, + TEST_MODE_VARIANT_DUPLICATE_SHALLOW, + TEST_MODE_VARIANT_DUPLICATE_DEEP, + TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE, +}; + +class DuplicateGuineaPigData : public Object { + GDSOFTCLASS(DuplicateGuineaPigData, Object) + +public: + const Variant SENTINEL_1 = "A"; + const Variant SENTINEL_2 = 645; + const Variant SENTINEL_3 = StringName("X"); + const Variant SENTINEL_4 = true; + + Ref SUBRES_1 = memnew(Resource); + Ref SUBRES_2 = memnew(Resource); + Ref SUBRES_3 = memnew(Resource); + Ref SUBRES_SL_1 = memnew(Resource); + Ref SUBRES_SL_2 = memnew(Resource); + Ref SUBRES_SL_3 = memnew(Resource); + + Variant obj; // Variant helps with lifetime so duplicates pointing to the same don't try to double-free it. + Array arr; + Dictionary dict; + Variant packed; // A PackedByteArray, but using Variant to be able to tell if the array is shared or not. + Ref subres; + Ref subres_sl; + + void set_defaults() { + SUBRES_1->set_name("juan"); + SUBRES_2->set_name("you"); + SUBRES_3->set_name("tree"); + SUBRES_SL_1->set_name("maybe_scene_local"); + SUBRES_SL_2->set_name("perhaps_local_to_scene"); + SUBRES_SL_3->set_name("sometimes_locality_scenial"); + + // To try some cases of internal and external. + SUBRES_1->set_path_cache(""); + SUBRES_2->set_path_cache("local://hehe"); + SUBRES_3->set_path_cache("res://some.tscn::1"); + DEV_ASSERT(SUBRES_1->is_built_in()); + DEV_ASSERT(SUBRES_2->is_built_in()); + DEV_ASSERT(SUBRES_3->is_built_in()); + SUBRES_SL_1->set_path_cache("res://thing.scn"); + SUBRES_SL_2->set_path_cache("C:/not/really/possible/but/still/external"); + SUBRES_SL_3->set_path_cache("/this/neither"); + DEV_ASSERT(!SUBRES_SL_1->is_built_in()); + DEV_ASSERT(!SUBRES_SL_2->is_built_in()); + DEV_ASSERT(!SUBRES_SL_3->is_built_in()); + + obj = memnew(Object); + + // Construct enough cases to test deep recursion involving resources; + // we mix some primitive values with recurses nested in different ways, + // acting as array values and dictionary keys and values, some of those + // being marked as scene-local when for subcases where scene-local is relevant. + + arr.push_back(SENTINEL_1); + arr.push_back(SUBRES_1); + arr.push_back(SUBRES_SL_1); + { + Dictionary d; + d[SENTINEL_2] = SENTINEL_3; + d[SENTINEL_4] = SUBRES_2; + d[SUBRES_3] = SUBRES_SL_2; + d[SUBRES_SL_3] = SUBRES_1; + arr.push_back(d); + } + + dict[SENTINEL_4] = SENTINEL_1; + dict[SENTINEL_2] = SUBRES_2; + dict[SUBRES_3] = SUBRES_SL_1; + dict[SUBRES_SL_2] = SUBRES_1; + { + Array a; + a.push_back(SENTINEL_3); + a.push_back(SUBRES_2); + a.push_back(SUBRES_SL_3); + dict[SENTINEL_4] = a; + } + + packed = PackedByteArray{ 0xaa, 0xbb, 0xcc }; + + subres = SUBRES_1; + subres_sl = SUBRES_SL_1; + } + + void verify_empty() const { + CHECK(obj.get_type() == Variant::NIL); + CHECK(arr.size() == 0); + CHECK(dict.size() == 0); + CHECK(packed.get_type() == Variant::NIL); + CHECK(subres.is_null()); + } + + void verify_duplication(const DuplicateGuineaPigData *p_orig, uint32_t p_property_usage, TestDuplicateMode p_test_mode, ResourceDeepDuplicateMode p_deep_mode) const { + if (!(p_property_usage & PROPERTY_USAGE_STORAGE)) { + verify_empty(); + return; + } + + // To see if each resource involved is copied once at most, + // and then the reference to the duplicate reused. + HashMap duplicates; + + auto _verify_resource = [&](const Ref &p_dupe_res, const Ref &p_orig_res, bool p_is_property = false) { + bool expect_true_copy = (p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP && p_orig_res->is_built_in()) || + (p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_INTERNAL && p_orig_res->is_built_in()) || + (p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_ALL) || + (p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE && p_orig_res->is_local_to_scene()) || + (p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_INTERNAL && p_orig_res->is_built_in()) || + (p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_ALL); + + if (expect_true_copy) { + if (p_deep_mode == RESOURCE_DEEP_DUPLICATE_NONE) { + expect_true_copy = false; + } else if (p_deep_mode == RESOURCE_DEEP_DUPLICATE_INTERNAL) { + expect_true_copy = p_orig_res->is_built_in(); + } + } + + if (p_is_property) { + if ((p_property_usage & PROPERTY_USAGE_ALWAYS_DUPLICATE)) { + expect_true_copy = true; + } else if ((p_property_usage & PROPERTY_USAGE_NEVER_DUPLICATE)) { + expect_true_copy = false; + } + } + + if (expect_true_copy) { + CHECK(p_dupe_res != p_orig_res); + CHECK(p_dupe_res->get_name() == p_orig_res->get_name()); + if (duplicates.has(p_orig_res.ptr())) { + CHECK(duplicates[p_orig_res.ptr()] == p_dupe_res.ptr()); + } else { + duplicates[p_orig_res.ptr()] = p_dupe_res.ptr(); + } + } else { + CHECK(p_dupe_res == p_orig_res); + } + }; + + std::function _verify_deep_copied_variants = [&](const Variant &p_a, const Variant &p_b) { + CHECK(p_a.get_type() == p_b.get_type()); + const Ref &res_a = p_a; + const Ref &res_b = p_b; + if (res_a.is_valid()) { + _verify_resource(res_a, res_b); + } else if (p_a.get_type() == Variant::ARRAY) { + const Array &arr_a = p_a; + const Array &arr_b = p_b; + CHECK(!arr_a.is_same_instance(arr_b)); + CHECK(arr_a.size() == arr_b.size()); + for (int i = 0; i < arr_a.size(); i++) { + _verify_deep_copied_variants(arr_a[i], arr_b[i]); + } + } else if (p_a.get_type() == Variant::DICTIONARY) { + const Dictionary &dict_a = p_a; + const Dictionary &dict_b = p_b; + CHECK(!dict_a.is_same_instance(dict_b)); + CHECK(dict_a.size() == dict_b.size()); + for (int i = 0; i < dict_a.size(); i++) { + _verify_deep_copied_variants(dict_a.get_key_at_index(i), dict_b.get_key_at_index(i)); + _verify_deep_copied_variants(dict_a.get_value_at_index(i), dict_b.get_value_at_index(i)); + } + } else { + CHECK(p_a == p_b); + } + }; + + CHECK(this != p_orig); + + CHECK((Object *)obj == (Object *)p_orig->obj); + + bool expect_true_copy = p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP || + p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE || + p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE || + p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP || + p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE; + if (expect_true_copy) { + _verify_deep_copied_variants(arr, p_orig->arr); + _verify_deep_copied_variants(dict, p_orig->dict); + CHECK(!packed.identity_compare(p_orig->packed)); + } else { + CHECK(arr.is_same_instance(p_orig->arr)); + CHECK(dict.is_same_instance(p_orig->dict)); + CHECK(packed.identity_compare(p_orig->packed)); + } + + _verify_resource(subres, p_orig->subres, true); + _verify_resource(subres_sl, p_orig->subres_sl, true); + } + + void enable_scene_local_subresources() { + SUBRES_SL_1->set_local_to_scene(true); + SUBRES_SL_2->set_local_to_scene(true); + SUBRES_SL_3->set_local_to_scene(true); + } + + virtual ~DuplicateGuineaPigData() { + Object *obj_ptr = obj.get_validated_object(); + if (obj_ptr) { + memdelete(obj_ptr); + } + } +}; + +#define DEFINE_DUPLICATE_GUINEA_PIG(m_class_name, m_property_usage) \ + class m_class_name : public Resource { \ + GDCLASS(m_class_name, Resource) \ + \ + DuplicateGuineaPigData data; \ + \ + public: \ + void set_obj(Object *p_obj) { \ + data.obj = p_obj; \ + } \ + Object *get_obj() const { \ + return data.obj; \ + } \ + \ + void set_arr(const Array &p_arr) { \ + data.arr = p_arr; \ + } \ + Array get_arr() const { \ + return data.arr; \ + } \ + \ + void set_dict(const Dictionary &p_dict) { \ + data.dict = p_dict; \ + } \ + Dictionary get_dict() const { \ + return data.dict; \ + } \ + \ + void set_packed(const Variant &p_packed) { \ + data.packed = p_packed; \ + } \ + Variant get_packed() const { \ + return data.packed; \ + } \ + \ + void set_subres(const Ref &p_subres) { \ + data.subres = p_subres; \ + } \ + Ref get_subres() const { \ + return data.subres; \ + } \ + \ + void set_subres_sl(const Ref &p_subres) { \ + data.subres_sl = p_subres; \ + } \ + Ref get_subres_sl() const { \ + return data.subres_sl; \ + } \ + \ + void set_defaults() { \ + data.set_defaults(); \ + } \ + \ + Object *get_data() { \ + return &data; \ + } \ + \ + void verify_duplication(const Ref &p_orig, int p_test_mode, int p_deep_mode) const { \ + const DuplicateGuineaPigData *orig_data = Object::cast_to(p_orig->call("get_data")); \ + data.verify_duplication(orig_data, m_property_usage, (TestDuplicateMode)p_test_mode, (ResourceDeepDuplicateMode)p_deep_mode); \ + } \ + \ + protected: \ + static void _bind_methods() { \ + ClassDB::bind_method(D_METHOD("set_obj", "obj"), &m_class_name::set_obj); \ + ClassDB::bind_method(D_METHOD("get_obj"), &m_class_name::get_obj); \ + \ + ClassDB::bind_method(D_METHOD("set_arr", "arr"), &m_class_name::set_arr); \ + ClassDB::bind_method(D_METHOD("get_arr"), &m_class_name::get_arr); \ + \ + ClassDB::bind_method(D_METHOD("set_dict", "dict"), &m_class_name::set_dict); \ + ClassDB::bind_method(D_METHOD("get_dict"), &m_class_name::get_dict); \ + \ + ClassDB::bind_method(D_METHOD("set_packed", "packed"), &m_class_name::set_packed); \ + ClassDB::bind_method(D_METHOD("get_packed"), &m_class_name::get_packed); \ + \ + ClassDB::bind_method(D_METHOD("set_subres", "subres"), &m_class_name::set_subres); \ + ClassDB::bind_method(D_METHOD("get_subres"), &m_class_name::get_subres); \ + \ + ClassDB::bind_method(D_METHOD("set_subres_sl", "subres"), &m_class_name::set_subres_sl); \ + ClassDB::bind_method(D_METHOD("get_subres_sl"), &m_class_name::get_subres_sl); \ + \ + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "obj", PROPERTY_HINT_NONE, "", m_property_usage), "set_obj", "get_obj"); \ + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "arr", PROPERTY_HINT_NONE, "", m_property_usage), "set_arr", "get_arr"); \ + ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "dict", PROPERTY_HINT_NONE, "", m_property_usage), "set_dict", "get_dict"); \ + ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "packed", PROPERTY_HINT_NONE, "", m_property_usage), "set_packed", "get_packed"); \ + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "subres", PROPERTY_HINT_NONE, "", m_property_usage), "set_subres", "get_subres"); \ + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "subres_sl", PROPERTY_HINT_NONE, "", m_property_usage), "set_subres_sl", "get_subres_sl"); \ + \ + ClassDB::bind_method(D_METHOD("set_defaults"), &m_class_name::set_defaults); \ + ClassDB::bind_method(D_METHOD("get_data"), &m_class_name::get_data); \ + ClassDB::bind_method(D_METHOD("verify_duplication", "orig", "test_mode", "deep_mode"), &m_class_name::verify_duplication); \ + } \ + \ + public: \ + static m_class_name *register_and_instantiate() { \ + static bool registered = false; \ + if (!registered) { \ + GDREGISTER_CLASS(m_class_name); \ + registered = true; \ + } \ + return memnew(m_class_name); \ + } \ + }; + +DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_None, PROPERTY_USAGE_NONE) +DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Always, PROPERTY_USAGE_ALWAYS_DUPLICATE) +DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Storage, PROPERTY_USAGE_STORAGE) +DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Storage_Always, (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_ALWAYS_DUPLICATE)) +DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Storage_Never, (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_NEVER_DUPLICATE)) + TEST_CASE("[Resource] Duplication") { - Ref resource = memnew(Resource); - resource->set_name("Hello world"); - Ref child_resource = memnew(Resource); - child_resource->set_name("I'm a child resource"); - resource->set_meta("other_resource", child_resource); + auto _run_test = []( + TestDuplicateMode p_test_mode, + ResourceDeepDuplicateMode p_deep_mode, + Ref (*p_duplicate_fn)(const Ref &)) -> void { + LocalVector> resources = { + DuplicateGuineaPig_None::register_and_instantiate(), + DuplicateGuineaPig_Always::register_and_instantiate(), + DuplicateGuineaPig_Storage::register_and_instantiate(), + DuplicateGuineaPig_Storage_Always::register_and_instantiate(), + DuplicateGuineaPig_Storage_Never::register_and_instantiate(), + }; - Ref resource_dupe = resource->duplicate(); - const Ref &resource_dupe_reference = resource_dupe; - resource_dupe->set_name("Changed name"); - child_resource->set_name("My name was changed too"); + for (const Ref &orig : resources) { + INFO(std::string(String(orig->get_class_name()).utf8().get_data())); - CHECK_MESSAGE( - resource_dupe->get_name() == "Changed name", - "Duplicated resource should have the new name."); - CHECK_MESSAGE( - resource_dupe_reference->get_name() == "Changed name", - "Reference to the duplicated resource should have the new name."); - CHECK_MESSAGE( - resource->get_name() == "Hello world", - "Original resource name should not be affected after editing the duplicate's name."); - CHECK_MESSAGE( - Ref(resource_dupe->get_meta("other_resource"))->get_name() == "My name was changed too", - "Duplicated resource should share its child resource with the original."); + orig->call("set_defaults"); + + const Ref &dupe = p_duplicate_fn(orig); + if ((p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE || p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE) && p_deep_mode == RESOURCE_DEEP_DUPLICATE_MAX) { + CHECK(dupe.is_null()); + } else { + dupe->call("verify_duplication", orig, p_test_mode, p_deep_mode); + } + } + }; + + SUBCASE("Resource::duplicate(), shallow") { + _run_test( + TEST_MODE_RESOURCE_DUPLICATE_SHALLOW, + RESOURCE_DEEP_DUPLICATE_MAX, + [](const Ref &p_res) -> Ref { + return p_res->duplicate(false); + }); + } + + SUBCASE("Resource::duplicate(), deep") { + _run_test( + TEST_MODE_RESOURCE_DUPLICATE_DEEP, + RESOURCE_DEEP_DUPLICATE_MAX, + [](const Ref &p_res) -> Ref { + return p_res->duplicate(true); + }); + } + + SUBCASE("Resource::duplicate_deep()") { + static int deep_mode = 0; + for (deep_mode = 0; deep_mode <= RESOURCE_DEEP_DUPLICATE_MAX; deep_mode++) { + _run_test( + TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE, + (ResourceDeepDuplicateMode)deep_mode, + [](const Ref &p_res) -> Ref { + return p_res->duplicate_deep((ResourceDeepDuplicateMode)deep_mode); + }); + } + } + + SUBCASE("Resource::duplicate_for_local_scene()") { + static int mark_main_as_local = 0; + static int mark_some_subs_as_local = 0; + for (mark_main_as_local = 0; mark_main_as_local < 2; ++mark_main_as_local) { // Whether main is local-to-scene shouldn't matter. + for (mark_some_subs_as_local = 0; mark_some_subs_as_local < 2; ++mark_some_subs_as_local) { + _run_test( + TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE, + RESOURCE_DEEP_DUPLICATE_MAX, + [](const Ref &p_res) -> Ref { + if (mark_main_as_local) { + p_res->set_local_to_scene(true); + } + if (mark_some_subs_as_local) { + Object::cast_to(p_res->call("get_data"))->enable_scene_local_subresources(); + } + HashMap, Ref> remap_cache; + Node fake_scene; + return p_res->duplicate_for_local_scene(&fake_scene, remap_cache); + }); + } + } + } + + SUBCASE("Variant::duplicate(), shallow") { + _run_test( + TEST_MODE_VARIANT_DUPLICATE_SHALLOW, + RESOURCE_DEEP_DUPLICATE_MAX, + [](const Ref &p_res) -> Ref { + return Variant(p_res).duplicate(false); + }); + } + + SUBCASE("Variant::duplicate(), deep") { + _run_test( + TEST_MODE_VARIANT_DUPLICATE_DEEP, + RESOURCE_DEEP_DUPLICATE_MAX, + [](const Ref &p_res) -> Ref { + return Variant(p_res).duplicate(true); + }); + } + + SUBCASE("Variant::duplicate_deep()") { + static int deep_mode = 0; + for (deep_mode = 0; deep_mode <= RESOURCE_DEEP_DUPLICATE_MAX; deep_mode++) { + _run_test( + TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE, + (ResourceDeepDuplicateMode)deep_mode, + [](const Ref &p_res) -> Ref { + return Variant(p_res).duplicate_deep((ResourceDeepDuplicateMode)deep_mode); + }); + } + } + + SUBCASE("Via Variant, resource not being the root") { + // Variant controls the deep copy, recursing until resources are found, and then + // it's Resource who controls the deep copy from it onwards. + // Therefore, we have to test if Variant is able to track unique duplicates across + // multiple times Resource takes over. + // Since the other test cases already prove Resource's mechanism to have at most + // one duplicate per resource involved, the test for Variant is simple. + + Ref res; + res.instantiate(); + res->set_name("risi"); + Array a; + a.push_back(res); + { + Dictionary d; + d[res] = res; + a.push_back(d); + } + + Array dupe_a; + Ref dupe_res; + + SUBCASE("Variant::duplicate(), shallow") { + dupe_a = Variant(a).duplicate(false); + // Ensure it's referencing the original. + dupe_res = dupe_a[0]; + CHECK(dupe_res == res); + } + SUBCASE("Variant::duplicate(), deep") { + dupe_a = Variant(a).duplicate(true); + // Ensure it's referencing the original. + dupe_res = dupe_a[0]; + CHECK(dupe_res == res); + } + SUBCASE("Variant::duplicate_deep(), no resources") { + dupe_a = Variant(a).duplicate_deep(RESOURCE_DEEP_DUPLICATE_NONE); + // Ensure it's referencing the original. + dupe_res = dupe_a[0]; + CHECK(dupe_res == res); + } + SUBCASE("Variant::duplicate_deep(), with resources") { + dupe_a = Variant(a).duplicate_deep(RESOURCE_DEEP_DUPLICATE_ALL); + // Ensure it's a copy. + dupe_res = dupe_a[0]; + CHECK(dupe_res != res); + CHECK(dupe_res->get_name() == "risi"); + + // Ensure the map is already gone so we get new instances. + Array dupe_a_2 = Variant(a).duplicate_deep(RESOURCE_DEEP_DUPLICATE_ALL); + CHECK(dupe_a_2[0] != dupe_a[0]); + } + + // Ensure all the usages are of the same resource. + CHECK(((Dictionary)dupe_a[1]).get_key_at_index(0) == dupe_res); + CHECK(((Dictionary)dupe_a[1]).get_value_at_index(0) == dupe_res); + } } TEST_CASE("[Resource] Saving and loading") {