mirror of
https://github.com/Ed94/Odin.git
synced 2026-08-06 07:38:48 +00:00
Merge pull request #5189 from Feoramund/fix-ansi-log-terminal
Add `core:terminal`, fix test runner/`core:log` ANSI code issues
This commit is contained in:
@@ -2,10 +2,12 @@
|
|||||||
#+build !orca
|
#+build !orca
|
||||||
package log
|
package log
|
||||||
|
|
||||||
import "core:encoding/ansi"
|
import "base:runtime"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
|
import "core:terminal"
|
||||||
|
import "core:terminal/ansi"
|
||||||
import "core:time"
|
import "core:time"
|
||||||
|
|
||||||
Level_Headers := [?]string{
|
Level_Headers := [?]string{
|
||||||
@@ -37,11 +39,36 @@ File_Console_Logger_Data :: struct {
|
|||||||
ident: string,
|
ident: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(private) global_subtract_stdout_options: Options
|
||||||
|
@(private) global_subtract_stderr_options: Options
|
||||||
|
|
||||||
|
@(init, private)
|
||||||
|
init_standard_stream_status :: proc() {
|
||||||
|
// NOTE(Feoramund): While it is technically possible for these streams to
|
||||||
|
// be redirected during the runtime of the program, the cost of checking on
|
||||||
|
// every single log message is not worth it to support such an
|
||||||
|
// uncommonly-used feature.
|
||||||
|
if terminal.color_enabled {
|
||||||
|
// This is done this way because it's possible that only one of these
|
||||||
|
// streams could be redirected to a file.
|
||||||
|
if !terminal.is_terminal(os.stdout) {
|
||||||
|
global_subtract_stdout_options = {.Terminal_Color}
|
||||||
|
}
|
||||||
|
if !terminal.is_terminal(os.stderr) {
|
||||||
|
global_subtract_stderr_options = {.Terminal_Color}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Override any terminal coloring.
|
||||||
|
global_subtract_stdout_options = {.Terminal_Color}
|
||||||
|
global_subtract_stderr_options = {.Terminal_Color}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
create_file_logger :: proc(h: os.Handle, lowest := Level.Debug, opt := Default_File_Logger_Opts, ident := "", allocator := context.allocator) -> Logger {
|
create_file_logger :: proc(h: os.Handle, lowest := Level.Debug, opt := Default_File_Logger_Opts, ident := "", allocator := context.allocator) -> Logger {
|
||||||
data := new(File_Console_Logger_Data, allocator)
|
data := new(File_Console_Logger_Data, allocator)
|
||||||
data.file_handle = h
|
data.file_handle = h
|
||||||
data.ident = ident
|
data.ident = ident
|
||||||
return Logger{file_console_logger_proc, data, lowest, opt}
|
return Logger{file_logger_proc, data, lowest, opt}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy_file_logger :: proc(log: Logger, allocator := context.allocator) {
|
destroy_file_logger :: proc(log: Logger, allocator := context.allocator) {
|
||||||
@@ -56,19 +83,15 @@ create_console_logger :: proc(lowest := Level.Debug, opt := Default_Console_Logg
|
|||||||
data := new(File_Console_Logger_Data, allocator)
|
data := new(File_Console_Logger_Data, allocator)
|
||||||
data.file_handle = os.INVALID_HANDLE
|
data.file_handle = os.INVALID_HANDLE
|
||||||
data.ident = ident
|
data.ident = ident
|
||||||
return Logger{file_console_logger_proc, data, lowest, opt}
|
return Logger{console_logger_proc, data, lowest, opt}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy_console_logger :: proc(log: Logger, allocator := context.allocator) {
|
destroy_console_logger :: proc(log: Logger, allocator := context.allocator) {
|
||||||
free(log.data, allocator)
|
free(log.data, allocator)
|
||||||
}
|
}
|
||||||
|
|
||||||
file_console_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) {
|
@(private)
|
||||||
data := cast(^File_Console_Logger_Data)logger_data
|
_file_console_logger_proc :: proc(h: os.Handle, ident: string, level: Level, text: string, options: Options, location: runtime.Source_Code_Location) {
|
||||||
h: os.Handle = os.stdout if level <= Level.Error else os.stderr
|
|
||||||
if data.file_handle != os.INVALID_HANDLE {
|
|
||||||
h = data.file_handle
|
|
||||||
}
|
|
||||||
backing: [1024]byte //NOTE(Hoej): 1024 might be too much for a header backing, unless somebody has really long paths.
|
backing: [1024]byte //NOTE(Hoej): 1024 might be too much for a header backing, unless somebody has really long paths.
|
||||||
buf := strings.builder_from_bytes(backing[:])
|
buf := strings.builder_from_bytes(backing[:])
|
||||||
|
|
||||||
@@ -86,13 +109,32 @@ file_console_logger_proc :: proc(logger_data: rawptr, level: Level, text: string
|
|||||||
fmt.sbprintf(&buf, "[{}] ", os.current_thread_id())
|
fmt.sbprintf(&buf, "[{}] ", os.current_thread_id())
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.ident != "" {
|
if ident != "" {
|
||||||
fmt.sbprintf(&buf, "[%s] ", data.ident)
|
fmt.sbprintf(&buf, "[%s] ", ident)
|
||||||
}
|
}
|
||||||
//TODO(Hoej): When we have better atomics and such, make this thread-safe
|
//TODO(Hoej): When we have better atomics and such, make this thread-safe
|
||||||
fmt.fprintf(h, "%s%s\n", strings.to_string(buf), text)
|
fmt.fprintf(h, "%s%s\n", strings.to_string(buf), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
file_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) {
|
||||||
|
data := cast(^File_Console_Logger_Data)logger_data
|
||||||
|
_file_console_logger_proc(data.file_handle, data.ident, level, text, options, location)
|
||||||
|
}
|
||||||
|
|
||||||
|
console_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) {
|
||||||
|
options := options
|
||||||
|
data := cast(^File_Console_Logger_Data)logger_data
|
||||||
|
h: os.Handle = ---
|
||||||
|
if level < Level.Error {
|
||||||
|
h = os.stdout
|
||||||
|
options -= global_subtract_stdout_options
|
||||||
|
} else {
|
||||||
|
h = os.stderr
|
||||||
|
options -= global_subtract_stderr_options
|
||||||
|
}
|
||||||
|
_file_console_logger_proc(h, data.ident, level, text, options, location)
|
||||||
|
}
|
||||||
|
|
||||||
do_level_header :: proc(opts: Options, str: ^strings.Builder, level: Level) {
|
do_level_header :: proc(opts: Options, str: ^strings.Builder, level: Level) {
|
||||||
|
|
||||||
RESET :: ansi.CSI + ansi.RESET + ansi.SGR
|
RESET :: ansi.CSI + ansi.RESET + ansi.SGR
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/*
|
||||||
|
This package is for interacting with the command line interface of the system.
|
||||||
|
*/
|
||||||
|
package terminal
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#+private
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
// Reference documentation:
|
||||||
|
//
|
||||||
|
// - [[ https://no-color.org/ ]]
|
||||||
|
// - [[ https://github.com/termstandard/colors ]]
|
||||||
|
// - [[ https://invisible-island.net/ncurses/terminfo.src.html ]]
|
||||||
|
|
||||||
|
get_no_color :: proc() -> bool {
|
||||||
|
if no_color, ok := os.lookup_env("NO_COLOR"); ok {
|
||||||
|
defer delete(no_color)
|
||||||
|
return no_color != ""
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
get_environment_color :: proc() -> Color_Depth {
|
||||||
|
// `COLORTERM` is non-standard but widespread and unambiguous.
|
||||||
|
if colorterm, ok := os.lookup_env("COLORTERM"); ok {
|
||||||
|
defer delete(colorterm)
|
||||||
|
// These are the only values that are typically advertised that have
|
||||||
|
// anything to do with color depth.
|
||||||
|
if colorterm == "truecolor" || colorterm == "24bit" {
|
||||||
|
return .True_Color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if term, ok := os.lookup_env("TERM"); ok {
|
||||||
|
defer delete(term)
|
||||||
|
if strings.contains(term, "-truecolor") {
|
||||||
|
return .True_Color
|
||||||
|
}
|
||||||
|
if strings.contains(term, "-256color") {
|
||||||
|
return .Eight_Bit
|
||||||
|
}
|
||||||
|
if strings.contains(term, "-16color") {
|
||||||
|
return .Four_Bit
|
||||||
|
}
|
||||||
|
|
||||||
|
// The `terminfo` database, which is stored in binary on *nix
|
||||||
|
// platforms, has an undocumented format that is not guaranteed to be
|
||||||
|
// portable, so beyond this point, we can only make safe assumptions.
|
||||||
|
//
|
||||||
|
// This section should only be necessary for terminals that do not
|
||||||
|
// define any of the previous environment values.
|
||||||
|
//
|
||||||
|
// Only a small sampling of some common values are checked here.
|
||||||
|
switch term {
|
||||||
|
case "ansi": fallthrough
|
||||||
|
case "konsole": fallthrough
|
||||||
|
case "putty": fallthrough
|
||||||
|
case "rxvt": fallthrough
|
||||||
|
case "rxvt-color": fallthrough
|
||||||
|
case "screen": fallthrough
|
||||||
|
case "st": fallthrough
|
||||||
|
case "tmux": fallthrough
|
||||||
|
case "vte": fallthrough
|
||||||
|
case "xterm": fallthrough
|
||||||
|
case "xterm-color":
|
||||||
|
return .Three_Bit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .None
|
||||||
|
}
|
||||||
|
|
||||||
|
@(init)
|
||||||
|
init_terminal :: proc() {
|
||||||
|
_init_terminal()
|
||||||
|
|
||||||
|
// We respect `NO_COLOR` specifically as a color-disabler but not as a
|
||||||
|
// blanket ban on any terminal manipulation codes, hence why this comes
|
||||||
|
// after `_init_terminal` which will allow Windows to enable Virtual
|
||||||
|
// Terminal Processing for non-color control sequences.
|
||||||
|
if !get_no_color() {
|
||||||
|
color_enabled = color_depth > .None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@(fini)
|
||||||
|
fini_terminal :: proc() {
|
||||||
|
_fini_terminal()
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package terminal
|
||||||
|
|
||||||
|
import "core:os"
|
||||||
|
|
||||||
|
/*
|
||||||
|
This describes the range of colors that a terminal is capable of supporting.
|
||||||
|
*/
|
||||||
|
Color_Depth :: enum {
|
||||||
|
None, // No color support
|
||||||
|
Three_Bit, // 8 colors
|
||||||
|
Four_Bit, // 16 colors
|
||||||
|
Eight_Bit, // 256 colors
|
||||||
|
True_Color, // 24-bit true color
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Returns true if the file `handle` is attached to a terminal.
|
||||||
|
|
||||||
|
This is normally true for `os.stdout` and `os.stderr` unless they are
|
||||||
|
redirected to a file.
|
||||||
|
*/
|
||||||
|
@(require_results)
|
||||||
|
is_terminal :: proc(handle: os.Handle) -> bool {
|
||||||
|
return _is_terminal(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is true if the terminal is accepting any form of colored text output.
|
||||||
|
*/
|
||||||
|
color_enabled: bool
|
||||||
|
|
||||||
|
/*
|
||||||
|
This value reports the color depth support as reported by the terminal at the
|
||||||
|
start of the program.
|
||||||
|
*/
|
||||||
|
color_depth: Color_Depth
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#+private
|
||||||
|
#+build linux, darwin, netbsd, openbsd, freebsd, haiku
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import "core:os"
|
||||||
|
import "core:sys/posix"
|
||||||
|
|
||||||
|
_is_terminal :: proc(handle: os.Handle) -> bool {
|
||||||
|
return bool(posix.isatty(posix.FD(handle)))
|
||||||
|
}
|
||||||
|
|
||||||
|
_init_terminal :: proc() {
|
||||||
|
color_depth = get_environment_color()
|
||||||
|
}
|
||||||
|
|
||||||
|
_fini_terminal :: proc() { }
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#+private
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import "core:os"
|
||||||
|
import "core:sys/windows"
|
||||||
|
|
||||||
|
_is_terminal :: proc(handle: os.Handle) -> bool {
|
||||||
|
is_tty := windows.GetFileType(windows.HANDLE(handle)) == windows.FILE_TYPE_CHAR
|
||||||
|
return is_tty
|
||||||
|
}
|
||||||
|
|
||||||
|
old_modes: [2]struct{
|
||||||
|
handle: windows.DWORD,
|
||||||
|
mode: windows.DWORD,
|
||||||
|
} = {
|
||||||
|
{windows.STD_OUTPUT_HANDLE, 0},
|
||||||
|
{windows.STD_ERROR_HANDLE, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
@(init)
|
||||||
|
_init_terminal :: proc() {
|
||||||
|
vtp_enabled: bool
|
||||||
|
|
||||||
|
for &v in old_modes {
|
||||||
|
handle := windows.GetStdHandle(v.handle)
|
||||||
|
if handle == windows.INVALID_HANDLE || handle == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if windows.GetConsoleMode(handle, &v.mode) {
|
||||||
|
windows.SetConsoleMode(handle, v.mode | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
||||||
|
|
||||||
|
new_mode: windows.DWORD
|
||||||
|
windows.GetConsoleMode(handle, &new_mode)
|
||||||
|
|
||||||
|
if new_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 {
|
||||||
|
vtp_enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if vtp_enabled {
|
||||||
|
// This color depth is available on Windows 10 since build 10586.
|
||||||
|
color_depth = .Four_Bit
|
||||||
|
} else {
|
||||||
|
// The user may be on a non-default terminal emulator.
|
||||||
|
color_depth = get_environment_color()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@(fini)
|
||||||
|
_fini_terminal :: proc() {
|
||||||
|
for v in old_modes {
|
||||||
|
handle := windows.GetStdHandle(v.handle)
|
||||||
|
if handle == windows.INVALID_HANDLE || handle == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
windows.SetConsoleMode(handle, v.mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,12 +10,12 @@ package testing
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import "base:runtime"
|
import "base:runtime"
|
||||||
import "core:encoding/ansi"
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:io"
|
import "core:io"
|
||||||
import "core:mem"
|
import "core:mem"
|
||||||
import "core:path/filepath"
|
import "core:path/filepath"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
import "core:terminal/ansi"
|
||||||
|
|
||||||
// Definitions of colors for use in the test runner.
|
// Definitions of colors for use in the test runner.
|
||||||
SGR_RESET :: ansi.CSI + ansi.RESET + ansi.SGR
|
SGR_RESET :: ansi.CSI + ansi.RESET + ansi.SGR
|
||||||
|
|||||||
+93
-69
@@ -13,7 +13,6 @@ package testing
|
|||||||
import "base:intrinsics"
|
import "base:intrinsics"
|
||||||
import "base:runtime"
|
import "base:runtime"
|
||||||
import "core:bytes"
|
import "core:bytes"
|
||||||
import "core:encoding/ansi"
|
|
||||||
@require import "core:encoding/base64"
|
@require import "core:encoding/base64"
|
||||||
@require import "core:encoding/json"
|
@require import "core:encoding/json"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
@@ -25,6 +24,8 @@ import "core:os"
|
|||||||
import "core:slice"
|
import "core:slice"
|
||||||
@require import "core:strings"
|
@require import "core:strings"
|
||||||
import "core:sync/chan"
|
import "core:sync/chan"
|
||||||
|
import "core:terminal"
|
||||||
|
import "core:terminal/ansi"
|
||||||
import "core:thread"
|
import "core:thread"
|
||||||
import "core:time"
|
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,...`
|
// The format is: `package.test_name,test_name_only,...`
|
||||||
TEST_NAMES : string : #config(ODIN_TEST_NAMES, "")
|
TEST_NAMES : string : #config(ODIN_TEST_NAMES, "")
|
||||||
// Show the fancy animated progress report.
|
// 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)
|
FANCY_OUTPUT : bool : #config(ODIN_TEST_FANCY, true)
|
||||||
// Copy failed tests to the clipboard when done.
|
// Copy failed tests to the clipboard when done.
|
||||||
USE_CLIPBOARD : bool : #config(ODIN_TEST_CLIPBOARD, false)
|
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 {
|
JSON :: struct {
|
||||||
total: int,
|
total: int,
|
||||||
success: int,
|
success: int,
|
||||||
@@ -129,11 +134,16 @@ run_test_task :: proc(task: thread.Task) {
|
|||||||
|
|
||||||
context.assertion_failure_proc = test_assertion_failure_proc
|
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 = {
|
context.logger = {
|
||||||
procedure = test_logger_proc,
|
procedure = test_logger_proc,
|
||||||
data = &data.t,
|
data = &data.t,
|
||||||
lowest_level = get_log_level(),
|
lowest_level = get_log_level(),
|
||||||
options = Default_Test_Logger_Opts,
|
options = logger_options,
|
||||||
}
|
}
|
||||||
|
|
||||||
random_generator_state: runtime.Default_Random_State
|
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))
|
stdout := io.to_writer(os.stream_from_handle(os.stdout))
|
||||||
stderr := io.to_writer(os.stream_from_handle(os.stderr))
|
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.
|
// -- Prepare test data.
|
||||||
|
|
||||||
alloc_error: mem.Allocator_Error
|
alloc_error: mem.Allocator_Error
|
||||||
@@ -268,12 +281,12 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
|
|||||||
total_done_count := 0
|
total_done_count := 0
|
||||||
total_test_count := len(internal_tests)
|
total_test_count := len(internal_tests)
|
||||||
|
|
||||||
when !FANCY_OUTPUT {
|
|
||||||
// This is strictly for updating the window title when the progress
|
// This is strictly for updating the window title when the progress
|
||||||
// report is disabled. We're otherwise able to depend on the call to
|
// report is disabled. We're otherwise able to depend on the call to
|
||||||
// `needs_to_redraw`.
|
// `needs_to_redraw`.
|
||||||
last_done_count := -1
|
last_done_count := -1
|
||||||
}
|
|
||||||
|
|
||||||
if total_test_count == 0 {
|
if total_test_count == 0 {
|
||||||
// Exit early.
|
// 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)
|
fmt.assertf(alloc_error == nil, "Error allocating memory for test report: %v", alloc_error)
|
||||||
defer destroy_report(&report)
|
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 = ---
|
// We cannot make use of the ANSI save/restore cursor codes, because they
|
||||||
{
|
// work by absolute screen coordinates. This will cause unnecessary
|
||||||
PADDING :: PROGRESS_COLUMN_SPACING + PROGRESS_WIDTH
|
// 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)
|
||||||
|
|
||||||
unpadded := fmt.tprintf("%i thread%s", thread_count, "" if thread_count == 1 else "s")
|
thread_count_status_string: string = ---
|
||||||
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.")
|
PADDING :: PROGRESS_COLUMN_SPACING + PROGRESS_WIDTH
|
||||||
}
|
|
||||||
defer delete(thread_count_status_string)
|
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: []Task_Data = ---
|
||||||
task_data_slots, alloc_error = make([]Task_Data, thread_count)
|
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.
|
// digging through the source to divine everywhere it is used for that.
|
||||||
shared_log_allocator := context.allocator
|
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 = {
|
context.logger = {
|
||||||
procedure = runner_logger_proc,
|
procedure = runner_logger_proc,
|
||||||
data = &log_messages,
|
data = &log_messages,
|
||||||
lowest_level = get_log_level(),
|
lowest_level = get_log_level(),
|
||||||
options = Default_Test_Logger_Opts - {.Short_File_Path, .Line, .Procedure},
|
options = logger_options,
|
||||||
}
|
}
|
||||||
|
|
||||||
run_index: int
|
run_index: int
|
||||||
@@ -481,11 +499,13 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
|
|||||||
|
|
||||||
setup_signal_handler()
|
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)
|
redraw_report(stdout, report)
|
||||||
draw_status_bar(stdout, thread_count_status_string, total_done_count, total_test_count)
|
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
|
break main_loop
|
||||||
}
|
}
|
||||||
|
|
||||||
when FANCY_OUTPUT {
|
|
||||||
// Because the bounds checking procs send directly to STDERR with
|
// Because the bounds checking procs send directly to STDERR with
|
||||||
// no way to redirect or handle them, we need to at least try to
|
// 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
|
// 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
|
// report. This flag may be set by the block of code below if a
|
||||||
// signal is raised.
|
// signal is raised.
|
||||||
//
|
//
|
||||||
// It'll be purely by luck if the output is interleaved properly,
|
// It'll be purely by luck if the output is interleaved properly,
|
||||||
// given the nature of non-thread-safe printing.
|
// given the nature of non-thread-safe printing.
|
||||||
//
|
//
|
||||||
// At worst, if Odin did not print any error for this signal, we'll
|
// 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
|
// just re-display the progress report. The fatal log error message
|
||||||
// should be enough to clue the user in that something dire has
|
// should be enough to clue the user in that something dire has
|
||||||
// occurred.
|
// occurred.
|
||||||
bypass_progress_overwrite := false
|
bypass_progress_overwrite := false
|
||||||
}
|
|
||||||
|
|
||||||
if test_index, reason, ok := should_stop_test(); ok {
|
if test_index, reason, ok := should_stop_test(); ok {
|
||||||
#no_bounds_check report.all_test_states[test_index] = .Failed
|
#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)
|
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
|
bypass_progress_overwrite = true
|
||||||
signals_were_raised = true
|
signals_were_raised = true
|
||||||
}
|
}
|
||||||
@@ -766,7 +786,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
|
|||||||
|
|
||||||
// -- Redraw.
|
// -- Redraw.
|
||||||
|
|
||||||
when FANCY_OUTPUT {
|
if should_show_animations {
|
||||||
if len(log_messages) == 0 && !needs_to_redraw(report) {
|
if len(log_messages) == 0 && !needs_to_redraw(report) {
|
||||||
continue main_loop
|
continue main_loop
|
||||||
}
|
}
|
||||||
@@ -776,7 +796,9 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if total_done_count != last_done_count {
|
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
|
last_done_count = total_done_count
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,7 +823,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
|
|||||||
clear(&log_messages)
|
clear(&log_messages)
|
||||||
bytes.buffer_reset(&batch_buffer)
|
bytes.buffer_reset(&batch_buffer)
|
||||||
|
|
||||||
when FANCY_OUTPUT {
|
if should_show_animations {
|
||||||
redraw_report(batch_writer, report)
|
redraw_report(batch_writer, report)
|
||||||
draw_status_bar(batch_writer, thread_count_status_string, total_done_count, total_test_count)
|
draw_status_bar(batch_writer, thread_count_status_string, total_done_count, total_test_count)
|
||||||
fmt.wprint(stdout, bytes.buffer_to_string(&batch_buffer))
|
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)
|
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
|
// One line to space out the results, since we don't have the status
|
||||||
// bar in plain mode.
|
// bar in plain mode.
|
||||||
fmt.wprintln(batch_writer)
|
fmt.wprintln(batch_writer)
|
||||||
@@ -836,24 +858,28 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
|
|||||||
|
|
||||||
if total_done_count != total_test_count {
|
if total_done_count != total_test_count {
|
||||||
not_run_count := total_test_count - total_done_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,
|
fmt.wprintf(batch_writer,
|
||||||
" " + SGR_READY + "%i" + SGR_RESET + " %s left undone.",
|
message,
|
||||||
not_run_count,
|
not_run_count,
|
||||||
"test was" if not_run_count == 1 else "tests were")
|
"test was" if not_run_count == 1 else "tests were")
|
||||||
}
|
}
|
||||||
|
|
||||||
if total_success_count == total_test_count {
|
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,
|
fmt.wprintfln(batch_writer,
|
||||||
" %s " + SGR_SUCCESS + "successful." + SGR_RESET,
|
message,
|
||||||
"The test was" if total_test_count == 1 else "All tests were")
|
"The test was" if total_test_count == 1 else "All tests were")
|
||||||
} else if total_failure_count > 0 {
|
} else if total_failure_count > 0 {
|
||||||
if total_failure_count == total_test_count {
|
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,
|
fmt.wprintfln(batch_writer,
|
||||||
" %s " + SGR_FAILED + "failed." + SGR_RESET,
|
message,
|
||||||
"The test" if total_test_count == 1 else "All tests")
|
"The test" if total_test_count == 1 else "All tests")
|
||||||
} else {
|
} else {
|
||||||
|
message := " %i test%s failed." if global_log_colors_disabled else " " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed."
|
||||||
fmt.wprintfln(batch_writer,
|
fmt.wprintfln(batch_writer,
|
||||||
" " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed.",
|
message,
|
||||||
total_failure_count,
|
total_failure_count,
|
||||||
"" if total_failure_count == 1 else "s")
|
"" 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 {
|
if signals_were_raised {
|
||||||
fmt.wprintln(batch_writer, `
|
fmt.wprintln(batch_writer, `
|
||||||
Signals were raised during this test run. Log messages are likely to have collided with each other.
|
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)
|
fmt.assertf(err == nil, "Error writing JSON report: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
when ODIN_OS == .Windows {
|
|
||||||
console_ansi_fini()
|
|
||||||
}
|
|
||||||
|
|
||||||
return total_success_count == total_test_count
|
return total_success_count == total_test_count
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
#+private
|
|
||||||
package testing
|
|
||||||
|
|
||||||
import win32 "core:sys/windows"
|
|
||||||
|
|
||||||
old_stdout_mode: u32
|
|
||||||
old_stderr_mode: u32
|
|
||||||
|
|
||||||
console_ansi_init :: proc() {
|
|
||||||
stdout := win32.GetStdHandle(win32.STD_OUTPUT_HANDLE)
|
|
||||||
if stdout != win32.INVALID_HANDLE && stdout != nil {
|
|
||||||
if win32.GetConsoleMode(stdout, &old_stdout_mode) {
|
|
||||||
win32.SetConsoleMode(stdout, old_stdout_mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr := win32.GetStdHandle(win32.STD_ERROR_HANDLE)
|
|
||||||
if stderr != win32.INVALID_HANDLE && stderr != nil {
|
|
||||||
if win32.GetConsoleMode(stderr, &old_stderr_mode) {
|
|
||||||
win32.SetConsoleMode(stderr, old_stderr_mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore the cursor on exit
|
|
||||||
console_ansi_fini :: proc() {
|
|
||||||
stdout := win32.GetStdHandle(win32.STD_OUTPUT_HANDLE)
|
|
||||||
if stdout != win32.INVALID_HANDLE && stdout != nil {
|
|
||||||
win32.SetConsoleMode(stdout, old_stdout_mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr := win32.GetStdHandle(win32.STD_ERROR_HANDLE)
|
|
||||||
if stderr != win32.INVALID_HANDLE && stderr != nil {
|
|
||||||
win32.SetConsoleMode(stderr, old_stderr_mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,9 +12,9 @@ package testing
|
|||||||
|
|
||||||
import "base:intrinsics"
|
import "base:intrinsics"
|
||||||
import "core:c/libc"
|
import "core:c/libc"
|
||||||
import "core:encoding/ansi"
|
|
||||||
import "core:sync"
|
|
||||||
import "core:os"
|
import "core:os"
|
||||||
|
import "core:sync"
|
||||||
|
import "core:terminal/ansi"
|
||||||
|
|
||||||
@(private="file") stop_runner_flag: libc.sig_atomic_t
|
@(private="file") stop_runner_flag: libc.sig_atomic_t
|
||||||
|
|
||||||
@@ -63,9 +63,11 @@ stop_test_callback :: proc "c" (sig: libc.int) {
|
|||||||
// NOTE(Feoramund): Using these write calls in a signal handler is
|
// NOTE(Feoramund): Using these write calls in a signal handler is
|
||||||
// undefined behavior in C99 but possibly tolerated in POSIX 2008.
|
// undefined behavior in C99 but possibly tolerated in POSIX 2008.
|
||||||
// Either way, we may as well try to salvage what we can.
|
// Either way, we may as well try to salvage what we can.
|
||||||
show_cursor := ansi.CSI + ansi.DECTCEM_SHOW
|
if !global_ansi_disabled {
|
||||||
libc.fwrite(raw_data(show_cursor), size_of(byte), len(show_cursor), libc.stdout)
|
show_cursor := ansi.CSI + ansi.DECTCEM_SHOW
|
||||||
libc.fflush(libc.stdout)
|
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.
|
// This is an attempt at being compliant by avoiding printf.
|
||||||
sigbuf: [8]byte
|
sigbuf: [8]byte
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ import trace "core:debug/trace"
|
|||||||
import dynlib "core:dynlib"
|
import dynlib "core:dynlib"
|
||||||
import net "core:net"
|
import net "core:net"
|
||||||
|
|
||||||
import ansi "core:encoding/ansi"
|
|
||||||
import base32 "core:encoding/base32"
|
import base32 "core:encoding/base32"
|
||||||
import base64 "core:encoding/base64"
|
import base64 "core:encoding/base64"
|
||||||
import cbor "core:encoding/cbor"
|
import cbor "core:encoding/cbor"
|
||||||
@@ -129,6 +128,9 @@ import strings "core:strings"
|
|||||||
import sync "core:sync"
|
import sync "core:sync"
|
||||||
import testing "core:testing"
|
import testing "core:testing"
|
||||||
|
|
||||||
|
import terminal "core:terminal"
|
||||||
|
import ansi "core:terminal/ansi"
|
||||||
|
|
||||||
import edit "core:text/edit"
|
import edit "core:text/edit"
|
||||||
import i18n "core:text/i18n"
|
import i18n "core:text/i18n"
|
||||||
import match "core:text/match"
|
import match "core:text/match"
|
||||||
@@ -201,7 +203,6 @@ _ :: pe
|
|||||||
_ :: trace
|
_ :: trace
|
||||||
_ :: dynlib
|
_ :: dynlib
|
||||||
_ :: net
|
_ :: net
|
||||||
_ :: ansi
|
|
||||||
_ :: base32
|
_ :: base32
|
||||||
_ :: base64
|
_ :: base64
|
||||||
_ :: csv
|
_ :: csv
|
||||||
@@ -257,6 +258,8 @@ _ :: strconv
|
|||||||
_ :: strings
|
_ :: strings
|
||||||
_ :: sync
|
_ :: sync
|
||||||
_ :: testing
|
_ :: testing
|
||||||
|
_ :: terminal
|
||||||
|
_ :: ansi
|
||||||
_ :: scanner
|
_ :: scanner
|
||||||
_ :: i18n
|
_ :: i18n
|
||||||
_ :: match
|
_ :: match
|
||||||
|
|||||||
Reference in New Issue
Block a user