Browse Source

Modify Dictionary::operator== to do real key/value comparison with recursive support (and add unittests)

Emmanuel Leblond 5 years ago
parent
commit
f9ba2efe1e

+ 5 - 1
core/templates/ordered_hash_map.h

@@ -207,8 +207,12 @@ public:
 			(*list_element)->get().second = p_value;
 			(*list_element)->get().second = p_value;
 			return Element(*list_element);
 			return Element(*list_element);
 		}
 		}
-		typename InternalList::Element *new_element = list.push_back(Pair<const K *, V>(nullptr, p_value));
+		// Incorrectly set the first value of the pair with a value that will
+		// be invalid as soon as we leave this function...
+		typename InternalList::Element *new_element = list.push_back(Pair<const K *, V>(&p_key, p_value));
+		// ...this is needed here in case the hashmap recursively reference itself...
 		typename InternalMap::Element *e = map.set(p_key, new_element);
 		typename InternalMap::Element *e = map.set(p_key, new_element);
+		// ...now we can set the right value !
 		new_element->get().first = &e->key();
 		new_element->get().first = &e->key();
 
 
 		return Element(new_element);
 		return Element(new_element);

+ 3 - 0
core/typedefs.h

@@ -277,6 +277,9 @@ struct BuildIndexSequence : BuildIndexSequence<N - 1, N - 1, Is...> {};
 template <size_t... Is>
 template <size_t... Is>
 struct BuildIndexSequence<0, Is...> : IndexSequence<Is...> {};
 struct BuildIndexSequence<0, Is...> : IndexSequence<Is...> {};
 
 
+// Limit the depth of recursive algorithms when dealing with Array/Dictionary
+#define MAX_RECURSION 100
+
 #ifdef DEBUG_ENABLED
 #ifdef DEBUG_ENABLED
 #define DEBUG_METHODS_ENABLED
 #define DEBUG_METHODS_ENABLED
 #endif
 #endif

+ 60 - 6
core/variant/array.cpp

