added io::stdout().flush() - to force printing test name before possible deadlock

mem::scoped() and long jump resilience fixed #1963
fixed --test-nosort argument + extra test for teardown_fn memory leak
Some renaming. Simplify robust test allocator handling. Pop temp allocators in test runner.
`Thread` no longer allocates memory on posix.
Update unprintable struct output.
Correctly give an error if a character literal contains a line break.
This commit is contained in:
Alex Veden
2025-02-12 12:02:11 +04:00
committed by Christoffer Lerno
parent 535151a2a5
commit 5046608d1f
12 changed files with 109 additions and 107 deletions

View File

@@ -3,6 +3,7 @@
// a copy of which can be found in the LICENSE_STDLIB file.
module std::core::runtime;
import std::core::test @public;
import std::core::mem::allocator @public;
import libc, std::time, std::io, std::sort;
import std::os::env;
@@ -30,8 +31,12 @@ struct TestContext
char* error_buffer;
usz error_buffer_capacity;
File fake_stdout;
File orig_stdout;
File orig_stderr;
struct stored
{
File stdout;
File stderr;
Allocator allocator;
}
}
struct TestUnit
@@ -107,43 +112,29 @@ fn void test_panic(String message, String file, String function, uint line) @loc
}
test_context.is_in_panic = false;
allocator::thread_allocator = test_context.stored.allocator;
libc::longjmp(&test_context.buf, 1);
}
fn void mute_output() @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();
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 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);
if (!test_context.fake_stdout.file) return;
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;
*stderr = test_context.stored.stderr;
*stdout = test_context.stored.stdout;
usz log_size = test_context.fake_stdout.seek(0, Seek.CURSOR)!!;
if (has_error)
@@ -187,6 +178,9 @@ fn bool run_tests(String[] args, TestUnit[] tests) @private
.breakpoint_on_assert = false,
.test_filter = "",
.has_ansi_codes = terminal_has_ansi_codes(),
.stored.allocator = allocator::heap(),
.stored.stderr = *io::stderr(),
.stored.stdout = *io::stdout(),
};
for (int i = 1; i < args.len; i++)
{
@@ -221,9 +215,9 @@ fn bool run_tests(String[] args, TestUnit[] tests) @private
// Buffer for hijacking the output
$if (!env::NO_LIBC):
test_context.fake_stdout.file = libc::tmpfile();
context.fake_stdout.file = libc::tmpfile();
$endif
if (test_context.fake_stdout.file == null)
if (context.fake_stdout.file == null)
{
io::print("Failed to hijack stdout, tests will print everything");
}
@@ -241,29 +235,33 @@ fn bool run_tests(String[] args, TestUnit[] tests) @private
name.append_repeat('-', len - len / 2);
io::printn(name);
name.clear();
TempState temp_state = mem::temp_push();
defer mem::temp_pop(temp_state);
foreach(unit : tests)
{
if (test_context.test_filter && !unit.name.contains(test_context.test_filter))
mem::temp_pop(temp_state);
if (context.test_filter && !unit.name.contains(context.test_filter))
{
tests_skipped++;
continue;
}
test_context.setup_fn = null;
test_context.teardown_fn = null;
test_context.current_test_name = unit.name;
context.setup_fn = null;
context.teardown_fn = null;
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();
TrackingAllocator mem;
mem.init(allocator::heap());
mem.init(context.stored.allocator);
if (libc::setjmp(&context.buf) == 0)
{
mute_output();
mem.clear();
mem::@scoped(&mem)
{
allocator::thread_allocator = &mem;
$if(!$$OLD_TEST):
unit.func();
$else
@@ -273,7 +271,13 @@ fn bool run_tests(String[] args, TestUnit[] tests) @private
continue;
}
$endif
};
// track cleanup that may take place in teardown_fn
if (test_context.teardown_fn)
{
test_context.teardown_fn();
}
allocator::thread_allocator = context.stored.allocator;
unmute_output(false); // all good, discard output
if (mem.has_leaks())
{
@@ -286,10 +290,6 @@ fn bool run_tests(String[] args, TestUnit[] tests) @private
io::printfn(test_context.has_ansi_codes ? "[\e[0;32mPASS\e[0m]" : "[PASS]");
tests_passed++;
}
if (test_context.teardown_fn)
{
test_context.teardown_fn();
}
}
mem.free();
}

View File

