Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a way to write custom assert functions #32

Merged
merged 1 commit into from
Nov 28, 2024
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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Test API
* [assert_not_close](#assert_not_close)
* [assert_nil](#assert_nil)
* [assert_not_nil](#assert_not_nil)
* [test_helper](#test_helper)
* [test_fail](#test_fail)

Introduction
------------
Expand Down Expand Up @@ -142,6 +144,40 @@ Asserts that `actual` is not `nil`.

[Back to TOC](#test-api)

test_helper
-----------

**syntax:** *test_helper()*

`test_helper` marks the calling function as a test helper function.
When printing file and line information in GUI, that function will be
skipped.

[Back to TOC](#test-api)

test_fail
---------

**syntax:** *test_fail(err)*

Generates test error which stops current test execution and shows error to
the user. In the GUI, the error will be presented together with a file name
and line number where the `test_fail` function was executed. If you run
`test_fail` from your own assert function, and want to see a place where this
assert function was executed instead, please run the [test_helper()](#test_helper) function in the beginning of your assert function:
```lua
function custom_assert(....)
test_helper() -- mark custom_assert function as test helper
if .... then
test_fail("message")
end
end
```

`err` is an error message as a string or a table. All table fields will be presented in the GUI. Table could contain special `msg` field which will always be presented first.

[Back to TOC](#test-api)

Development - how to work on unitron
====================================

Expand Down
94 changes: 40 additions & 54 deletions api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,6 @@ local function equal(expected, actual, visited_values)
return expected == actual
end

local function get_caller()
local traceback = debug.traceback("", 3)
local loc = split(traceback, "\n")[3]
loc = string.gsub(loc, "(%d+):.*", "%1") -- drop message
loc = string.gsub(loc, "\t", "") -- drop tabulator
return loc
end

local function serialize_arg(v)
if v == nil then
return nil
Expand All @@ -99,11 +91,10 @@ local function serialize_arg(v)
return serialized:gsub("\\093", "]") -- TODO unescape all special characters
end

local function serialize_message(msg)
local function msg_or(msg, default)
if msg == nil then
return nil
return default
end

return tostring(msg)
end

Expand All @@ -121,32 +112,31 @@ end
---
---@param expected any
---@param actual any
---@param msg? any message which will be presented in the unitron ui.
---@param msg? any message which will be presented in the unitron ui, instead of standard message
function assert_eq(expected, actual, msg)
test_helper()

if not equal(expected, actual) then
local err = {
assert = "eq",
expected = serialize_arg(expected),
actual = serialize_arg(actual),
msg = serialize_message(msg),
file = get_caller(),
test_fail {
msg = msg_or(msg, "args not equal"),
expect = serialize_arg(expected),
actual = serialize_arg(actual)
}
error(err)
end
end

---@param not_expected any
---@param actual any
---@param msg? any message which will be presented in the unitron ui.
---@param msg? any message which will be presented in the unitron ui, instead of standard message
function assert_not_eq(not_expected, actual, msg)
test_helper()

if equal(not_expected, actual) then
local err = {
assert = "not_eq",
test_fail {
msg = msg_or(msg, "args are equal"),
not_expect = serialize_arg(not_expected),
actual = serialize_arg(actual),
msg = serialize_message(msg),
file = get_caller(),
}
error(err)
end
end

Expand All @@ -163,64 +153,60 @@ end
---@param expected number
---@param actual number
---@param delta number
---@param msg? any message which will be presented in the unitron ui.
---@param msg? any message which will be presented in the unitron ui, instead of standard message
function assert_close(expected, actual, delta, msg)
test_helper()

local invalid_args = expected == nil or actual == nil or delta == nil
if invalid_args or abs(expected - actual) > delta then
local err = {
assert = "close",
expected = as_string(expected), -- TODO Picotron has a bug that small numbers are not properly serialized
test_fail {
msg = msg_or(msg, "args not close"),
expect = as_string(expected), -- TODO Picotron has a bug that small numbers are not properly serialized
actual = as_string(actual), -- TODO Picotron has a bug that small numbers are not properly serialized
delta = as_string(delta), -- TODO Picotron has a bug that small numbers are not properly serialized
msg = serialize_message(msg),
file = get_caller(),
}
error(err)
end
end

---@param not_expected number
---@param actual number
---@param delta number
---@param msg? any message which will be presented in the unitron ui.
---@param msg? any message which will be presented in the unitron ui, instead of standard message
function assert_not_close(not_expected, actual, delta, msg)
test_helper()

local invalid_args = not_expected == nil or actual == nil or delta == nil
if invalid_args or abs(not_expected - actual) <= delta then
local err = {
assert = "not_close",
not_expected = as_string(not_expected), -- TODO Picotron has a bug that small numbers are not properly serialized
actual = as_string(actual), -- TODO Picotron has a bug that small numbers are not properly serialized
delta = as_string(delta), -- TODO Picotron has a bug that small numbers are not properly serialized
msg = serialize_message(msg),
file = get_caller(),
test_fail {
msg = msg_or(msg, "args too close"),
not_expect = as_string(not_expected), -- TODO Picotron has a bug that small numbers are not properly serialized
actual = as_string(actual), -- TODO Picotron has a bug that small numbers are not properly serialized
delta = as_string(delta), -- TODO Picotron has a bug that small numbers are not properly serialized
}
error(err)
end
end

---@param actual any
---@param msg? any message which will be presented in the unitron ui.
---@param msg? any message which will be presented in the unitron ui, instead of standard message
function assert_not_nil(actual, msg)
test_helper()

if actual == nil then
local err = {
assert = "not_nil",
msg = serialize_message(msg),
file = get_caller(),
test_fail {
msg = msg_or(msg, "arg is nil")
}
error(err)
end
end

---@param actual any
---@param msg? any message which will be presented in the unitron ui.
---@param msg? any message which will be presented in the unitron ui, instead of standard message
function assert_nil(actual, msg)
test_helper()

if actual != nil then
local err = {
assert = "nil",
actual = as_string(actual),
msg = serialize_message(msg),
file = get_caller(),
test_fail {
msg = msg_or(msg, "arg is not nil"),
actual = as_string(actual)
}
error(err)
end
end
22 changes: 22 additions & 0 deletions examples/subject_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,28 @@ test("table driven tests", function()
end
end)

-- you can write your own custom assert functions
test("custom assert function", function()
local function assert_even(n)
-- test_helper() marks the assert_even function as a test helper function.
-- When printing file and line information in GUI, this function will be
-- skipped.
test_helper()

if n % 2 != 0 then
test_fail {
-- msg will be presented in the GUI when assertion failed:
msg = "arg is not even",
-- you can add as many fields as you want. All will be presented
-- in the GUI along with msg:
actual = n
}
end
end

assert_even(3) -- change to even number in order to remove assertion error
end)

-- test can be slow, but don't worry - it does not block the unitron ui
test("slow test", function()
for i = 1, 1000000 do
Expand Down
49 changes: 16 additions & 33 deletions gui/gui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -211,46 +211,29 @@ on_event("test_finished", function(e)
message = "\fbTest successful"
color = "\fb"
else
if err.file != nil then
print_line(e.test, "\f8Error \f7at " .. err.file)
if err.__traceback != nil and #err.__traceback > 0 then
local file = err.__traceback[1]
print_line(e.test, "\f8Error \f7at " .. file)

-- print additional message provided by user
if err.msg != nil then
print_line(e.test, err.msg)
end

if err.assert == "eq" then
print_line(e.test, "args not equal:")
print_line(e.test, "\f5 expect=\f6" .. tostring(err.expected))
print_line(e.test, "\f5 actual=\f6" .. tostring(err.actual))
elseif err.assert == "not_eq" then
print_line(e.test, "args are equal:")
-- always print expected first
if err.expect != nil then
print_line(e.test, "\f5 expect=\f6" .. tostring(err.expect))
end
-- then actual
if err.actual != nil then
print_line(e.test, "\f5 actual=\f6" .. tostring(err.actual))
elseif err.assert == "same" then
print_line(e.test, "args are not the same:")
print_line(e.test, "\f5 expect=\f6" .. err.expected)
print_line(e.test, "\f5 actual=\f6" .. err.actual)
elseif err.assert == "not_same" then
print_line(e.test, "args are the same:")
print_line(e.test, "\f5 actual=\f6" .. err.actual)
elseif err.assert == "close" then
print_line(e.test, "args not close")
print_line(e.test, "\f5 expect=\f6" .. err.expected)
print_line(e.test, "\f5 actual=\f6" .. err.actual)
print_line(e.test, "\f5 delta =\f6" .. err.delta)
elseif err.assert == "not_close" then
print_line(e.test, "args too close")
print_line(e.test, "\f5 not_ex=\f6" .. err.not_expected)
print_line(e.test, "\f5 actual=\f6" .. err.actual)
print_line(e.test, "\f5 delta =\f6" .. err.delta)
elseif err.assert == "not_nil" then
print_line(e.test, "arg is nil")
elseif err.assert == "nil" then
print_line(e.test, err.actual .. " is not nil")
elseif err.assert == "true" then
print_line(e.test, "arg is false")
elseif err.assert == "false" then
print_line(e.test, "arg is true")
end

-- TODO sort alphabetically?
for k, v in pairs(err) do
if k != "msg" and k != "expect" and k != "actual" and k != "__traceback" then
print_line(e.test, "\f5 " .. k .. "=\f6" .. tostring(v))
end
end
end

Expand Down
69 changes: 62 additions & 7 deletions runner.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ local tests <const> = {} -- {id=1,name=..}

local publish_throttler <const> = new_throttler(50) -- max 50 messages per frame

-- key is a file:linedefined, value is always true:
local helpers <const> = {}

local function publish(msg)
publish_throttler:throttle()
send_message(parent_pid, msg)
Expand Down Expand Up @@ -64,16 +67,17 @@ function test(name, test)
if not success then
if type(err) == "string" then
local escaped_work_dir = work_dir:gsub("([%W])", "%%%1")
local file = ""
-- file locator is file path with line no, eg. "/workdir/file.lua:10: "
local file_locator_pattern = escaped_work_dir .. "[^ ]+:%d+: "
local msg = err
if string.match(err, escaped_work_dir) then
file = string.gsub(err, "(%d+):.*", "%1") -- drop message
msg = string.gsub(err, file .. ": ", "")

local file = string.match(err, file_locator_pattern)
if file then
msg = err:sub(#file + 1, #err)
file = file:sub(1, #file - 2) -- drop ": "
end
err = {
assert = "generic",
original_error = err,
file = file,
__traceback = { file },
msg = msg,
}
end
Expand All @@ -90,6 +94,57 @@ function test(name, test)
publish { event = "test_finished", test = current_test, error = err }
end

-- test_helper marks the calling function as a test helper function.
-- When printing file and line information in GUI, that function will be
-- skipped.
function test_helper()
local info = debug.getinfo(2, "Sl")
local info_string = string.format("%s:%d", info.short_src, info.linedefined)
helpers[info_string] = true
end

---Generates stack traceback (skipping helpers)
---@return table
local function traceback()
local trace = {}

for level = 3, math.huge do
local info = debug.getinfo(level, "Sl")
if info == nil then break end
local info_string = string.format("%s:%d", info.short_src, info.linedefined)
if not helpers[info_string] then
table.insert(trace, string.format("%s:%d", info.short_src, info.currentline))
end
end

return trace
end

---Generates test error which stops current test execution and shows error to
---the user. In the GUI, the error will be presented together with a file name
---and line number where the `test_fail` function was executed. If you run
---`test_fail` from your own assert function, and want to see a place where this
---assert function was executed instead, please run the test_helper() function
---in the beginning of your assert function:
---```
--- function custom_assert(....)
--- test_helper() -- mark custom_assert function as test helper
--- if .... then
--- test_fail("message")
--- end
--- end
---```
---@param err string|table Error message as a string or a table. All table fields will be presented in the GUI. Table could contain special `msg` field which will always be presented first.
function test_fail(err)
if type(err) != "table" then
err = { msg = tostring(err) }
end

err.__traceback = traceback()

error(err)
end

local originalPrint <const> = print

-- override picotron print, so all text is sent to the parent process
Expand Down