@@ -97,11 +97,38 @@ void Array::clear() {
 }
 }
 
 
 bool Array::operator==(const Array &p_array) const {
 bool Array::operator==(const Array &p_array) const {
-	return _p == p_array._p;
+	return recursive_equal(p_array, 0);
 }
 }
 
 
 bool Array::operator!=(const Array &p_array) const {
 bool Array::operator!=(const Array &p_array) const {
-	return !operator==(p_array);
+	return !recursive_equal(p_array, 0);
+}
+
+bool Array::recursive_equal(const Array &p_array, int recursion_count) const {
+	// Cheap checks
+	if (_p == p_array._p) {
+		return true;
+	}
+	const Vector<Variant> &a1 = _p->array;
+	const Vector<Variant> &a2 = p_array._p->array;
+	const int size = a1.size();
+	if (size != a2.size()) {
+		return false;
+	}
+
+	// Heavy O(n) check
+	if (recursion_count > MAX_RECURSION) {
+		ERR_PRINT("Max recursion reached");
+		return true;
+	}
+	recursion_count++;
+	for (int i = 0; i < size; i++) {
+		if (!a1[i].hash_compare(a2[i], recursion_count)) {
+			return false;
+		}
+	}
+
+	return true;
 }
 }
 
 
 bool Array::operator<(const Array &p_array) const {
 bool Array::operator<(const Array &p_array) const {
@@ -132,10 +159,20 @@ bool Array::operator>=(const Array &p_array) const {
 }
 }
 
 
 uint32_t Array::hash() const {
 uint32_t Array::hash() const {
-	uint32_t h = hash_djb2_one_32(0);
+	return recursive_hash(0);
+}
 
 
+uint32_t Array::recursive_hash(int recursion_count) const {
+	if (recursion_count > MAX_RECURSION) {
+		ERR_PRINT("Max recursion reached");
+		return 0;
+	}
+
+	uint32_t h = hash_djb2_one_32(Variant::ARRAY);
+
+	recursion_count++;
 	for (int i = 0; i < _p->array.size(); i++) {
 	for (int i = 0; i < _p->array.size(); i++) {
-		h = hash_djb2_one_32(_p->array[i].hash(), h);
+		h = hash_djb2_one_32(_p->array[i].recursive_hash(recursion_count), h);
 	}
 	}
 	return h;
 	return h;
 }
 }
@@ -300,12 +337,29 @@ const Variant &Array::get(int p_idx) const {
 }
 }
 
 
 Array Array::duplicate(bool p_deep) const {
 Array Array::duplicate(bool p_deep) const {
+	return recursive_duplicate(p_deep, 0);
+}
+
+Array Array::recursive_duplicate(bool p_deep, int recursion_count) const {
 	Array new_arr;
 	Array new_arr;
+
+	if (recursion_count > MAX_RECURSION) {
+		ERR_PRINT("Max recursion reached");
+		return new_arr;
+	}
+
 	int element_count = size();
 	int element_count = size();
 	new_arr.resize(element_count);
 	new_arr.resize(element_count);
 	new_arr._p->typed = _p->typed;
 	new_arr._p->typed = _p->typed;
-	for (int i = 0; i < element_count; i++) {
-		new_arr[i] = p_deep ? get(i).duplicate(p_deep) : get(i);
+	if (p_deep) {
+		recursion_count++;
+		for (int i = 0; i < element_count; i++) {
+			new_arr[i] = get(i).recursive_duplicate(true, recursion_count);
+		}
+	} else {
+		for (int i = 0; i < element_count; i++) {
+			new_arr[i] = get(i);
+		}
 	}
 	}
 
 
 	return new_arr;
 	return new_arr;

+ 3 - 0
core/variant/array.h

@@ -63,8 +63,10 @@ public:
 
 
 	bool operator==(const Array &p_array) const;
 	bool operator==(const Array &p_array) const;
 	bool operator!=(const Array &p_array) const;
 	bool operator!=(const Array &p_array) const;
+	bool recursive_equal(const Array &p_array, int recursion_count) const;
 
 
 	uint32_t hash() const;
 	uint32_t hash() const;
+	uint32_t recursive_hash(int recursion_count) const;
 	void operator=(const Array &p_array);
 	void operator=(const Array &p_array);
 
 
 	void push_back(const Variant &p_value);
 	void push_back(const Variant &p_value);
@@ -100,6 +102,7 @@ public:
 	Variant pop_at(int p_pos);
 	Variant pop_at(int p_pos);
 
 
 	Array duplicate(bool p_deep = false) const;
 	Array duplicate(bool p_deep = false) const;
+	Array recursive_duplicate(bool p_deep, int recursion_count) const;
 
 
 	Array slice(int p_begin, int p_end, int p_step = 1, bool p_deep = false) const;
 	Array slice(int p_begin, int p_end, int p_step = 1, bool p_deep = false) const;
 	Array filter(const Callable &p_callable) const;
 	Array filter(const Callable &p_callable) const;

+ 56 - 6
core/variant/dictionary.cpp

@@ -188,11 +188,35 @@ bool Dictionary::erase(const Variant &p_key) {
 }
 }
 
 
 bool Dictionary::operator==(const Dictionary &p_dictionary) const {
 bool Dictionary::operator==(const Dictionary &p_dictionary) const {
-	return _p == p_dictionary._p;
+	return recursive_equal(p_dictionary, 0);
 }
 }
 
 
 bool Dictionary::operator!=(const Dictionary &p_dictionary) const {
 bool Dictionary::operator!=(const Dictionary &p_dictionary) const {
-	return _p != p_dictionary._p;
+	return !recursive_equal(p_dictionary, 0);
+}
+
+bool Dictionary::recursive_equal(const Dictionary &p_dictionary, int recursion_count) const {
+	// Cheap checks
+	if (_p == p_dictionary._p) {
+		return true;
+	}
+	if (_p->variant_map.size() != p_dictionary._p->variant_map.size()) {
+		return false;
+	}
+
+	// Heavy O(n) check
+	if (recursion_count > MAX_RECURSION) {
+		ERR_PRINT("Max recursion reached");
+		return true;
+	}
+	recursion_count++;
+	for (OrderedHashMap<Variant, Variant, VariantHasher, VariantComparator>::ConstElement this_E = ((const OrderedHashMap<Variant, Variant, VariantHasher, VariantComparator> *)&_p->variant_map)->front(); this_E; this_E = this_E.next()) {
+		OrderedHashMap<Variant, Variant, VariantHasher, VariantComparator>::ConstElement other_E = ((const OrderedHashMap<Variant, Variant, VariantHasher, VariantComparator> *)&p_dictionary._p->variant_map)->find(this_E.key());
+		if (!other_E || !this_E.value().hash_compare(other_E.value(), recursion_count)) {
+			return false;
+		}
+	}
+	return true;
 }
 }
 
 
 void Dictionary::_ref(const Dictionary &p_from) const {
 void Dictionary::_ref(const Dictionary &p_from) const {
@@ -225,11 +249,21 @@ void Dictionary::_unref() const {
 }
 }
 
 
 uint32_t Dictionary::hash() const {
 uint32_t Dictionary::hash() const {
+	return recursive_hash(0);
+}
+
+uint32_t Dictionary::recursive_hash(int recursion_count) const {
+	if (recursion_count > MAX_RECURSION) {
+		ERR_PRINT("Max recursion reached");
+		return 0;
+	}
+
 	uint32_t h = hash_djb2_one_32(Variant::DICTIONARY);
 	uint32_t h = hash_djb2_one_32(Variant::DICTIONARY);
 
 
+	recursion_count++;
 	for (OrderedHashMap<Variant, Variant, VariantHasher, VariantComparator>::Element E = _p->variant_map.front(); E; E = E.next()) {
 	for (OrderedHashMap<Variant, Variant, VariantHasher, VariantComparator>::Element E = _p->variant_map.front(); E; E = E.next()) {
-		h = hash_djb2_one_32(E.key().hash(), h);
-		h = hash_djb2_one_32(E.value().hash(), h);
+		h = hash_djb2_one_32(E.key().recursive_hash(recursion_count), h);
+		h = hash_djb2_one_32(E.value().recursive_hash(recursion_count), h);
 	}
 	}
 
 
 	return h;
 	return h;
@@ -286,10 +320,26 @@ const Variant *Dictionary::next(const Variant *p_key) const {
 }
 }
 
 
 Dictionary Dictionary::duplicate(bool p_deep) const {
 Dictionary Dictionary::duplicate(bool p_deep) const {
+	return recursive_duplicate(p_deep, 0);
+}
+
+Dictionary Dictionary::recursive_duplicate(bool p_deep, int recursion_count) const {
 	Dictionary n;
 	Dictionary n;
 
 
-	for (OrderedHashMap<Variant, Variant, VariantHasher, VariantComparator>::Element E = _p->variant_map.front(); E; E = E.next()) {
-		n[E.key()] = p_deep ? E.value().duplicate(true) : E.value();
+	if (recursion_count > MAX_RECURSION) {
+		ERR_PRINT("Max recursion reached");
+		return n;
+	}
+
+	if (p_deep) {
+		recursion_count++;
+		for (OrderedHashMap<Variant, Variant, VariantHasher, VariantComparator>::Element E = _p->variant_map.front(); E; E = E.next()) {
+			n[E.key().recursive_duplicate(true, recursion_count)] = E.value().recursive_duplicate(true, recursion_count);
+		}
+	} else {
+		for (OrderedHashMap<Variant, Variant, VariantHasher, VariantComparator>::Element E = _p->variant_map.front(); E; E = E.next()) {
+			n[E.key()] = E.value();
+		}
 	}
 	}
 
 
 	return n;
 	return n;

+ 3 - 0
core/variant/dictionary.h

@@ -70,8 +70,10 @@ public:
 
 
 	bool operator==(const Dictionary &p_dictionary) const;
 	bool operator==(const Dictionary &p_dictionary) const;
 	bool operator!=(const Dictionary &p_dictionary) const;
 	bool operator!=(const Dictionary &p_dictionary) const;
+	bool recursive_equal(const Dictionary &p_dictionary, int recursion_count) const;
 
 
 	uint32_t hash() const;
 	uint32_t hash() const;
+	uint32_t recursive_hash(int recursion_count) const;
 	void operator=(const Dictionary &p_dictionary);
 	void operator=(const Dictionary &p_dictionary);
 
 
 	const Variant *next(const Variant *p_key = nullptr) const;
 	const Variant *next(const Variant *p_key = nullptr) const;
@@ -80,6 +82,7 @@ public:
 	Array values() const;
 	Array values() const;
 
 
 	Dictionary duplicate(bool p_deep = false) const;
 	Dictionary duplicate(bool p_deep = false) const;
+	Dictionary recursive_duplicate(bool p_deep, int recursion_count) const;
 
 
 	const void *id() const;
 	const void *id() const;
 
 

+ 41 - 43
core/variant/variant.cpp

@@ -784,16 +784,11 @@ bool Variant::can_convert_strict(Variant::Type p_type_from, Variant::Type p_type
 }
 }
 
 
 bool Variant::operator==(const Variant &p_variant) const {
 bool Variant::operator==(const Variant &p_variant) const {
-	if (type != p_variant.type) { //evaluation of operator== needs to be more strict
-		return false;
-	}
-	bool v;
-	Variant r;
-	evaluate(OP_EQUAL, *this, p_variant, r, v);
-	return r;
+	return hash_compare(p_variant);
 }
 }
 
 
 bool Variant::operator!=(const Variant &p_variant) const {
 bool Variant::operator!=(const Variant &p_variant) const {
+	// Don't use `!hash_compare(p_variant)` given it makes use of OP_EQUAL
 	if (type != p_variant.type) { //evaluation of operator== needs to be more strict
 	if (type != p_variant.type) { //evaluation of operator== needs to be more strict
 		return true;
 		return true;
 	}
 	}
@@ -1617,25 +1612,23 @@ struct _VariantStrPair {
 };
 };
 
 
 Variant::operator String() const {
 Variant::operator String() const {
-	List<const void *> stack;
-
-	return stringify(stack);
+	return stringify(0);
 }
 }
 
 
 template <class T>
 template <class T>
-String stringify_vector(const T &vec, List<const void *> &stack) {
+String stringify_vector(const T &vec, int recursion_count) {
 	String str("[");
 	String str("[");
 	for (int i = 0; i < vec.size(); i++) {
 	for (int i = 0; i < vec.size(); i++) {
 		if (i > 0) {
 		if (i > 0) {
 			str += ", ";
 			str += ", ";
 		}
 		}
-		str = str + Variant(vec[i]).stringify(stack);
+		str = str + Variant(vec[i]).stringify(recursion_count);
 	}
 	}
 	str += "]";
 	str += "]";
 	return str;
 	return str;
 }
 }
 
 
-String Variant::stringify(List<const void *> &stack) const {
+String Variant::stringify(int recursion_count) const {
 	switch (type) {
 	switch (type) {
 		case NIL:
 		case NIL:
 			return "null";
 			return "null";
@@ -1679,23 +1672,22 @@ String Variant::stringify(List<const void *> &stack) const {
 			return operator Color();
 			return operator Color();
 		case DICTIONARY: {
 		case DICTIONARY: {
 			const Dictionary &d = *reinterpret_cast<const Dictionary *>(_data._mem);
 			const Dictionary &d = *reinterpret_cast<const Dictionary *>(_data._mem);
-			if (stack.find(d.id())) {
+			if (recursion_count > MAX_RECURSION) {
+				ERR_PRINT("Max recursion reached");
 				return "{...}";
 				return "{...}";
 			}
 			}
 
 
-			stack.push_back(d.id());
-
-			//const String *K=nullptr;
 			String str("{");
 			String str("{");
 			List<Variant> keys;
 			List<Variant> keys;
 			d.get_key_list(&keys);
 			d.get_key_list(&keys);
 
 
 			Vector<_VariantStrPair> pairs;
 			Vector<_VariantStrPair> pairs;
 
 
-			for (const Variant &E : keys) {
+			recursion_count++;
+			for (List<Variant>::Element *E = keys.front(); E; E = E->next()) {
 				_VariantStrPair sp;
 				_VariantStrPair sp;
-				sp.key = E.stringify(stack);
-				sp.value = d[E].stringify(stack);
+				sp.key = E->get().stringify(recursion_count);
+				sp.value = d[E->get()].stringify(recursion_count);
 
 
 				pairs.push_back(sp);
 				pairs.push_back(sp);
 			}
 			}
@@ -1710,46 +1702,43 @@ String Variant::stringify(List<const void *> &stack) const {
 			}
 			}
 			str += "}";
 			str += "}";
 
 
-			stack.erase(d.id());
 			return str;
 			return str;
 		} break;
 		} break;
 		case PACKED_VECTOR2_ARRAY: {
 		case PACKED_VECTOR2_ARRAY: {
-			return stringify_vector(operator Vector<Vector2>(), stack);
+			return stringify_vector(operator Vector<Vector2>(), recursion_count);
 		} break;
 		} break;
 		case PACKED_VECTOR3_ARRAY: {
 		case PACKED_VECTOR3_ARRAY: {
-			return stringify_vector(operator Vector<Vector3>(), stack);
+			return stringify_vector(operator Vector<Vector3>(), recursion_count);
 		} break;
 		} break;
 		case PACKED_COLOR_ARRAY: {
 		case PACKED_COLOR_ARRAY: {
-			return stringify_vector(operator Vector<Color>(), stack);
+			return stringify_vector(operator Vector<Color>(), recursion_count);
 		} break;
 		} break;
 		case PACKED_STRING_ARRAY: {
 		case PACKED_STRING_ARRAY: {
-			return stringify_vector(operator Vector<String>(), stack);
+			return stringify_vector(operator Vector<String>(), recursion_count);
 		} break;
 		} break;
 		case PACKED_BYTE_ARRAY: {
 		case PACKED_BYTE_ARRAY: {
-			return stringify_vector(operator Vector<uint8_t>(), stack);
+			return stringify_vector(operator Vector<uint8_t>(), recursion_count);
 		} break;
 		} break;
 		case PACKED_INT32_ARRAY: {
 		case PACKED_INT32_ARRAY: {
-			return stringify_vector(operator Vector<int32_t>(), stack);
+			return stringify_vector(operator Vector<int32_t>(), recursion_count);
 		} break;
 		} break;
 		case PACKED_INT64_ARRAY: {
 		case PACKED_INT64_ARRAY: {
-			return stringify_vector(operator Vector<int64_t>(), stack);
+			return stringify_vector(operator Vector<int64_t>(), recursion_count);
 		} break;
 		} break;
 		case PACKED_FLOAT32_ARRAY: {
 		case PACKED_FLOAT32_ARRAY: {
-			return stringify_vector(operator Vector<float>(), stack);
+			return stringify_vector(operator Vector<float>(), recursion_count);
 		} break;
 		} break;
 		case PACKED_FLOAT64_ARRAY: {
 		case PACKED_FLOAT64_ARRAY: {
-			return stringify_vector(operator Vector<double>(), stack);
+			return stringify_vector(operator Vector<double>(), recursion_count);
 		} break;
 		} break;
 		case ARRAY: {
 		case ARRAY: {
 			Array arr = operator Array();
 			Array arr = operator Array();
-			if (stack.find(arr.id())) {
+			if (recursion_count > MAX_RECURSION) {
+				ERR_PRINT("Max recursion reached");
 				return "[...]";
 				return "[...]";
 			}
 			}
-			stack.push_back(arr.id());
 
 
-			String str = stringify_vector(arr, stack);
-
-			stack.erase(arr.id());
+			String str = stringify_vector(arr, recursion_count);
 			return str;
 			return str;
 
 
 		} break;
 		} break;
@@ -2768,6 +2757,10 @@ Variant::Variant(const Variant &p_variant) {
 }
 }
 
 
 uint32_t Variant::hash() const {
 uint32_t Variant::hash() const {
+	return recursive_hash(0);
+}
+
+uint32_t Variant::recursive_hash(int recursion_count) const {
 	switch (type) {
 	switch (type) {
 		case NIL: {
 		case NIL: {
 			return 0;
 			return 0;
@@ -2895,7 +2888,7 @@ uint32_t Variant::hash() const {
 			return reinterpret_cast<const NodePath *>(_data._mem)->hash();
 			return reinterpret_cast<const NodePath *>(_data._mem)->hash();
 		} break;
 		} break;
 		case DICTIONARY: {
 		case DICTIONARY: {
-			return reinterpret_cast<const Dictionary *>(_data._mem)->hash();
+			return reinterpret_cast<const Dictionary *>(_data._mem)->recursive_hash(recursion_count);
 
 
 		} break;
 		} break;
 		case CALLABLE: {
 		case CALLABLE: {
@@ -2909,7 +2902,7 @@ uint32_t Variant::hash() const {
 		} break;
 		} break;
 		case ARRAY: {
 		case ARRAY: {
 			const Array &arr = *reinterpret_cast<const Array *>(_data._mem);
 			const Array &arr = *reinterpret_cast<const Array *>(_data._mem);
-			return arr.hash();
+			return arr.recursive_hash(recursion_count);
 
 
 		} break;
 		} break;
 		case PACKED_BYTE_ARRAY: {
 		case PACKED_BYTE_ARRAY: {
@@ -3083,7 +3076,7 @@ uint32_t Variant::hash() const {
                                                                         \
                                                                         \
 	return true
 	return true
 
 
-bool Variant::hash_compare(const Variant &p_variant) const {
+bool Variant::hash_compare(const Variant &p_variant, int recursion_count) const {
 	if (type != p_variant.type) {
 	if (type != p_variant.type) {
 		return false;
 		return false;
 	}
 	}
@@ -3214,14 +3207,19 @@ bool Variant::hash_compare(const Variant &p_variant) const {
 			const Array &l = *(reinterpret_cast<const Array *>(_data._mem));
 			const Array &l = *(reinterpret_cast<const Array *>(_data._mem));
 			const Array &r = *(reinterpret_cast<const Array *>(p_variant._data._mem));
 			const Array &r = *(reinterpret_cast<const Array *>(p_variant._data._mem));
 
 
-			if (l.size() != r.size()) {
+			if (!l.recursive_equal(r, recursion_count + 1)) {
 				return false;
 				return false;
 			}
 			}
 
 
-			for (int i = 0; i < l.size(); ++i) {
-				if (!l[i].hash_compare(r[i])) {
-					return false;
-				}
+			return true;
+		} break;
+
+		case DICTIONARY: {
+			const Dictionary &l = *(reinterpret_cast<const Dictionary *>(_data._mem));
+			const Dictionary &r = *(reinterpret_cast<const Dictionary *>(p_variant._data._mem));
+
+			if (!l.recursive_equal(r, recursion_count + 1)) {
+				return false;
 			}
 			}
 
 
 			return true;
 			return true;

+ 5 - 3
core/variant/variant.h

@@ -481,7 +481,8 @@ public:
 	static PTROperatorEvaluator get_ptr_operator_evaluator(Operator p_operator, Type p_type_a, Type p_type_b);
 	static PTROperatorEvaluator get_ptr_operator_evaluator(Operator p_operator, Type p_type_a, Type p_type_b);
 
 
 	void zero();
 	void zero();
-	Variant duplicate(bool deep = false) const;
+	Variant duplicate(bool p_deep = false) const;
+	Variant recursive_duplicate(bool p_deep, int recursion_count) const;
 	static void blend(const Variant &a, const Variant &b, float c, Variant &r_dst);
 	static void blend(const Variant &a, const Variant &b, float c, Variant &r_dst);
 	static void interpolate(const Variant &a, const Variant &b, float c, Variant &r_dst);
 	static void interpolate(const Variant &a, const Variant &b, float c, Variant &r_dst);
 
 
@@ -659,10 +660,11 @@ public:
 	bool operator!=(const Variant &p_variant) const;
 	bool operator!=(const Variant &p_variant) const;
 	bool operator<(const Variant &p_variant) const;
 	bool operator<(const Variant &p_variant) const;
 	uint32_t hash() const;
 	uint32_t hash() const;
+	uint32_t recursive_hash(int recursion_count) const;
 
 
-	bool hash_compare(const Variant &p_variant) const;
+	bool hash_compare(const Variant &p_variant, int recursion_count = 0) const;
 	bool booleanize() const;
 	bool booleanize() const;
-	String stringify(List<const void *> &stack) const;
+	String stringify(int recursion_count = 0) const;
 	String to_json_string() const;
 	String to_json_string() const;
 
 
 	void static_assign(const Variant &p_variant);
 	void static_assign(const Variant &p_variant);

+ 44 - 29
core/variant/variant_parser.cpp

@@ -1443,7 +1443,7 @@ static String rtos_fix(double p_value) {
 	}
 	}
 }
 }
 
 
-Error VariantWriter::write(const Variant &p_variant, StoreStringFunc p_store_string_func, void *p_store_string_ud, EncodeResourceFunc p_encode_res_func, void *p_encode_res_ud) {
+Error VariantWriter::write(const Variant &p_variant, StoreStringFunc p_store_string_func, void *p_store_string_ud, EncodeResourceFunc p_encode_res_func, void *p_encode_res_ud, int recursion_count) {
 	switch (p_variant.get_type()) {
 	switch (p_variant.get_type()) {
 		case Variant::NIL: {
 		case Variant::NIL: {
 			p_store_string_func(p_store_string_ud, "null");
 			p_store_string_func(p_store_string_ud, "null");
@@ -1639,41 +1639,56 @@ Error VariantWriter::write(const Variant &p_variant, StoreStringFunc p_store_str
 
 
 		case Variant::DICTIONARY: {
 		case Variant::DICTIONARY: {
 			Dictionary dict = p_variant;
 			Dictionary dict = p_variant;
-
-			List<Variant> keys;
-			dict.get_key_list(&keys);
-			keys.sort();
-
-			p_store_string_func(p_store_string_ud, "{\n");
-			for (List<Variant>::Element *E = keys.front(); E; E = E->next()) {
-				/*
-				if (!_check_type(dict[E]))
-					continue;
-				*/
-				write(E->get(), p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud);
-				p_store_string_func(p_store_string_ud, ": ");
-				write(dict[E->get()], p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud);
-				if (E->next()) {
-					p_store_string_func(p_store_string_ud, ",\n");
-				} else {
-					p_store_string_func(p_store_string_ud, "\n");
+			if (recursion_count > MAX_RECURSION) {
+				ERR_PRINT("Max recursion reached");
+				p_store_string_func(p_store_string_ud, "{}");
+			} else {
+				recursion_count++;
+
+				List<Variant> keys;
+				dict.get_key_list(&keys);
+				keys.sort();
+
+				p_store_string_func(p_store_string_ud, "{\n");
+				for (List<Variant>::Element *E = keys.front(); E; E = E->next()) {
+					/*
+					if (!_check_type(dict[E->get()]))
+						continue;
+					*/
+					write(E->get(), p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud, recursion_count);
+					p_store_string_func(p_store_string_ud, ": ");
+					write(dict[E->get()], p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud, recursion_count);
+					if (E->next()) {
+						p_store_string_func(p_store_string_ud, ",\n");
+					} else {
+						p_store_string_func(p_store_string_ud, "\n");
+					}
 				}
 				}
-			}
 
 
-			p_store_string_func(p_store_string_ud, "}");
+				p_store_string_func(p_store_string_ud, "}");
+			}
 
 
 		} break;
 		} break;
+
 		case Variant::ARRAY: {
 		case Variant::ARRAY: {
-			p_store_string_func(p_store_string_ud, "[");
-			Array array = p_variant;
-			int len = array.size();
-			for (int i = 0; i < len; i++) {
-				if (i > 0) {
-					p_store_string_func(p_store_string_ud, ", ");
+			if (recursion_count > MAX_RECURSION) {
+				ERR_PRINT("Max recursion reached");
+				p_store_string_func(p_store_string_ud, "[]");
+			} else {
+				recursion_count++;
+
+				p_store_string_func(p_store_string_ud, "[");
+				Array array = p_variant;
+				int len = array.size();
+				for (int i = 0; i < len; i++) {
+					if (i > 0) {
+						p_store_string_func(p_store_string_ud, ", ");
+					}
+					write(array[i], p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud, recursion_count);
 				}
 				}
-				write(array[i], p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud);
+
+				p_store_string_func(p_store_string_ud, "]");
 			}
 			}
-			p_store_string_func(p_store_string_ud, "]");
 
 
 		} break;
 		} break;
 
 

+ 1 - 1
core/variant/variant_parser.h

@@ -140,7 +140,7 @@ public:
 	typedef Error (*StoreStringFunc)(void *ud, const String &p_string);
 	typedef Error (*StoreStringFunc)(void *ud, const String &p_string);
 	typedef String (*EncodeResourceFunc)(void *ud, const RES &p_resource);
 	typedef String (*EncodeResourceFunc)(void *ud, const RES &p_resource);
 
 
-	static Error write(const Variant &p_variant, StoreStringFunc p_store_string_func, void *p_store_string_ud, EncodeResourceFunc p_encode_res_func, void *p_encode_res_ud);
+	static Error write(const Variant &p_variant, StoreStringFunc p_store_string_func, void *p_store_string_ud, EncodeResourceFunc p_encode_res_func, void *p_encode_res_ud, int recursion_count = 0);
 	static Error write_to_string(const Variant &p_variant, String &r_string, EncodeResourceFunc p_encode_res_func = nullptr, void *p_encode_res_ud = nullptr);
 	static Error write_to_string(const Variant &p_variant, String &r_string, EncodeResourceFunc p_encode_res_func = nullptr, void *p_encode_res_ud = nullptr);
 };
 };
 
 

+ 8 - 4
core/variant/variant_setget.cpp

@@ -1824,11 +1824,15 @@ Variant Variant::iter_get(const Variant &r_iter, bool &r_valid) const {
 	return Variant();
 	return Variant();
 }
 }
 
 
-Variant Variant::duplicate(bool deep) const {
+Variant Variant::duplicate(bool p_deep) const {
+	return recursive_duplicate(p_deep, 0);
+}
+
+Variant Variant::recursive_duplicate(bool p_deep, int recursion_count) const {
 	switch (type) {
 	switch (type) {
 		case OBJECT: {
 		case OBJECT: {
 			/*  breaks stuff :(
 			/*  breaks stuff :(
-			if (deep && !_get_obj().ref.is_null()) {
+			if (p_deep && !_get_obj().ref.is_null()) {
 				Ref<Resource> resource = _get_obj().ref;
 				Ref<Resource> resource = _get_obj().ref;
 				if (resource.is_valid()) {
 				if (resource.is_valid()) {
 					return resource->duplicate(true);
 					return resource->duplicate(true);
@@ -1838,9 +1842,9 @@ Variant Variant::duplicate(bool deep) const {
 			return *this;
 			return *this;
 		} break;
 		} break;
 		case DICTIONARY:
 		case DICTIONARY:
-			return operator Dictionary().duplicate(deep);
+			return operator Dictionary().recursive_duplicate(p_deep, recursion_count);
 		case ARRAY:
 		case ARRAY:
-			return operator Array().duplicate(deep);
+			return operator Array().recursive_duplicate(p_deep, recursion_count);
 		case PACKED_BYTE_ARRAY:
 		case PACKED_BYTE_ARRAY:
 			return operator Vector<uint8_t>().duplicate();
 			return operator Vector<uint8_t>().duplicate();
 		case PACKED_INT32_ARRAY:
 		case PACKED_INT32_ARRAY:

+ 234 - 0
tests/test_array.h

@@ -43,6 +43,25 @@
 
 
 namespace TestArray {
 namespace TestArray {
 
 
+static inline Array build_array() {
+	return Array();
+}
+template <typename... Targs>
+static inline Array build_array(Variant item, Targs... Fargs) {
+	Array a = build_array(Fargs...);
+	a.push_front(item);
+	return a;
+}
+static inline Dictionary build_dictionary() {
+	return Dictionary();
+}
+template <typename... Targs>
+static inline Dictionary build_dictionary(Variant key, Variant item, Targs... Fargs) {
+	Dictionary d = build_dictionary(Fargs...);
+	d[key] = item;
+	return d;
+}
+
 TEST_CASE("[Array] size(), clear(), and is_empty()") {
 TEST_CASE("[Array] size(), clear(), and is_empty()") {
 	Array arr;
 	Array arr;
 	CHECK(arr.size() == 0);
 	CHECK(arr.size() == 0);
@@ -232,6 +251,221 @@ TEST_CASE("[Array] max() and min()") {
 	CHECK(max == 5);
 	CHECK(max == 5);
 	CHECK(min == 2);
 	CHECK(min == 2);
 }
 }
+
+TEST_CASE("[Array] Duplicate array") {
+	// a = [1, [2, 2], {3: 3}]
+	Array a = build_array(1, build_array(2, 2), build_dictionary(3, 3));
+
+	// Deep copy
+	Array deep_a = a.duplicate(true);
+	CHECK_MESSAGE(deep_a.id() != a.id(), "Should create a new array");
+	CHECK_MESSAGE(Array(deep_a[1]).id() != Array(a[1]).id(), "Should clone nested array");
+	CHECK_MESSAGE(Dictionary(deep_a[2]).id() != Dictionary(a[2]).id(), "Should clone nested dictionary");
+	CHECK_EQ(deep_a, a);
+	deep_a.push_back(1);
+	CHECK_NE(deep_a, a);
+	deep_a.pop_back();
+	Array(deep_a[1]).push_back(1);
+	CHECK_NE(deep_a, a);
+	Array(deep_a[1]).pop_back();
+	CHECK_EQ(deep_a, a);
+
+	// Shallow copy
+	Array shallow_a = a.duplicate(false);
+	CHECK_MESSAGE(shallow_a.id() != a.id(), "Should create a new array");
+	CHECK_MESSAGE(Array(shallow_a[1]).id() == Array(a[1]).id(), "Should keep nested array");
+	CHECK_MESSAGE(Dictionary(shallow_a[2]).id() == Dictionary(a[2]).id(), "Should keep nested dictionary");
+	CHECK_EQ(shallow_a, a);
+	Array(shallow_a).push_back(1);
+	CHECK_NE(shallow_a, a);
+}
+
+TEST_CASE("[Array] Duplicate recursive array") {
+	// Self recursive
+	Array a;
+	a.push_back(a);
+
+	Array a_shallow = a.duplicate(false);
+	CHECK_EQ(a, a_shallow);
+
+	// Deep copy of recursive array endup with recursion limit and return
+	// an invalid result (multiple nested arrays), the point is we should
+	// not end up with a segfault and an error log should be printed
+	ERR_PRINT_OFF;
+	a.duplicate(true);
+	ERR_PRINT_ON;
+
+	// Nested recursive
+	Array a1;
+	Array a2;
+	a2.push_back(a1);
+	a1.push_back(a2);
+
+	Array a1_shallow = a1.duplicate(false);
+	CHECK_EQ(a1, a1_shallow);
+
+	// Same deep copy issue as above
+	ERR_PRINT_OFF;
+	a1.duplicate(true);
+	ERR_PRINT_ON;
+
+	// Break the recursivity otherwise Array teardown will leak memory
+	a.clear();
+	a1.clear();
+	a2.clear();
+}
+
+TEST_CASE("[Array] Hash array") {
+	// a = [1, [2, 2], {3: 3}]
+	Array a = build_array(1, build_array(2, 2), build_dictionary(3, 3));
+	uint32_t original_hash = a.hash();
+
+	a.push_back(1);
+	CHECK_NE(a.hash(), original_hash);
+
+	a.pop_back();
+	CHECK_EQ(a.hash(), original_hash);
+
+	Array(a[1]).push_back(1);
+	CHECK_NE(a.hash(), original_hash);
+	Array(a[1]).pop_back();
+	CHECK_EQ(a.hash(), original_hash);
+
+	(Dictionary(a[2]))[1] = 1;
+	CHECK_NE(a.hash(), original_hash);
+	Dictionary(a[2]).erase(1);
+	CHECK_EQ(a.hash(), original_hash);
+
+	Array a2 = a.duplicate(true);
+	CHECK_EQ(a2.hash(), a.hash());
+}
+
+TEST_CASE("[Array] Hash recursive array") {
+	Array a1;
+	a1.push_back(a1);
+
+	Array a2;
+	a2.push_back(a2);
+
+	// Hash should reach recursion limit
+	ERR_PRINT_OFF;
+	CHECK_EQ(a1.hash(), a2.hash());
+	ERR_PRINT_ON;
+
+	// Break the recursivity otherwise Array teardown will leak memory
+	a1.clear();
+	a2.clear();
+}
+
+TEST_CASE("[Array] Empty comparison") {
+	Array a1;
+	Array a2;
+
+	// test both operator== and operator!=
+	CHECK_EQ(a1, a2);
+	CHECK_FALSE(a1 != a2);
+}
+
+TEST_CASE("[Array] Flat comparison") {
+	Array a1 = build_array(1);
+	Array a2 = build_array(1);
+	Array other_a = build_array(2);
+
+	// test both operator== and operator!=
+	CHECK_EQ(a1, a1); // compare self
+	CHECK_FALSE(a1 != a1);
+	CHECK_EQ(a1, a2); // different equivalent arrays
+	CHECK_FALSE(a1 != a2);
+	CHECK_NE(a1, other_a); // different arrays with different content
+	CHECK_FALSE(a1 == other_a);
+}
+
+TEST_CASE("[Array] Nested array comparison") {
+	// a1 = [[[1], 2], 3]
+	Array a1 = build_array(build_array(build_array(1), 2), 3);
+
+	Array a2 = a1.duplicate(true);
+
+	// other_a = [[[1, 0], 2], 3]
+	Array other_a = build_array(build_array(build_array(1, 0), 2), 3);
+
+	// test both operator== and operator!=
+	CHECK_EQ(a1, a1); // compare self
+	CHECK_FALSE(a1 != a1);
+	CHECK_EQ(a1, a2); // different equivalent arrays
+	CHECK_FALSE(a1 != a2);
+	CHECK_NE(a1, other_a); // different arrays with different content
+	CHECK_FALSE(a1 == other_a);
+}
+
+TEST_CASE("[Array] Nested dictionary comparison") {
+	// a1 = [{1: 2}, 3]
+	Array a1 = build_array(build_dictionary(1, 2), 3);
+
+	Array a2 = a1.duplicate(true);
+
+	// other_a = [{1: 0}, 3]
+	Array other_a = build_array(build_dictionary(1, 0), 3);
+
+	// test both operator== and operator!=
+	CHECK_EQ(a1, a1); // compare self
+	CHECK_FALSE(a1 != a1);
+	CHECK_EQ(a1, a2); // different equivalent arrays
+	CHECK_FALSE(a1 != a2);
+	CHECK_NE(a1, other_a); // different arrays with different content
+	CHECK_FALSE(a1 == other_a);
+}
+
+TEST_CASE("[Array] Recursive comparison") {
+	Array a1;
+	a1.push_back(a1);
+
+	Array a2;
+	a2.push_back(a2);
+
+	// Comparison should reach recursion limit
+	ERR_PRINT_OFF;
+	CHECK_EQ(a1, a2);
+	CHECK_FALSE(a1 != a2);
+	ERR_PRINT_ON;
+
+	a1.push_back(1);
+	a2.push_back(1);
+
+	// Comparison should reach recursion limit
+	ERR_PRINT_OFF;
+	CHECK_EQ(a1, a2);
+	CHECK_FALSE(a1 != a2);
+	ERR_PRINT_ON;
+
+	a1.push_back(1);
+	a2.push_back(2);
+
+	// Comparison should reach recursion limit
+	ERR_PRINT_OFF;
+	CHECK_NE(a1, a2);
+	CHECK_FALSE(a1 == a2);
+	ERR_PRINT_ON;
+
+	// Break the recursivity otherwise Array tearndown will leak memory
+	a1.clear();
+	a2.clear();
+}
+
+TEST_CASE("[Array] Recursive self comparison") {
+	Array a1;
+	Array a2;
+	a2.push_back(a1);
+	a1.push_back(a2);
+
+	CHECK_EQ(a1, a1);
+	CHECK_FALSE(a1 != a1);
+
+	// Break the recursivity otherwise Array tearndown will leak memory
+	a1.clear();
+	a2.clear();
+}
+
 } // namespace TestArray
 } // namespace TestArray
 
 
 #endif // TEST_ARRAY_H
 #endif // TEST_ARRAY_H

+ 358 - 9
tests/test_dictionary.h

@@ -39,6 +39,25 @@
 
 
 namespace TestDictionary {
 namespace TestDictionary {
 
 
+static inline Array build_array() {
+	return Array();
+}
+template <typename... Targs>
+static inline Array build_array(Variant item, Targs... Fargs) {
+	Array a = build_array(Fargs...);
+	a.push_front(item);
+	return a;
+}
+static inline Dictionary build_dictionary() {
+	return Dictionary();
+}
+template <typename... Targs>
+static inline Dictionary build_dictionary(Variant key, Variant item, Targs... Fargs) {
+	Dictionary d = build_dictionary(Fargs...);
+	d[key] = item;
+	return d;
+}
+
 TEST_CASE("[Dictionary] Assignment using bracket notation ([])") {
 TEST_CASE("[Dictionary] Assignment using bracket notation ([])") {
 	Dictionary map;
 	Dictionary map;
 	map["Hello"] = 0;
 	map["Hello"] = 0;
@@ -61,15 +80,6 @@ TEST_CASE("[Dictionary] Assignment using bracket notation ([])") {
 	CHECK(int(map[false]) == 128);
 	CHECK(int(map[false]) == 128);
 }
 }
 
 
-TEST_CASE("[Dictionary] == and != operators") {
-	Dictionary map1;
-	Dictionary map2;
-	CHECK(map1 != map2);
-	map1[1] = 3;
-	map2 = map1;
-	CHECK(map1 == map2);
-}
-
 TEST_CASE("[Dictionary] get_key_lists()") {
 TEST_CASE("[Dictionary] get_key_lists()") {
 	Dictionary map;
 	Dictionary map;
 	List<Variant> keys;
 	List<Variant> keys;
@@ -155,5 +165,344 @@ TEST_CASE("[Dictionary] keys() and values()") {
 	CHECK(int(keys[0]) == 1);
 	CHECK(int(keys[0]) == 1);
 	CHECK(int(values[0]) == 3);
 	CHECK(int(values[0]) == 3);
 }
 }
+
+TEST_CASE("[Dictionary] Duplicate dictionary") {
+	// d = {1: {1: 1}, {2: 2}: [2], [3]: 3}
+	Dictionary k2 = build_dictionary(2, 2);
+	Array k3 = build_array(3);
+	Dictionary d = build_dictionary(1, build_dictionary(1, 1), k2, build_array(2), k3, 3);
+
+	// Deep copy
+	Dictionary deep_d = d.duplicate(true);
+	CHECK_MESSAGE(deep_d.id() != d.id(), "Should create a new dictionary");
+	CHECK_MESSAGE(Dictionary(deep_d[1]).id() != Dictionary(d[1]).id(), "Should clone nested dictionary");
+	CHECK_MESSAGE(Array(deep_d[k2]).id() != Array(d[k2]).id(), "Should clone nested array");
+	CHECK_EQ(deep_d, d);
+	deep_d[0] = 0;
+	CHECK_NE(deep_d, d);
+	deep_d.erase(0);
+	Dictionary(deep_d[1]).operator[](0) = 0;
+	CHECK_NE(deep_d, d);
+	Dictionary(deep_d[1]).erase(0);
+	CHECK_EQ(deep_d, d);
+	// Keys should also be copied
+	k2[0] = 0;
+	CHECK_NE(deep_d, d);
+	k2.erase(0);
+	CHECK_EQ(deep_d, d);
+	k3.push_back(0);
+	CHECK_NE(deep_d, d);
+	k3.pop_back();
+	CHECK_EQ(deep_d, d);
+
+	// Shallow copy
+	Dictionary shallow_d = d.duplicate(false);
+	CHECK_MESSAGE(shallow_d.id() != d.id(), "Should create a new array");
+	CHECK_MESSAGE(Dictionary(shallow_d[1]).id() == Dictionary(d[1]).id(), "Should keep nested dictionary");
+	CHECK_MESSAGE(Array(shallow_d[2]).id() == Array(d[2]).id(), "Should keep nested array");
+	CHECK_EQ(shallow_d, d);
+	shallow_d[0] = 0;
+	CHECK_NE(shallow_d, d);
+	shallow_d.erase(0);
+#if 0 // TODO: recursion in dict key currently is buggy
+	// Keys should also be shallowed
+	k2[0] = 0;
+	CHECK_EQ(shallow_d, d);
+	k2.erase(0);
+	k3.push_back(0);
+	CHECK_EQ(shallow_d, d);
+#endif
+}
+
+TEST_CASE("[Dictionary] Duplicate recursive dictionary") {
+	// Self recursive
+	Dictionary d;
+	d[1] = d;
+
+	Dictionary d_shallow = d.duplicate(false);
+	CHECK_EQ(d, d_shallow);
+
+	// Deep copy of recursive dictionary endup with recursion limit and return
+	// an invalid result (multiple nested dictionaries), the point is we should
+	// not end up with a segfault and an error log should be printed
+	ERR_PRINT_OFF;
+	d.duplicate(true);
+	ERR_PRINT_ON;
+
+	// Nested recursive
+	Dictionary d1;
+	Dictionary d2;
+	d1[2] = d2;
+	d2[1] = d1;
+
+	Dictionary d1_shallow = d1.duplicate(false);
+	CHECK_EQ(d1, d1_shallow);
+
+	// Same deep copy issue as above
+	ERR_PRINT_OFF;
+	d1.duplicate(true);
+	ERR_PRINT_ON;
+
+	// Break the recursivity otherwise Dictionary teardown will leak memory
+	d.clear();
+	d1.clear();
+	d2.clear();
+}
+
+#if 0 // TODO: duplicate recursion in dict key is currently buggy
+TEST_CASE("[Dictionary] Duplicate recursive dictionary on keys") {
+	// Self recursive
+	Dictionary d;
+	d[d] = d;
+
+	Dictionary d_shallow = d.duplicate(false);
+	CHECK_EQ(d, d_shallow);
+
+	// Deep copy of recursive dictionary endup with recursion limit and return
+	// an invalid result (multiple nested dictionaries), the point is we should
+	// not end up with a segfault and an error log should be printed
+	ERR_PRINT_OFF;
+	d.duplicate(true);
+	ERR_PRINT_ON;
+
+	// Nested recursive
+	Dictionary d1;
+	Dictionary d2;
+	d1[d2] = d2;
+	d2[d1] = d1;
+
+	Dictionary d1_shallow = d1.duplicate(false);
+	CHECK_EQ(d1, d1_shallow);
+
+	// Same deep copy issue as above
+	ERR_PRINT_OFF;
+	d1.duplicate(true);
+	ERR_PRINT_ON;
+
+	// Break the recursivity otherwise Dictionary teardown will leak memory
+	d.clear();
+	d1.clear();
+	d2.clear();
+}
+#endif
+
+TEST_CASE("[Dictionary] Hash dictionary") {
+	// d = {1: {1: 1}, {2: 2}: [2], [3]: 3}
+	Dictionary k2 = build_dictionary(2, 2);
+	Array k3 = build_array(3);
+	Dictionary d = build_dictionary(1, build_dictionary(1, 1), k2, build_array(2), k3, 3);
+	uint32_t original_hash = d.hash();
+
+	// Modify dict change the hash
+	d[0] = 0;
+	CHECK_NE(d.hash(), original_hash);
+	d.erase(0);
+	CHECK_EQ(d.hash(), original_hash);
+
+	// Modify nested item change the hash
+	Dictionary(d[1]).operator[](0) = 0;
+	CHECK_NE(d.hash(), original_hash);
+	Dictionary(d[1]).erase(0);
+	Array(d[k2]).push_back(0);
+	CHECK_NE(d.hash(), original_hash);
+	Array(d[k2]).pop_back();
+
+	// Modify a key change the hash
+	k2[0] = 0;
+	CHECK_NE(d.hash(), original_hash);
+	k2.erase(0);
+	CHECK_EQ(d.hash(), original_hash);
+	k3.push_back(0);
+	CHECK_NE(d.hash(), original_hash);
+	k3.pop_back();
+	CHECK_EQ(d.hash(), original_hash);
+
+	// Duplication doesn't change the hash
+	Dictionary d2 = d.duplicate(true);
+	CHECK_EQ(d2.hash(), original_hash);
+}
+
+TEST_CASE("[Dictionary] Hash recursive dictionary") {
+	Dictionary d;
+	d[1] = d;
+
+	// Hash should reach recursion limit, we just make sure this doesn't blow up
+	ERR_PRINT_OFF;
+	d.hash();
+	ERR_PRINT_ON;
+
+	// Break the recursivity otherwise Dictionary teardown will leak memory
+	d.clear();
+}
+
+#if 0 // TODO: recursion in dict key is currently buggy
+TEST_CASE("[Dictionary] Hash recursive dictionary on keys") {
+	Dictionary d;
+	d[d] = 1;
+
+	// Hash should reach recursion limit, we just make sure this doesn't blow up
+	ERR_PRINT_OFF;
+	d.hash();
+	ERR_PRINT_ON;
+
+	// Break the recursivity otherwise Dictionary teardown will leak memory
+	d.clear();
+}
+#endif
+
+TEST_CASE("[Dictionary] Empty comparison") {
+	Dictionary d1;
+	Dictionary d2;
+
+	// test both operator== and operator!=
+	CHECK_EQ(d1, d2);
+	CHECK_FALSE(d1 != d2);
+}
+
+TEST_CASE("[Dictionary] Flat comparison") {
+	Dictionary d1 = build_dictionary(1, 1);
+	Dictionary d2 = build_dictionary(1, 1);
+	Dictionary other_d = build_dictionary(2, 1);
+
+	// test both operator== and operator!=
+	CHECK_EQ(d1, d1); // compare self
+	CHECK_FALSE(d1 != d1);
+	CHECK_EQ(d1, d2); // different equivalent arrays
+	CHECK_FALSE(d1 != d2);
+	CHECK_NE(d1, other_d); // different arrays with different content
+	CHECK_FALSE(d1 == other_d);
+}
+
+TEST_CASE("[Dictionary] Nested dictionary comparison") {
+	// d1 = {1: {2: {3: 4}}}
+	Dictionary d1 = build_dictionary(1, build_dictionary(2, build_dictionary(3, 4)));
+
+	Dictionary d2 = d1.duplicate(true);
+
+	// other_d = {1: {2: {3: 0}}}
+	Dictionary other_d = build_dictionary(1, build_dictionary(2, build_dictionary(3, 0)));
+
+	// test both operator== and operator!=
+	CHECK_EQ(d1, d1); // compare self
+	CHECK_FALSE(d1 != d1);
+	CHECK_EQ(d1, d2); // different equivalent arrays
+	CHECK_FALSE(d1 != d2);
+	CHECK_NE(d1, other_d); // different arrays with different content
+	CHECK_FALSE(d1 == other_d);
+}
+
+TEST_CASE("[Dictionary] Nested array comparison") {
+	// d1 = {1: [2, 3]}
+	Dictionary d1 = build_dictionary(1, build_array(2, 3));
+
+	Dictionary d2 = d1.duplicate(true);
+
+	// other_d = {1: [2, 0]}
+	Dictionary other_d = build_dictionary(1, build_array(2, 0));
+
+	// test both operator== and operator!=
+	CHECK_EQ(d1, d1); // compare self
+	CHECK_FALSE(d1 != d1);
+	CHECK_EQ(d1, d2); // different equivalent arrays
+	CHECK_FALSE(d1 != d2);
+	CHECK_NE(d1, other_d); // different arrays with different content
+	CHECK_FALSE(d1 == other_d);
+}
+
+TEST_CASE("[Dictionary] Recursive comparison") {
+	Dictionary d1;
+	d1[1] = d1;
+
+	Dictionary d2;
+	d2[1] = d2;
+
+	// Comparison should reach recursion limit
+	ERR_PRINT_OFF;
+	CHECK_EQ(d1, d2);
+	CHECK_FALSE(d1 != d2);
+	ERR_PRINT_ON;
+
+	d1[2] = 2;
+	d2[2] = 2;
+
+	// Comparison should reach recursion limit
+	ERR_PRINT_OFF;
+	CHECK_EQ(d1, d2);
+	CHECK_FALSE(d1 != d2);
+	ERR_PRINT_ON;
+
+	d1[3] = 3;
+	d2[3] = 0;
+
+	// Comparison should reach recursion limit
+	ERR_PRINT_OFF;
+	CHECK_NE(d1, d2);
+	CHECK_FALSE(d1 == d2);
+	ERR_PRINT_ON;
+
+	// Break the recursivity otherwise Dictionary teardown will leak memory
+	d1.clear();
+	d2.clear();
+}
+
+#if 0 // TODO: recursion in dict key is currently buggy
+TEST_CASE("[Dictionary] Recursive comparison on keys") {
+	Dictionary d1;
+	// Hash computation should reach recursion limit
+	ERR_PRINT_OFF;
+	d1[d1] = 1;
+	ERR_PRINT_ON;
+
+	Dictionary d2;
+	// Hash computation should reach recursion limit
+	ERR_PRINT_OFF;
+	d2[d2] = 1;
+	ERR_PRINT_ON;
+
+	// Comparison should reach recursion limit
+	ERR_PRINT_OFF;
+	CHECK_EQ(d1, d2);
+	CHECK_FALSE(d1 != d2);
+	ERR_PRINT_ON;
+
+	d1[2] = 2;
+	d2[2] = 2;
+
+	// Comparison should reach recursion limit
+	ERR_PRINT_OFF;
+	CHECK_EQ(d1, d2);
+	CHECK_FALSE(d1 != d2);
+	ERR_PRINT_ON;
+
+	d1[3] = 3;
+	d2[3] = 0;
+
+	// Comparison should reach recursion limit
+	ERR_PRINT_OFF;
+	CHECK_NE(d1, d2);
+	CHECK_FALSE(d1 == d2);
+	ERR_PRINT_ON;
+
+	// Break the recursivity otherwise Dictionary teardown will leak memory
+	d1.clear();
+	d2.clear();
+}
+#endif
+
+TEST_CASE("[Dictionary] Recursive self comparison") {
+	Dictionary d1;
+	Dictionary d2;
+	d1[1] = d2;
+	d2[1] = d1;
+
+	CHECK_EQ(d1, d1);
+	CHECK_FALSE(d1 != d1);
+
+	// Break the recursivity otherwise Dictionary teardown will leak memory
+	d1.clear();
+	d2.clear();
+}
+
 } // namespace TestDictionary
 } // namespace TestDictionary
+
 #endif // TEST_DICTIONARY_H
 #endif // TEST_DICTIONARY_H

+ 1 - 0
tests/test_macros.h

@@ -33,6 +33,7 @@
 
 
 #include "core/object/callable_method_pointer.h"
 #include "core/object/callable_method_pointer.h"
 #include "core/object/class_db.h"
 #include "core/object/class_db.h"
+#include "core/string/print_string.h"
 #include "core/templates/map.h"
 #include "core/templates/map.h"
 #include "core/variant/variant.h"
 #include "core/variant/variant.h"
 
 

+ 211 - 0
tests/test_variant.h

@@ -38,6 +38,25 @@
 
 
 namespace TestVariant {
 namespace TestVariant {
 
 
+static inline Array build_array() {
+	return Array();
+}
+template <typename... Targs>
+static inline Array build_array(Variant item, Targs... Fargs) {
+	Array a = build_array(Fargs...);
+	a.push_front(item);
+	return a;
+}
+static inline Dictionary build_dictionary() {
+	return Dictionary();
+}
+template <typename... Targs>
+static inline Dictionary build_dictionary(Variant key, Variant item, Targs... Fargs) {
+	Dictionary d = build_dictionary(Fargs...);
+	d[key] = item;
+	return d;
+}
+
 TEST_CASE("[Variant] Writer and parser integer") {
 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;
 	String a32_str;
@@ -700,6 +719,198 @@ TEST_CASE("[Variant] Assignment To Color from Bool,Int,Float,String,Vec2,Vec2i,V
 	vec3i_v = col_v;
 	vec3i_v = col_v;
 	CHECK(vec3i_v.get_type() == Variant::COLOR);
 	CHECK(vec3i_v.get_type() == Variant::COLOR);
 }
 }
+TEST_CASE("[Variant] Writer and parser array") {
+	Array a = build_array(1, String("hello"), build_array(Variant()));
+	String a_str;
+	VariantWriter::write_to_string(a, a_str);
+
+	CHECK_EQ(a_str, "[1, \"hello\", [null]]");
+
+	VariantParser::StreamString ss;
+	String errs;
+	int line;
+	Variant a_parsed;
+
+	ss.s = a_str;
+	VariantParser::parse(&ss, a_parsed, errs, line);
+
+	CHECK_MESSAGE(a_parsed == Variant(a), "Should parse back.");
+}
+
+TEST_CASE("[Variant] Writer recursive array") {
+	// There is no way to accurately represent a recursive array,
+	// the only thing we can do is make sure the writer doesn't blow up
+
+	// Self recursive
+	Array a;
+	a.push_back(a);
+
+	// Writer should it recursion limit while visiting the array
+	ERR_PRINT_OFF;
+	String a_str;
+	VariantWriter::write_to_string(a, a_str);
+	ERR_PRINT_ON;
+
+	// Nested recursive
+	Array a1;
+	Array a2;
+	a1.push_back(a2);
+	a2.push_back(a1);
+
+	// Writer should it recursion limit while visiting the array
+	ERR_PRINT_OFF;
+	String a1_str;
+	VariantWriter::write_to_string(a1, a1_str);
+	ERR_PRINT_ON;
+
+	// Break the recursivity otherwise Dictionary tearndown will leak memory
+	a.clear();
+	a1.clear();
+	a2.clear();
+}
+
+TEST_CASE("[Variant] Writer and parser dictionary") {
+	// d = {{1: 2}: 3, 4: "hello", 5: {null: []}}
+	Dictionary d = build_dictionary(build_dictionary(1, 2), 3, 4, String("hello"), 5, build_dictionary(Variant(), build_array()));
+	String d_str;
+	VariantWriter::write_to_string(d, d_str);
+
+	CHECK_EQ(d_str, "{\n4: \"hello\",\n5: {\nnull: []\n},\n{\n1: 2\n}: 3\n}");
+
+	VariantParser::StreamString ss;
+	String errs;
+	int line;
+	Variant d_parsed;
+
+	ss.s = d_str;
+	VariantParser::parse(&ss, d_parsed, errs, line);
+
+	CHECK_MESSAGE(d_parsed == Variant(d), "Should parse back.");
+}
+
+TEST_CASE("[Variant] Writer recursive dictionary") {
+	// There is no way to accurately represent a recursive dictionary,
+	// the only thing we can do is make sure the writer doesn't blow up
+
+	// Self recursive
+	Dictionary d;
+	d[1] = d;
+
+	// Writer should it recursion limit while visiting the dictionary
+	ERR_PRINT_OFF;
+	String d_str;
+	VariantWriter::write_to_string(d, d_str);
+	ERR_PRINT_ON;
+
+	// Nested recursive
+	Dictionary d1;
+	Dictionary d2;
+	d1[2] = d2;
+	d2[1] = d1;
+
+	// Writer should it recursion limit while visiting the dictionary
+	ERR_PRINT_OFF;
+	String d1_str;
+	VariantWriter::write_to_string(d1, d1_str);
+	ERR_PRINT_ON;
+
+	// Break the recursivity otherwise Dictionary tearndown will leak memory
+	d.clear();
+	d1.clear();
+	d2.clear();
+}
+
+#if 0 // TODO: recursion in dict key is currently buggy
+TEST_CASE("[Variant] Writer recursive dictionary on keys") {
+	// There is no way to accurately represent a recursive dictionary,
+	// the only thing we can do is make sure the writer doesn't blow up
+
+	// Self recursive
+	Dictionary d;
+	d[d] = 1;
+
+	// Writer should it recursion limit while visiting the dictionary
+	ERR_PRINT_OFF;
+	String d_str;
+	VariantWriter::write_to_string(d, d_str);
+	ERR_PRINT_ON;
+
+	// Nested recursive
+	Dictionary d1;
+	Dictionary d2;
+	d1[d2] = 2;
+	d2[d1] = 1;
+
+	// Writer should it recursion limit while visiting the dictionary
+	ERR_PRINT_OFF;
+	String d1_str;
+	VariantWriter::write_to_string(d1, d1_str);
+	ERR_PRINT_ON;
+
+	// Break the recursivity otherwise Dictionary tearndown will leak memory
+	d.clear();
+	d1.clear();
+	d2.clear();
+}
+#endif
+
+TEST_CASE("[Variant] Basic comparison") {
+	CHECK_EQ(Variant(1), Variant(1));
+	CHECK_FALSE(Variant(1) != Variant(1));
+	CHECK_NE(Variant(1), Variant(2));
+	CHECK_EQ(Variant(String("foo")), Variant(String("foo")));
+	CHECK_NE(Variant(String("foo")), Variant(String("bar")));
+	// Check "empty" version of different types are not equivalents
+	CHECK_NE(Variant(0), Variant());
+	CHECK_NE(Variant(String()), Variant());
+	CHECK_NE(Variant(Array()), Variant());
+	CHECK_NE(Variant(Dictionary()), Variant());
+}
+
+TEST_CASE("[Variant] Nested array comparison") {
+	Array a1 = build_array(1, build_array(2, 3));
+	Array a2 = build_array(1, build_array(2, 3));
+	Array a_other = build_array(1, build_array(2, 4));
+	Variant v_a1 = a1;
+	Variant v_a1_ref2 = a1;
+	Variant v_a2 = a2;
+	Variant v_a_other = a_other;
+
+	// test both operator== and operator!=
+	CHECK_EQ(v_a1, v_a1);
+	CHECK_FALSE(v_a1 != v_a1);
+	CHECK_EQ(v_a1, v_a1_ref2);
+	CHECK_FALSE(v_a1 != v_a1_ref2);
+	CHECK_EQ(v_a1, v_a2);
+	CHECK_FALSE(v_a1 != v_a2);
+	CHECK_NE(v_a1, v_a_other);
+	CHECK_FALSE(v_a1 == v_a_other);
+}
+
+TEST_CASE("[Variant] Nested dictionary comparison") {
+	Dictionary d1 = build_dictionary(build_dictionary(1, 2), build_dictionary(3, 4));
+	Dictionary d2 = build_dictionary(build_dictionary(1, 2), build_dictionary(3, 4));
+	Dictionary d_other_key = build_dictionary(build_dictionary(1, 0), build_dictionary(3, 4));
+	Dictionary d_other_val = build_dictionary(build_dictionary(1, 2), build_dictionary(3, 0));
+	Variant v_d1 = d1;
+	Variant v_d1_ref2 = d1;
+	Variant v_d2 = d2;
+	Variant v_d_other_key = d_other_key;
+	Variant v_d_other_val = d_other_val;
+
+	// test both operator== and operator!=
+	CHECK_EQ(v_d1, v_d1);
+	CHECK_FALSE(v_d1 != v_d1);
+	CHECK_EQ(v_d1, v_d1_ref2);
+	CHECK_FALSE(v_d1 != v_d1_ref2);
+	CHECK_EQ(v_d1, v_d2);
+	CHECK_FALSE(v_d1 != v_d2);
+	CHECK_NE(v_d1, v_d_other_key);
+	CHECK_FALSE(v_d1 == v_d_other_key);
+	CHECK_NE(v_d1, v_d_other_val);
+	CHECK_FALSE(v_d1 == v_d_other_val);
+}
+
 } // namespace TestVariant
 } // namespace TestVariant
 
 
 #endif // TEST_VARIANT_H
 #endif // TEST_VARIANT_H