@@ -145,7 +145,10 @@ fn usz! Formatter.print_with_function(&self, Printable arg)
return SearchResult.MISSING?;
}
fn usz! Formatter.out_unknown(&self, String category, any arg) @private
{
return self.out_substr("[") + self.out_substr(category) + self.out_substr(" type:") + self.ntoa((iptr)arg.type, false, 16) + self.out_substr(", addr:") + self.ntoa((iptr)arg.ptr, false, 16) + self.out_substr("]");
}
fn usz! Formatter.out_str(&self, any arg) @private
{
switch (arg.type.kindof)
@@ -199,13 +202,13 @@ fn usz! Formatter.out_str(&self, any arg) @private
assert(i < arg.type.names.len, "Illegal enum value found, numerical value was %d.", i);
return self.out_substr(arg.type.names[i]);
case STRUCT:
return self.out_substr("<struct>");
return self.out_unknown("struct", arg);
case UNION:
return self.out_substr("<union>");
return self.out_unknown("union", arg);
case BITSTRUCT:
return self.out_substr("<bitstruct>");
return self.out_unknown("bitstruct", arg);
case FUNC:
return self.out_substr("<function>");
return self.out_unknown("function", arg);
case DISTINCT:
if (arg.type == String.typeid)
{

View File

@@ -8,7 +8,14 @@ struct NativeMutex
}
def NativeConditionVariable = Pthread_cond_t;
def NativeThread = Pthread_t;
struct NativeThread
{
inline Pthread_t pthread;
ThreadFn thread_fn;
void* arg;
}
def NativeOnceFlag = Pthread_once_t;
<*
@@ -148,59 +155,49 @@ fn void! NativeConditionVariable.wait_timeout(&cond, NativeMutex* mtx, ulong ms)
}
}
tlocal PosixThreadData *_thread_data @private;
tlocal NativeThread current_thread @private;
fn void free_thread_data() @private
{
if (_thread_data)
{
allocator::free(_thread_data.allocator, _thread_data);
_thread_data = null;
}
}
fn void* callback(void* arg) @private
{
_thread_data = arg;
defer free_thread_data();
return (void*)(iptr)_thread_data.thread_fn(_thread_data.arg);
NativeThread* thread = arg;
current_thread = *thread;
return (void*)(iptr)thread.thread_fn(thread.arg);
}
fn void! NativeThread.create(&thread, ThreadFn thread_fn, void* arg)
{
PosixThreadData *thread_data = mem::new(PosixThreadData, { .thread_fn = thread_fn, .arg = arg, .allocator = allocator::heap() });
if (posix::pthread_create(thread, null, &callback, thread_data) != 0)
thread.thread_fn = thread_fn;
thread.arg = arg;
if (posix::pthread_create(&thread.pthread, null, &callback, thread) != 0)
{
*thread = null;
free(thread_data);
return ThreadFault.INIT_FAILED?;
}
}
fn void! NativeThread.detach(thread)
{
if (posix::pthread_detach(thread)) return ThreadFault.DETACH_FAILED?;
if (posix::pthread_detach(thread.pthread)) return ThreadFault.DETACH_FAILED?;
}
fn void native_thread_exit(int result)
{
free_thread_data();
posix::pthread_exit((void*)(iptr)result);
}
fn NativeThread native_thread_current()
{
return (NativeThread)posix::pthread_self();
return current_thread;
}
fn bool NativeThread.equals(thread, NativeThread other)
{
return (bool)posix::pthread_equal(thread, other);
return (bool)posix::pthread_equal(thread.pthread, other.pthread);
}
fn int! NativeThread.join(thread)
{
void *pres;
if (posix::pthread_join(thread, &pres)) return ThreadFault.JOIN_FAILED?;
if (posix::pthread_join(thread.pthread, &pres)) return ThreadFault.JOIN_FAILED?;
return (int)(iptr)pres;
}
@@ -214,13 +211,6 @@ fn void native_thread_yield()
posix::sched_yield();
}
struct PosixThreadData @private
{
ThreadFn thread_fn;
void* arg;
Allocator allocator;
}
fn void! native_sleep_nano(NanoDuration nano)
{
if (nano <= 0) return;

View File

@@ -13,7 +13,7 @@ distinct TimedMutex = inline Mutex;
distinct RecursiveMutex = inline Mutex;
distinct TimedRecursiveMutex = inline Mutex;
distinct ConditionVariable = NativeConditionVariable;
distinct Thread = NativeThread;
distinct Thread = inline NativeThread;
distinct OnceFlag = NativeOnceFlag;
def OnceFn = fn void();
@@ -59,11 +59,10 @@ macro void! ConditionVariable.wait_timeout(&cond, Mutex* mutex, ulong ms)
return NativeConditionVariable.wait_timeout((NativeConditionVariable*)cond, (NativeMutex*)mutex, ms);
}
macro void! Thread.create(&thread, ThreadFn thread_fn, void* arg) => NativeThread.create((NativeThread*)thread, thread_fn, arg);
macro void! Thread.detach(thread) => NativeThread.detach((NativeThread)thread);
macro int! Thread.join(thread) => NativeThread.join((NativeThread)thread);
macro bool Thread.equals(thread, Thread other) => NativeThread.equals((NativeThread)thread, (NativeThread)other);
macro void! Thread.create(&thread, ThreadFn thread_fn, void* arg) => NativeThread.create(thread, thread_fn, arg);
macro void! Thread.detach(thread) => NativeThread.detach(thread);
macro int! Thread.join(thread) => NativeThread.join(thread);
macro bool Thread.equals(thread, Thread other) => NativeThread.equals(thread, other);
macro void OnceFlag.call(&flag, OnceFn func) => NativeOnceFlag.call_once((NativeOnceFlag*)flag, func);

