From 03f88fb5a362e0a5b75f561ee33cb4e4bd0416b2 Mon Sep 17 00:00:00 2001 From: FourierTransformer Date: Mon, 11 May 2026 07:00:04 -0500 Subject: [PATCH] validate options and clean up error messages --- build/tested.lua | 35 +++++++++++++++++++++++++++++------ build/tested/main.lua | 14 +++++++------- build/tested/test_runner.lua | 4 +++- src/tested.tl | 35 +++++++++++++++++++++++++++++------ src/tested/main.tl | 14 +++++++------- src/tested/test_runner.tl | 4 +++- 6 files changed, 78 insertions(+), 28 deletions(-) diff --git a/build/tested.lua b/build/tested.lua index 66fba6b..f0ce143 100644 --- a/build/tested.lua +++ b/build/tested.lua @@ -3,7 +3,25 @@ local assert_table = require("tested.assert_table") local tested = { tests = {}, run_only_tests = false } -local function extract_fn_and_options(fn_or_options, fn) +local function validate_options(test_name, options, test_src) + local error_prefix = test_src .. " in \"" .. test_name .. "\": " + if options.expected ~= nil then + if type(options.expected) ~= "string" then + error(error_prefix .. "options.expected takes in a 'string', but received '" .. type(options.expected) .. "'", 0) + end + if not (options.expected == "FAIL" or options.expected == "EXCEPTION" or options.expected == "UNKNOWN") then + error(error_prefix .. "options.expected should be one of 'FAIL', 'EXCEPTION', or 'UNKNOWN'", 0) + end + end + + if options.run_when ~= nil then + if type(options.run_when) ~= "boolean" then + error(error_prefix .. "options.run_when takes in a 'boolean', but received '" .. type(options.run_when) .. "'", 0) + end + end +end + +local function extract_fn_and_options(test_name, fn_or_options, fn, test_src) local options = {} if type(fn_or_options) == "function" then fn = fn_or_options @@ -14,22 +32,27 @@ local function extract_fn_and_options(fn_or_options, fn) fn = fn end + validate_options(test_name, options, test_src or "?") + return fn, options end function tested.test(name, fn_or_options, fn) - local func, options = extract_fn_and_options(fn_or_options, fn) + local test_src = debug.getinfo(2, "S").short_src + local func, options = extract_fn_and_options(name, fn_or_options, fn, test_src) table.insert(tested.tests, { name = name, fn = func, options = options, kind = "test" }) end function tested.skip(name, fn_or_options, fn) - local func, options = extract_fn_and_options(fn_or_options, fn) + local test_src = debug.getinfo(2, "S").short_src + local func, options = extract_fn_and_options(name, fn_or_options, fn, test_src) table.insert(tested.tests, { name = name, fn = func, options = options, kind = "skip" }) end function tested.only(name, fn_or_options, fn) - local func, options = extract_fn_and_options(fn_or_options, fn) + local test_src = debug.getinfo(2, "S").short_src + local func, options = extract_fn_and_options(name, fn_or_options, fn, test_src) table.insert(tested.tests, { name = name, fn = func, options = options, kind = "only" }) end @@ -37,14 +60,14 @@ function tested.assert(assertion) local errors = {} if assertion.expected == nil then table.insert(errors, "'expected'") end if assertion.actual == nil then table.insert(errors, "'actual'") end - assert(#errors == 0, "The assertion table must include 'expected' and 'actual' whose values cannot be 'nil'. Missing (or 'nil') fields: " .. table.concat(errors, ", ")) + if #errors ~= 0 then error("The assertion table must include 'expected' and 'actual' whose values cannot be 'nil'. Missing (or 'nil') fields: " .. table.concat(errors, ", "), 0) end if assertion.given and type(assertion.given) ~= "string" then table.insert(errors, "In assertion, 'given' should be a 'string'. It appears to be a '" .. type(assertion.given) .. "' with value: '" .. tostring(assertion.given)) end if assertion.should and type(assertion.should) ~= "string" then table.insert(errors, "In assertion, 'should' should be a 'string'. It appears to be a '" .. type(assertion.should) .. "' with value: " .. tostring(assertion.should)) end - assert(#errors == 0, table.concat(errors, ". ")) + if #errors ~= 0 then error(table.concat(errors, ". "), 0) end local expected_type = type(assertion.expected) local actual_type = type(assertion.actual) diff --git a/build/tested/main.lua b/build/tested/main.lua index eb25246..b8f1f31 100644 --- a/build/tested/main.lua +++ b/build/tested/main.lua @@ -122,7 +122,7 @@ local function validate_args(args) for _, path in ipairs(args.paths) do local info, err = lfs.attributes(path) if err then error("The file or directory '" .. path .. "' does not appear to exist. Unable to run tests") end - assert(info.mode == "directory" or info.mode == "file", "tested requires the paths passed in to be a directory or file") + if not (info.mode == "directory" or info.mode == "file") then error("tested requires the paths passed in to be a directory or file", 0) end if info.mode == "directory" then table.insert(args.test_directories, path) end if info.mode == "file" then table.insert(args.test_files, path) end end @@ -144,14 +144,14 @@ local function load_result_formatter(args) logger:info("Unable to load as module, attempting to load from filepath") local info, err = lfs.attributes(args.custom_formatter) if err then error("Unable to load custom formatter, the file/module '" .. args.custom_formatter .. "' could not be loaded.") end - assert(info.mode == "file", "The custom formatter should point to a file, but currently appears to be a: " .. info.mode) + if info.mode ~= "file" then error("The custom formatter should point to a file, but currently appears to be a: " .. info.mode, 0) end formatter = file_loader.load_file(args.custom_formatter) end if formatter then - assert(formatter.header and type(formatter.header) == "function", "Custom formatter must include a 'header', 'results', and 'summary' section. Missing 'header'.") - assert(formatter.results and type(formatter.results) == "function", "Custom formatter must include a 'header', 'results', and 'summary' section. Missing 'results'.") - assert(formatter.summary and type(formatter.summary) == "function", "Custom formatter must include a 'header', 'results', and 'summary' section. Missing 'summary'.") + if not (formatter.header and type(formatter.header) == "function") then error("Custom formatter must include a 'header', 'results', and 'summary' section. Missing 'header'.", 0) end + if not (formatter.results and type(formatter.results) == "function") then error("Custom formatter must include a 'header', 'results', and 'summary' section. Missing 'results'.", 0) end + if not (formatter.summary and type(formatter.summary) == "function") then error("Custom formatter must include a 'header', 'results', and 'summary' section. Missing 'summary'.", 0) end return formatter else error("Unable to load custom formatter from: " .. args.custom_formatter) @@ -172,7 +172,7 @@ local function register_format_handler(handlers) local info, err = lfs.attributes(handler) if err then error("Unable to load format handler, the file/module '" .. handler .. "' was not able to be loaded.") end - assert(info.mode == "file", "The custom format loader should point to a file, but currently appears to be a: " .. info.mode) + if info.mode ~= "file" then error("The custom format loader should point to a file, but currently appears to be a: " .. info.mode, 0) end file_loader.load_and_register_handler(handler) end @@ -293,7 +293,7 @@ local function main() local test_files = get_all_test_files(args) - assert(#test_files > 0, "Unable to find any tests to run in: " .. table.concat(args.paths, ", ")) + if #test_files == 0 then error("Unable to find any tests to run in: " .. table.concat(args.paths, ", "), 0) end formatter.header(TESTED_VERSION, args.paths) diff --git a/build/tested/test_runner.lua b/build/tested/test_runner.lua index 1477d40..288a6cb 100644 --- a/build/tested/test_runner.lua +++ b/build/tested/test_runner.lua @@ -13,7 +13,9 @@ function test_runner.run_with_cleanup(file_loader, test_file, options) for package_name, _ in pairs(package.loaded) do pre_test_loaded_packages[package_name] = true end local test_module = file_loader.load_file(test_file) - assert(type(test_module) == "table" and type(test_module.tests) == "table" and type(test_module.run_only_tests) == "boolean", "It does not appear that '" .. test_file .. "' returns the 'tested' module") + if not (type(test_module) == "table" and type(test_module.tests) == "table" and type(test_module.run_only_tests) == "boolean") then + error(test_file .. ": does not 'return tested' at end of file - unable to run tests", 0) + end local test_results = test_module:run(test_file, options) diff --git a/src/tested.tl b/src/tested.tl index 0e6c9ac..da9352c 100644 --- a/src/tested.tl +++ b/src/tested.tl @@ -3,7 +3,25 @@ local assert_table = require("tested.assert_table") local tested: types.Tested = { tests = {}, run_only_tests = false } -local function extract_fn_and_options(fn_or_options: function() | types.TestedOptions, fn?: function()): function(), types.TestedOptions +local function validate_options(test_name: string, options: types.TestedOptions, test_src: string) + local error_prefix = test_src .. " in \"" .. test_name .. "\": " + if options.expected ~= nil then + if type(options.expected) ~= "string" then + error(error_prefix .. "options.expected takes in a 'string', but received '" .. type(options.expected) .. "'", 0) + end + if not (options.expected == "FAIL" or options.expected == "EXCEPTION" or options.expected == "UNKNOWN") then + error(error_prefix .. "options.expected should be one of 'FAIL', 'EXCEPTION', or 'UNKNOWN'", 0) + end + end + + if options.run_when ~= nil then + if type(options.run_when) ~= "boolean" then + error(error_prefix .. "options.run_when takes in a 'boolean', but received '" .. type(options.run_when) .. "'", 0) + end + end +end + +local function extract_fn_and_options(test_name: string, fn_or_options: function() | types.TestedOptions, fn?: function(), test_src?: string): function(), types.TestedOptions local options: types.TestedOptions = {} if type(fn_or_options) == "function" then fn = fn_or_options as function() @@ -14,22 +32,27 @@ local function extract_fn_and_options(fn_or_options: function() | types.TestedOp fn = fn end + validate_options(test_name, options, test_src or "?") + return fn, options end -- teal currently doesn't support polymorphism, so we gotta kinda handle it ourselves function tested.test(name: string, fn_or_options: function() | types.TestedOptions, fn?: function()) - local func, options = extract_fn_and_options(fn_or_options, fn) + local test_src = debug.getinfo(2, "S").short_src + local func, options = extract_fn_and_options(name, fn_or_options, fn, test_src) table.insert(tested.tests, {name=name, fn=func, options=options, kind="test"}) end function tested.skip(name: string, fn_or_options: function() | types.TestedOptions, fn?: function()) - local func, options = extract_fn_and_options(fn_or_options, fn) + local test_src = debug.getinfo(2, "S").short_src + local func, options = extract_fn_and_options(name, fn_or_options, fn, test_src) table.insert(tested.tests, {name=name, fn=func, options=options, kind="skip"}) end function tested.only(name: string, fn_or_options: function() | types.TestedOptions, fn?: function()) - local func, options = extract_fn_and_options(fn_or_options, fn) + local test_src = debug.getinfo(2, "S").short_src + local func, options = extract_fn_and_options(name, fn_or_options, fn, test_src) table.insert(tested.tests, {name=name, fn=func, options=options, kind="only"}) end @@ -37,14 +60,14 @@ function tested.assert(assertion: types.Assertion): boolean, string local errors = {} if assertion.expected == nil then table.insert(errors, "'expected'") end if assertion.actual == nil then table.insert(errors, "'actual'") end - assert(#errors == 0, "The assertion table must include 'expected' and 'actual' whose values cannot be 'nil'. Missing (or 'nil') fields: " .. table.concat(errors, ", ")) + if #errors ~= 0 then error("The assertion table must include 'expected' and 'actual' whose values cannot be 'nil'. Missing (or 'nil') fields: " .. table.concat(errors, ", "), 0) end if assertion.given and type(assertion.given) ~= "string" then table.insert(errors, "In assertion, 'given' should be a 'string'. It appears to be a '" .. type(assertion.given) .. "' with value: '" .. tostring(assertion.given)) end if assertion.should and type(assertion.should) ~= "string" then table.insert(errors, "In assertion, 'should' should be a 'string'. It appears to be a '" .. type(assertion.should) .. "' with value: " .. tostring(assertion.should)) end - assert(#errors == 0, table.concat(errors, ". ")) + if #errors ~= 0 then error(table.concat(errors, ". "), 0) end local expected_type = type(assertion.expected) local actual_type = type(assertion.actual) diff --git a/src/tested/main.tl b/src/tested/main.tl index 5c51739..7e20fce 100644 --- a/src/tested/main.tl +++ b/src/tested/main.tl @@ -122,7 +122,7 @@ local function validate_args(args: CLIOptions) for _, path in ipairs(args.paths) do local info, err = lfs.attributes(path) if err then error("The file or directory '" .. path .. "' does not appear to exist. Unable to run tests") end - assert(info.mode == "directory" or info.mode == "file", "tested requires the paths passed in to be a directory or file") + if not (info.mode == "directory" or info.mode == "file") then error("tested requires the paths passed in to be a directory or file", 0) end if info.mode == "directory" then table.insert(args.test_directories, path) end if info.mode == "file" then table.insert(args.test_files, path) end end @@ -144,14 +144,14 @@ local function load_result_formatter(args: CLIOptions): types.ResultFormatter logger:info("Unable to load as module, attempting to load from filepath") local info, err = lfs.attributes(args.custom_formatter) if err then error("Unable to load custom formatter, the file/module '" .. args.custom_formatter .. "' could not be loaded.") end - assert(info.mode == "file", "The custom formatter should point to a file, but currently appears to be a: " .. info.mode) + if info.mode ~= "file" then error("The custom formatter should point to a file, but currently appears to be a: " .. info.mode, 0) end formatter = file_loader.load_file(args.custom_formatter) as types.ResultFormatter end if formatter then - assert(formatter.header and type(formatter.header) == "function", "Custom formatter must include a 'header', 'results', and 'summary' section. Missing 'header'.") - assert(formatter.results and type(formatter.results) == "function", "Custom formatter must include a 'header', 'results', and 'summary' section. Missing 'results'.") - assert(formatter.summary and type(formatter.summary) == "function", "Custom formatter must include a 'header', 'results', and 'summary' section. Missing 'summary'.") + if not (formatter.header and type(formatter.header) == "function") then error("Custom formatter must include a 'header', 'results', and 'summary' section. Missing 'header'.", 0) end + if not (formatter.results and type(formatter.results) == "function") then error("Custom formatter must include a 'header', 'results', and 'summary' section. Missing 'results'.", 0) end + if not (formatter.summary and type(formatter.summary) == "function") then error("Custom formatter must include a 'header', 'results', and 'summary' section. Missing 'summary'.", 0) end return formatter else error("Unable to load custom formatter from: " .. args.custom_formatter) @@ -172,7 +172,7 @@ local function register_format_handler(handlers: {string}) local info, err = lfs.attributes(handler) if err then error("Unable to load format handler, the file/module '" .. handler .."' was not able to be loaded.") end - assert(info.mode == "file", "The custom format loader should point to a file, but currently appears to be a: " .. info.mode) + if info.mode ~= "file" then error("The custom format loader should point to a file, but currently appears to be a: " .. info.mode, 0) end file_loader.load_and_register_handler(handler) end @@ -293,7 +293,7 @@ local function main() -- finding all the files local test_files = get_all_test_files(args) - assert(#test_files > 0, "Unable to find any tests to run in: " .. table.concat(args.paths, ", ")) + if #test_files == 0 then error("Unable to find any tests to run in: " .. table.concat(args.paths, ", "), 0) end -- running the tests formatter.header(TESTED_VERSION, args.paths) diff --git a/src/tested/test_runner.tl b/src/tested/test_runner.tl index d053ac0..8a930f7 100644 --- a/src/tested/test_runner.tl +++ b/src/tested/test_runner.tl @@ -13,7 +13,9 @@ function test_runner.run_with_cleanup(file_loader: types.FileLoader, test_file: for package_name, _ in pairs(package.loaded) do pre_test_loaded_packages[package_name] = true end local test_module = file_loader.load_file(test_file) as types.Tested - assert(type(test_module) == "table" and type(test_module.tests) == "table" and type(test_module.run_only_tests) == "boolean", "It does not appear that '" .. test_file .."' returns the 'tested' module") + if not (type(test_module) == "table" and type(test_module.tests) == "table" and type(test_module.run_only_tests) == "boolean") then + error(test_file ..": does not 'return tested' at end of file - unable to run tests", 0) + end local test_results = test_module:run(test_file, options)