New test runner

This commit is contained in:
Christoffer Lerno
2025-01-29 15:29:09 +01:00
committed by Christoffer Lerno
parent 6848753a10
commit 3c50376175
13 changed files with 866 additions and 76 deletions

View File

@@ -2,11 +2,38 @@
// Use of this source code is governed by the MIT license
// a copy of which can be found in the LICENSE_STDLIB file.
module std::core::runtime;
import std::core::test @public;
import libc, std::time, std::io, std::sort;
import std::os::env;
def TestFn = fn void!() @if($$OLD_TEST);
def TestFn = fn void() @if(!$$OLD_TEST);
TestContext* test_context @private;
struct TestContext
{
JmpBuf buf;
// Allows filtering test cased or modules by substring, e.g. 'foo::', 'foo::test_add'
String test_filter;
// Triggers debugger breakpoint when assert or test:: checks failed
bool breakpoint_on_assert;
// internal state
bool assert_print_backtrace;
bool has_ansi_codes;
bool is_in_panic;
String current_test_name;
TestFn setup_fn;
TestFn teardown_fn;
char* error_buffer;
usz error_buffer_capacity;
File fake_stdout;
File orig_stdout;
File orig_stderr;
}
struct TestUnit
{
String name;
@@ -25,11 +52,6 @@ fn TestUnit[] test_collection_create(Allocator allocator = allocator::heap())
return tests;
}
struct TestContext
{
JmpBuf buf;
}
// Sort the tests by their name in ascending order.
fn int cmp_test_unit(TestUnit a, TestUnit b)
{
@@ -44,82 +66,175 @@ fn int cmp_test_unit(TestUnit a, TestUnit b)
return (int)(an - bn);
}
TestContext* test_context @private;
fn void test_panic(String message, String file, String function, uint line)
fn bool terminal_has_ansi_codes() @local
{
io::printn("[error]");
io::print("\n Error: ");
io::print(message);
io::printn();
io::printfn(" - in %s %s:%s.\n", function, file, line);
@pool()
{
if (try v = env::get_var_temp("TERM"))
{
if (v.contains("xterm") || v.contains("vt100") || v.contains("screen")) return true;
}
$if env::WIN32 || env::NO_LIBC:
return false;
$else
return io::stdout().isatty();
$endif
};
}
fn void test_panic(String message, String file, String function, uint line) @local
{
if (test_context.is_in_panic) return;
test_context.is_in_panic = true;
unmute_output(true);
(void)io::stdout().flush();
if (test_context.assert_print_backtrace)
{
$if env::NATIVE_STACKTRACE:
builtin::print_backtrace(message, 0);
$endif
}
io::printf("\nTest failed ^^^ ( %s:%s ) %s\n", file, line, message);
test_context.assert_print_backtrace = true;
if (test_context.breakpoint_on_assert)
{
breakpoint();
}
if (test_context.teardown_fn)
{
test_context.teardown_fn();
}
test_context.is_in_panic = false;
libc::longjmp(&test_context.buf, 1);
}
fn bool run_tests(TestUnit[] tests) @if($$OLD_TEST)
fn void mute_output() @local
{
usz max_name;
foreach (&unit : tests)
{
if (max_name < unit.name.len) max_name = unit.name.len;
}
quicksort(tests, &cmp_test_unit);
if (!test_context.fake_stdout.file) return;
assert(!test_context.orig_stderr.file);
assert(!test_context.orig_stdout.file);
TestContext context;
test_context = &context;
File* stdout = io::stdout();
File* stderr = io::stderr();
PanicFn old_panic = builtin::panic;
defer builtin::panic = old_panic;
builtin::panic = &test_panic;
int tests_passed = 0;
int test_count = tests.len;
DString name = dstring::temp_with_capacity(64);
usz len = max_name + 9;
name.append_repeat('-', len / 2);
name.append(" TESTS ");
name.append_repeat('-', len - len / 2);
io::printn(name);
name.clear();
foreach(unit : tests)
{
defer name.clear();
name.appendf("Testing %s ", unit.name);
name.append_repeat('.', max_name - unit.name.len + 2);
io::printf("%s ", name.str_view());
(void)io::stdout().flush();
if (libc::setjmp(&context.buf) == 0)
{
if (catch err = unit.func())
{
io::printfn("[failed] Failed due to: %s", err);
continue;
}
io::printn("[ok]");
tests_passed++;
}
}
io::printfn("\n%d test%s run.\n", test_count, test_count > 1 ? "s" : "");
io::printfn("Test Result: %s. %d passed, %d failed.",
tests_passed < test_count ? "FAILED" : "ok", tests_passed, test_count - tests_passed);
return test_count == tests_passed;
test_context.orig_stderr = *stderr;
test_context.orig_stdout = *stdout;
*stderr = test_context.fake_stdout;
*stdout = test_context.fake_stdout;
(void)test_context.fake_stdout.seek(0, Seek.SET)!!;
}
fn bool run_tests(TestUnit[] tests) @if(!$$OLD_TEST)
fn void unmute_output(bool has_error) @local
{
if (!test_context.fake_stdout.file)
{
return;
}
assert(test_context.orig_stderr.file);
assert(test_context.orig_stdout.file);
File* stdout = io::stdout();
File* stderr = io::stderr();
*stderr = test_context.orig_stderr;
*stdout = test_context.orig_stdout;
test_context.orig_stderr.file = null;
test_context.orig_stdout.file = null;
usz log_size = test_context.fake_stdout.seek(0, Seek.CURSOR)!!;
if (has_error)
{
io::printn(test_context.has_ansi_codes ? "[\e[0;31mFAIL\e[0m]" : "[FAIL]");
}
if (has_error && log_size > 0)
{
test_context.fake_stdout.write_byte('\n')!!;
test_context.fake_stdout.write_byte('\0')!!;
(void)test_context.fake_stdout.seek(0, Seek.SET)!!;
io::printfn("\n========== TEST LOG ============");
io::printfn("%s\n", test_context.current_test_name);
while (try c = test_context.fake_stdout.read_byte())
{
if (@unlikely(c == '\0'))
{
// ignore junk from previous tests
break;
}
libc::putchar(c);
}
io::printf("========== TEST END ============");
}
(void)stdout.flush();
}
fn bool run_tests(String[] args, TestUnit[] tests) @private
{
usz max_name;
bool sort_tests = true;
foreach (&unit : tests)
{
if (max_name < unit.name.len) max_name = unit.name.len;
}
quicksort(tests, &cmp_test_unit);
TestContext context;
TestContext context =
{
.assert_print_backtrace = true,
.breakpoint_on_assert = false,
.test_filter = "",
.has_ansi_codes = terminal_has_ansi_codes(),
};
for (int i = 1; i < args.len; i++)
{
switch (args[i])
{
case "breakpoint":
context.breakpoint_on_assert = true;
case "nosort":
sort_tests = false;
case "noansi":
context.has_ansi_codes = false;
case "useansi":
context.has_ansi_codes = true;
case "filter":
if (i == args.len - 1)
{
io::printn("Invalid arguments to test runner.");
return false;
}
context.test_filter = args[i + 1];
i++;
default:
io::printfn("Unknown argument: %s", args[i]);
}
}
test_context = &context;
if (sort_tests)
{
quicksort(tests, &cmp_test_unit);
}
// Buffer for hijacking the output
$if (!env::NO_LIBC):
test_context.fake_stdout.file = libc::tmpfile();
$endif
if (test_context.fake_stdout.file == null)
{
io::print("Failed to hijack stdout, tests will print everything");
}
PanicFn old_panic = builtin::panic;
defer builtin::panic = old_panic;
builtin::panic = &test_panic;
int tests_passed = 0;
int tests_skipped = 0;
int test_count = tests.len;
DString name = dstring::temp_with_capacity(64);
usz len = max_name + 9;
@@ -130,28 +245,71 @@ fn bool run_tests(TestUnit[] tests) @if(!$$OLD_TEST)
name.clear();
foreach(unit : tests)
{
if (test_context.test_filter && !unit.name.contains(test_context.test_filter))
{
tests_skipped++;
continue;
}
test_context.setup_fn = null;
test_context.teardown_fn = null;
test_context.current_test_name = unit.name;
defer name.clear();
name.appendf("Testing %s ", unit.name);
name.append_repeat('.', max_name - unit.name.len + 2);
io::printf("%s ", name.str_view());
(void)io::stdout().flush();
if (libc::setjmp(&context.buf) == 0)
{
unit.func();
io::printn("[ok]");
mute_output();
$if(!$$OLD_TEST):
unit.func();
$else
if (catch err = unit.func())
{
io::printf("[FAIL] Failed due to: %s", err);
continue;
}
$endif
unmute_output(false); // all good, discard output
io::printfn(test_context.has_ansi_codes ? "[\e[0;32mPASS\e[0m]" : "[PASS]");
tests_passed++;
if (test_context.teardown_fn)
{
test_context.teardown_fn();
}
}
}
io::printfn("\n%d test%s run.\n", test_count, test_count > 1 ? "s" : "");
io::printfn("Test Result: %s. %d passed, %d failed.",
tests_passed < test_count ? "FAILED" : "ok", tests_passed, test_count - tests_passed);
return test_count == tests_passed;
io::printfn("\n%d test%s run.\n", test_count-tests_skipped, test_count > 1 ? "s" : "");
int n_failed = test_count - tests_passed - tests_skipped;
io::printf("Test Result: %s%s%s: ",
test_context.has_ansi_codes ? (n_failed ? "\e[0;31m" : "\e[0;32m") : "",
n_failed ? "FAILED" : "PASSED",
test_context.has_ansi_codes ? "\e[0m" : "",
);
io::printfn("%d passed, %d failed, %d skipped.",
tests_passed,
n_failed,
tests_skipped);
// cleanup fake_stdout file
if (test_context.fake_stdout.file) libc::fclose(test_context.fake_stdout.file);
test_context.fake_stdout.file = null;
return n_failed == 0;
}
fn bool default_test_runner(String[] args)
{
@pool()
{
return run_tests(test_collection_create(allocator::temp()));
assert(test_context == null, "test suite is already running");
return run_tests(args, test_collection_create(allocator::temp()));
};
}