View File

@@ -6,9 +6,14 @@
- Increase precedence of `(Foo) { 1, 2 }`
- Add `--enable-new-generics` to enable `Foo{int}` generic syntax.
- `{| |}` expression blocks deprecated.
- c3c `--test-leak-report` flag for displaying full memory lead report if any
### Fixes
- Bug appearing when `??` was combined with boolean in some cases.
- Test runner --test-disable-sort didn't work, c3c was expecting --test-nosort
- Test runner with tracking allocator assertion at failed test #1963
- Test runner with tracking allocator didn't properly handle teardown_fn
- Correctly give an error if a character literal contains a line break.
### Stdlib changes
@@ -34,6 +39,7 @@
- Test runner will also check for leaks.
- Improve inference on `??` #1943.
- Detect unaligned loads #1951.
- `Thread` no longer allocates memory on posix.
### Fixes
- Fix issue requiring prefix on a generic interface declaration.

View File

@@ -135,7 +135,7 @@ static void usage(bool full)
print_opt("--ansi=<yes|no>", "Set colour output using ansi on/off, default is to try to detect it.");
print_opt("--test-filter <arg>", "Set a filter when running tests, running only matching tests.");
print_opt("--test-breakpoint", "When running tests, trigger a breakpoint on failure.");
print_opt("--test-disable-sort", "Do not sort tests.");
print_opt("--test-nosort", "Do not sort tests.");
}
PRINTF("");
print_opt("-l <library>", "Link with the static or dynamic library provided.");

View File

