From 087857372f6f3bb583262c9ea2c8f35da5f1680e Mon Sep 17 00:00:00 2001 From: FourierTransformer Date: Sun, 10 May 2026 08:03:39 -0500 Subject: [PATCH 1/2] got a lot of the __eq table compare and cycle compare working --- build/tested.lua | 8 ++- build/tested/assert_table.lua | 55 +++++++++++++-- src/tested.tl | 10 +-- src/tested/assert_table.tl | 61 ++++++++++++++--- tests/tables_test.tl | 122 +++++++++++++++++++++++++++++++++- 5 files changed, 233 insertions(+), 23 deletions(-) diff --git a/build/tested.lua b/build/tested.lua index 1642b6e..280f509 100644 --- a/build/tested.lua +++ b/build/tested.lua @@ -45,14 +45,16 @@ function tested.assert(assertion) actual_type .. "'). Expected: " .. tostring(assertion.expected) .. " (as '" .. expected_type .. "')" end - if assertion.actual == assertion.expected then - return true, "" - end + if actual_type == "table" and expected_type == "table" then return assert_table(assertion.expected, assertion.actual) end + if assertion.actual == assertion.expected then + return true, "" + end + return false, "Actual: " .. tostring(assertion.actual) .. "\nExpected: " .. tostring(assertion.expected) end diff --git a/build/tested/assert_table.lua b/build/tested/assert_table.lua index 1d845a9..1c486bc 100644 --- a/build/tested/assert_table.lua +++ b/build/tested/assert_table.lua @@ -60,7 +60,27 @@ local function add_key_error(prefix, key, error_type, expected, actual) tadd.add("\n") end -local function deep_compare(prefix, expected, actual) +local function add_index_eq_error(prefix, index) + tadd.add("~ ", prefix, "[", tostring(index), "]: Not equal according to __eq\n") +end + +local function add_key_eq_error(prefix, key) + tadd.add("~ ", prefix, ".", tostring(key), ": Not equal according to __eq\n") +end + + +local function get_shared_eq_function(a, b) + local mt = getmetatable(a) + if mt and mt == getmetatable(b) then + return mt.__eq + end +end + +local function deep_compare(prefix, expected, actual, visited) + + if visited[expected] and visited[expected][actual] then return end + if not visited[expected] then visited[expected] = {} end + visited[expected][actual] = true local keys, _key_length, sequence = inspect.getKeys(expected) for i = 1, sequence do @@ -68,8 +88,14 @@ local function deep_compare(prefix, expected, actual) add_index_error(prefix, i, "missing_key") elseif type(expected[i]) == "table" and type(actual[i]) == "table" then - - deep_compare(prefix .. "[" .. tostring(i) .. "]", expected[i], actual[i]) + local eq_fn = get_shared_eq_function(expected[i], actual[i]) + if eq_fn then + if not eq_fn(expected[i], actual[i]) then + add_index_eq_error(prefix, i) + end + else + deep_compare(prefix .. "[" .. tostring(i) .. "]", expected[i], actual[i], visited) + end elseif actual[i] ~= expected[i] then add_index_error(prefix, i, "different_value", expected[i], actual[i]) @@ -80,8 +106,14 @@ local function deep_compare(prefix, expected, actual) add_key_error(prefix, k, "missing_key") elseif type(expected[k]) == "table" and type(actual[k]) == "table" then - - deep_compare(prefix .. "." .. tostring(k), expected[k], actual[k]) + local eq_fn = get_shared_eq_function(expected[k], actual[k]) + if eq_fn then + if not eq_fn(expected[k], actual[k]) then + add_key_eq_error(prefix, k) + end + else + deep_compare(prefix .. "." .. tostring(k), expected[k], actual[k], visited) + end elseif actual[k] ~= expected[k] then add_key_error(prefix, k, "different_value", expected[k], actual[k]) @@ -99,7 +131,18 @@ local function deep_compare(prefix, expected, actual) end local function assert_tables(expected, actual) - deep_compare("", expected, actual) + local eq_fn = get_shared_eq_function(expected, actual) + if eq_fn then + if eq_fn(expected, actual) then + return true, "" + else + tadd.add("Not equal according to __eq at root\n") + end + + else + deep_compare("", expected, actual, {}) + + end diff --git a/src/tested.tl b/src/tested.tl index a703175..5610128 100644 --- a/src/tested.tl +++ b/src/tested.tl @@ -45,14 +45,16 @@ function tested.assert(assertion: types.Assertion): boolean, string actual_type .."'). Expected: " .. tostring(assertion.expected) .. " (as '" .. expected_type .. "')" end - if assertion.actual == assertion.expected then - return true, "" - end - + -- perform table comparison first, so we can explicitly handle __eq + -- as opposed to it being handles in the assertion compare below if actual_type == "table" and expected_type == "table" then return assert_table(assertion.expected as table, assertion.actual as table) end + if assertion.actual == assertion.expected then + return true, "" + end + return false, "Actual: " .. tostring(assertion.actual) .. "\nExpected: " .. tostring(assertion.expected) end diff --git a/src/tested/assert_table.tl b/src/tested/assert_table.tl index 4058441..0e3371f 100644 --- a/src/tested/assert_table.tl +++ b/src/tested/assert_table.tl @@ -60,16 +60,42 @@ local function add_key_error(prefix: string, key: any, error_type: TableDiff, ex tadd.add("\n") end -local function deep_compare(prefix: string, expected: table, actual: table) - -- not cycle safe, but might be good enough to get us started? +local function add_index_eq_error(prefix: string, index: integer) + tadd.add("~ ", prefix, "[", tostring(index), "]: Not equal according to __eq\n") +end + +local function add_key_eq_error(prefix: string, key: any) + tadd.add("~ ", prefix, ".", tostring(key), ": Not equal according to __eq\n") +end + +-- returns the shared __eq function if both tables use the same metatable that defines it +local function get_shared_eq_function(a: table, b: table): function(any, any): boolean + local mt = getmetatable(a) + if mt and mt == getmetatable(b) then + return mt.__eq + end +end + +local function deep_compare(prefix: string, expected: table, actual: table, visited: {table: {table: boolean}}) + -- if we've already started comparing this exact pair, we're in a cycle — assume equal + if visited[expected] and visited[expected][actual] then return end + if not visited[expected] then visited[expected] = {} end + visited[expected][actual] = true + local keys, _key_length, sequence = inspect.getKeys(expected) for i = 1, sequence do if actual[i] == nil then add_index_error(prefix, i, "missing_key") elseif type(expected[i]) == "table" and type(actual[i]) == "table" then - -- DESCEND! - deep_compare(prefix .. "[" .. tostring(i) .."]", expected[i] as {any:any}, actual[i] as {any:any}) + local eq_fn = get_shared_eq_function(expected[i] as table, actual[i] as table) + if eq_fn then + if not eq_fn(expected[i], actual[i]) then + add_index_eq_error(prefix, i) + end + else + deep_compare(prefix .. "[" .. tostring(i) .."]", expected[i] as {any:any}, actual[i] as {any:any}, visited) + end elseif actual[i] ~= expected[i] then add_index_error(prefix, i, "different_value", expected[i], actual[i]) @@ -80,8 +106,14 @@ local function deep_compare(prefix: string, expected: table, actual: table) add_key_error(prefix, k, "missing_key") elseif type(expected[k]) == "table" and type(actual[k]) == "table" then - -- DESCEND! - deep_compare(prefix .. "." .. tostring(k), expected[k] as {any:any}, actual[k] as {any:any}) + local eq_fn = get_shared_eq_function(expected[k] as table, actual[k] as table) + if eq_fn then + if not eq_fn(expected[k], actual[k]) then + add_key_eq_error(prefix, k) + end + else + deep_compare(prefix .. "." .. tostring(k), expected[k] as {any:any}, actual[k] as {any:any}, visited) + end elseif actual[k] ~= expected[k] then add_key_error(prefix, k, "different_value", expected[k], actual[k]) @@ -90,7 +122,7 @@ local function deep_compare(prefix: string, expected: table, actual: table) -- check if there are any additional keys floating around keys, _key_length, sequence = inspect.getKeys(actual) - for i = 1, sequence do + for i = 1, sequence do if expected[i] == nil then add_index_error(prefix, i, "additional_key") end end for _i, k in ipairs(keys) do @@ -99,7 +131,18 @@ local function deep_compare(prefix: string, expected: table, actual: table) end local function assert_tables(expected: table, actual: table): (boolean, string) - deep_compare("", expected, actual) + local eq_fn = get_shared_eq_function(expected, actual) + if eq_fn then + if eq_fn(expected, actual) then + return true, "" + else + tadd.add("Not equal according to __eq at root\n") + end + + else + deep_compare("", expected, actual, {}) + + end -- since change table starts off in there. -- using tadd here feels super risky to me... might switch to a bool or something? @@ -114,4 +157,4 @@ local function assert_tables(expected: table, actual: table): (boolean, string) end -return assert_tables \ No newline at end of file +return assert_tables diff --git a/tests/tables_test.tl b/tests/tables_test.tl index cbc0dc7..726cb76 100644 --- a/tests/tables_test.tl +++ b/tests/tables_test.tl @@ -42,4 +42,124 @@ tested.test("table compare should work", function() }) end) -return tested \ No newline at end of file +-- Cycle tests +local cycle_a: {any:any} = {} +cycle_a["self"] = cycle_a + +local cycle_b: {any:any} = {} +cycle_b["self"] = cycle_b + +tested.test("self-referential tables with equivalent structure should be equal", function() + tested.assert({ + given = "two tables that each contain a reference to themselves", + should = "be considered structurally equal", + expected = cycle_a, + actual = cycle_b + }) +end) + +local mutual_a: {any:any} = {} +local mutual_b: {any:any} = {} +mutual_a["other"] = mutual_b +mutual_b["other"] = mutual_a + +local mutual_c: {any:any} = {} +local mutual_d: {any:any} = {} +mutual_c["other"] = mutual_d +mutual_d["other"] = mutual_c + +tested.test("mutually-referential tables with equivalent structure should be equal", function() + tested.assert({ + given = "two pairs of tables that reference each other", + should = "be considered structurally equal", + expected = mutual_a, + actual = mutual_c + }) +end) + +-- Cycle deeper in the tree: child holds a back-reference to its parent +local node_a: {any:any} = { name = "root", child = {} } +(node_a.child as {any:any})["parent"] = node_a + +local node_b: {any:any} = { name = "root", child = {} } +(node_b.child as {any:any})["parent"] = node_b + +tested.test("back-reference cycle two levels deep should be considered equal", function() + tested.assert({ + given = "two trees where each child holds a reference back to its parent", + should = "be considered structurally equal", + expected = node_a, + actual = node_b + }) +end) + +-- __eq tests +local record Point + x: number + y: number + _internal: string + metamethod __eq: function(Point, Point): boolean +end + +local point_mt: metatable = { + __eq = function(a: Point, b: Point): boolean + return a.x == b.x and a.y == b.y + end +} + +-- same logical value, but _internal differs — __eq should make them equal +local p1 = setmetatable({x=3, y=4, _internal="abc"} as Point, point_mt) +local p2 = setmetatable({x=3, y=4, _internal="xyz"} as Point, point_mt) + +tested.test("tables with matching __eq should be considered equal", function() + tested.assert({ + given = "two Point tables at the same coordinates with differing internal fields", + should = "be equal according to their __eq metamethod", + expected = p1, + actual = p2 + }) +end) + +-- __eq returning false should still produce a failure, not a deep-field diff +local p3 = setmetatable({x=1, y=2} as Point, point_mt) +local p4 = setmetatable({x=9, y=9} as Point, point_mt) + +tested.test("tables with non-matching __eq should be considered not equal", function() + tested.assert({ + given = "two Point tables at different coordinates", + should = "not be equal according to their __eq metamethod", + expected = p3, + actual = p4 + }) +end) + +-- deeply nested __eq tests +-- Shape has no __eq of its own; comparison falls through to deep_compare, which +-- then encounters Point fields that do have __eq +local p5 = setmetatable({x=3, y=4, _internal="abc"} as Point, point_mt) +local p6 = setmetatable({x=3, y=4, _internal="xyz"} as Point, point_mt) -- same coords, differs only in _internal +local p7 = setmetatable({x=7, y=8} as Point, point_mt) -- different coords + +local shape_a = { name = "circle", center = p5 } +local shape_b = { name = "circle", center = p6 } +local shape_c = { name = "circle", center = p7 } + +tested.test("container table whose nested field has matching __eq should be equal", function() + tested.assert({ + given = "two shapes whose center Points share coordinates but differ in _internal", + should = "be equal because the nested Point's __eq only checks x and y", + expected = shape_a, + actual = shape_b + }) +end) + +tested.test("container table whose nested field has non-matching __eq should not be equal", function() + tested.assert({ + given = "two shapes whose center Points have different coordinates", + should = "not be equal because the nested Point's __eq returns false", + expected = shape_a, + actual = shape_c + }) +end) + +return tested From c5854132b8bb2e561bc3f53249ac866eae6cb68e Mon Sep 17 00:00:00 2001 From: FourierTransformer Date: Sun, 10 May 2026 10:11:03 -0500 Subject: [PATCH 2/2] updated docs and readme --- README.md | 8 +-- build/tested/assert_table.lua | 25 ++++++--- docs/teal-support.md | 2 +- docs/unit-testing.md | 26 +++++++++- src/tested/assert_table.tl | 25 ++++++--- tests/tables_test.tl | 95 +++++++++++++++++++++++++---------- 6 files changed, 129 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 87c7ba1..0d98d84 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,7 @@ Running the tests are as simple as placing the file in a `tests` folder and then You can see more tests in this repo's [tests](https://github.com/FourierTransformer/tested/tree/main/tests) folder! ## AI Disclosure -- AI was not used to write any of this code. It's all been hand written over the course of 2-3 weeks. -- AI was used to help research Lua internals (mostly around file loading and the debug module) -- AI helped generate a prettier terminal output. - - I fed it my [original terminal output](./docs/original-output.txt), and it re-formatted it to something that looks a lot closer to the final terminal output. -- AI has been used to help debug issues +As of version 0.0.3, AI has been used to help implement features, research Lua/Teal internals, debug issues, and make more readable output. Before this version, the code was hand-written, but some research was done with the help of AI. The docs will remain hand-written for now. I am personally still a little skeptical of AI and its place in open source, but at the moment am willing to evaluate it. ## Licenses Parts of the following are included in the source code present in this repo: @@ -60,4 +56,4 @@ Parts of the following are included in the source code present in this repo: - Also bundles a slightly modified [ansicolors.lua](https://github.com/kikito/ansicolors.lua) - MIT - A function from [Luacov](https://github.com/lunarmodules/luacov) code to help merge stats files in process - MIT -Major thanks to hishamhm, kikito, and benoit-germain for their work in the Lua space. Without them, tested wouldn't be possible. +Major thanks to hishamhm, kikito, and benoit-germain for their work in the Lua space. Without them, `tested` wouldn't be possible. diff --git a/build/tested/assert_table.lua b/build/tested/assert_table.lua index 1c486bc..33e03cd 100644 --- a/build/tested/assert_table.lua +++ b/build/tested/assert_table.lua @@ -76,11 +76,17 @@ local function get_shared_eq_function(a, b) end end -local function deep_compare(prefix, expected, actual, visited) - - if visited[expected] and visited[expected][actual] then return end - if not visited[expected] then visited[expected] = {} end - visited[expected][actual] = true +local function deep_compare(prefix, expected, actual, ancestors_expected, ancestors_actual) + local expected_is_ancestor = ancestors_expected[expected] + local actual_is_ancestor = ancestors_actual[actual] + if expected_is_ancestor or actual_is_ancestor then + if not (expected_is_ancestor and actual_is_ancestor) then + tadd.add("~ ", prefix, ": Cycle structure mismatch\n") + end + return + end + ancestors_expected[expected] = true + ancestors_actual[actual] = true local keys, _key_length, sequence = inspect.getKeys(expected) for i = 1, sequence do @@ -94,7 +100,7 @@ local function deep_compare(prefix, expected, actual, visited) add_index_eq_error(prefix, i) end else - deep_compare(prefix .. "[" .. tostring(i) .. "]", expected[i], actual[i], visited) + deep_compare(prefix .. "[" .. tostring(i) .. "]", expected[i], actual[i], ancestors_expected, ancestors_actual) end elseif actual[i] ~= expected[i] then @@ -112,7 +118,7 @@ local function deep_compare(prefix, expected, actual, visited) add_key_eq_error(prefix, k) end else - deep_compare(prefix .. "." .. tostring(k), expected[k], actual[k], visited) + deep_compare(prefix .. "." .. tostring(k), expected[k], actual[k], ancestors_expected, ancestors_actual) end elseif actual[k] ~= expected[k] then @@ -128,6 +134,9 @@ local function deep_compare(prefix, expected, actual, visited) for _i, k in ipairs(keys) do if expected[k] == nil then add_key_error(prefix, k, "additional_key") end end + + ancestors_expected[expected] = nil + ancestors_actual[actual] = nil end local function assert_tables(expected, actual) @@ -140,7 +149,7 @@ local function assert_tables(expected, actual) end else - deep_compare("", expected, actual, {}) + deep_compare("", expected, actual, {}, {}) end diff --git a/docs/teal-support.md b/docs/teal-support.md index 63570d5..5ff8970 100644 --- a/docs/teal-support.md +++ b/docs/teal-support.md @@ -3,7 +3,7 @@ `tested` is built from the ground up with with Teal and makes it a first class citizen. Unit tests can be written in Teal and all the functionality (including code coverage!) works across both languages wonderfully. However, there are a couple of things to keep in mind when using `tested` with Teal projects. ## Build, then test if perf matters -If you have a Teal project and are writing your unit tests in Teal, _every_ test file will compile the Teal as it gets loaded. +If you have a Teal project and are writing your unit tests in Teal, _every_ test file will compile the Teal as it gets loaded. However, it is worth knowing that `tested` will attmept to run the Teal files even if they are not typed correctly. Example Teal unit test: ```lua diff --git a/docs/unit-testing.md b/docs/unit-testing.md index 48146bf..e41871f 100644 --- a/docs/unit-testing.md +++ b/docs/unit-testing.md @@ -78,7 +78,7 @@ To see the entire list of CLI options, check out the [CLI Reference](./cli.md) ## Testing tables -`tested.assert` can also deep compare tables, and will generate a little summary of the differences as well as print out the expected and actual table. +`tested.assert` will also deep compare tables, and will generate a little summary of the differences as well as print out the expected and actual table. === "Test" @@ -143,6 +143,28 @@ To see the entire list of CLI options, check out the [CLI Reference](./cli.md) scores = { 10, 20, 30 } } ``` + +### Table cycle compare +`tested` can also check for cycles within a table. It performs a basic structural check to ensure the _structure_ of the cycles are the same. So, if you're writing an assertion that compares tables, you should mirror the cycle in the `expected` table. If you instead reference the `actual` table's cycle it will be considerd a failure. + +Example of a working cycle test: +```lua +tested.test("tables with self-cycles, but the same structure should be equal", function() + local cycle_a: {any:any} = {} + cycle_a["self"] = cycle_a + + local cycle_b: {any:any} = {} + cycle_b["self"] = cycle_b + + tested.assert({ + given = "two tables that each contain a reference to themselves", + should = "be considered structurally equal", + expected = cycle_a, + actual = cycle_b + }) +end) +``` + ## Truthy/Falsy tests Sometimes in Lua you want to check if _anything_ returned (like a `string.match` or that a value exists in a table), we've added in an `assert_truthy` and `assert_falsy` to help out in those cases. @@ -255,4 +277,4 @@ If a test file has a test that throws an unhandled exception or `tested` finds a ? should return unknown since no tested.assert called (0.00ms) No assertions run during test - \ No newline at end of file + diff --git a/src/tested/assert_table.tl b/src/tested/assert_table.tl index 0e3371f..5531558 100644 --- a/src/tested/assert_table.tl +++ b/src/tested/assert_table.tl @@ -76,11 +76,17 @@ local function get_shared_eq_function(a: table, b: table): function(any, any): b end end -local function deep_compare(prefix: string, expected: table, actual: table, visited: {table: {table: boolean}}) - -- if we've already started comparing this exact pair, we're in a cycle — assume equal - if visited[expected] and visited[expected][actual] then return end - if not visited[expected] then visited[expected] = {} end - visited[expected][actual] = true +local function deep_compare(prefix: string, expected: table, actual: table, ancestors_expected: {table: boolean}, ancestors_actual: {table: boolean}) + local expected_is_ancestor = ancestors_expected[expected] + local actual_is_ancestor = ancestors_actual[actual] + if expected_is_ancestor or actual_is_ancestor then + if not (expected_is_ancestor and actual_is_ancestor) then + tadd.add("~ ", prefix, ": Cycle structure mismatch\n") + end + return + end + ancestors_expected[expected] = true + ancestors_actual[actual] = true local keys, _key_length, sequence = inspect.getKeys(expected) for i = 1, sequence do @@ -94,7 +100,7 @@ local function deep_compare(prefix: string, expected: table, actual: table, visi add_index_eq_error(prefix, i) end else - deep_compare(prefix .. "[" .. tostring(i) .."]", expected[i] as {any:any}, actual[i] as {any:any}, visited) + deep_compare(prefix .. "[" .. tostring(i) .."]", expected[i] as {any:any}, actual[i] as {any:any}, ancestors_expected, ancestors_actual) end elseif actual[i] ~= expected[i] then @@ -112,7 +118,7 @@ local function deep_compare(prefix: string, expected: table, actual: table, visi add_key_eq_error(prefix, k) end else - deep_compare(prefix .. "." .. tostring(k), expected[k] as {any:any}, actual[k] as {any:any}, visited) + deep_compare(prefix .. "." .. tostring(k), expected[k] as {any:any}, actual[k] as {any:any}, ancestors_expected, ancestors_actual) end elseif actual[k] ~= expected[k] then @@ -128,6 +134,9 @@ local function deep_compare(prefix: string, expected: table, actual: table, visi for _i, k in ipairs(keys) do if expected[k] == nil then add_key_error(prefix, k, "additional_key") end end + + ancestors_expected[expected] = nil + ancestors_actual[actual] = nil end local function assert_tables(expected: table, actual: table): (boolean, string) @@ -140,7 +149,7 @@ local function assert_tables(expected: table, actual: table): (boolean, string) end else - deep_compare("", expected, actual, {}) + deep_compare("", expected, actual, {}, {}) end diff --git a/tests/tables_test.tl b/tests/tables_test.tl index 726cb76..05e6d5c 100644 --- a/tests/tables_test.tl +++ b/tests/tables_test.tl @@ -43,13 +43,13 @@ tested.test("table compare should work", function() end) -- Cycle tests -local cycle_a: {any:any} = {} -cycle_a["self"] = cycle_a +tested.test("tables with self-cycles, but the same structure should be equal", function() + local cycle_a: {any:any} = {} + cycle_a["self"] = cycle_a -local cycle_b: {any:any} = {} -cycle_b["self"] = cycle_b + local cycle_b: {any:any} = {} + cycle_b["self"] = cycle_b -tested.test("self-referential tables with equivalent structure should be equal", function() tested.assert({ given = "two tables that each contain a reference to themselves", should = "be considered structurally equal", @@ -58,17 +58,58 @@ tested.test("self-referential tables with equivalent structure should be equal", }) end) -local mutual_a: {any:any} = {} -local mutual_b: {any:any} = {} -mutual_a["other"] = mutual_b -mutual_b["other"] = mutual_a +tested.test("slightly complicated tables with self-cycles, but with different structures should not be equal", function() -local mutual_c: {any:any} = {} -local mutual_d: {any:any} = {} -mutual_c["other"] = mutual_d -mutual_d["other"] = mutual_c + local cycle_a: {any:any} = {} + cycle_a[1] = "first" + + local weird_loop = {cycle_a} + cycle_a[2] = weird_loop + + local cycle_b: {any:any} = {} + cycle_b[1] = "first" + cycle_b[2] = weird_loop + + tested.assert({ + given = "two tables where one contains a reference to itself and the other does not", + should = "not be considered structurally equal", + expected = cycle_a, + actual = cycle_b + }) +end) + +tested.test("slightly complicated tables with self-cycles, but the same structure should be equal", function() + + local cycle_a: {any:any} = {} + cycle_a[1] = "first" + + local weird_loop_a = {cycle_a} + cycle_a[2] = weird_loop_a + + local cycle_b: {any:any} = {} + cycle_b[1] = "first" + local weird_loop_b = {cycle_b} + cycle_b[2] = weird_loop_b + + tested.assert({ + given = "two tables where one contains a reference to itself and the other does not", + should = "be considered structurally equal", + expected = cycle_a, + actual = cycle_b + }) +end) tested.test("mutually-referential tables with equivalent structure should be equal", function() + local mutual_a: {any:any} = {} + local mutual_b: {any:any} = {} + mutual_a["other"] = mutual_b + mutual_b["other"] = mutual_a + + local mutual_c: {any:any} = {} + local mutual_d: {any:any} = {} + mutual_c["other"] = mutual_d + mutual_d["other"] = mutual_c + tested.assert({ given = "two pairs of tables that reference each other", should = "be considered structurally equal", @@ -77,14 +118,14 @@ tested.test("mutually-referential tables with equivalent structure should be equ }) end) --- Cycle deeper in the tree: child holds a back-reference to its parent -local node_a: {any:any} = { name = "root", child = {} } -(node_a.child as {any:any})["parent"] = node_a +tested.test("back-reference cycle two levels deep should be considered equal", function() + -- Cycle deeper in the tree: child holds a back-reference to its parent + local node_a: {any:any} = { name = "root", child = {} } + (node_a.child as {any:any})["parent"] = node_a -local node_b: {any:any} = { name = "root", child = {} } -(node_b.child as {any:any})["parent"] = node_b + local node_b: {any:any} = { name = "root", child = {} } + (node_b.child as {any:any})["parent"] = node_b -tested.test("back-reference cycle two levels deep should be considered equal", function() tested.assert({ given = "two trees where each child holds a reference back to its parent", should = "be considered structurally equal", @@ -107,11 +148,11 @@ local point_mt: metatable = { end } --- same logical value, but _internal differs — __eq should make them equal -local p1 = setmetatable({x=3, y=4, _internal="abc"} as Point, point_mt) -local p2 = setmetatable({x=3, y=4, _internal="xyz"} as Point, point_mt) - tested.test("tables with matching __eq should be considered equal", function() + -- same logical value, but _internal differs — __eq should make them equal + local p1 = setmetatable({x=3, y=4, _internal="abc"} as Point, point_mt) + local p2 = setmetatable({x=3, y=4, _internal="xyz"} as Point, point_mt) + tested.assert({ given = "two Point tables at the same coordinates with differing internal fields", should = "be equal according to their __eq metamethod", @@ -120,11 +161,11 @@ tested.test("tables with matching __eq should be considered equal", function() }) end) --- __eq returning false should still produce a failure, not a deep-field diff -local p3 = setmetatable({x=1, y=2} as Point, point_mt) -local p4 = setmetatable({x=9, y=9} as Point, point_mt) - tested.test("tables with non-matching __eq should be considered not equal", function() + -- __eq returning false should still produce a failure, not a deep-field diff + local p3 = setmetatable({x=1, y=2} as Point, point_mt) + local p4 = setmetatable({x=9, y=9} as Point, point_mt) + tested.assert({ given = "two Point tables at different coordinates", should = "not be equal according to their __eq metamethod",