mirror of
https://github.com/odin-lang/Odin.git
synced 2026-06-09 20:08:11 +00:00
I was encountering bounds-check error messages being overwritten during a test, if the test failed for another reason and sent a log message. The original intent of having this check inside of the above `if` block was that if a test sent an error message, then it was assumed an overwrite would be safe, but it's completely possible for a test to fail for a legitimate reason, then do an unrelated bounds check somewhere else that would be buried under the animation. This change will make sure that, no matter what, the progress display will not trigger a clear if a signal was raised. There's still no guarantee that bounds-check messages will be printed properly, and it's best to redirect STDERR. The only way that can be fixed is if they get a similar hook to `context.assertion_failure_proc`.
819 lines
27 KiB
Odin
819 lines
27 KiB
Odin
//+private
|
|
package testing
|
|
|
|
import "base:intrinsics"
|
|
import "base:runtime"
|
|
import "core:bytes"
|
|
import "core:encoding/ansi"
|
|
@require import "core:encoding/base64"
|
|
import "core:fmt"
|
|
import "core:io"
|
|
@require import pkg_log "core:log"
|
|
import "core:mem"
|
|
import "core:os"
|
|
import "core:slice"
|
|
@require import "core:strings"
|
|
import "core:sync/chan"
|
|
import "core:thread"
|
|
import "core:time"
|
|
|
|
// Specify how many threads to use when running tests.
|
|
TEST_THREADS : int : #config(ODIN_TEST_THREADS, 0)
|
|
// Track the memory used by each test.
|
|
TRACKING_MEMORY : bool : #config(ODIN_TEST_TRACK_MEMORY, true)
|
|
// Always report how much memory is used, even when there are no leaks or bad frees.
|
|
ALWAYS_REPORT_MEMORY : bool : #config(ODIN_TEST_ALWAYS_REPORT_MEMORY, false)
|
|
// Specify how much memory each thread allocator starts with.
|
|
PER_THREAD_MEMORY : int : #config(ODIN_TEST_THREAD_MEMORY, mem.ROLLBACK_STACK_DEFAULT_BLOCK_SIZE)
|
|
// Select a specific set of tests to run by name.
|
|
// Each test is separated by a comma and may optionally include the package name.
|
|
// This may be useful when running tests on multiple packages with `-all-packages`.
|
|
// The format is: `package.test_name,test_name_only,...`
|
|
TEST_NAMES : string : #config(ODIN_TEST_NAMES, "")
|
|
// Show the fancy animated progress report.
|
|
FANCY_OUTPUT : bool : #config(ODIN_TEST_FANCY, true)
|
|
// Copy failed tests to the clipboard when done.
|
|
USE_CLIPBOARD : bool : #config(ODIN_TEST_CLIPBOARD, false)
|
|
// How many test results to show at a time per package.
|
|
PROGRESS_WIDTH : int : #config(ODIN_TEST_PROGRESS_WIDTH, 24)
|
|
// This is the random seed that will be sent to each test.
|
|
// If it is unspecified, it will be set to the system cycle counter at startup.
|
|
SHARED_RANDOM_SEED : u64 : #config(ODIN_TEST_RANDOM_SEED, 0)
|
|
// Set the lowest log level for this test run.
|
|
LOG_LEVEL : string : #config(ODIN_TEST_LOG_LEVEL, "info")
|
|
|
|
|
|
get_log_level :: #force_inline proc() -> runtime.Logger_Level {
|
|
when ODIN_DEBUG {
|
|
// Always use .Debug in `-debug` mode.
|
|
return .Debug
|
|
} else {
|
|
when LOG_LEVEL == "debug" { return .Debug }
|
|
else when LOG_LEVEL == "info" { return .Info }
|
|
else when LOG_LEVEL == "warning" { return .Warning }
|
|
else when LOG_LEVEL == "error" { return .Error }
|
|
else when LOG_LEVEL == "fatal" { return .Fatal }
|
|
}
|
|
}
|
|
|
|
end_t :: proc(t: ^T) {
|
|
for i := len(t.cleanups)-1; i >= 0; i -= 1 {
|
|
#no_bounds_check c := t.cleanups[i]
|
|
context = c.ctx
|
|
c.procedure(c.user_data)
|
|
}
|
|
|
|
delete(t.cleanups)
|
|
t.cleanups = {}
|
|
}
|
|
|
|
Task_Data :: struct {
|
|
it: Internal_Test,
|
|
t: T,
|
|
allocator_index: int,
|
|
}
|
|
|
|
Task_Timeout :: struct {
|
|
test_index: int,
|
|
at_time: time.Time,
|
|
location: runtime.Source_Code_Location,
|
|
}
|
|
|
|
run_test_task :: proc(task: thread.Task) {
|
|
data := cast(^Task_Data)(task.data)
|
|
|
|
setup_task_signal_handler(task.user_index)
|
|
|
|
chan.send(data.t.channel, Event_New_Test {
|
|
test_index = task.user_index,
|
|
})
|
|
|
|
chan.send(data.t.channel, Event_State_Change {
|
|
new_state = .Running,
|
|
})
|
|
|
|
context.assertion_failure_proc = test_assertion_failure_proc
|
|
|
|
context.logger = {
|
|
procedure = test_logger_proc,
|
|
data = &data.t,
|
|
lowest_level = get_log_level(),
|
|
options = Default_Test_Logger_Opts,
|
|
}
|
|
|
|
free_all(context.temp_allocator)
|
|
|
|
data.it.p(&data.t)
|
|
|
|
end_t(&data.t)
|
|
|
|
new_state : Test_State = .Failed if failed(&data.t) else .Successful
|
|
|
|
chan.send(data.t.channel, Event_State_Change {
|
|
new_state = new_state,
|
|
})
|
|
}
|
|
|
|
runner :: proc(internal_tests: []Internal_Test) -> bool {
|
|
BATCH_BUFFER_SIZE :: 32 * mem.Kilobyte
|
|
POOL_BLOCK_SIZE :: 16 * mem.Kilobyte
|
|
CLIPBOARD_BUFFER_SIZE :: 16 * mem.Kilobyte
|
|
|
|
BUFFERED_EVENTS_PER_CHANNEL :: 16
|
|
RESERVED_LOG_MESSAGES :: 64
|
|
RESERVED_TEST_FAILURES :: 64
|
|
|
|
ERROR_STRING_TIMEOUT : string : "Test timed out."
|
|
ERROR_STRING_UNKNOWN : string : "Test failed for unknown reasons."
|
|
OSC_WINDOW_TITLE : string : ansi.OSC + ansi.WINDOW_TITLE + ";Odin test runner (%i/%i)" + ansi.ST
|
|
|
|
safe_delete_string :: proc(s: string, allocator := context.allocator) {
|
|
// Guard against bad frees on static strings.
|
|
switch raw_data(s) {
|
|
case raw_data(ERROR_STRING_TIMEOUT), raw_data(ERROR_STRING_UNKNOWN):
|
|
return
|
|
case:
|
|
delete(s, allocator)
|
|
}
|
|
}
|
|
|
|
stdout := io.to_writer(os.stream_from_handle(os.stdout))
|
|
stderr := io.to_writer(os.stream_from_handle(os.stderr))
|
|
|
|
// -- Prepare test data.
|
|
|
|
alloc_error: mem.Allocator_Error
|
|
|
|
when TEST_NAMES != "" {
|
|
select_internal_tests: [dynamic]Internal_Test
|
|
defer delete(select_internal_tests)
|
|
|
|
{
|
|
index_list := TEST_NAMES
|
|
for selector in strings.split_iterator(&index_list, ",") {
|
|
// Temp allocator is fine since we just need to identify which test it's referring to.
|
|
split_selector := strings.split(selector, ".", context.temp_allocator)
|
|
|
|
found := false
|
|
switch len(split_selector) {
|
|
case 1:
|
|
// Only the test name?
|
|
#no_bounds_check name := split_selector[0]
|
|
find_test_by_name: for it in internal_tests {
|
|
if it.name == name {
|
|
found = true
|
|
_, alloc_error = append(&select_internal_tests, it)
|
|
fmt.assertf(alloc_error == nil, "Error appending to select internal tests: %v", alloc_error)
|
|
break find_test_by_name
|
|
}
|
|
}
|
|
case 2:
|
|
#no_bounds_check pkg := split_selector[0]
|
|
#no_bounds_check name := split_selector[1]
|
|
find_test_by_pkg_and_name: for it in internal_tests {
|
|
if it.pkg == pkg && it.name == name {
|
|
found = true
|
|
_, alloc_error = append(&select_internal_tests, it)
|
|
fmt.assertf(alloc_error == nil, "Error appending to select internal tests: %v", alloc_error)
|
|
break find_test_by_pkg_and_name
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
fmt.wprintfln(stderr, "No test found for the name: %q", selector)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Intentional shadow with user-specified tests.
|
|
internal_tests := select_internal_tests[:]
|
|
}
|
|
|
|
total_failure_count := 0
|
|
total_success_count := 0
|
|
total_done_count := 0
|
|
total_test_count := len(internal_tests)
|
|
|
|
when !FANCY_OUTPUT {
|
|
// This is strictly for updating the window title when the progress
|
|
// report is disabled. We're otherwise able to depend on the call to
|
|
// `needs_to_redraw`.
|
|
last_done_count := -1
|
|
}
|
|
|
|
if total_test_count == 0 {
|
|
// Exit early.
|
|
fmt.wprintln(stdout, "No tests to run.")
|
|
return true
|
|
}
|
|
|
|
for it in internal_tests {
|
|
// NOTE(Feoramund): The old test runner skipped over tests with nil
|
|
// procedures, but I couldn't find any case where they occurred.
|
|
// This assert stands to prevent any oversight on my part.
|
|
fmt.assertf(it.p != nil, "Test %s.%s has <nil> procedure.", it.pkg, it.name)
|
|
}
|
|
|
|
slice.sort_by(internal_tests, proc(a, b: Internal_Test) -> bool {
|
|
if a.pkg == b.pkg {
|
|
return a.name < b.name
|
|
} else {
|
|
return a.pkg < b.pkg
|
|
}
|
|
})
|
|
|
|
// -- Set thread count.
|
|
|
|
when TEST_THREADS == 0 {
|
|
thread_count := os.processor_core_count()
|
|
} else {
|
|
thread_count := max(1, TEST_THREADS)
|
|
}
|
|
|
|
thread_count = min(thread_count, total_test_count)
|
|
|
|
// -- Allocate.
|
|
|
|
pool_stack: mem.Rollback_Stack
|
|
alloc_error = mem.rollback_stack_init(&pool_stack, POOL_BLOCK_SIZE)
|
|
fmt.assertf(alloc_error == nil, "Error allocating memory for thread pool: %v", alloc_error)
|
|
defer mem.rollback_stack_destroy(&pool_stack)
|
|
|
|
pool: thread.Pool
|
|
thread.pool_init(&pool, mem.rollback_stack_allocator(&pool_stack), thread_count)
|
|
defer thread.pool_destroy(&pool)
|
|
|
|
task_channels: []Task_Channel = ---
|
|
task_channels, alloc_error = make([]Task_Channel, thread_count)
|
|
fmt.assertf(alloc_error == nil, "Error allocating memory for update channels: %v", alloc_error)
|
|
defer delete(task_channels)
|
|
|
|
for &task_channel, index in task_channels {
|
|
task_channel.channel, alloc_error = chan.create_buffered(Update_Channel, BUFFERED_EVENTS_PER_CHANNEL, context.allocator)
|
|
fmt.assertf(alloc_error == nil, "Error allocating memory for update channel #%i: %v", index, alloc_error)
|
|
}
|
|
defer for &task_channel in task_channels {
|
|
chan.destroy(&task_channel.channel)
|
|
}
|
|
|
|
// This buffer is used to batch writes to STDOUT or STDERR, to help reduce
|
|
// screen flickering.
|
|
batch_buffer: bytes.Buffer
|
|
bytes.buffer_init_allocator(&batch_buffer, 0, BATCH_BUFFER_SIZE)
|
|
batch_writer := io.to_writer(bytes.buffer_to_stream(&batch_buffer))
|
|
defer bytes.buffer_destroy(&batch_buffer)
|
|
|
|
report: Report = ---
|
|
report, alloc_error = make_report(internal_tests)
|
|
fmt.assertf(alloc_error == nil, "Error allocating memory for test report: %v", alloc_error)
|
|
defer destroy_report(&report)
|
|
|
|
when FANCY_OUTPUT {
|
|
// We cannot make use of the ANSI save/restore cursor codes, because they
|
|
// work by absolute screen coordinates. This will cause unnecessary
|
|
// scrollback if we print at the bottom of someone's terminal.
|
|
ansi_redraw_string := fmt.aprintf(
|
|
// ANSI for "go up N lines then erase the screen from the cursor forward."
|
|
ansi.CSI + "%i" + ansi.CPL + ansi.CSI + ansi.ED +
|
|
// We'll combine this with the window title format string, since it
|
|
// can be printed at the same time.
|
|
"%s",
|
|
// 1 extra line for the status bar.
|
|
1 + len(report.packages), OSC_WINDOW_TITLE)
|
|
assert(len(ansi_redraw_string) > 0, "Error allocating ANSI redraw string.")
|
|
defer delete(ansi_redraw_string)
|
|
|
|
thread_count_status_string: string = ---
|
|
{
|
|
PADDING :: PROGRESS_COLUMN_SPACING + PROGRESS_WIDTH
|
|
|
|
unpadded := fmt.tprintf("%i thread%s", thread_count, "" if thread_count == 1 else "s")
|
|
thread_count_status_string = fmt.aprintf("%- *[1]s", unpadded, report.pkg_column_len + PADDING)
|
|
assert(len(thread_count_status_string) > 0, "Error allocating thread count status string.")
|
|
}
|
|
defer delete(thread_count_status_string)
|
|
}
|
|
|
|
task_data_slots: []Task_Data = ---
|
|
task_data_slots, alloc_error = make([]Task_Data, thread_count)
|
|
fmt.assertf(alloc_error == nil, "Error allocating memory for task data slots: %v", alloc_error)
|
|
defer delete(task_data_slots)
|
|
|
|
// Tests rotate through these allocators as they finish.
|
|
task_allocators: []mem.Rollback_Stack = ---
|
|
task_allocators, alloc_error = make([]mem.Rollback_Stack, thread_count)
|
|
fmt.assertf(alloc_error == nil, "Error allocating memory for task allocators: %v", alloc_error)
|
|
defer delete(task_allocators)
|
|
|
|
when TRACKING_MEMORY {
|
|
task_memory_trackers: []mem.Tracking_Allocator = ---
|
|
task_memory_trackers, alloc_error = make([]mem.Tracking_Allocator, thread_count)
|
|
fmt.assertf(alloc_error == nil, "Error allocating memory for memory trackers: %v", alloc_error)
|
|
defer delete(task_memory_trackers)
|
|
}
|
|
|
|
#no_bounds_check for i in 0 ..< thread_count {
|
|
alloc_error = mem.rollback_stack_init(&task_allocators[i], PER_THREAD_MEMORY)
|
|
fmt.assertf(alloc_error == nil, "Error allocating memory for task allocator #%i: %v", i, alloc_error)
|
|
when TRACKING_MEMORY {
|
|
mem.tracking_allocator_init(&task_memory_trackers[i], mem.rollback_stack_allocator(&task_allocators[i]))
|
|
}
|
|
}
|
|
|
|
defer #no_bounds_check for i in 0 ..< thread_count {
|
|
when TRACKING_MEMORY {
|
|
mem.tracking_allocator_destroy(&task_memory_trackers[i])
|
|
}
|
|
mem.rollback_stack_destroy(&task_allocators[i])
|
|
}
|
|
|
|
task_timeouts: [dynamic]Task_Timeout = ---
|
|
task_timeouts, alloc_error = make([dynamic]Task_Timeout, 0, thread_count)
|
|
fmt.assertf(alloc_error == nil, "Error allocating memory for task timeouts: %v", alloc_error)
|
|
defer delete(task_timeouts)
|
|
|
|
failed_test_reason_map: map[int]string = ---
|
|
failed_test_reason_map, alloc_error = make(map[int]string, RESERVED_TEST_FAILURES)
|
|
fmt.assertf(alloc_error == nil, "Error allocating memory for failed test reasons: %v", alloc_error)
|
|
defer delete(failed_test_reason_map)
|
|
|
|
log_messages: [dynamic]Log_Message = ---
|
|
log_messages, alloc_error = make([dynamic]Log_Message, 0, RESERVED_LOG_MESSAGES)
|
|
fmt.assertf(alloc_error == nil, "Error allocating memory for log message queue: %v", alloc_error)
|
|
defer delete(log_messages)
|
|
|
|
sorted_failed_test_reasons: [dynamic]int = ---
|
|
sorted_failed_test_reasons, alloc_error = make([dynamic]int, 0, RESERVED_TEST_FAILURES)
|
|
fmt.assertf(alloc_error == nil, "Error allocating memory for sorted failed test reasons: %v", alloc_error)
|
|
defer delete(sorted_failed_test_reasons)
|
|
|
|
when USE_CLIPBOARD {
|
|
clipboard_buffer: bytes.Buffer
|
|
bytes.buffer_init_allocator(&clipboard_buffer, 0, CLIPBOARD_BUFFER_SIZE)
|
|
defer bytes.buffer_destroy(&clipboard_buffer)
|
|
}
|
|
|
|
when SHARED_RANDOM_SEED == 0 {
|
|
shared_random_seed := cast(u64)intrinsics.read_cycle_counter()
|
|
} else {
|
|
shared_random_seed := SHARED_RANDOM_SEED
|
|
}
|
|
|
|
// -- Setup initial tasks.
|
|
|
|
// NOTE(Feoramund): This is the allocator that will be used by threads to
|
|
// persist log messages past their lifetimes. It has its own variable name
|
|
// in the event it needs to be changed from `context.allocator` without
|
|
// digging through the source to divine everywhere it is used for that.
|
|
shared_log_allocator := context.allocator
|
|
|
|
context.logger = {
|
|
procedure = runner_logger_proc,
|
|
data = &log_messages,
|
|
lowest_level = get_log_level(),
|
|
options = Default_Test_Logger_Opts - {.Short_File_Path, .Line, .Procedure},
|
|
}
|
|
|
|
run_index: int
|
|
|
|
setup_tasks: for &data, task_index in task_data_slots {
|
|
setup_next_test: for run_index < total_test_count {
|
|
#no_bounds_check it := internal_tests[run_index]
|
|
defer run_index += 1
|
|
|
|
data.it = it
|
|
data.t.seed = shared_random_seed
|
|
#no_bounds_check data.t.channel = chan.as_send(task_channels[task_index].channel)
|
|
data.t._log_allocator = shared_log_allocator
|
|
data.allocator_index = task_index
|
|
|
|
#no_bounds_check when TRACKING_MEMORY {
|
|
task_allocator := mem.tracking_allocator(&task_memory_trackers[task_index])
|
|
} else {
|
|
task_allocator := mem.rollback_stack_allocator(&task_allocators[task_index])
|
|
}
|
|
|
|
thread.pool_add_task(&pool, task_allocator, run_test_task, &data, run_index)
|
|
|
|
continue setup_tasks
|
|
}
|
|
}
|
|
|
|
// -- Run tests.
|
|
|
|
setup_signal_handler()
|
|
|
|
fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_HIDE)
|
|
|
|
when FANCY_OUTPUT {
|
|
signals_were_raised := false
|
|
|
|
redraw_report(stdout, report)
|
|
draw_status_bar(stdout, thread_count_status_string, total_done_count, total_test_count)
|
|
}
|
|
|
|
when TEST_THREADS == 0 {
|
|
pkg_log.infof("Starting test runner with %i thread%s. Set with -define:ODIN_TEST_THREADS=n.",
|
|
thread_count,
|
|
"" if thread_count == 1 else "s")
|
|
} else {
|
|
pkg_log.infof("Starting test runner with %i thread%s.",
|
|
thread_count,
|
|
"" if thread_count == 1 else "s")
|
|
}
|
|
|
|
when SHARED_RANDOM_SEED == 0 {
|
|
pkg_log.infof("The random seed sent to every test is: %v. Set with -define:ODIN_TEST_RANDOM_SEED=n.", shared_random_seed)
|
|
} else {
|
|
pkg_log.infof("The random seed sent to every test is: %v.", shared_random_seed)
|
|
}
|
|
|
|
when TRACKING_MEMORY {
|
|
when ALWAYS_REPORT_MEMORY {
|
|
pkg_log.info("Memory tracking is enabled. Tests will log their memory usage when complete.")
|
|
} else {
|
|
pkg_log.info("Memory tracking is enabled. Tests will log their memory usage if there's an issue.")
|
|
}
|
|
pkg_log.info("< Final Mem/ Total Mem> < Peak Mem> (#Free/Alloc) :: [package.test_name]")
|
|
} else when ALWAYS_REPORT_MEMORY {
|
|
pkg_log.warn("ODIN_TEST_ALWAYS_REPORT_MEMORY is true, but ODIN_TRACK_MEMORY is false.")
|
|
}
|
|
|
|
start_time := time.now()
|
|
|
|
thread.pool_start(&pool)
|
|
main_loop: for !thread.pool_is_empty(&pool) {
|
|
{
|
|
events_pending := thread.pool_num_done(&pool) > 0
|
|
|
|
if !events_pending {
|
|
poll_tasks: for &task_channel in task_channels {
|
|
if chan.len(task_channel.channel) > 0 {
|
|
events_pending = true
|
|
break poll_tasks
|
|
}
|
|
}
|
|
}
|
|
|
|
if !events_pending {
|
|
// Keep the main thread from pegging a core at 100% usage.
|
|
time.sleep(1 * time.Microsecond)
|
|
}
|
|
}
|
|
|
|
cycle_pool: for task in thread.pool_pop_done(&pool) {
|
|
data := cast(^Task_Data)(task.data)
|
|
|
|
when TRACKING_MEMORY {
|
|
#no_bounds_check tracker := &task_memory_trackers[data.allocator_index]
|
|
|
|
when ALWAYS_REPORT_MEMORY {
|
|
should_report := true
|
|
} else {
|
|
should_report := len(tracker.allocation_map) + len(tracker.bad_free_array) > 0
|
|
}
|
|
|
|
if should_report {
|
|
write_memory_report(batch_writer, tracker, data.it.pkg, data.it.name)
|
|
|
|
pkg_log.info(bytes.buffer_to_string(&batch_buffer))
|
|
bytes.buffer_reset(&batch_buffer)
|
|
}
|
|
|
|
mem.tracking_allocator_reset(tracker)
|
|
}
|
|
|
|
free_all(task.allocator)
|
|
|
|
if run_index < total_test_count {
|
|
#no_bounds_check it := internal_tests[run_index]
|
|
defer run_index += 1
|
|
|
|
data.it = it
|
|
data.t.seed = shared_random_seed
|
|
data.t.error_count = 0
|
|
|
|
thread.pool_add_task(&pool, task.allocator, run_test_task, data, run_index)
|
|
}
|
|
}
|
|
|
|
handle_events: for &task_channel in task_channels {
|
|
for ev in chan.try_recv(task_channel.channel) {
|
|
switch event in ev {
|
|
case Event_New_Test:
|
|
task_channel.test_index = event.test_index
|
|
|
|
case Event_State_Change:
|
|
#no_bounds_check report.all_test_states[task_channel.test_index] = event.new_state
|
|
|
|
#no_bounds_check it := internal_tests[task_channel.test_index]
|
|
#no_bounds_check pkg := report.packages_by_name[it.pkg]
|
|
|
|
#partial switch event.new_state {
|
|
case .Failed:
|
|
if task_channel.test_index not_in failed_test_reason_map {
|
|
failed_test_reason_map[task_channel.test_index] = ERROR_STRING_UNKNOWN
|
|
}
|
|
total_failure_count += 1
|
|
total_done_count += 1
|
|
case .Successful:
|
|
total_success_count += 1
|
|
total_done_count += 1
|
|
}
|
|
|
|
when ODIN_DEBUG {
|
|
pkg_log.debugf("Test #%i %s.%s changed state to %v.", task_channel.test_index, it.pkg, it.name, event.new_state)
|
|
}
|
|
|
|
pkg.last_change_state = event.new_state
|
|
pkg.last_change_name = it.name
|
|
pkg.frame_ready = false
|
|
|
|
case Event_Set_Fail_Timeout:
|
|
_, alloc_error = append(&task_timeouts, Task_Timeout {
|
|
test_index = task_channel.test_index,
|
|
at_time = event.at_time,
|
|
location = event.location,
|
|
})
|
|
fmt.assertf(alloc_error == nil, "Error appending to task timeouts: %v", alloc_error)
|
|
|
|
case Event_Log_Message:
|
|
_, alloc_error = append(&log_messages, Log_Message {
|
|
level = event.level,
|
|
text = event.formatted_text,
|
|
time = event.time,
|
|
allocator = shared_log_allocator,
|
|
})
|
|
fmt.assertf(alloc_error == nil, "Error appending to log messages: %v", alloc_error)
|
|
|
|
if event.level >= .Error {
|
|
// Save the message for the final summary.
|
|
if old_error, ok := failed_test_reason_map[task_channel.test_index]; ok {
|
|
safe_delete_string(old_error, shared_log_allocator)
|
|
}
|
|
failed_test_reason_map[task_channel.test_index] = event.text
|
|
} else {
|
|
delete(event.text, shared_log_allocator)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
check_timeouts: for i := len(task_timeouts) - 1; i >= 0; i -= 1 {
|
|
#no_bounds_check timeout := &task_timeouts[i]
|
|
|
|
if time.since(timeout.at_time) < 0 {
|
|
continue check_timeouts
|
|
}
|
|
|
|
defer unordered_remove(&task_timeouts, i)
|
|
|
|
#no_bounds_check if report.all_test_states[timeout.test_index] > .Running {
|
|
continue check_timeouts
|
|
}
|
|
|
|
if !thread.pool_stop_task(&pool, timeout.test_index) {
|
|
// The task may have stopped a split second after we started
|
|
// checking, but we haven't handled the new state yet.
|
|
continue check_timeouts
|
|
}
|
|
|
|
#no_bounds_check report.all_test_states[timeout.test_index] = .Failed
|
|
#no_bounds_check it := internal_tests[timeout.test_index]
|
|
#no_bounds_check pkg := report.packages_by_name[it.pkg]
|
|
pkg.frame_ready = false
|
|
|
|
if old_error, ok := failed_test_reason_map[timeout.test_index]; ok {
|
|
safe_delete_string(old_error, shared_log_allocator)
|
|
}
|
|
failed_test_reason_map[timeout.test_index] = ERROR_STRING_TIMEOUT
|
|
total_failure_count += 1
|
|
total_done_count += 1
|
|
|
|
now := time.now()
|
|
_, alloc_error = append(&log_messages, Log_Message {
|
|
level = .Error,
|
|
text = format_log_text(.Error, ERROR_STRING_TIMEOUT, Default_Test_Logger_Opts, timeout.location, now),
|
|
time = now,
|
|
allocator = context.allocator,
|
|
})
|
|
fmt.assertf(alloc_error == nil, "Error appending to log messages: %v", alloc_error)
|
|
|
|
find_task_data: for &data in task_data_slots {
|
|
if data.it.pkg == it.pkg && data.it.name == it.name {
|
|
end_t(&data.t)
|
|
break find_task_data
|
|
}
|
|
}
|
|
}
|
|
|
|
if should_stop_runner() {
|
|
fmt.wprintln(stderr, "\nCaught interrupt signal. Stopping all tests.")
|
|
thread.pool_shutdown(&pool)
|
|
break main_loop
|
|
}
|
|
|
|
when FANCY_OUTPUT {
|
|
// Because the bounds checking procs send directly to STDERR with
|
|
// no way to redirect or handle them, we need to at least try to
|
|
// let the user see those messages when using the animated progress
|
|
// report. This flag may be set by the block of code below if a
|
|
// signal is raised.
|
|
//
|
|
// It'll be purely by luck if the output is interleaved properly,
|
|
// given the nature of non-thread-safe printing.
|
|
//
|
|
// At worst, if Odin did not print any error for this signal, we'll
|
|
// just re-display the progress report. The fatal log error message
|
|
// should be enough to clue the user in that something dire has
|
|
// occurred.
|
|
bypass_progress_overwrite := false
|
|
}
|
|
|
|
if test_index, reason, ok := should_stop_test(); ok {
|
|
#no_bounds_check report.all_test_states[test_index] = .Failed
|
|
#no_bounds_check it := internal_tests[test_index]
|
|
#no_bounds_check pkg := report.packages_by_name[it.pkg]
|
|
pkg.frame_ready = false
|
|
|
|
fmt.assertf(thread.pool_stop_task(&pool, test_index),
|
|
"A signal (%v) was raised to stop test #%i %s.%s, but it was unable to be found.",
|
|
reason, test_index, it.pkg, it.name)
|
|
|
|
if test_index not_in failed_test_reason_map {
|
|
// We only write a new error message here if there wasn't one
|
|
// already, because the message we can provide based only on
|
|
// the signal won't be very useful, whereas asserts and panics
|
|
// will provide a user-written error message.
|
|
failed_test_reason_map[test_index] = fmt.aprintf("Signal caught: %v", reason, allocator = shared_log_allocator)
|
|
pkg_log.fatalf("Caught signal to stop test #%i %s.%s for: %v.", test_index, it.pkg, it.name, reason)
|
|
|
|
}
|
|
|
|
when FANCY_OUTPUT {
|
|
bypass_progress_overwrite = true
|
|
signals_were_raised = true
|
|
}
|
|
|
|
total_failure_count += 1
|
|
total_done_count += 1
|
|
}
|
|
|
|
// -- Redraw.
|
|
|
|
when FANCY_OUTPUT {
|
|
if len(log_messages) == 0 && !needs_to_redraw(report) {
|
|
continue main_loop
|
|
}
|
|
|
|
if !bypass_progress_overwrite {
|
|
fmt.wprintf(stdout, ansi_redraw_string, total_done_count, total_test_count)
|
|
}
|
|
} else {
|
|
if total_done_count != last_done_count {
|
|
fmt.wprintf(stdout, OSC_WINDOW_TITLE, total_done_count, total_test_count)
|
|
last_done_count = total_done_count
|
|
}
|
|
|
|
if len(log_messages) == 0 {
|
|
continue main_loop
|
|
}
|
|
}
|
|
|
|
// Because each thread has its own messenger channel, log messages
|
|
// arrive in chunks that are in-order, but when they're merged with the
|
|
// logs from other threads, they become out-of-order.
|
|
slice.stable_sort_by(log_messages[:], proc(a, b: Log_Message) -> bool {
|
|
return time.diff(a.time, b.time) > 0
|
|
})
|
|
|
|
for message in log_messages {
|
|
fmt.wprintln(batch_writer, message.text)
|
|
delete(message.text, message.allocator)
|
|
}
|
|
|
|
fmt.wprint(stderr, bytes.buffer_to_string(&batch_buffer))
|
|
clear(&log_messages)
|
|
bytes.buffer_reset(&batch_buffer)
|
|
|
|
when FANCY_OUTPUT {
|
|
redraw_report(batch_writer, report)
|
|
draw_status_bar(batch_writer, thread_count_status_string, total_done_count, total_test_count)
|
|
fmt.wprint(stdout, bytes.buffer_to_string(&batch_buffer))
|
|
bytes.buffer_reset(&batch_buffer)
|
|
}
|
|
}
|
|
|
|
// -- All tests are complete, or the runner has been interrupted.
|
|
|
|
// NOTE(Feoramund): If you've arrived here after receiving signal 11 or
|
|
// SIGSEGV on the main runner thread, while using a UNIX-like platform,
|
|
// there is the possibility that you may have encountered a rare edge case
|
|
// involving the joining of threads.
|
|
//
|
|
// At the time of writing, the thread library is undergoing a rewrite that
|
|
// should solve this problem; it is not an issue with the test runner itself.
|
|
thread.pool_join(&pool)
|
|
|
|
finished_in := time.since(start_time)
|
|
|
|
when !FANCY_OUTPUT {
|
|
// One line to space out the results, since we don't have the status
|
|
// bar in plain mode.
|
|
fmt.wprintln(batch_writer)
|
|
}
|
|
|
|
fmt.wprintf(batch_writer,
|
|
"Finished %i test%s in %v.",
|
|
total_done_count,
|
|
"" if total_done_count == 1 else "s",
|
|
finished_in)
|
|
|
|
if total_done_count != total_test_count {
|
|
not_run_count := total_test_count - total_done_count
|
|
fmt.wprintf(batch_writer,
|
|
" " + SGR_READY + "%i" + SGR_RESET + " %s left undone.",
|
|
not_run_count,
|
|
"test was" if not_run_count == 1 else "tests were")
|
|
}
|
|
|
|
if total_success_count == total_test_count {
|
|
fmt.wprintfln(batch_writer,
|
|
" %s " + SGR_SUCCESS + "successful." + SGR_RESET,
|
|
"The test was" if total_test_count == 1 else "All tests were")
|
|
} else if total_failure_count > 0 {
|
|
if total_failure_count == total_test_count {
|
|
fmt.wprintfln(batch_writer,
|
|
" %s " + SGR_FAILED + "failed." + SGR_RESET,
|
|
"The test" if total_test_count == 1 else "All tests")
|
|
} else {
|
|
fmt.wprintfln(batch_writer,
|
|
" " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed.",
|
|
total_failure_count,
|
|
"" if total_failure_count == 1 else "s")
|
|
}
|
|
|
|
for test_index in failed_test_reason_map {
|
|
_, alloc_error = append(&sorted_failed_test_reasons, test_index)
|
|
fmt.assertf(alloc_error == nil, "Error appending to sorted failed test reasons: %v", alloc_error)
|
|
}
|
|
|
|
slice.sort(sorted_failed_test_reasons[:])
|
|
|
|
for test_index in sorted_failed_test_reasons {
|
|
#no_bounds_check last_error := failed_test_reason_map[test_index]
|
|
#no_bounds_check it := internal_tests[test_index]
|
|
pkg_and_name := fmt.tprintf("%s.%s", it.pkg, it.name)
|
|
fmt.wprintfln(batch_writer, " - %- *[1]s\t%s",
|
|
pkg_and_name,
|
|
report.pkg_column_len + report.test_column_len,
|
|
last_error)
|
|
safe_delete_string(last_error, shared_log_allocator)
|
|
}
|
|
|
|
if total_success_count > 0 {
|
|
when USE_CLIPBOARD {
|
|
clipboard_writer := io.to_writer(bytes.buffer_to_stream(&clipboard_buffer))
|
|
fmt.wprint(clipboard_writer, "-define:ODIN_TEST_NAMES=")
|
|
for test_index in sorted_failed_test_reasons {
|
|
#no_bounds_check it := internal_tests[test_index]
|
|
fmt.wprintf(clipboard_writer, "%s.%s,", it.pkg, it.name)
|
|
}
|
|
|
|
encoded_names := base64.encode(bytes.buffer_to_bytes(&clipboard_buffer), allocator = context.temp_allocator)
|
|
|
|
fmt.wprintf(batch_writer,
|
|
ansi.OSC + ansi.CLIPBOARD + ";c;%s" + ansi.ST +
|
|
"\nThe name%s of the failed test%s been copied to your clipboard.",
|
|
encoded_names,
|
|
"" if total_failure_count == 1 else "s",
|
|
" has" if total_failure_count == 1 else "s have")
|
|
} else {
|
|
fmt.wprintf(batch_writer, "\nTo run only the failed test%s, use:\n\t-define:ODIN_TEST_NAMES=",
|
|
"" if total_failure_count == 1 else "s")
|
|
for test_index in sorted_failed_test_reasons {
|
|
#no_bounds_check it := internal_tests[test_index]
|
|
fmt.wprintf(batch_writer, "%s.%s,", it.pkg, it.name)
|
|
}
|
|
fmt.wprint(batch_writer, "\n\nIf your terminal supports OSC 52, you may use -define:ODIN_TEST_CLIPBOARD to have this copied directly to your clipboard.")
|
|
}
|
|
|
|
fmt.wprintln(batch_writer)
|
|
}
|
|
}
|
|
|
|
fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_SHOW)
|
|
|
|
when FANCY_OUTPUT {
|
|
if signals_were_raised {
|
|
fmt.wprintln(batch_writer, `
|
|
Signals were raised during this test run. Log messages are likely to have collided with each other.
|
|
To partly mitigate this, redirect STDERR to a file or use the -define:ODIN_TEST_FANCY=false option.`)
|
|
}
|
|
}
|
|
|
|
fmt.wprintln(stderr, bytes.buffer_to_string(&batch_buffer))
|
|
|
|
return total_success_count == total_test_count
|
|
}
|