diff --git a/lib/std/core/runtime_test.c3 b/lib/std/core/runtime_test.c3 index 9bb110232..123fa4552 100644 --- a/lib/std/core/runtime_test.c3 +++ b/lib/std/core/runtime_test.c3 @@ -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; - + File* stderr = io::stderr(); *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,39 +235,49 @@ 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 + if (catch err = unit.func()) + { + io::printf("[FAIL] Failed due to: %s", err); + continue; + } + $endif + // track cleanup that may take place in teardown_fn + if (test_context.teardown_fn) { - $if(!$$OLD_TEST): - unit.func(); - $else - if (catch err = unit.func()) - { - io::printf("[FAIL] Failed due to: %s", err); - continue; - } - $endif - }; + 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(); } diff --git a/lib/std/io/formatter.c3 b/lib/std/io/formatter.c3 index 7c6df8eac..85f499545 100644 --- a/lib/std/io/formatter.c3 +++ b/lib/std/io/formatter.c3 @@ -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(""); + return self.out_unknown("struct", arg); case UNION: - return self.out_substr(""); + return self.out_unknown("union", arg); case BITSTRUCT: - return self.out_substr(""); + return self.out_unknown("bitstruct", arg); case FUNC: - return self.out_substr(""); + return self.out_unknown("function", arg); case DISTINCT: if (arg.type == String.typeid) { diff --git a/lib/std/threads/os/thread_posix.c3 b/lib/std/threads/os/thread_posix.c3 index a6b9e6e2b..034498058 100644 --- a/lib/std/threads/os/thread_posix.c3 +++ b/lib/std/threads/os/thread_posix.c3 @@ -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; diff --git a/lib/std/threads/thread.c3 b/lib/std/threads/thread.c3 index d6f208839..172087ec5 100644 --- a/lib/std/threads/thread.c3 +++ b/lib/std/threads/thread.c3 @@ -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); diff --git a/releasenotes.md b/releasenotes.md index 8f1964b78..9d9aa5f2e 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -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. diff --git a/src/build/build_options.c b/src/build/build_options.c index df1b11382..92736e386 100644 --- a/src/build/build_options.c +++ b/src/build/build_options.c @@ -135,7 +135,7 @@ static void usage(bool full) print_opt("--ansi=", "Set colour output using ansi on/off, default is to try to detect it."); print_opt("--test-filter ", "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 ", "Link with the static or dynamic library provided."); diff --git a/test/unit/stdlib/atomic.c3 b/test/unit/stdlib/atomic.c3 index 3842ebd33..b7ba45bb1 100644 --- a/test/unit/stdlib/atomic.c3 +++ b/test/unit/stdlib/atomic.c3 @@ -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; diff --git a/test/unit/stdlib/atomic_types.c3 b/test/unit/stdlib/atomic_types.c3 index 0cd721855..8c9c434c5 100644 --- a/test/unit/stdlib/atomic_types.c3 +++ b/test/unit/stdlib/atomic_types.c3 @@ -7,7 +7,7 @@ def AtomicFloat = Atomic(); 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); diff --git a/test/unit/stdlib/core/test_test.c3 b/test/unit/stdlib/core/test_test.c3 index 36f44724b..afb584fe1 100644 --- a/test/unit/stdlib/core/test_test.c3 +++ b/test/unit/stdlib/core/test_test.c3 @@ -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; } diff --git a/test/unit/stdlib/threads/mutex.c3 b/test/unit/stdlib/threads/mutex.c3 index ca29ea947..8e47b32ea 100644 --- a/test/unit/stdlib/threads/mutex.c3 +++ b/test/unit/stdlib/threads/mutex.c3 @@ -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()!!; diff --git a/test/unit/stdlib/threads/pool.c3 b/test/unit/stdlib/threads/pool.c3 index 80b06f939..decd54044 100644 --- a/test/unit/stdlib/threads/pool.c3 +++ b/test/unit/stdlib/threads/pool.c3 @@ -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++) { diff --git a/test/unit/stdlib/threads/simple_thread.c3 b/test/unit/stdlib/threads/simple_thread.c3 index b391281bc..211599209 100644 --- a/test/unit/stdlib/threads/simple_thread.c3 +++ b/test/unit/stdlib/threads/simple_thread.c3 @@ -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);