Skip to content

Commit 1b5dd5b

Browse files
committed
Implement global is_equal function for deep equals check of Variant types
1 parent 9f03bbf commit 1b5dd5b

File tree

4 files changed

+354
-0
lines changed

4 files changed

+354
-0
lines changed

core/variant/variant_utility.cpp

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,6 +1234,117 @@ bool VariantUtilityFunctions::is_same(const Variant &p_a, const Variant &p_b) {
12341234
return p_a.identity_compare(p_b);
12351235
}
12361236

1237+
bool _deep_equals(const Variant &p_a, const Variant &p_b, int p_max_depth) {
1238+
if (p_max_depth <= 0) {
1239+
ERR_PRINT("Maximum recursion depth reached");
1240+
return true;
1241+
}
1242+
1243+
const Variant::Type type_a = p_a.get_type();
1244+
const Variant::Type type_b = p_b.get_type();
1245+
1246+
if (type_a != type_b) {
1247+
return false;
1248+
}
1249+
1250+
// Handle non-recursive types.
1251+
if (type_a != Variant::Type::DICTIONARY &&
1252+
type_a != Variant::Type::OBJECT &&
1253+
type_a != Variant::Type::ARRAY) {
1254+
return p_a == p_b;
1255+
}
1256+
1257+
// Handle recursive types, e.g., Array, Dictionary, and Object.
1258+
if (type_a == Variant::Type::ARRAY) {
1259+
const Array arr_a = p_a;
1260+
const Array arr_b = p_b;
1261+
const int64_t size_a = arr_a.size();
1262+
1263+
if (size_a != arr_b.size()) {
1264+
return false;
1265+
}
1266+
1267+
for (int64_t i = 0; i < size_a; ++i) {
1268+
if (!_deep_equals(arr_a[i], arr_b[i], p_max_depth - 1)) {
1269+
return false;
1270+
}
1271+
}
1272+
1273+
return true;
1274+
}
1275+
1276+
if (type_a == Variant::Type::DICTIONARY) {
1277+
const Dictionary dict_a = p_a;
1278+
const Dictionary dict_b = p_b;
1279+
1280+
const Array keys_a = dict_a.keys();
1281+
const Array keys_b = dict_b.keys();
1282+
1283+
if (keys_a.size() != keys_b.size()) {
1284+
return false;
1285+
}
1286+
1287+
for (int64_t i = 0; i < keys_a.size(); ++i) {
1288+
const Variant &key = keys_a[i];
1289+
if (!dict_b.has(key)) {
1290+
return false;
1291+
}
1292+
if (!_deep_equals(dict_a[key], dict_b[key], p_max_depth - 1)) {
1293+
return false;
1294+
}
1295+
}
1296+
1297+
return true;
1298+
}
1299+
1300+
if (type_a == Variant::Type::OBJECT) {
1301+
const Object *obj_a_ptr = p_a;
1302+
const Object *obj_b_ptr = p_b;
1303+
1304+
if (obj_a_ptr == nullptr && obj_b_ptr == nullptr) {
1305+
return true;
1306+
}
1307+
if (obj_a_ptr == nullptr || obj_b_ptr == nullptr) {
1308+
return false;
1309+
}
1310+
1311+
// Optimization: Check for instance equality.
1312+
if (obj_a_ptr == obj_b_ptr) {
1313+
return true;
1314+
}
1315+
1316+
List<PropertyInfo> props_a;
1317+
obj_a_ptr->get_property_list(&props_a);
1318+
List<PropertyInfo> props_b;
1319+
obj_b_ptr->get_property_list(&props_b);
1320+
1321+
if (props_a.size() != props_b.size()) {
1322+
return false;
1323+
}
1324+
1325+
for (const PropertyInfo &prop_info : props_a) {
1326+
if (props_b.find(prop_info) == nullptr) {
1327+
return false;
1328+
}
1329+
}
1330+
1331+
for (const PropertyInfo &prop_info : props_a) {
1332+
if (!_deep_equals(obj_a_ptr->get(prop_info.name), obj_b_ptr->get(prop_info.name), p_max_depth - 1)) {
1333+
return false;
1334+
}
1335+
}
1336+
1337+
return true;
1338+
}
1339+
1340+
ERR_PRINT("Unhandled Variant type in deep_equals. This is a bug.");
1341+
return false;
1342+
}
1343+
1344+
bool VariantUtilityFunctions::deep_equals(const Variant &a, const Variant &b) {
1345+
return _deep_equals(a, b, MAX_RECURSION);
1346+
}
1347+
12371348
#ifdef DEBUG_METHODS_ENABLED
12381349
#define VCALLR *ret = p_func(VariantCasterAndValidate<P>::cast(p_args, Is, r_error)...)
12391350
#define VCALL p_func(VariantCasterAndValidate<P>::cast(p_args, Is, r_error)...)
@@ -1855,6 +1966,7 @@ void Variant::_register_variant_utility_functions() {
18551966
FUNCBINDR(rid_from_int64, sarray("base"), Variant::UTILITY_FUNC_TYPE_GENERAL);
18561967

18571968
FUNCBINDR(is_same, sarray("a", "b"), Variant::UTILITY_FUNC_TYPE_GENERAL);
1969+
FUNCBINDR(deep_equals, sarray("a", "b"), Variant::UTILITY_FUNC_TYPE_GENERAL);
18581970
}
18591971

