mirror of
https://github.com/odin-lang/Odin.git
synced 2025-12-28 17:04:34 +00:00
This change was made in order to allow things produced with Odin and using Odin's core library, to not require the LICENSE to also be distributed alongside the binary form.
338 lines
9.1 KiB
Odin
338 lines
9.1 KiB
Odin
#+private
|
|
package testing
|
|
|
|
/*
|
|
(c) Copyright 2024 Feoramund <rune@swevencraft.org>.
|
|
Made available under Odin's license.
|
|
|
|
List of contributors:
|
|
Feoramund: Total rewrite.
|
|
*/
|
|
|
|
import "base:runtime"
|
|
import "core:fmt"
|
|
import "core:io"
|
|
import "core:mem"
|
|
import "core:path/filepath"
|
|
import "core:strings"
|
|
import "core:terminal/ansi"
|
|
|
|
// Definitions of colors for use in the test runner.
|
|
SGR_RESET :: ansi.CSI + ansi.RESET + ansi.SGR
|
|
SGR_READY :: ansi.CSI + ansi.FG_BRIGHT_BLACK + ansi.SGR
|
|
SGR_RUNNING :: ansi.CSI + ansi.FG_YELLOW + ansi.SGR
|
|
SGR_SUCCESS :: ansi.CSI + ansi.FG_GREEN + ansi.SGR
|
|
SGR_FAILED :: ansi.CSI + ansi.FG_RED + ansi.SGR
|
|
|
|
MAX_PROGRESS_WIDTH :: 100
|
|
|
|
// More than enough bytes to cover long package names, long test names, dozens
|
|
// of ANSI codes, et cetera.
|
|
LINE_BUFFER_SIZE :: (MAX_PROGRESS_WIDTH * 8 + 224) * runtime.Byte
|
|
|
|
PROGRESS_COLUMN_SPACING :: 2
|
|
|
|
Package_Run :: struct {
|
|
name: string,
|
|
header: string,
|
|
|
|
frame_ready: bool,
|
|
|
|
redraw_buffer: [LINE_BUFFER_SIZE]byte,
|
|
redraw_string: string,
|
|
|
|
last_change_state: Test_State,
|
|
last_change_name: string,
|
|
|
|
tests: []Internal_Test,
|
|
test_states: []Test_State,
|
|
}
|
|
|
|
Report :: struct {
|
|
packages: []Package_Run,
|
|
packages_by_name: map[string]^Package_Run,
|
|
|
|
pkg_column_len: int,
|
|
test_column_len: int,
|
|
progress_width: int,
|
|
|
|
all_tests: []Internal_Test,
|
|
all_test_states: []Test_State,
|
|
}
|
|
|
|
// Organize all tests by package and sort out test state data.
|
|
make_report :: proc(internal_tests: []Internal_Test) -> (report: Report, error: runtime.Allocator_Error) {
|
|
assert(len(internal_tests) > 0, "make_report called with no tests")
|
|
|
|
packages: [dynamic]Package_Run
|
|
|
|
report.all_tests = internal_tests
|
|
report.all_test_states = make([]Test_State, len(internal_tests)) or_return
|
|
|
|
// First, figure out what belongs where.
|
|
#no_bounds_check cur_pkg := internal_tests[0].pkg
|
|
pkg_start: int
|
|
|
|
// This loop assumes the tests are sorted by package already.
|
|
for it, index in internal_tests {
|
|
if cur_pkg != it.pkg {
|
|
#no_bounds_check {
|
|
append(&packages, Package_Run {
|
|
name = cur_pkg,
|
|
tests = report.all_tests[pkg_start:index],
|
|
test_states = report.all_test_states[pkg_start:index],
|
|
}) or_return
|
|
}
|
|
|
|
when PROGRESS_WIDTH == 0 {
|
|
report.progress_width = max(report.progress_width, index - pkg_start)
|
|
}
|
|
|
|
pkg_start = index
|
|
report.pkg_column_len = max(report.pkg_column_len, len(cur_pkg))
|
|
cur_pkg = it.pkg
|
|
}
|
|
report.test_column_len = max(report.test_column_len, len(it.name))
|
|
}
|
|
|
|
// Handle the last (or only) package.
|
|
#no_bounds_check {
|
|
append(&packages, Package_Run {
|
|
name = cur_pkg,
|
|
header = cur_pkg,
|
|
tests = report.all_tests[pkg_start:],
|
|
test_states = report.all_test_states[pkg_start:],
|
|
}) or_return
|
|
}
|
|
when PROGRESS_WIDTH == 0 {
|
|
report.progress_width = max(report.progress_width, len(internal_tests) - pkg_start)
|
|
} else {
|
|
report.progress_width = PROGRESS_WIDTH
|
|
}
|
|
report.progress_width = min(report.progress_width, MAX_PROGRESS_WIDTH)
|
|
|
|
report.pkg_column_len = PROGRESS_COLUMN_SPACING + max(report.pkg_column_len, len(cur_pkg))
|
|
|
|
shrink(&packages) or_return
|
|
|
|
for &pkg in packages {
|
|
pkg.header = fmt.aprintf("%- *[1]s[", pkg.name, report.pkg_column_len)
|
|
assert(len(pkg.header) > 0, "Error allocating package header string.")
|
|
|
|
// This is safe because the array is done resizing, and it has the same
|
|
// lifetime as the map.
|
|
report.packages_by_name[pkg.name] = &pkg
|
|
}
|
|
|
|
// It's okay to discard the dynamic array's allocator information here,
|
|
// because its capacity has been shrunk to its length, it was allocated by
|
|
// the caller's context allocator, and it will be deallocated by the same.
|
|
//
|
|
// `delete_slice` is equivalent to `delete_dynamic_array` in this case.
|
|
report.packages = packages[:]
|
|
|
|
return
|
|
}
|
|
|
|
destroy_report :: proc(report: ^Report) {
|
|
for pkg in report.packages {
|
|
delete(pkg.header)
|
|
}
|
|
|
|
delete(report.packages)
|
|
delete(report.packages_by_name)
|
|
delete(report.all_test_states)
|
|
}
|
|
|
|
redraw_package :: proc(w: io.Writer, report: Report, pkg: ^Package_Run) {
|
|
if pkg.frame_ready {
|
|
io.write_string(w, pkg.redraw_string)
|
|
return
|
|
}
|
|
|
|
// Write the output line here so we can cache it.
|
|
line_builder := strings.builder_from_bytes(pkg.redraw_buffer[:])
|
|
line_writer := strings.to_writer(&line_builder)
|
|
|
|
highest_run_index: int
|
|
failed_count: int
|
|
done_count: int
|
|
#no_bounds_check for i := 0; i < len(pkg.test_states); i += 1 {
|
|
switch pkg.test_states[i] {
|
|
case .Ready:
|
|
continue
|
|
case .Running:
|
|
highest_run_index = max(highest_run_index, i)
|
|
case .Successful:
|
|
done_count += 1
|
|
case .Failed:
|
|
failed_count += 1
|
|
done_count += 1
|
|
}
|
|
}
|
|
|
|
start := max(0, highest_run_index - (report.progress_width - 1))
|
|
end := min(start + report.progress_width, len(pkg.test_states))
|
|
|
|
// This variable is to keep track of the last ANSI code emitted, in
|
|
// order to avoid repeating the same code over in a sequence.
|
|
//
|
|
// This should help reduce screen flicker.
|
|
last_state := Test_State(-1)
|
|
|
|
io.write_string(line_writer, pkg.header)
|
|
|
|
#no_bounds_check for state in pkg.test_states[start:end] {
|
|
switch state {
|
|
case .Ready:
|
|
if last_state != state {
|
|
io.write_string(line_writer, SGR_READY)
|
|
last_state = state
|
|
}
|
|
case .Running:
|
|
if last_state != state {
|
|
io.write_string(line_writer, SGR_RUNNING)
|
|
last_state = state
|
|
}
|
|
case .Successful:
|
|
if last_state != state {
|
|
io.write_string(line_writer, SGR_SUCCESS)
|
|
last_state = state
|
|
}
|
|
case .Failed:
|
|
if last_state != state {
|
|
io.write_string(line_writer, SGR_FAILED)
|
|
last_state = state
|
|
}
|
|
}
|
|
io.write_byte(line_writer, '|')
|
|
}
|
|
|
|
for _ in 0 ..< report.progress_width - (end - start) {
|
|
io.write_byte(line_writer, ' ')
|
|
}
|
|
|
|
io.write_string(line_writer, SGR_RESET + "] ")
|
|
|
|
ticker: string
|
|
if done_count == len(pkg.test_states) {
|
|
ticker = "[package done]"
|
|
if failed_count > 0 {
|
|
ticker = fmt.tprintf("%s (" + SGR_FAILED + "%i" + SGR_RESET + " failed)", ticker, failed_count)
|
|
}
|
|
} else {
|
|
if len(pkg.last_change_name) == 0 {
|
|
#no_bounds_check pkg.last_change_name = pkg.tests[0].name
|
|
}
|
|
|
|
switch pkg.last_change_state {
|
|
case .Ready:
|
|
ticker = fmt.tprintf(SGR_READY + "%s" + SGR_RESET, pkg.last_change_name)
|
|
case .Running:
|
|
ticker = fmt.tprintf(SGR_RUNNING + "%s" + SGR_RESET, pkg.last_change_name)
|
|
case .Failed:
|
|
ticker = fmt.tprintf(SGR_FAILED + "%s" + SGR_RESET, pkg.last_change_name)
|
|
case .Successful:
|
|
ticker = fmt.tprintf(SGR_SUCCESS + "%s" + SGR_RESET, pkg.last_change_name)
|
|
}
|
|
}
|
|
|
|
if done_count == len(pkg.test_states) {
|
|
fmt.wprintfln(line_writer, " % 4i :: %s",
|
|
len(pkg.test_states),
|
|
ticker,
|
|
)
|
|
} else {
|
|
fmt.wprintfln(line_writer, "% 4i/% 4i :: %s",
|
|
done_count,
|
|
len(pkg.test_states),
|
|
ticker,
|
|
)
|
|
}
|
|
|
|
pkg.redraw_string = strings.to_string(line_builder)
|
|
pkg.frame_ready = true
|
|
io.write_string(w, pkg.redraw_string)
|
|
}
|
|
|
|
redraw_report :: proc(w: io.Writer, report: Report) {
|
|
// If we print a line longer than the user's terminal can handle, it may
|
|
// wrap around, shifting the progress report out of alignment.
|
|
//
|
|
// There are ways to get the current terminal width, and that would be the
|
|
// ideal way to handle this, but it would require system-specific code such
|
|
// as setting STDIN to be non-blocking in order to read the response from
|
|
// the ANSI DSR escape code, or reading environment variables.
|
|
//
|
|
// The DECAWM escape codes control whether or not the terminal will wrap
|
|
// long lines or overwrite the last visible character.
|
|
// This should be fine for now.
|
|
//
|
|
// Note that we only do this for the animated summary; log messages are
|
|
// still perfectly fine to wrap, as they're printed in their own batch,
|
|
// whereas the animation depends on each package being only on one line.
|
|
//
|
|
// Of course, if you resize your terminal while it's printing, things can
|
|
// still break...
|
|
fmt.wprint(w, ansi.CSI + ansi.DECAWM_OFF)
|
|
for &pkg in report.packages {
|
|
redraw_package(w, report, &pkg)
|
|
}
|
|
fmt.wprint(w, ansi.CSI + ansi.DECAWM_ON)
|
|
}
|
|
|
|
needs_to_redraw :: proc(report: Report) -> bool {
|
|
for pkg in report.packages {
|
|
if !pkg.frame_ready {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
draw_status_bar :: proc(w: io.Writer, threads_string: string, total_done_count, total_test_count: int) {
|
|
if total_done_count == total_test_count {
|
|
// All tests are done; print a blank line to maintain the same height
|
|
// of the progress report.
|
|
fmt.wprintln(w)
|
|
} else {
|
|
fmt.wprintfln(w,
|
|
"%s % 4i/% 4i :: total",
|
|
threads_string,
|
|
total_done_count,
|
|
total_test_count)
|
|
}
|
|
}
|
|
|
|
write_memory_report :: proc(w: io.Writer, tracker: ^mem.Tracking_Allocator, pkg, name: string) {
|
|
fmt.wprintf(w,
|
|
"<% 10M/% 10M> <% 10M> (% 5i/% 5i) :: %s.%s",
|
|
tracker.current_memory_allocated,
|
|
tracker.total_memory_allocated,
|
|
tracker.peak_memory_allocated,
|
|
tracker.total_free_count,
|
|
tracker.total_allocation_count,
|
|
pkg,
|
|
name)
|
|
|
|
for ptr, entry in tracker.allocation_map {
|
|
fmt.wprintf(w,
|
|
"\n +++ leak % 10M @ %p [%s:%i:%s()]",
|
|
entry.size,
|
|
ptr,
|
|
filepath.base(entry.location.file_path),
|
|
entry.location.line,
|
|
entry.location.procedure)
|
|
}
|
|
|
|
for entry in tracker.bad_free_array {
|
|
fmt.wprintf(w,
|
|
"\n +++ bad free @ %p [%s:%i:%s()]",
|
|
entry.memory,
|
|
filepath.base(entry.location.file_path),
|
|
entry.location.line,
|
|
entry.location.procedure)
|
|
}
|
|
}
|