|
@@ -13,7 +13,6 @@ package testing
|
|
|
import "base:intrinsics"
|
|
|
import "base:runtime"
|
|
|
import "core:bytes"
|
|
|
-import "core:encoding/ansi"
|
|
|
@require import "core:encoding/base64"
|
|
|
@require import "core:encoding/json"
|
|
|
import "core:fmt"
|
|
@@ -25,6 +24,8 @@ import "core:os"
|
|
|
import "core:slice"
|
|
|
@require import "core:strings"
|
|
|
import "core:sync/chan"
|
|
|
+import "core:terminal"
|
|
|
+import "core:terminal/ansi"
|
|
|
import "core:thread"
|
|
|
import "core:time"
|
|
|
|
|
@@ -44,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)
|
|
@@ -70,6 +72,9 @@ get_log_level :: #force_inline proc() -> runtime.Logger_Level {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+@(private) global_log_colors_disabled: bool
|
|
|
+@(private) global_ansi_disabled: bool
|
|
|
+
|
|
|
JSON :: struct {
|
|
|
total: int,
|
|
|
success: int,
|
|
@@ -129,11 +134,16 @@ run_test_task :: proc(task: thread.Task) {
|
|
|
|
|
|
context.assertion_failure_proc = test_assertion_failure_proc
|
|
|
|
|
|
+ logger_options := Default_Test_Logger_Opts
|
|
|
+ if global_log_colors_disabled {
|
|
|
+ logger_options -= {.Terminal_Color}
|
|
|
+ }
|
|
|
+
|
|
|
context.logger = {
|
|
|
procedure = test_logger_proc,
|
|
|
data = &data.t,
|
|
|
lowest_level = get_log_level(),
|
|
|
- options = Default_Test_Logger_Opts,
|
|
|
+ options = logger_options,
|
|
|
}
|
|
|
|
|
|
random_generator_state: runtime.Default_Random_State
|
|
@@ -204,13 +214,16 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- when ODIN_OS == .Windows {
|
|
|
- console_ansi_init()
|
|
|
- }
|
|
|
-
|
|
|
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.
|
|
|
|
|
|
alloc_error: mem.Allocator_Error
|
|
@@ -268,12 +281,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.
|
|
@@ -342,31 +355,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)
|
|
@@ -442,11 +455,16 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
|
|
|
// digging through the source to divine everywhere it is used for that.
|
|
|
shared_log_allocator := context.allocator
|
|
|
|
|
|
+ logger_options := Default_Test_Logger_Opts - {.Short_File_Path, .Line, .Procedure}
|
|
|
+ if global_log_colors_disabled {
|
|
|
+ logger_options -= {.Terminal_Color}
|
|
|
+ }
|
|
|
+
|
|
|
context.logger = {
|
|
|
procedure = runner_logger_proc,
|
|
|
data = &log_messages,
|
|
|
lowest_level = get_log_level(),
|
|
|
- options = Default_Test_Logger_Opts - {.Short_File_Path, .Line, .Procedure},
|
|
|
+ options = logger_options,
|
|
|
}
|
|
|
|
|
|
run_index: int
|
|
@@ -481,11 +499,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)
|
|
|
}
|
|
@@ -703,22 +723,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
|
|
@@ -752,7 +772,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
|
|
|
}
|
|
@@ -766,7 +786,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
|
|
|
}
|
|
@@ -776,7 +796,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
|
|
|
}
|
|
|
|
|
@@ -801,7 +823,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))
|
|
@@ -822,7 +844,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)
|
|
@@ -836,24 +858,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")
|
|
|
}
|
|
@@ -907,9 +933,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.
|
|
@@ -949,9 +977,5 @@ To partly mitigate this, redirect STDERR to a file or use the -define:ODIN_TEST_
|
|
|
fmt.assertf(err == nil, "Error writing JSON report: %v", err)
|
|
|
}
|
|
|
|
|
|
- when ODIN_OS == .Windows {
|
|
|
- console_ansi_fini()
|
|
|
- }
|
|
|
-
|
|
|
return total_success_count == total_test_count
|
|
|
}
|