aboutsummaryrefslogtreecommitdiff
path: root/core/testing
diff options
context:
space:
mode:
authorFeoramund <161657516+Feoramund@users.noreply.github.com>2025-05-20 16:17:14 -0400
committerFeoramund <161657516+Feoramund@users.noreply.github.com>2025-05-20 19:28:07 -0400
commit1b407ef20789b1d04d61b0a53ca8c98fe6621db2 (patch)
tree88b663421eb12b5a0af7f1025ccd0343f2a4e3ca /core/testing
parent3c40a54dcd9ff8e1b608fac5c91ed0f1d0ed7d00 (diff)
Add animation detection support to test runner
Diffstat (limited to 'core/testing')
-rw-r--r--core/testing/runner.odin135
-rw-r--r--core/testing/signal_handler_libc.odin8
2 files changed, 81 insertions, 62 deletions
diff --git a/core/testing/runner.odin b/core/testing/runner.odin
index ff8ca00b9..a184eb28c 100644
--- a/core/testing/runner.odin
+++ b/core/testing/runner.odin
@@ -45,6 +45,7 @@ PER_THREAD_MEMORY : int : #config(ODIN_TEST_THREAD_MEMORY, mem.ROLLBACK_S
// The format is: `package.test_name,test_name_only,...`
TEST_NAMES : string : #config(ODIN_TEST_NAMES, "")
// Show the fancy animated progress report.
+// This requires terminal color support, as well as STDOUT to not be redirected to a file.
FANCY_OUTPUT : bool : #config(ODIN_TEST_FANCY, true)
// Copy failed tests to the clipboard when done.
USE_CLIPBOARD : bool : #config(ODIN_TEST_CLIPBOARD, false)
@@ -72,6 +73,7 @@ get_log_level :: #force_inline proc() -> runtime.Logger_Level {
}
@(private) global_log_colors_disabled: bool
+@(private) global_ansi_disabled: bool
JSON :: struct {
total: int,
@@ -219,7 +221,12 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
stdout := io.to_writer(os.stream_from_handle(os.stdout))
stderr := io.to_writer(os.stream_from_handle(os.stderr))
+ // The animations are only ever shown through STDOUT;
+ // STDERR is used exclusively for logging regardless of error level.
global_log_colors_disabled = !terminal.color_enabled || !terminal.is_terminal(os.stderr)
+ global_ansi_disabled = !terminal.is_terminal(os.stdout)
+
+ should_show_animations := FANCY_OUTPUT && terminal.color_enabled && !global_ansi_disabled
// -- Prepare test data.
@@ -278,12 +285,12 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
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
- }
+
+ // 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.
@@ -352,31 +359,31 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
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)
+ // 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)
@@ -496,11 +503,13 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
setup_signal_handler()
- fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_HIDE)
+ if !global_ansi_disabled {
+ fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_HIDE)
+ }
- when FANCY_OUTPUT {
- signals_were_raised := false
+ signals_were_raised := false
+ if should_show_animations {
redraw_report(stdout, report)
draw_status_bar(stdout, thread_count_status_string, total_done_count, total_test_count)
}
@@ -718,22 +727,22 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
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
- }
+
+ // 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
@@ -767,7 +776,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
log.fatalf("Caught signal to stop test #%i %s.%s for: %v.", test_index, it.pkg, it.name, reason)
}
- when FANCY_OUTPUT {
+ if should_show_animations {
bypass_progress_overwrite = true
signals_were_raised = true
}
@@ -781,7 +790,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
// -- Redraw.
- when FANCY_OUTPUT {
+ if should_show_animations {
if len(log_messages) == 0 && !needs_to_redraw(report) {
continue main_loop
}
@@ -791,7 +800,9 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
}
} else {
if total_done_count != last_done_count {
- fmt.wprintf(stdout, OSC_WINDOW_TITLE, total_done_count, total_test_count)
+ if !global_ansi_disabled {
+ fmt.wprintf(stdout, OSC_WINDOW_TITLE, total_done_count, total_test_count)
+ }
last_done_count = total_done_count
}
@@ -816,7 +827,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
clear(&log_messages)
bytes.buffer_reset(&batch_buffer)
- when FANCY_OUTPUT {
+ if should_show_animations {
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))
@@ -837,7 +848,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
finished_in := time.since(start_time)
- when !FANCY_OUTPUT {
+ if !should_show_animations || !terminal.is_terminal(os.stderr) {
// One line to space out the results, since we don't have the status
// bar in plain mode.
fmt.wprintln(batch_writer)
@@ -851,24 +862,28 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
if total_done_count != total_test_count {
not_run_count := total_test_count - total_done_count
+ message := " %i %s left undone." if global_log_colors_disabled else " " + SGR_READY + "%i" + SGR_RESET + " %s left undone."
fmt.wprintf(batch_writer,
- " " + SGR_READY + "%i" + SGR_RESET + " %s left undone.",
+ message,
not_run_count,
"test was" if not_run_count == 1 else "tests were")
}
if total_success_count == total_test_count {
+ message := " %s successful." if global_log_colors_disabled else " %s " + SGR_SUCCESS + "successful." + SGR_RESET
fmt.wprintfln(batch_writer,
- " %s " + SGR_SUCCESS + "successful." + SGR_RESET,
+ message,
"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 {
+ message := " %s failed." if global_log_colors_disabled else " %s " + SGR_FAILED + "failed." + SGR_RESET
fmt.wprintfln(batch_writer,
- " %s " + SGR_FAILED + "failed." + SGR_RESET,
+ message,
"The test" if total_test_count == 1 else "All tests")
} else {
+ message := " %i test%s failed." if global_log_colors_disabled else " " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed."
fmt.wprintfln(batch_writer,
- " " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed.",
+ message,
total_failure_count,
"" if total_failure_count == 1 else "s")
}
@@ -922,9 +937,11 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
}
}
- fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_SHOW)
+ if !global_ansi_disabled {
+ fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_SHOW)
+ }
- when FANCY_OUTPUT {
+ if should_show_animations {
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.
diff --git a/core/testing/signal_handler_libc.odin b/core/testing/signal_handler_libc.odin
index d17a6d6dc..f9527e22f 100644
--- a/core/testing/signal_handler_libc.odin
+++ b/core/testing/signal_handler_libc.odin
@@ -63,9 +63,11 @@ stop_test_callback :: proc "c" (sig: libc.int) {
// NOTE(Feoramund): Using these write calls in a signal handler is
// undefined behavior in C99 but possibly tolerated in POSIX 2008.
// Either way, we may as well try to salvage what we can.
- show_cursor := ansi.CSI + ansi.DECTCEM_SHOW
- libc.fwrite(raw_data(show_cursor), size_of(byte), len(show_cursor), libc.stdout)
- libc.fflush(libc.stdout)
+ if !global_ansi_disabled {
+ show_cursor := ansi.CSI + ansi.DECTCEM_SHOW
+ libc.fwrite(raw_data(show_cursor), size_of(byte), len(show_cursor), libc.stdout)
+ libc.fflush(libc.stdout)
+ }
// This is an attempt at being compliant by avoiding printf.
sigbuf: [8]byte