mirror of
https://github.com/c3lang/c3c.git
synced 2026-02-27 12:01:16 +00:00
New test runner
This commit is contained in:
committed by
Christoffer Lerno
parent
6848753a10
commit
3c50376175
@@ -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
223
lib/std/core/test.c3
Normal 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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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("");
|
||||
|
||||
|
||||
@@ -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 == "");
|
||||
|
||||
327
test/unit/stdlib/core/test_test.c3
Normal file
327
test/unit/stdlib/core/test_test.c3
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user