Sfoglia il codice sorgente

Add tests for resource duplication

Pedro J. Estébanez 7 mesi fa
parent
commit
6841b45552

+ 4 - 0
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);
 }

+ 1 - 0
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;

+ 1 - 1
doc/classes/Array.xml

@@ -332,7 +332,7 @@
 			<param index="0" name="deep" type="bool" default="false" />
 			<description>
 				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.
 			</description>
 		</method>

+ 1 - 1
doc/classes/Dictionary.xml

@@ -188,7 +188,7 @@
 			<param index="0" name="deep" type="bool" default="false" />
 			<description>
 				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.
 			</description>
 		</method>

+ 40 - 0
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)

+ 13 - 0
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 }

+ 489 - 21
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 <functional>
+
 namespace TestResource {
 
+enum TestDuplicateMode {
+	TEST_MODE_RESOURCE_DUPLICATE_SHALLOW,
+	TEST_MODE_RESOURCE_DUPLICATE_DEEP,
+	TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE,
+	TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE,
+	TEST_MODE_VARIANT_DUPLICATE_SHALLOW,
+	TEST_MODE_VARIANT_DUPLICATE_DEEP,
+	TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE,
+};
+
+class DuplicateGuineaPigData : public Object {
+	GDSOFTCLASS(DuplicateGuineaPigData, Object)
+
+public:
+	const Variant SENTINEL_1 = "A";
+	const Variant SENTINEL_2 = 645;
+	const Variant SENTINEL_3 = StringName("X");
+	const Variant SENTINEL_4 = true;
+
+	Ref<Resource> SUBRES_1 = memnew(Resource);
+	Ref<Resource> SUBRES_2 = memnew(Resource);
+	Ref<Resource> SUBRES_3 = memnew(Resource);
+	Ref<Resource> SUBRES_SL_1 = memnew(Resource);
+	Ref<Resource> SUBRES_SL_2 = memnew(Resource);
+	Ref<Resource> SUBRES_SL_3 = memnew(Resource);
+
+	Variant obj; // Variant helps with lifetime so duplicates pointing to the same don't try to double-free it.
+	Array arr;
+	Dictionary dict;
+	Variant packed; // A PackedByteArray, but using Variant to be able to tell if the array is shared or not.
+	Ref<Resource> subres;
+	Ref<Resource> subres_sl;
+
+	void set_defaults() {
+		SUBRES_1->set_name("juan");
+		SUBRES_2->set_name("you");
+		SUBRES_3->set_name("tree");
+		SUBRES_SL_1->set_name("maybe_scene_local");
+		SUBRES_SL_2->set_name("perhaps_local_to_scene");
+		SUBRES_SL_3->set_name("sometimes_locality_scenial");
+
+		// To try some cases of internal and external.
+		SUBRES_1->set_path_cache("");
+		SUBRES_2->set_path_cache("local://hehe");
+		SUBRES_3->set_path_cache("res://some.tscn::1");
+		DEV_ASSERT(SUBRES_1->is_built_in());
+		DEV_ASSERT(SUBRES_2->is_built_in());
+		DEV_ASSERT(SUBRES_3->is_built_in());
+		SUBRES_SL_1->set_path_cache("res://thing.scn");
+		SUBRES_SL_2->set_path_cache("C:/not/really/possible/but/still/external");
+		SUBRES_SL_3->set_path_cache("/this/neither");
+		DEV_ASSERT(!SUBRES_SL_1->is_built_in());
+		DEV_ASSERT(!SUBRES_SL_2->is_built_in());
+		DEV_ASSERT(!SUBRES_SL_3->is_built_in());
+
+		obj = memnew(Object);
+
+		// Construct enough cases to test deep recursion involving resources;
+		// we mix some primitive values with recurses nested in different ways,
+		// acting as array values and dictionary keys and values, some of those
+		// being marked as scene-local when for subcases where scene-local is relevant.
+
+		arr.push_back(SENTINEL_1);
+		arr.push_back(SUBRES_1);
+		arr.push_back(SUBRES_SL_1);
+		{
+			Dictionary d;
+			d[SENTINEL_2] = SENTINEL_3;
+			d[SENTINEL_4] = SUBRES_2;
+			d[SUBRES_3] = SUBRES_SL_2;
+			d[SUBRES_SL_3] = SUBRES_1;
+			arr.push_back(d);
+		}
+
+		dict[SENTINEL_4] = SENTINEL_1;
+		dict[SENTINEL_2] = SUBRES_2;
+		dict[SUBRES_3] = SUBRES_SL_1;
+		dict[SUBRES_SL_2] = SUBRES_1;
+		{
+			Array a;
+			a.push_back(SENTINEL_3);
+			a.push_back(SUBRES_2);
+			a.push_back(SUBRES_SL_3);
+			dict[SENTINEL_4] = a;
+		}
+
+		packed = PackedByteArray{ 0xaa, 0xbb, 0xcc };
+
+		subres = SUBRES_1;
+		subres_sl = SUBRES_SL_1;
+	}
+
+	void verify_empty() const {
+		CHECK(obj.get_type() == Variant::NIL);
+		CHECK(arr.size() == 0);
+		CHECK(dict.size() == 0);
+		CHECK(packed.get_type() == Variant::NIL);
+		CHECK(subres.is_null());
+	}
+
+	void verify_duplication(const DuplicateGuineaPigData *p_orig, uint32_t p_property_usage, TestDuplicateMode p_test_mode, ResourceDeepDuplicateMode p_deep_mode) const {
+		if (!(p_property_usage & PROPERTY_USAGE_STORAGE)) {
+			verify_empty();
+			return;
+		}
+
+		// To see if each resource involved is copied once at most,
+		// and then the reference to the duplicate reused.
+		HashMap<Resource *, Resource *> duplicates;
+
+		auto _verify_resource = [&](const Ref<Resource> &p_dupe_res, const Ref<Resource> &p_orig_res, bool p_is_property = false) {
+			bool expect_true_copy = (p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP && p_orig_res->is_built_in()) ||
+					(p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_INTERNAL && p_orig_res->is_built_in()) ||
+					(p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_ALL) ||
+					(p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE && p_orig_res->is_local_to_scene()) ||
+					(p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_INTERNAL && p_orig_res->is_built_in()) ||
+					(p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_ALL);
+
+			if (expect_true_copy) {
+				if (p_deep_mode == RESOURCE_DEEP_DUPLICATE_NONE) {
+					expect_true_copy = false;
+				} else if (p_deep_mode == RESOURCE_DEEP_DUPLICATE_INTERNAL) {
+					expect_true_copy = p_orig_res->is_built_in();
+				}
+			}
+
+			if (p_is_property) {
+				if ((p_property_usage & PROPERTY_USAGE_ALWAYS_DUPLICATE)) {
+					expect_true_copy = true;
+				} else if ((p_property_usage & PROPERTY_USAGE_NEVER_DUPLICATE)) {
+					expect_true_copy = false;
+				}
+			}
+
+			if (expect_true_copy) {
+				CHECK(p_dupe_res != p_orig_res);
+				CHECK(p_dupe_res->get_name() == p_orig_res->get_name());
+				if (duplicates.has(p_orig_res.ptr())) {
+					CHECK(duplicates[p_orig_res.ptr()] == p_dupe_res.ptr());
+				} else {
+					duplicates[p_orig_res.ptr()] = p_dupe_res.ptr();
+				}
+			} else {
+				CHECK(p_dupe_res == p_orig_res);
+			}
+		};
+
+		std::function<void(const Variant &p_a, const Variant &p_b)> _verify_deep_copied_variants = [&](const Variant &p_a, const Variant &p_b) {
+			CHECK(p_a.get_type() == p_b.get_type());
+			const Ref<Resource> &res_a = p_a;
+			const Ref<Resource> &res_b = p_b;
+			if (res_a.is_valid()) {
+				_verify_resource(res_a, res_b);
+			} else if (p_a.get_type() == Variant::ARRAY) {
+				const Array &arr_a = p_a;
+				const Array &arr_b = p_b;
+				CHECK(!arr_a.is_same_instance(arr_b));
+				CHECK(arr_a.size() == arr_b.size());
+				for (int i = 0; i < arr_a.size(); i++) {
+					_verify_deep_copied_variants(arr_a[i], arr_b[i]);
+				}
+			} else if (p_a.get_type() == Variant::DICTIONARY) {
+				const Dictionary &dict_a = p_a;
+				const Dictionary &dict_b = p_b;
+				CHECK(!dict_a.is_same_instance(dict_b));
+				CHECK(dict_a.size() == dict_b.size());
+				for (int i = 0; i < dict_a.size(); i++) {
+					_verify_deep_copied_variants(dict_a.get_key_at_index(i), dict_b.get_key_at_index(i));
+					_verify_deep_copied_variants(dict_a.get_value_at_index(i), dict_b.get_value_at_index(i));
+				}
+			} else {
+				CHECK(p_a == p_b);
+			}
+		};
+
+		CHECK(this != p_orig);
+
+		CHECK((Object *)obj == (Object *)p_orig->obj);
+
+		bool expect_true_copy = p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP ||
+				p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE ||
+				p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE ||
+				p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP ||
+				p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE;
+		if (expect_true_copy) {
+			_verify_deep_copied_variants(arr, p_orig->arr);
+			_verify_deep_copied_variants(dict, p_orig->dict);
+			CHECK(!packed.identity_compare(p_orig->packed));
+		} else {
+			CHECK(arr.is_same_instance(p_orig->arr));
+			CHECK(dict.is_same_instance(p_orig->dict));
+			CHECK(packed.identity_compare(p_orig->packed));
+		}
+
+		_verify_resource(subres, p_orig->subres, true);
+		_verify_resource(subres_sl, p_orig->subres_sl, true);
+	}
+
+	void enable_scene_local_subresources() {
+		SUBRES_SL_1->set_local_to_scene(true);
+		SUBRES_SL_2->set_local_to_scene(true);
+		SUBRES_SL_3->set_local_to_scene(true);
+	}
+
+	virtual ~DuplicateGuineaPigData() {
+		Object *obj_ptr = obj.get_validated_object();
+		if (obj_ptr) {
+			memdelete(obj_ptr);
+		}
+	}
+};
+
+#define DEFINE_DUPLICATE_GUINEA_PIG(m_class_name, m_property_usage)                                                                                 \
+	class m_class_name : public Resource {                                                                                                          \
+		GDCLASS(m_class_name, Resource)                                                                                                             \
+                                                                                                                                                    \
+		DuplicateGuineaPigData data;                                                                                                                \
+                                                                                                                                                    \
+	public:                                                                                                                                         \
+		void set_obj(Object *p_obj) {                                                                                                               \
+			data.obj = p_obj;                                                                                                                       \
+		}                                                                                                                                           \
+		Object *get_obj() const {                                                                                                                   \
+			return data.obj;                                                                                                                        \
+		}                                                                                                                                           \
+                                                                                                                                                    \
+		void set_arr(const Array &p_arr) {                                                                                                          \
+			data.arr = p_arr;                                                                                                                       \
+		}                                                                                                                                           \
+		Array get_arr() const {                                                                                                                     \
+			return data.arr;                                                                                                                        \
+		}                                                                                                                                           \
+                                                                                                                                                    \
+		void set_dict(const Dictionary &p_dict) {                                                                                                   \
+			data.dict = p_dict;                                                                                                                     \
+		}                                                                                                                                           \
+		Dictionary get_dict() const {                                                                                                               \
+			return data.dict;                                                                                                                       \
+		}                                                                                                                                           \
+                                                                                                                                                    \
+		void set_packed(const Variant &p_packed) {                                                                                                  \
+			data.packed = p_packed;                                                                                                                 \
+		}                                                                                                                                           \
+		Variant get_packed() const {                                                                                                                \
+			return data.packed;                                                                                                                     \
+		}                                                                                                                                           \
+                                                                                                                                                    \
+		void set_subres(const Ref<Resource> &p_subres) {                                                                                            \
+			data.subres = p_subres;                                                                                                                 \
+		}                                                                                                                                           \
+		Ref<Resource> get_subres() const {                                                                                                          \
+			return data.subres;                                                                                                                     \
+		}                                                                                                                                           \
+                                                                                                                                                    \
+		void set_subres_sl(const Ref<Resource> &p_subres) {                                                                                         \
+			data.subres_sl = p_subres;                                                                                                              \
+		}                                                                                                                                           \
+		Ref<Resource> get_subres_sl() const {                                                                                                       \
+			return data.subres_sl;                                                                                                                  \
+		}                                                                                                                                           \
+                                                                                                                                                    \
+		void set_defaults() {                                                                                                                       \
+			data.set_defaults();                                                                                                                    \
+		}                                                                                                                                           \
+                                                                                                                                                    \
+		Object *get_data() {                                                                                                                        \
+			return &data;                                                                                                                           \
+		}                                                                                                                                           \
+                                                                                                                                                    \
+		void verify_duplication(const Ref<Resource> &p_orig, int p_test_mode, int p_deep_mode) const {                                              \
+			const DuplicateGuineaPigData *orig_data = Object::cast_to<DuplicateGuineaPigData>(p_orig->call("get_data"));                            \
+			data.verify_duplication(orig_data, m_property_usage, (TestDuplicateMode)p_test_mode, (ResourceDeepDuplicateMode)p_deep_mode);           \
+		}                                                                                                                                           \
+                                                                                                                                                    \
+	protected:                                                                                                                                      \
+		static void _bind_methods() {                                                                                                               \
+			ClassDB::bind_method(D_METHOD("set_obj", "obj"), &m_class_name::set_obj);                                                               \
+			ClassDB::bind_method(D_METHOD("get_obj"), &m_class_name::get_obj);                                                                      \
+                                                                                                                                                    \
+			ClassDB::bind_method(D_METHOD("set_arr", "arr"), &m_class_name::set_arr);                                                               \
+			ClassDB::bind_method(D_METHOD("get_arr"), &m_class_name::get_arr);                                                                      \
+                                                                                                                                                    \
+			ClassDB::bind_method(D_METHOD("set_dict", "dict"), &m_class_name::set_dict);                                                            \
+			ClassDB::bind_method(D_METHOD("get_dict"), &m_class_name::get_dict);                                                                    \
+                                                                                                                                                    \
+			ClassDB::bind_method(D_METHOD("set_packed", "packed"), &m_class_name::set_packed);                                                      \
+			ClassDB::bind_method(D_METHOD("get_packed"), &m_class_name::get_packed);                                                                \
+                                                                                                                                                    \
+			ClassDB::bind_method(D_METHOD("set_subres", "subres"), &m_class_name::set_subres);                                                      \
+			ClassDB::bind_method(D_METHOD("get_subres"), &m_class_name::get_subres);                                                                \
+                                                                                                                                                    \
+			ClassDB::bind_method(D_METHOD("set_subres_sl", "subres"), &m_class_name::set_subres_sl);                                                \
+			ClassDB::bind_method(D_METHOD("get_subres_sl"), &m_class_name::get_subres_sl);                                                          \
+                                                                                                                                                    \
+			ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "obj", PROPERTY_HINT_NONE, "", m_property_usage), "set_obj", "get_obj");                     \
+			ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "arr", PROPERTY_HINT_NONE, "", m_property_usage), "set_arr", "get_arr");                      \
+			ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "dict", PROPERTY_HINT_NONE, "", m_property_usage), "set_dict", "get_dict");              \
+			ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "packed", PROPERTY_HINT_NONE, "", m_property_usage), "set_packed", "get_packed"); \
+			ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "subres", PROPERTY_HINT_NONE, "", m_property_usage), "set_subres", "get_subres");            \
+			ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "subres_sl", PROPERTY_HINT_NONE, "", m_property_usage), "set_subres_sl", "get_subres_sl");   \
+                                                                                                                                                    \
+			ClassDB::bind_method(D_METHOD("set_defaults"), &m_class_name::set_defaults);                                                            \
+			ClassDB::bind_method(D_METHOD("get_data"), &m_class_name::get_data);                                                                    \
+			ClassDB::bind_method(D_METHOD("verify_duplication", "orig", "test_mode", "deep_mode"), &m_class_name::verify_duplication);              \
+		}                                                                                                                                           \
+                                                                                                                                                    \
+	public:                                                                                                                                         \
+		static m_class_name *register_and_instantiate() {                                                                                           \
+			static bool registered = false;                                                                                                         \
+			if (!registered) {                                                                                                                      \
+				GDREGISTER_CLASS(m_class_name);                                                                                                     \
+				registered = true;                                                                                                                  \
+			}                                                                                                                                       \
+			return memnew(m_class_name);                                                                                                            \
+		}                                                                                                                                           \
+	};
+
+DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_None, PROPERTY_USAGE_NONE)
+DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Always, PROPERTY_USAGE_ALWAYS_DUPLICATE)
+DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Storage, PROPERTY_USAGE_STORAGE)
+DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Storage_Always, (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_ALWAYS_DUPLICATE))
+DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Storage_Never, (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_NEVER_DUPLICATE))
+
 TEST_CASE("[Resource] Duplication") {
-	Ref<Resource> resource = memnew(Resource);
-	resource->set_name("Hello world");
-	Ref<Resource> child_resource = memnew(Resource);
-	child_resource->set_name("I'm a child resource");
-	resource->set_meta("other_resource", child_resource);
+	auto _run_test = [](
+							 TestDuplicateMode p_test_mode,
+							 ResourceDeepDuplicateMode p_deep_mode,
+							 Ref<Resource> (*p_duplicate_fn)(const Ref<Resource> &)) -> void {
+		LocalVector<Ref<Resource>> resources = {
+			DuplicateGuineaPig_None::register_and_instantiate(),
+			DuplicateGuineaPig_Always::register_and_instantiate(),
+			DuplicateGuineaPig_Storage::register_and_instantiate(),
+			DuplicateGuineaPig_Storage_Always::register_and_instantiate(),
+			DuplicateGuineaPig_Storage_Never::register_and_instantiate(),
+		};
 
-	Ref<Resource> resource_dupe = resource->duplicate();
-	const Ref<Resource> &resource_dupe_reference = resource_dupe;
-	resource_dupe->set_name("Changed name");
-	child_resource->set_name("My name was changed too");
+		for (const Ref<Resource> &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>(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<Resource> &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<Resource> &p_res) -> Ref<Resource> {
+					return p_res->duplicate(false);
+				});
+	}
+
+	SUBCASE("Resource::duplicate(), deep") {
+		_run_test(
+				TEST_MODE_RESOURCE_DUPLICATE_DEEP,
+				RESOURCE_DEEP_DUPLICATE_MAX,
+				[](const Ref<Resource> &p_res) -> Ref<Resource> {
+					return p_res->duplicate(true);
+				});
+	}
+
+	SUBCASE("Resource::duplicate_deep()") {
+		static int deep_mode = 0;
+		for (deep_mode = 0; deep_mode <= RESOURCE_DEEP_DUPLICATE_MAX; deep_mode++) {
+			_run_test(
+					TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE,
+					(ResourceDeepDuplicateMode)deep_mode,
+					[](const Ref<Resource> &p_res) -> Ref<Resource> {
+						return p_res->duplicate_deep((ResourceDeepDuplicateMode)deep_mode);
+					});
+		}
+	}
+
+	SUBCASE("Resource::duplicate_for_local_scene()") {
+		static int mark_main_as_local = 0;
+		static int mark_some_subs_as_local = 0;
+		for (mark_main_as_local = 0; mark_main_as_local < 2; ++mark_main_as_local) { // Whether main is local-to-scene shouldn't matter.
+			for (mark_some_subs_as_local = 0; mark_some_subs_as_local < 2; ++mark_some_subs_as_local) {
+				_run_test(
+						TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE,
+						RESOURCE_DEEP_DUPLICATE_MAX,
+						[](const Ref<Resource> &p_res) -> Ref<Resource> {
+							if (mark_main_as_local) {
+								p_res->set_local_to_scene(true);
+							}
+							if (mark_some_subs_as_local) {
+								Object::cast_to<DuplicateGuineaPigData>(p_res->call("get_data"))->enable_scene_local_subresources();
+							}
+							HashMap<Ref<Resource>, Ref<Resource>> remap_cache;
+							Node fake_scene;
+							return p_res->duplicate_for_local_scene(&fake_scene, remap_cache);
+						});
+			}
+		}
+	}
+
+	SUBCASE("Variant::duplicate(), shallow") {
+		_run_test(
+				TEST_MODE_VARIANT_DUPLICATE_SHALLOW,
+				RESOURCE_DEEP_DUPLICATE_MAX,
+				[](const Ref<Resource> &p_res) -> Ref<Resource> {
+					return Variant(p_res).duplicate(false);
+				});
+	}
+
+	SUBCASE("Variant::duplicate(), deep") {
+		_run_test(
+				TEST_MODE_VARIANT_DUPLICATE_DEEP,
+				RESOURCE_DEEP_DUPLICATE_MAX,
+				[](const Ref<Resource> &p_res) -> Ref<Resource> {
+					return Variant(p_res).duplicate(true);
+				});
+	}
+
+	SUBCASE("Variant::duplicate_deep()") {
+		static int deep_mode = 0;
+		for (deep_mode = 0; deep_mode <= RESOURCE_DEEP_DUPLICATE_MAX; deep_mode++) {
+			_run_test(
+					TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE,
+					(ResourceDeepDuplicateMode)deep_mode,
+					[](const Ref<Resource> &p_res) -> Ref<Resource> {
+						return Variant(p_res).duplicate_deep((ResourceDeepDuplicateMode)deep_mode);
+					});
+		}
+	}
+
+	SUBCASE("Via Variant, resource not being the root") {
+		// Variant controls the deep copy, recursing until resources are found, and then
+		// it's Resource who controls the deep copy from it onwards.
+		// Therefore, we have to test if Variant is able to track unique duplicates across
+		// multiple times Resource takes over.
+		// Since the other test cases already prove Resource's mechanism to have at most
+		// one duplicate per resource involved, the test for Variant is simple.
+
+		Ref<Resource> res;
+		res.instantiate();
+		res->set_name("risi");
+		Array a;
+		a.push_back(res);
+		{
+			Dictionary d;
+			d[res] = res;
+			a.push_back(d);
+		}
+
+		Array dupe_a;
+		Ref<Resource> dupe_res;
+
+		SUBCASE("Variant::duplicate(), shallow") {
+			dupe_a = Variant(a).duplicate(false);
+			// Ensure it's referencing the original.
+			dupe_res = dupe_a[0];
+			CHECK(dupe_res == res);
+		}
+		SUBCASE("Variant::duplicate(), deep") {
+			dupe_a = Variant(a).duplicate(true);
+			// Ensure it's referencing the original.
+			dupe_res = dupe_a[0];
+			CHECK(dupe_res == res);
+		}
+		SUBCASE("Variant::duplicate_deep(), no resources") {
+			dupe_a = Variant(a).duplicate_deep(RESOURCE_DEEP_DUPLICATE_NONE);
+			// Ensure it's referencing the original.
+			dupe_res = dupe_a[0];
+			CHECK(dupe_res == res);
+		}
+		SUBCASE("Variant::duplicate_deep(), with resources") {
+			dupe_a = Variant(a).duplicate_deep(RESOURCE_DEEP_DUPLICATE_ALL);
+			// Ensure it's a copy.
+			dupe_res = dupe_a[0];
+			CHECK(dupe_res != res);
+			CHECK(dupe_res->get_name() == "risi");
+
+			// Ensure the map is already gone so we get new instances.
+			Array dupe_a_2 = Variant(a).duplicate_deep(RESOURCE_DEEP_DUPLICATE_ALL);
+			CHECK(dupe_a_2[0] != dupe_a[0]);
+		}
+
+		// Ensure all the usages are of the same resource.
+		CHECK(((Dictionary)dupe_a[1]).get_key_at_index(0) == dupe_res);
+		CHECK(((Dictionary)dupe_a[1]).get_value_at_index(0) == dupe_res);
+	}
 }
 
 TEST_CASE("[Resource] Saving and loading") {