Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,12 @@ 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:
- Bundles a slightly modified [inspect.lua](https://github.com/kikito/inspect.lua) for table diffing and viewing - MIT
- 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.
8 changes: 5 additions & 3 deletions build/tested.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
64 changes: 58 additions & 6 deletions build/tested/assert_table.lua
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,48 @@ 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, 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
if actual[i] == nil then
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], ancestors_expected, ancestors_actual)
end

elseif actual[i] ~= expected[i] then
add_index_error(prefix, i, "different_value", expected[i], actual[i])
Expand All @@ -80,8 +112,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], ancestors_expected, ancestors_actual)
end

elseif actual[k] ~= expected[k] then
add_key_error(prefix, k, "different_value", expected[k], actual[k])
Expand All @@ -96,10 +134,24 @@ local function deep_compare(prefix, expected, actual)
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)
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



Expand Down
2 changes: 1 addition & 1 deletion docs/teal-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 24 additions & 2 deletions docs/unit-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -255,4 +277,4 @@ If a test file has a test that throws an unhandled exception or `tested` finds a
<span class="nf">? should return unknown since no tested.assert called (0.00ms)</span>
No assertions run during test
</pre>
</code>
</code>
10 changes: 6 additions & 4 deletions src/tested.tl
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@ function tested.assert<T>(assertion: types.Assertion<T>): 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

Expand Down
70 changes: 61 additions & 9 deletions src/tested/assert_table.tl
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,48 @@ 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, 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
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}, ancestors_expected, ancestors_actual)
end

elseif actual[i] ~= expected[i] then
add_index_error(prefix, i, "different_value", expected[i], actual[i])
Expand All @@ -80,8 +112,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}, ancestors_expected, ancestors_actual)
end

elseif actual[k] ~= expected[k] then
add_key_error(prefix, k, "different_value", expected[k], actual[k])
Expand All @@ -90,16 +128,30 @@ 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
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)
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?
Expand All @@ -114,4 +166,4 @@ local function assert_tables(expected: table, actual: table): (boolean, string)

end

return assert_tables
return assert_tables
Loading
Loading