From f9ba2efe1e60fa919b0efe70994416149b54ee26 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Sat, 1 Feb 2020 07:04:14 +0100 Subject: [PATCH] Modify Dictionary::operator== to do real key/value comparison with recursive support (and add unittests) --- core/templates/ordered_hash_map.h | 6 +- core/typedefs.h | 3 + core/variant/array.cpp | 66 +++++- core/variant/array.h | 3 + core/variant/dictionary.cpp | 62 ++++- core/variant/dictionary.h | 3 + core/variant/variant.cpp | 84 ++++--- core/variant/variant.h | 8 +- core/variant/variant_parser.cpp | 69 +++--- core/variant/variant_parser.h | 2 +- core/variant/variant_setget.cpp | 12 +- tests/test_array.h | 234 +++++++++++++++++++ tests/test_dictionary.h | 367 +++++++++++++++++++++++++++++- tests/test_macros.h | 1 + tests/test_variant.h | 211 +++++++++++++++++ 15 files changed, 1031 insertions(+), 100 deletions(-) diff --git a/core/templates/ordered_hash_map.h b/core/templates/ordered_hash_map.h index 7a17eeb644c..4996b881905 100644 --- a/core/templates/ordered_hash_map.h +++ b/core/templates/ordered_hash_map.h @@ -207,8 +207,12 @@ public: (*list_element)->get().second = p_value; return Element(*list_element); } - typename InternalList::Element *new_element = list.push_back(Pair(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(&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); + // ...now we can set the right value ! new_element->get().first = &e->key(); return Element(new_element); diff --git a/core/typedefs.h b/core/typedefs.h index 8ca3d13e637..9ab874b2f0f 100644 --- a/core/typedefs.h +++ b/core/typedefs.h @@ -277,6 +277,9 @@ struct BuildIndexSequence : BuildIndexSequence {}; template struct BuildIndexSequence<0, Is...> : IndexSequence {}; +// Limit the depth of recursive algorithms when dealing with Array/Dictionary +#define MAX_RECURSION 100 + #ifdef DEBUG_ENABLED #define DEBUG_METHODS_ENABLED #endif diff --git a/core/variant/array.cpp b/core/variant/array.cpp index b4d6dffc6fe..69a0fff1a1d 100644 --- a/core/variant/array.cpp +++ b/core/variant/array.cpp @@ -97,11 +97,38 @@ void Array::clear() { } 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 { - 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 &a1 = _p->array; + const Vector &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 { @@ -132,10 +159,20 @@ bool Array::operator>=(const Array &p_array) 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++) { - 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; } @@ -300,12 +337,29 @@ const Variant &Array::get(int p_idx) 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; + + if (recursion_count > MAX_RECURSION) { + ERR_PRINT("Max recursion reached"); + return new_arr; + } + int element_count = size(); new_arr.resize(element_count); 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; diff --git a/core/variant/array.h b/core/variant/array.h index 4a1b25c4a94..bd39b8e0b16 100644 --- a/core/variant/array.h +++ b/core/variant/array.h @@ -63,8 +63,10 @@ public: 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 recursive_hash(int recursion_count) const; void operator=(const Array &p_array); void push_back(const Variant &p_value); @@ -100,6 +102,7 @@ public: Variant pop_at(int p_pos); 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 filter(const Callable &p_callable) const; diff --git a/core/variant/dictionary.cpp b/core/variant/dictionary.cpp index 07b3a9a6754..24d21386a77 100644 --- a/core/variant/dictionary.cpp +++ b/core/variant/dictionary.cpp @@ -188,11 +188,35 @@ bool Dictionary::erase(const Variant &p_key) { } 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 { - 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::ConstElement this_E = ((const OrderedHashMap *)&_p->variant_map)->front(); this_E; this_E = this_E.next()) { + OrderedHashMap::ConstElement other_E = ((const OrderedHashMap *)&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 { @@ -225,11 +249,21 @@ void Dictionary::_unref() 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); + recursion_count++; for (OrderedHashMap::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; @@ -286,10 +320,26 @@ const Variant *Dictionary::next(const Variant *p_key) 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; - for (OrderedHashMap::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::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::Element E = _p->variant_map.front(); E; E = E.next()) { + n[E.key()] = E.value(); + } } return n; diff --git a/core/variant/dictionary.h b/core/variant/dictionary.h index 4067ff9fd93..f8a2a7573f5 100644 --- a/core/variant/dictionary.h +++ b/core/variant/dictionary.h @@ -70,8 +70,10 @@ public: 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 recursive_hash(int recursion_count) const; void operator=(const Dictionary &p_dictionary); const Variant *next(const Variant *p_key = nullptr) const; @@ -80,6 +82,7 @@ public: Array values() const; Dictionary duplicate(bool p_deep = false) const; + Dictionary recursive_duplicate(bool p_deep, int recursion_count) const; const void *id() const; diff --git a/core/variant/variant.cpp b/core/variant/variant.cpp index 1d70d4c5061..c43ff8626e2 100644 --- a/core/variant/variant.cpp +++ b/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 { - 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 { + // 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 return true; } @@ -1617,25 +1612,23 @@ struct _VariantStrPair { }; Variant::operator String() const { - List stack; - - return stringify(stack); + return stringify(0); } template -String stringify_vector(const T &vec, List &stack) { +String stringify_vector(const T &vec, int recursion_count) { String str("["); for (int i = 0; i < vec.size(); i++) { if (i > 0) { str += ", "; } - str = str + Variant(vec[i]).stringify(stack); + str = str + Variant(vec[i]).stringify(recursion_count); } str += "]"; return str; } -String Variant::stringify(List &stack) const { +String Variant::stringify(int recursion_count) const { switch (type) { case NIL: return "null"; @@ -1679,23 +1672,22 @@ String Variant::stringify(List &stack) const { return operator Color(); case DICTIONARY: { const Dictionary &d = *reinterpret_cast(_data._mem); - if (stack.find(d.id())) { + if (recursion_count > MAX_RECURSION) { + ERR_PRINT("Max recursion reached"); return "{...}"; } - stack.push_back(d.id()); - - //const String *K=nullptr; String str("{"); List keys; d.get_key_list(&keys); Vector<_VariantStrPair> pairs; - for (const Variant &E : keys) { + recursion_count++; + for (List::Element *E = keys.front(); E; E = E->next()) { _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); } @@ -1710,46 +1702,43 @@ String Variant::stringify(List &stack) const { } str += "}"; - stack.erase(d.id()); return str; } break; case PACKED_VECTOR2_ARRAY: { - return stringify_vector(operator Vector(), stack); + return stringify_vector(operator Vector(), recursion_count); } break; case PACKED_VECTOR3_ARRAY: { - return stringify_vector(operator Vector(), stack); + return stringify_vector(operator Vector(), recursion_count); } break; case PACKED_COLOR_ARRAY: { - return stringify_vector(operator Vector(), stack); + return stringify_vector(operator Vector(), recursion_count); } break; case PACKED_STRING_ARRAY: { - return stringify_vector(operator Vector(), stack); + return stringify_vector(operator Vector(), recursion_count); } break; case PACKED_BYTE_ARRAY: { - return stringify_vector(operator Vector(), stack); + return stringify_vector(operator Vector(), recursion_count); } break; case PACKED_INT32_ARRAY: { - return stringify_vector(operator Vector(), stack); + return stringify_vector(operator Vector(), recursion_count); } break; case PACKED_INT64_ARRAY: { - return stringify_vector(operator Vector(), stack); + return stringify_vector(operator Vector(), recursion_count); } break; case PACKED_FLOAT32_ARRAY: { - return stringify_vector(operator Vector(), stack); + return stringify_vector(operator Vector(), recursion_count); } break; case PACKED_FLOAT64_ARRAY: { - return stringify_vector(operator Vector(), stack); + return stringify_vector(operator Vector(), recursion_count); } break; case ARRAY: { Array arr = operator Array(); - if (stack.find(arr.id())) { + if (recursion_count > MAX_RECURSION) { + ERR_PRINT("Max recursion reached"); 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; } break; @@ -2768,6 +2757,10 @@ Variant::Variant(const Variant &p_variant) { } uint32_t Variant::hash() const { + return recursive_hash(0); +} + +uint32_t Variant::recursive_hash(int recursion_count) const { switch (type) { case NIL: { return 0; @@ -2895,7 +2888,7 @@ uint32_t Variant::hash() const { return reinterpret_cast(_data._mem)->hash(); } break; case DICTIONARY: { - return reinterpret_cast(_data._mem)->hash(); + return reinterpret_cast(_data._mem)->recursive_hash(recursion_count); } break; case CALLABLE: { @@ -2909,7 +2902,7 @@ uint32_t Variant::hash() const { } break; case ARRAY: { const Array &arr = *reinterpret_cast(_data._mem); - return arr.hash(); + return arr.recursive_hash(recursion_count); } break; case PACKED_BYTE_ARRAY: { @@ -3083,7 +3076,7 @@ uint32_t Variant::hash() const { \ 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) { return false; } @@ -3214,14 +3207,19 @@ bool Variant::hash_compare(const Variant &p_variant) const { const Array &l = *(reinterpret_cast(_data._mem)); const Array &r = *(reinterpret_cast(p_variant._data._mem)); - if (l.size() != r.size()) { + if (!l.recursive_equal(r, recursion_count + 1)) { 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(_data._mem)); + const Dictionary &r = *(reinterpret_cast(p_variant._data._mem)); + + if (!l.recursive_equal(r, recursion_count + 1)) { + return false; } return true; diff --git a/core/variant/variant.h b/core/variant/variant.h index d3f694e7ca4..8ce5e7dcd2b 100644 --- a/core/variant/variant.h +++ b/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); 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 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; 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; - String stringify(List &stack) const; + String stringify(int recursion_count = 0) const; String to_json_string() const; void static_assign(const Variant &p_variant); diff --git a/core/variant/variant_parser.cpp b/core/variant/variant_parser.cpp index 221a8c4f98c..3c19c2c7068 100644 --- a/core/variant/variant_parser.cpp +++ b/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()) { case Variant::NIL: { 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: { Dictionary dict = p_variant; + if (recursion_count > MAX_RECURSION) { + ERR_PRINT("Max recursion reached"); + p_store_string_func(p_store_string_ud, "{}"); + } else { + recursion_count++; - List keys; - dict.get_key_list(&keys); - keys.sort(); + List keys; + dict.get_key_list(&keys); + keys.sort(); - p_store_string_func(p_store_string_ud, "{\n"); - for (List::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"); + p_store_string_func(p_store_string_ud, "{\n"); + for (List::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; + 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; diff --git a/core/variant/variant_parser.h b/core/variant/variant_parser.h index 1ba26db6ed3..2e4baa6fff9 100644 --- a/core/variant/variant_parser.h +++ b/core/variant/variant_parser.h @@ -140,7 +140,7 @@ public: typedef Error (*StoreStringFunc)(void *ud, const String &p_string); 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); }; diff --git a/core/variant/variant_setget.cpp b/core/variant/variant_setget.cpp index 4abb51ca7c3..2530d77c62a 100644 --- a/core/variant/variant_setget.cpp +++ b/core/variant/variant_setget.cpp @@ -1824,11 +1824,15 @@ Variant Variant::iter_get(const Variant &r_iter, bool &r_valid) const { 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) { case OBJECT: { /* breaks stuff :( - if (deep && !_get_obj().ref.is_null()) { + if (p_deep && !_get_obj().ref.is_null()) { Ref resource = _get_obj().ref; if (resource.is_valid()) { return resource->duplicate(true); @@ -1838,9 +1842,9 @@ Variant Variant::duplicate(bool deep) const { return *this; } break; case DICTIONARY: - return operator Dictionary().duplicate(deep); + return operator Dictionary().recursive_duplicate(p_deep, recursion_count); case ARRAY: - return operator Array().duplicate(deep); + return operator Array().recursive_duplicate(p_deep, recursion_count); case PACKED_BYTE_ARRAY: return operator Vector().duplicate(); case PACKED_INT32_ARRAY: diff --git a/tests/test_array.h b/tests/test_array.h index 3bd476fd273..05b4eaea2ab 100644 --- a/tests/test_array.h +++ b/tests/test_array.h @@ -43,6 +43,25 @@ namespace TestArray { +static inline Array build_array() { + return Array(); +} +template +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 +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()") { Array arr; CHECK(arr.size() == 0); @@ -232,6 +251,221 @@ TEST_CASE("[Array] max() and min()") { CHECK(max == 5); 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 #endif // TEST_ARRAY_H diff --git a/tests/test_dictionary.h b/tests/test_dictionary.h index b94cf361090..64d1d68e216 100644 --- a/tests/test_dictionary.h +++ b/tests/test_dictionary.h @@ -39,6 +39,25 @@ namespace TestDictionary { +static inline Array build_array() { + return Array(); +} +template +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 +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 ([])") { Dictionary map; map["Hello"] = 0; @@ -61,15 +80,6 @@ TEST_CASE("[Dictionary] Assignment using bracket notation ([])") { 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()") { Dictionary map; List keys; @@ -155,5 +165,344 @@ TEST_CASE("[Dictionary] keys() and values()") { CHECK(int(keys[0]) == 1); CHECK(int(values[0]) == 3); } + +TEST_CASE("[Dictionary] Duplicate dictionary") { + // d = {1: {1: 1}, {2: 2}: [2], [3]: 3} + Dictionary k2 = 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 + #endif // TEST_DICTIONARY_H diff --git a/tests/test_macros.h b/tests/test_macros.h index 2f0bc6dcfae..6968f9df1fb 100644 --- a/tests/test_macros.h +++ b/tests/test_macros.h @@ -33,6 +33,7 @@ #include "core/object/callable_method_pointer.h" #include "core/object/class_db.h" +#include "core/string/print_string.h" #include "core/templates/map.h" #include "core/variant/variant.h" diff --git a/tests/test_variant.h b/tests/test_variant.h index 598fe488d70..0d16fa092c5 100644 --- a/tests/test_variant.h +++ b/tests/test_variant.h @@ -38,6 +38,25 @@ namespace TestVariant { +static inline Array build_array() { + return Array(); +} +template +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 +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") { int64_t a32 = 2147483648; // 2^31, so out of bounds for 32-bit signed int [-2^31, +2^31-1]. 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; 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 #endif // TEST_VARIANT_H