18601972
void Variant::_unregister_variant_utility_functions() {

core/variant/variant_utility.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,5 @@ struct VariantUtilityFunctions {
152152
static uint64_t rid_allocate_id();
153153
static RID rid_from_int64(uint64_t p_base);
154154
static bool is_same(const Variant &p_a, const Variant &p_b);
155+
static bool deep_equals(const Variant &a, const Variant &b);
155156
};

doc/classes/@GlobalScope.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,20 @@
352352
Converts from decibels to linear energy (audio).
353353
</description>
354354
</method>
355+
<method name="deep_equals">
356+
<return type="bool" />
357+
<param index="0" name="a" type="Variant" />
358+
<param index="1" name="b" type="Variant" />
359+
<description>
360+
Performs a deep recursive equality check between two Variant values, [param a] and [param b].
361+
362+
Returns [code]true[/code] if the Variants are considered equal based on their type and content, [code]false[/code] otherwise.
363+
- Basic types (non-Array/Dictionary/Object) use standard [code]==[/code] comparison.
364+
- Arrays are equal if sizes match and all elements are recursively equal.
365+
- Dictionaries are equal if keys match and all corresponding values are recursively equal (order doesn't matter).
366+
- Objects are equal if both are [code]nullptr[/code], or if both are valid Objects with matching property lists and recursively equal property values (order doesn't matter).
367+
</description>
368+
</method>
355369
<method name="deg_to_rad">
356370
<return type="float" />
357371
<param index="0" name="deg" type="float" />

tests/core/variant/test_variant_utility.h

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
#include "core/variant/variant_utility.h"
3434

35+
#include "scene/main/node.h"
3536
#include "tests/test_macros.h"
3637

3738
namespace TestVariantUtility {
@@ -128,4 +129,230 @@ TEST_CASE("[VariantUtility] Type conversion") {
128129
}
129130
}
130131

132+
TEST_CASE("[VariantUtility] deep_equals") {
133+
SUBCASE("Different types") {
134+
Variant v_int = 5;
135+
Variant v_str = "hello";
136+
Variant v_arr = Array();
137+
Variant v_dict = Dictionary();
138+
Variant v_null_obj = Variant();
139+
Node *node = memnew(Node);
140+
141+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(v_int, v_str));
142+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(v_str, v_int));
143+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(v_int, v_arr));
144+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(v_arr, v_dict));
145+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(v_dict, node));
146+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(node, v_null_obj));
147+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(v_int, v_null_obj));
148+
149+
memdelete(node);
150+
}
151+
152+
SUBCASE("Basic types") {
153+
CHECK(VariantUtilityFunctions::deep_equals(Variant(10), Variant(10)));
154+
CHECK(VariantUtilityFunctions::deep_equals(Variant(3.14), Variant(3.14)));
155+
CHECK(VariantUtilityFunctions::deep_equals(Variant(true), Variant(true)));
156+
CHECK(VariantUtilityFunctions::deep_equals(Variant("test"), Variant("test")));
157+
CHECK(VariantUtilityFunctions::deep_equals(Variant(Vector2(1, 2)), Variant(Vector2(1, 2))));
158+
CHECK(VariantUtilityFunctions::deep_equals(Variant(), Variant())); // Nil == Nil
159+
160+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(Variant(10), Variant(11)));
161+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(Variant(3.14), Variant(3.141)));
162+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(Variant(true), Variant(false)));
163+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(Variant("test"), Variant("Test")));
164+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(Variant("test"), Variant("")));
165+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(Variant(Vector2(1, 2)), Variant(Vector2(1, 3))));
166+
CHECK_FALSE(VariantUtilityFunctions::deep_equals(Variant(5), Variant()));
167+
}
168+
169+
SUBCASE("Arrays") {
170+
Array a1, a2;
171+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(a1, a2), "Empty arrays should be equal.");
172+
173+
a1.append(1);
174+
a1.append("hello");
175+
a2.append(1);
176+
a2.append("hello");
177+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(a1, a2), "Arrays with identical basic elements should be equal.");
178+
179+
Array a3;
180+
a3.append(1);
181+
a3.append("world");
182+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(a1, a3), "Arrays with different element values should not be equal.");
183+
184+
Array a4;
185+
a4.append(1);
186+
a4.append(2);
187+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(a1, a4), "Arrays with different element types at the same index should not be equal.");
188+
189+
Array a5;
190+
a5.append(1);
191+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(a1, a5), "Arrays with different sizes should not be equal.");
192+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(a5, a1), "Arrays with different sizes should not be equal.");
193+
194+
Array nested1, nested2, inner1, inner2;
195+
inner1.append(true);
196+
inner1.append(2.5);
197+
inner2.append(true);
198+
inner2.append(2.5);
199+
nested1.append("outer");
200+
nested1.append(inner1);
201+
nested2.append("outer");
202+
nested2.append(inner2);
203+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(nested1, nested2), "Arrays with equal nested arrays should be equal.");
204+
205+
Array nested3, inner3;
206+
inner3.append(true);
207+
inner3.append(3.0);
208+
nested3.append("outer");
209+
nested3.append(inner3);
210+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(nested1, nested3), "Arrays with unequal nested arrays should not be equal.");
211+
212+
Array ca1, ca2;
213+
ca1.append(1);
214+
ca1.append(ca1);
215+
ca2.append(1);
216+
ca2.append(ca2);
217+
ERR_PRINT_OFF
218+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(ca1, ca2), "Arrays with circular references should be equal as the maximum recursion depth is reached.");
219+
ERR_PRINT_ON
220+
}
221+
222+
SUBCASE("Dictionaries") {
223+
Dictionary d1, d2;
224+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(d1, d2), "Empty dictionaries should be equal.");
225+
226+
d1["key1"] = 100;
227+
d1["key2"] = "val";
228+
d2["key1"] = 100;
229+
d2["key2"] = "val";
230+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(d1, d2), "Dictionaries with identical key-value pairs should be equal.");
231+
232+
Dictionary d3;
233+
d3["key2"] = "val";
234+
d3["key1"] = 100;
235+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(d1, d3), "Dictionaries with identical key-value pairs in different order should be equal.");
236+
237+
Dictionary d4;
238+
d4["key1"] = 100;
239+
d4["key2"] = "VAL";
240+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(d1, d4), "Dictionaries with different values for the same key should not be equal.");
241+
242+
Dictionary d5;
243+
d5["key1"] = 100;
244+
d5["key_other"] = "val";
245+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(d1, d5), "Dictionaries with different keys should not be equal.");
246+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(d5, d1), "Dictionaries with different keys should not be equal.");
247+
248+
Dictionary d6;
249+
d6["key1"] = 100;
250+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(d1, d6), "Dictionaries with different sizes should not be equal.");
251+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(d6, d1), "Dictionaries with different sizes should not be equal.");
252+
253+
Dictionary nested_d1, nested_d2, inner_d1, inner_d2;
254+
inner_d1["sub_key"] = 9.9;
255+
inner_d2["sub_key"] = 9.9;
256+
nested_d1["level1"] = inner_d1;
257+
nested_d1["another"] = false;
258+
nested_d2["level1"] = inner_d2;
259+
nested_d2["another"] = false;
260+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(nested_d1, nested_d2), "Dictionaries with equal nested dictionaries should be equal.");
261+
262+
Dictionary nested_d3, inner_d3;
263+
inner_d3["sub_key"] = 10.0;
264+
nested_d3["level1"] = inner_d3;
265+
nested_d3["another"] = false;
266+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(nested_d1, nested_d3), "Dictionaries with unequal nested dictionaries should not be equal.");
267+
268+
Dictionary cd1, cd2;
269+
cd1["key"] = 1;
270+
cd1["self"] = cd1;
271+
cd2["key"] = 1;
272+
cd2["self"] = cd2;
273+
274+
ERR_PRINT_OFF
275+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(cd1, cd2), "Dictionaries with circular references should be equal as the maximum recursion depth is reached.");
276+
ERR_PRINT_ON
277+
}
278+
279+
SUBCASE("Objects") {
280+
Variant null_obj1;
281+
Variant null_obj2;
282+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(null_obj1, null_obj2), "Two null object Variants should be equal.");
283+
284+
Node *node1 = memnew(Node);
285+
Node *node2 = memnew(Node);
286+
Node *node3 = memnew(Node);
287+
288+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(null_obj1, node1), "A null object Variant and a non-null object Variant should not be equal.");
289+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(node1, null_obj1), "A non-null object Variant and a null object Variant should not be equal.");
290+
291+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(node1, node2), "Two distinct Node instances with default properties should be equal.");
292+
293+
node1->set_name("TestNode");
294+
node2->set_name("TestNode");
295+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(node1, node2), "Two Node instances with the same property value should be equal.");
296+
297+
node3->set_name("AnotherNode");
298+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(node1, node3), "Two Node instances with different property values should not be equal.");
299+
300+
Object *base_obj1_ptr = memnew(Object);
301+
Object *base_obj2_ptr = memnew(Object);
302+
Variant v_base_obj1 = base_obj1_ptr;
303+
Variant v_base_obj2 = base_obj2_ptr;
304+
305+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(v_base_obj1, v_base_obj2), "Two distinct base Object instances should be equal if properties match.");
306+
307+
base_obj1_ptr->set_meta("info", "data1");
308+
base_obj2_ptr->set_meta("info", "data1");
309+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(v_base_obj1, v_base_obj2), "Two objects with the same meta values should be equal.");
310+
311+
base_obj2_ptr->set_meta("info", "data2");
312+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(v_base_obj1, v_base_obj2), "Two objects with different meta values should not be equal.");
313+
314+
memdelete(base_obj1_ptr);
315+
memdelete(base_obj2_ptr);
316+
memdelete(node1);
317+
memdelete(node2);
318+
memdelete(node3);
319+
}
320+
321+
SUBCASE("Mixed Recursive Types") {
322+
Array mixed_a1, mixed_a2;
323+
Dictionary mixed_d1a, mixed_d1b;
324+
mixed_d1a["value"] = 50;
325+
mixed_d1b["value"] = 50;
326+
mixed_a1.append(mixed_d1a);
327+
mixed_a1.append("common");
328+
mixed_a2.append(mixed_d1b);
329+
mixed_a2.append("common");
330+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(mixed_a1, mixed_a2), "Arrays containing equal dictionaries should be equal.");
331+
332+
Array mixed_a3;
333+
Dictionary mixed_d1c;
334+
mixed_d1c["value"] = 51;
335+
mixed_a3.append(mixed_d1c);
336+
mixed_a3.append("common");
337+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(mixed_a1, mixed_a3), "Arrays containing unequal dictionaries should not be equal.");
338+
339+
Dictionary mixed_d2a, mixed_d2b;
340+
Array mixed_a1a, mixed_a1b;
341+
mixed_a1a.append(true);
342+
mixed_a1b.append(true);
343+
mixed_d2a["list"] = mixed_a1a;
344+
mixed_d2a["id"] = 1;
345+
mixed_d2b["list"] = mixed_a1b;
346+
mixed_d2b["id"] = 1;
347+
CHECK_MESSAGE(VariantUtilityFunctions::deep_equals(mixed_d2a, mixed_d2b), "Dictionaries containing equal arrays should be equal.");
348+
349+
Dictionary mixed_d2c;
350+
Array mixed_a1c;
351+
mixed_a1c.append(false);
352+
mixed_d2c["list"] = mixed_a1c;
353+
mixed_d2c["id"] = 1;
354+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::deep_equals(mixed_d2a, mixed_d2c), "Dictionaries containing unequal arrays should not be equal.");
355+
}
356+
}
357+
131358
} // namespace TestVariantUtility

0 commit comments

Comments
 (0)