223
lib/std/core/test.c3 Normal file
View File

@@ -0,0 +1,223 @@
<*
Unit test module
This module provides a toolset of macros for running unit test checks
Example:
```c3
module sample::m;
import std::io;
fault MathError
{
DIVISION_BY_ZERO
}
fn double! divide(int a, int b)
{
if (b == 0) return MathError.DIVISION_BY_ZERO?;
return (double)(a) / (double)(b);
}
fn void! test_div() @test
{
test::is_equal(2, divide(6, 3)!);
test::is_not_equal(1, 2);
test::is_almost_equal(m::divide(1, 3)!, 0.333, places: 3);
test::is_greater_equal(3, 3);
test::is_greater(2, divide(3, 3)!);
test::is_less(2, 3);
test::is_less_equal(2, 3);
test::@check(2 == 2, "divide: %d", divide(6, 3)!);
test::@error(m::divide(3, 0), MathError.DIVISION_BY_ZERO);
}
```
*>
// Copyright (c) 2025 Alex Veden <i@alexveden.com>. All rights reserved.
// Use of this source code is governed by the MIT license
// a copy of which can be found in the LICENSE_STDLIB file.
module std::core::test;
import std::core::runtime @public;
import std::math, std::io, libc;
<*
Initializes test case context.
@param setup_fn `initializer function for test case`
@param teardown_fn `cleanup function for test context (may be null)`
@require runtime::test_context != null "Only allowed in @test functions"
@require setup_fn != null "setup_fn must always be set"
*>
macro setup(TestFn setup_fn, TestFn teardown_fn = null)
{
runtime::test_context.setup_fn = setup_fn;
runtime::test_context.teardown_fn = teardown_fn;
runtime::test_context.setup_fn();
}
<*
Checks condition and fails assertion if not true
@param #condition `any boolean condition, will be expanded by text`
@param format `printf compatible format`
@param args `vargs for format`
@require runtime::test_context != null "Only allowed in @test functions"
*>
macro @check(#condition, String format = "", args...)
{
if (!#condition)
{
@stack_mem(512; Allocator allocator)
{
DString s;
s.new_init(allocator: allocator);
s.appendf("check `%s` failed. ", $stringify(#condition));
s.appendf(format, ...args);
print_panicf(s.str_view());
};
}
}
<*
Check if function returns specific error
@param #funcresult `result of function execution`
@param error_expected `expected error of function execution`
@require runtime::test_context != null "Only allowed in @test functions"
*>
macro @error(#funcresult, anyfault error_expected)
{
if (catch err = #funcresult)
{
if (err != error_expected)
{
print_panicf("`%s` expected to return error [%s], got [%s]",
$stringify(#funcresult), error_expected, err);
}
return;
}
print_panicf("`%s` error [%s] was not returned.", $stringify(#funcresult), error_expected);
}
<*
Check if left == right
@param left `left argument of any comparable type`
@param right `right argument of any comparable type`
@require runtime::test_context != null "Only allowed in @test functions"
*>
macro is_equal(left, right)
{
if (!equals(left, right))
{
print_panicf("`%s` != `%s`", left, right);
}
}
<*
Check left floating point value is approximately equals to right value
@param places `number of decimal places to compare (default: 7)`
@param delta `minimal allowed difference (overrides places parameter)`
@param equal_nan `allows comparing nan values, if left and right both nans result is ok`
@require places > 0, places <= 20 "too many decimal places"
@require delta >= 0, delta <= 1 "delta must be a small number"
@require runtime::test_context != null "Only allowed in @test functions"
*>
fn void is_almost_equal(double left, double right, uint places = 7, double delta = 0, bool equal_nan = true)
{
double diff = left - right;
double eps = delta;
if (eps == 0) eps = 1.0 / math::pow(10.0, places);
if (!math::is_approx(left, right, eps))
{
if (equal_nan && math::is_nan(left) && math::is_nan(right)) return;
print_panicf("Not almost equal: `%s` !~~ `%s` delta=%e diff: %e", left, right, eps, diff);
}
}
<*
Check if left != right
@param left `left argument of any comparable type`
@param right `right argument of any comparable type`
@require runtime::test_context != null "Only allowed in @test functions"
*>
macro void is_not_equal(left, right)
{
if (equals(left, right))
{
print_panicf("`%s` == `%s`", left, right);
}
}
<*
Check if left > right
@param left `left argument of any comparable type`
@param right `right argument of any comparable type`
@require runtime::test_context != null "Only allowed in @test functions"
*>
macro is_greater(left, right)
{
if (!builtin::greater(left, right))
{
print_panicf("`%s` <= `%s`", left, right);
}
}
<*
Check if left >= right
@param left `left argument of any comparable type`
@param right `right argument of any comparable type`
@require runtime::test_context != null "Only allowed in @test functions"
*>
macro is_greater_equal(left, right)
{
if (!builtin::greater_eq(left, right))
{
print_panicf("`%s` < `%s`", left, right);
}
}
<*
Check if left < right
@param left `left argument of any comparable type`
@param right `right argument of any comparable type`
@require runtime::test_context != null "Only allowed in @test functions"
*>
macro is_less(left, right)
{
if (!builtin::less(left, right))
{
print_panicf("`%s` >= `%s`", left, right);
}
}
<*
Check if left <= right
@param left `left argument of any comparable type`
@param right `right argument of any comparable type`
@require runtime::test_context != null "Only allowed in @test functions"
*>
macro is_less_equal(left, right)
{
if (!builtin::less_eq(left, right))
{
print_panicf("`%s` > `%s`", left, right);
}
}
macro void print_panicf(format, ...) @local
{
runtime::test_context.assert_print_backtrace = false;
builtin::panicf(format, $$FILE, $$FUNC, $$LINE, $vasplat);
}

View File

@@ -130,6 +130,15 @@ fn usz! File.write(&self, char[] buffer) @dynamic
return os::native_fwrite(self.file, buffer);
}
fn Fd File.fd(self) @if(env::LIBC)
{
return libc::fileno(self.file);
}
fn bool File.isatty(self) @if(env::LIBC)
{
return libc::isatty(self.fd()) > 0;
}
fn char! File.read_byte(&self) @dynamic
{

View File

@@ -227,7 +227,14 @@ fn void! out_putstream_fn(void* data, char c) @private
fn void! out_putchar_fn(void* data @unused, char c) @private
{
libc::putchar(c);
$if env::TESTING:
// HACK: this is used for the purpose of unit test output hijacking
File* stdout = io::stdout();
assert(stdout.file);
libc::fputc(c, stdout.file);
$else
libc::putchar(c);
$endif
}
<*
@@ -263,7 +270,7 @@ fn usz! printfn(String format, args...) @maydiscard
Formatter formatter;
formatter.init(&out_putchar_fn);
usz! len = formatter.vprintf(format, args);
putchar('\n');
out_putchar_fn(null, '\n')!;
io::stdout().flush()!;
return len + 1;
}

View File

@@ -172,6 +172,7 @@ extern fn usz strxfrm(char* dest, ZString src, usz n);
extern fn CInt system(ZString str);
extern fn Time_t timegm(Tm *timeptr) @if(!env::WIN32);
extern fn ZString tmpnam(ZString str);
extern fn CFile tmpfile();
extern fn CInt ungetc(CInt c, CFile stream);
extern fn CInt unsetenv(ZString name);
extern fn isz write(Fd fd, void* buffer, usz count) @if(!env::WIN32);

View File

@@ -41,6 +41,8 @@
- Added '%h' and '%H' for printing out binary data in hexadecimal using the formatter.
- Added weakly linked `__powidf2`
- Added channels for threads.
- New `std::core::test` module for unit testing machinery.
- New unit test default runner.
## 0.6.6 Change list

View File

@@ -157,6 +157,13 @@ typedef enum
VALIDATION_OBNOXIOUS = 2,
} ValidationLevel;
typedef enum
{
ANSI_DETECT = -1,
ANSI_OFF = 0,
ANSI_ON = 1
} Ansi;
typedef enum
{
SINGLE_MODULE_NOT_SET = -1,
@@ -484,6 +491,7 @@ typedef struct BuildOptions_
int build_threads;
const char **libraries_to_fetch;
const char **files;
const char *test_filter;
const char **args;
const char **feature_names;
const char **removed_feature_names;
@@ -495,6 +503,9 @@ typedef struct BuildOptions_
const char *template;
LinkerType linker_type;
ValidationLevel validation_level;
Ansi ansi;
bool test_breakpoint;
bool test_nosort;
const char *custom_linker_path;
uint32_t symtab_size;
unsigned version;

View File

@@ -111,6 +111,11 @@ static const char *validation_levels[3] = {
[VALIDATION_OBNOXIOUS] = "obnoxious",
};
static const char *ansi_use[2] = {
[ANSI_OFF] = "off",
[ANSI_ON] = "on",
};
static const char *backtrace_levels[2] = {
[SHOW_BACKTRACE_OFF] = "off",
[SHOW_BACKTRACE_ON] = "on",

View File

@@ -133,6 +133,14 @@ static void usage(bool full)
PRINTF(" -g - Emit debug info.");
PRINTF(" -g0 - Emit no debug info.");
PRINTF("");
if (full)
{
PRINTF(" --ansi=<yes|no> - Set colour output using ansi on/off, default is to try to detect it.");
PRINTF(" --test-filter <arg> - Set a filter when running tests, running only matching tests.");
PRINTF(" --test-breakpoint - When running tests, trigger a breakpoint on failure.");
PRINTF(" --test-disable-sort - Do not sort tests.");
}
PRINTF("");
PRINTF(" -l <library> - Link with the library provided.");
PRINTF(" -L <library dir> - Append the directory to the linker search paths.");
PRINTF(" -z <argument> - Send the <argument> as a parameter to the linker.");
@@ -711,6 +719,27 @@ static void parse_option(BuildOptions *options)
options->validation_level = (ValidationLevel)parse_multi_option(argopt, 3, validation_levels);
return;
}
if ((argopt = match_argopt("ansi")))
{
options->ansi = (Ansi)parse_multi_option(argopt, 2, ansi_use);
return;
}
if (match_longopt("test-filter"))
{
if (at_end() || next_is_opt()) error_exit("error: --test-filter needs an argument.");
options->test_filter = next_arg();
return;
}
if (match_longopt("test-breakpoint"))
{
options->test_breakpoint = true;
return;
}
if (match_longopt("test-nosort"))
{
options->test_nosort = true;
return;
}
if (match_longopt("max-mem"))
{
if (at_end() || next_is_opt()) error_exit("error: --max-mem needs a valid integer.");
@@ -1271,6 +1300,7 @@ BuildOptions parse_arguments(int argc, const char *argv[])
.use_stdlib = USE_STDLIB_NOT_SET,
.linker_type = LINKER_TYPE_NOT_SET,
.validation_level = VALIDATION_NOT_SET,
.ansi = ANSI_DETECT,
.strip_unused = STRIP_UNUSED_NOT_SET,
.single_module = SINGLE_MODULE_NOT_SET,
.sanitize_mode = SANITIZE_NOT_SET,
@@ -1491,7 +1521,7 @@ static int parse_multi_option(const char *start, unsigned count, const char **el
{
const char *arg = current_arg;
int select = str_findlist(start, count, elements);
if (select < 0) error_exit("error: %.*s invalid option '%s' given.", (int)(start - arg), start, arg);
if (select < 0) error_exit("error: '%.*s' invalid option '%s' given.", (int)(start - arg), start, arg);
return select;
}

View File

@@ -273,13 +273,29 @@ static void update_build_target_from_options(BuildTarget *target, BuildOptions *
case COMMAND_BENCHMARK:
target->run_after_compile = true;
target->type = TARGET_TYPE_BENCHMARK;
target->args = options->args;
break;
case COMMAND_COMPILE_TEST:
case COMMAND_TEST:
target->run_after_compile = true;
target->type = TARGET_TYPE_TEST;
target->args = options->args;
switch (options->ansi)
{
case ANSI_ON:
vec_add(target->args, "useansi");
break;
case ANSI_OFF:
vec_add(target->args, "noansi");
break;
default:
break;
}
if (options->test_filter)
{
vec_add(target->args, "filter");
vec_add(target->args, options->test_filter);
}
if (options->test_breakpoint) vec_add(target->args, "breakpoint");
if (options->test_nosort) vec_add(target->args, "nosort");
break;
case COMMAND_RUN:
case COMMAND_COMPILE_RUN:

View File

@@ -718,8 +718,9 @@ void compiler_compile(void)
}
name = scratch_buffer_to_string();
OUTF("Launching %s", name);
for (uint32_t i = 0; i < vec_size(compiler.build.args); ++i) {
OUTF(" %s", compiler.build.args[i]);
FOREACH(const char *, arg, compiler.build.args)
{
OUTF(" %s", arg);
}
OUTN("");

View File

@@ -12,7 +12,7 @@ fn void slice_bytes()
fn void slice_string()
{
String $a = "abcd";
assert($a == "abcd");
test::is_equal($a, "abcd");
var $c = $a[1:0];
String c = $c;
assert($c == "");

View File

@@ -0,0 +1,327 @@
module test::std::core::test @test;
import std::core::runtime @public;
import std::core::builtin;
import std::io;
struct TestState
{
int n_runs;
int n_fails;
bool expected_fail;
// NOTE: we must wrap setup/teardown functions to hide them from module @test runner
TestFn setup_fn;
TestFn teardown_fn;
PanicFn old_panic; // original test panic, use it when it's really fails
PanicFn panic_mock_fn; // mock panic, for testing the test:: failed
}
TestState state =
{
.setup_fn = fn void()
{
state.n_runs++;
state.n_fails = 0;
assert (runtime::test_context.assert_print_backtrace);
assert (builtin::panic != state.panic_mock_fn, "missing finalization of panic");
state.old_panic = builtin::panic;
builtin::panic = state.panic_mock_fn;
},
.teardown_fn = fn void()
{
builtin::panic = state.old_panic;
assert(state.n_runs > 0);
if (state.expected_fail)
{
assert(state.n_fails > 0, "test case expected to fail, but it's not");
}
state.n_fails = 0;
state.expected_fail = false;
state.n_runs = 0;
},
.panic_mock_fn = fn void (String message, String file, String function, uint line)
{
if (runtime::test_context.is_in_panic) return;
if (runtime::test_context.assert_print_backtrace)
{
$if env::NATIVE_STACKTRACE:
builtin::print_backtrace(message, 0);
$else
io::printfn("No print_backtrace() supported by this platform");
$endif
}
runtime::test_context.assert_print_backtrace = true;
if (state.expected_fail)
{
state.n_fails++;
}
else
{
builtin::panic = state.old_panic;
state.old_panic(message, file, function, line);
}
runtime::test_context.is_in_panic = false;
}
};
fn void test_eq()
{
test::is_equal(1, 1);
test::is_equal(true, true);
test::is_equal(1.31, 1.31);
test::is_equal("foo", "foo");
}
fn void test_almost_equal()
{
test::is_almost_equal(1, 1);
test::is_almost_equal(1.31, 1.31);
test::is_almost_equal(1.31f, 1.31f);
test::is_almost_equal(double.nan, double.nan);
test::is_almost_equal(float.nan, float.nan);
test::is_almost_equal(1.31, 1.31, delta: 0.01);
test::is_almost_equal(1.311, 1.312, delta: 0.01);
test::is_almost_equal(1.311, 1.312, places: 2);
// 7 decimal places are default
test::is_almost_equal(1.00000001, 1.00000000);
}
fn void test_almost_equal_fails()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
// 7 decimal places are default
test::is_almost_equal(1.0000001, 1.00000000);
}
fn void test_almost_equal_fails_nan()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::is_almost_equal(1.0000001, double.nan);
}
fn void test_almost_equal_fails_nan2()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::is_almost_equal(double.nan, 1);
}
fn void test_almost_equal_fails_equal_nan_false()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::is_almost_equal(double.nan, double.nan, equal_nan: false);
}
fn void setup_teardown()
{
state.n_runs = 0; // just in case of previous test failed
test::setup(state.setup_fn, state.teardown_fn);
test::is_equal(state.n_runs, 1);
test::is_equal(state.n_fails, 0);
test::is_equal(state.expected_fail, false);
}
fn void setup_no_teardown()
{
test::setup(state.setup_fn);
test::is_equal(state.n_runs, 1);
test::is_equal(state.n_fails, 0);
test::is_equal(state.expected_fail, false);
// WARNING: reverting back original panic func
builtin::panic = state.old_panic;
}
fn void expected_fail()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::is_equal(state.n_fails, 0);
test::is_equal(2, 1); // this fails, and we test it
test::is_equal(state.n_fails, 1);
}
fn void test_neq()
{
test::is_not_equal(2, 1);
test::is_not_equal(false, true);
test::is_not_equal(1.32, 1.31);
test::is_not_equal("foo", "bar");
}
fn void test_neq_fails()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::is_not_equal(1, 1);
}
fn void test_gt()
{
test::is_greater(2, 1);
test::is_greater(true, false);
test::is_greater(1.32, 1.31);
}
fn void test_gt_fails_when_equal()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::is_greater(2, 2);
}
fn void test_gt_fails_when_less()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::is_greater(1, 2);
}
fn void test_gte()
{
test::is_greater_equal(2, 1);
test::is_greater_equal(true, false);
test::is_greater_equal(1.32, 1.31);
test::is_greater_equal(2, 2);
test::is_greater_equal(true, true);
test::is_greater_equal(1.32, 1.32);
}
fn void test_gte_fails_when_less()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::is_greater_equal(1, 2);
}
fn void test_lt()
{
test::is_less(1, 2);
test::is_less(false, true);
test::is_less(1.31, 1.32);
}
fn void test_lt_fails_when_equal()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::is_less(2, 2);
}
fn void test_lt_fails_when_greater()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::is_less(2, 1);
}
fn void test_lte()
{
test::is_less_equal(1, 2);
test::is_less_equal(false, true);
test::is_less_equal(1.31, 1.32);
test::is_less_equal(2, 2);
test::is_less_equal(true, true);
test::is_less_equal(1.32, 1.32);
}
fn void test_lte_fails_when_greater()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::is_less_equal(2, 1);
}
fn void test_check(){
test::@check(1 == 1);
test::@check(1.2 == 1.2, "1 == 1");
test::@check(true == true, "1 == 1");
test::@check("foo" == "foo", "2 == %d", 1 );
}
fn void test_check_fails()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::@check(2 == 1, "2 == %d", 1 );
}
fn void test_check_fails_no_info()
{
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::@check(2 == 1);
}
def TestIntFn = fn int! (int a, int b);
def TestFailFn = fn void! (bool to_fail);
fault MyFault
{
FOO,
}
fn void test_error()
{
TestFailFn ffail_void = fn void!(bool to_fail)
{
if (to_fail) return IoError.FILE_NOT_FOUND?;
};
TestIntFn ffail_int = fn int! (int a, int b)
{
if (b == 0) return IoError.FILE_NOT_FOUND?;
return a / b;
};
test::setup(state.setup_fn, state.teardown_fn);
test::@error(ffail_void(true), IoError.FILE_NOT_FOUND);
test::@error(ffail_int(1, 0), IoError.FILE_NOT_FOUND);
}
fn void test_error_not_raised()
{
TestIntFn ffail_int = fn int! (int a, int b) {
if (b == 0) return IoError.FILE_NOT_FOUND?;
return a / b;
};
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::@error(ffail_int(1, 1), IoError.FILE_NOT_FOUND);
}
fn void test_error_wrong_error_expected()
{
TestIntFn ffail_int = fn int! (int a, int b) {
if (b == 0) return IoError.BUSY?;
return a / b;
};
test::setup(state.setup_fn, state.teardown_fn);
state.expected_fail = true;
test::@error(ffail_int(1, 0), IoError.FILE_NOT_FOUND);
}
fn void test_std_out_hijack()
{
io::print("print: aldsjalsdjlasjdlja\n");
io::printf("printf: aldsjalsdjlasjdlja\n");
io::eprint("eprint: aldsjalsdjlasjdlja\n");
io::eprintfn("eprintfn: aldsjalsdjlasjdlja\n");
io::fprint(io::stdout(), "fprint: stdout aldsjalsdjlasjdlja\n")!!;
io::fprint(io::stderr(), "fprint: stderr aldsjalsdjlasjdlja\n")!!;
io::fprintf(io::stderr(), "fprintf: stderr aldsjalsdjlasjdlja\n")!!;
io::fprintf(io::stderr(), "fprintfn: stderr aldsjalsdjlasjdlja\n")!!;
test::is_equal(true, true);
}