@@ -5,7 +5,7 @@ import std::atomic;
uint a;
float fa;
fn void add() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void add() @test
{
Thread[100] ts;
a = 0;
@@ -42,7 +42,7 @@ fn void add() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
assert(a == ts.len * 10 * 5, "Threads returned %d, expected %d", a, ts.len * 10 * 5);
}
fn void sub() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void sub() @test
{
Thread[100] ts;
a = ts.len * 10 * 5;
@@ -79,7 +79,7 @@ fn void sub() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
assert(a == 0, "Threads returned %d, expected %d", a, 0);
}
fn void div() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void div() @test
{
Thread[8] ts;
a = 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8;
@@ -98,7 +98,7 @@ fn void div() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
assert(a == 8, "Threads returned %d, expected %d", a, 8);
}
fn void max() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void max() @test
{
Thread[100] ts;
a = 0;
@@ -134,7 +134,7 @@ fn void max() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
assert(a == 5, "Threads returned %d, expected %d", a, 5);
}
fn void min() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void min() @test
{
Thread[100] ts;
a = 10;
@@ -170,7 +170,7 @@ fn void min() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
assert(a == 0, "Threads returned %d, expected %d", a, 0);
}
fn void fadd() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void fadd() @test
{
Thread[100] ts;
fa = 0;
@@ -207,7 +207,7 @@ fn void fadd() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
assert(fa == ts.len * 10 * 0.5, "Threads returned %f, expected %f", fa, ts.len * 10 * 0.5);
}
fn void fsub() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void fsub() @test
{
Thread[100] ts;
fa = ts.len * 10 * 0.5;

View File

@@ -7,7 +7,7 @@ def AtomicFloat = Atomic(<float>);
AtomicUint a;
AtomicFloat fa;
fn void add() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void add() @test
{
Thread[100] ts;
a.store(0);
@@ -44,7 +44,7 @@ fn void add() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
assert(a.load() == ts.len * 10 * 5, "Threads returned %d, expected %d", a.load(), ts.len * 10 * 5);
}
fn void sub() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void sub() @test
{
Thread[100] ts;
a.store(ts.len * 10 * 5);
@@ -81,7 +81,7 @@ fn void sub() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
assert(a.load() == 0, "Threads returned %d, expected %d", a.load(), 0);
}
fn void fadd() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void fadd() @test
{
Thread[100] ts;
fa.store(0);
@@ -118,7 +118,7 @@ fn void fadd() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
assert(fa.load() == ts.len * 10 * 0.5, "Threads returned %f, expected %f", fa.load(), ts.len * 10 * 0.5);
}
fn void fsub() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void fsub() @test
{
Thread[100] ts;
fa.store(ts.len * 10 * 0.5);

View File

@@ -14,6 +14,7 @@ struct TestState
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
void* buf;
}
TestState state =
@@ -25,6 +26,7 @@ TestState state =
assert (runtime::test_context.assert_print_backtrace);
assert (builtin::panic != state.panic_mock_fn, "missing finalization of panic");
state.buf = mem::alloc(int);
state.old_panic = builtin::panic;
builtin::panic = state.panic_mock_fn;
@@ -42,6 +44,7 @@ TestState state =
state.n_fails = 0;
state.expected_fail = false;
state.n_runs = 0;
mem::free(state.buf);
},
.panic_mock_fn = fn void (String message, String file, String function, uint line)
{
@@ -139,6 +142,8 @@ fn void setup_no_teardown()
test::eq(state.n_fails, 0);
test::eq(state.expected_fail, false);
mem::free(state.buf);
// WARNING: reverting back original panic func
builtin::panic = state.old_panic;
}

View File

@@ -5,7 +5,6 @@ import std::os;
const TEST_MAGNITUDE = 10;
fn void lock_control_test() @test
{
Mutex m;
@@ -37,7 +36,7 @@ fn void! own_mutex(Mutex* m)
m.unlock()!;
}
fn void ensure_owner_checks() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void ensure_owner_checks() @test
{
Mutex m;
m.init()!!;
@@ -77,7 +76,7 @@ fn void shared_mutex_decrement(ArgsWrapper1* args)
args.m.unlock()!!;
}
fn void shared_mutex() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void shared_mutex() @test
{
Mutex m;
m.init()!!;
@@ -127,7 +126,7 @@ fn void acquire_recursively(RecursiveMutex* m)
}
}
fn void test_recursive_mutex() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void test_recursive_mutex() @test
{
RecursiveMutex m;
m.init()!!;

View File

@@ -5,7 +5,7 @@ import std::thread::pool;
def Pool = ThreadPool(<4>);
fn void init_destroy() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void init_destroy() @test
{
for (usz i = 0; i < 20; i++)
{
@@ -15,7 +15,7 @@ fn void init_destroy() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
}
}
fn void push_destroy() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void push_destroy() @test
{
for FOO: (usz i = 0; i < 20; i++)
{
@@ -42,7 +42,7 @@ fn void push_destroy() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
}
}
fn void push_stop() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void push_stop() @test
{
for (usz i = 0; i < 20; i++)
{

View File

@@ -3,7 +3,7 @@ import std::io;
int a;
fn void testrun() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void testrun() @test
{
Thread t;
a = 0;
@@ -18,7 +18,7 @@ fn void testrun() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
Mutex m_global;
fn void testrun_mutex() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void testrun_mutex()
{
Thread[20] ts;
a = 0;
@@ -48,7 +48,7 @@ fn void testrun_mutex() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
m_global.destroy()!!;
}
fn void testrun_mutex_try() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void testrun_mutex_try() @test
{
Mutex m;
m.init()!!;
@@ -59,7 +59,7 @@ fn void testrun_mutex_try() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
m.unlock()!!;
}
fn void testrun_mutex_timeout() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void testrun_mutex_timeout() @test
{
TimedMutex m;
m.init()!!;
@@ -80,7 +80,7 @@ fn void call_once()
x_once += 100;
}
fn void testrun_once() @test => mem::@scoped(&allocator::LIBC_ALLOCATOR)
fn void testrun_once() @test
{
OnceFlag once;
once.call(&call_once);