From 23019020a1f4c3205e624b26f256865fa2a7210a Mon Sep 17 00:00:00 2001 From: Harold Brenes Date: Mon, 19 Jan 2026 10:51:59 -0500 Subject: [PATCH 01/11] Add runtime options for test runner via command line --- core/testing/runner.odin | 64 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/core/testing/runner.odin b/core/testing/runner.odin index 8f26aa6c6..f3dcc8a1b 100644 --- a/core/testing/runner.odin +++ b/core/testing/runner.odin @@ -196,6 +196,57 @@ run_test_task :: proc(task: thread.Task) { }) } +Options :: struct { + // Equivalent to the TEST_NAMES compile-time definition, but used dynamically at runtime. + test_names: string, +} + +parse_cli_options :: proc(argv: []string, opts: ^Options, stdout, stderr: io.Writer) { + argv:=argv[1:] + test_names: strings.Builder + + for i := 0; i < len(argv); i+=1 { + arg := argv[i] + if strings.starts_with(arg, "-tests:") { + tests := arg[strings.index(arg, ":")+1:] + if len(tests) < 1 { + fmt.wprintln(stderr, "No test names specified for '-tests:'") + os.exit(-1) + } + + if len(test_names.buf) > 0 { + strings.write_byte(&test_names, ',') + } + strings.write_bytes(&test_names, transmute([]u8)tests) + } + else if arg == "-help" { + exe_name := "test" + if path, err := os2.get_executable_path(context.temp_allocator); err == nil { + exe_name = slashpath.base(path) + } + + fmt.wprintfln(stdout, "Usage: %v [OPTIONS]", exe_name) + fmt.wprintfln(stdout, "OPTIONS:") + fmt.wprintfln(stdout, " -help:") + fmt.wprintfln(stdout, " Display this help text and exit.") + fmt.wprintln(stdout) + fmt.wprintfln(stdout, " -tests:") + fmt.wprintfln(stdout, " Specify a specific set of tests to run by name.\n" + + " Each test is separated by a comma and may optionally include the package name.\n" + + " This may be useful when running tests on multiple packages with `-all-packages`.\n" + + " The format is: `package.test_name,test_name_only,...`") + fmt.wprintln(stdout) + os.exit(0) + } + else { + fmt.wprintfln(stderr, "Unknown argument encountered '%v'", arg) + os.exit(-1) + } + } + + opts.test_names = string(test_names.buf[:]) +} + runner :: proc(internal_tests: []Internal_Test) -> bool { BATCH_BUFFER_SIZE :: 32 * mem.Kilobyte POOL_BLOCK_SIZE :: 16 * mem.Kilobyte @@ -229,16 +280,25 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { should_show_animations := FANCY_OUTPUT && terminal.color_enabled && !global_ansi_disabled + // -- Parse CLI options + opts: Options + parse_cli_options(os.args, &opts, stdout, stderr) + + test_names: string = TEST_NAMES + if len(opts.test_names) > 0 { + test_names = opts.test_names + } + // -- Prepare test data. alloc_error: mem.Allocator_Error - when TEST_NAMES != "" { + if test_names != "" { select_internal_tests: [dynamic]Internal_Test defer delete(select_internal_tests) { - index_list := TEST_NAMES + 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) From d56e655be73acb534c3636930980a4918af79284 Mon Sep 17 00:00:00 2001 From: Harold Brenes Date: Mon, 19 Jan 2026 11:17:32 -0500 Subject: [PATCH 02/11] Fix scoping issues caused by switching from `when` to `if` --- core/testing/runner.odin | 76 ++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/core/testing/runner.odin b/core/testing/runner.odin index f3dcc8a1b..e267d13c4 100644 --- a/core/testing/runner.odin +++ b/core/testing/runner.odin @@ -202,7 +202,6 @@ Options :: struct { } parse_cli_options :: proc(argv: []string, opts: ^Options, stdout, stderr: io.Writer) { - argv:=argv[1:] test_names: strings.Builder for i := 0; i < len(argv); i+=1 { @@ -270,6 +269,12 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { } } + + // `-vet` needs parameters to be shadowed by themselves first as an + // explicit declaration, to allow the next line to work. + // NOTE(@harold): Moved out of scope below as it's no longer under a `when` block, but under `if`. + internal_tests := internal_tests + stdout := os.to_stream(os.stdout) stderr := os.to_stream(os.stderr) @@ -282,7 +287,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { // -- Parse CLI options opts: Options - parse_cli_options(os.args, &opts, stdout, stderr) + parse_cli_options(os.args[1:], &opts, stdout, stderr) test_names: string = TEST_NAMES if len(opts.test_names) > 0 { @@ -293,50 +298,45 @@ runner :: proc(internal_tests: []Internal_Test) -> bool { alloc_error: mem.Allocator_Error + select_internal_tests: [dynamic]Internal_Test + defer delete(select_internal_tests) + if 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) - { - 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 - } + 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 } } - if !found { - fmt.wprintfln(stderr, "No test found for the name: %q", selector) + 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) + } } - // `-vet` needs parameters to be shadowed by themselves first as an - // explicit declaration, to allow the next line to work. - internal_tests := internal_tests // Intentional shadow with user-specified tests. internal_tests = select_internal_tests[:] } From 167afbc75d1e451a3cbf3b9739af6696a33d56d7 Mon Sep 17 00:00:00 2001 From: Harold Brenes Date: Thu, 5 Feb 2026 10:34:20 -0500 Subject: [PATCH 03/11] Fix formatting --- core/testing/runner.odin | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/testing/runner.odin b/core/testing/runner.odin index e267d13c4..8e1689df5 100644 --- a/core/testing/runner.odin +++ b/core/testing/runner.odin @@ -217,8 +217,7 @@ parse_cli_options :: proc(argv: []string, opts: ^Options, stdout, stderr: io.Wri strings.write_byte(&test_names, ',') } strings.write_bytes(&test_names, transmute([]u8)tests) - } - else if arg == "-help" { + } else if arg == "-help" { exe_name := "test" if path, err := os2.get_executable_path(context.temp_allocator); err == nil { exe_name = slashpath.base(path) @@ -236,8 +235,7 @@ parse_cli_options :: proc(argv: []string, opts: ^Options, stdout, stderr: io.Wri " The format is: `package.test_name,test_name_only,...`") fmt.wprintln(stdout) os.exit(0) - } - else { + } else { fmt.wprintfln(stderr, "Unknown argument encountered '%v'", arg) os.exit(-1) } From 3aed99093b32f8733692146ec015952b72e2b1c0 Mon Sep 17 00:00:00 2001 From: Harold Brenes Date: Wed, 11 Feb 2026 13:46:19 -0500 Subject: [PATCH 04/11] Rebase and update to reflect `os2` -> `os` --- core/testing/runner.odin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/testing/runner.odin b/core/testing/runner.odin index 8e1689df5..97f17b199 100644 --- a/core/testing/runner.odin +++ b/core/testing/runner.odin @@ -219,7 +219,7 @@ parse_cli_options :: proc(argv: []string, opts: ^Options, stdout, stderr: io.Wri strings.write_bytes(&test_names, transmute([]u8)tests) } else if arg == "-help" { exe_name := "test" - if path, err := os2.get_executable_path(context.temp_allocator); err == nil { + if path, err := os.get_executable_path(context.temp_allocator); err == nil { exe_name = slashpath.base(path) } From 557a1911085240184c39816f8de83fd7802fa943 Mon Sep 17 00:00:00 2001 From: Harold Brenes Date: Wed, 11 Feb 2026 13:51:51 -0500 Subject: [PATCH 05/11] Fix missing import after rebase --- core/testing/runner.odin | 1 + 1 file changed, 1 insertion(+) diff --git a/core/testing/runner.odin b/core/testing/runner.odin index 97f17b199..a2e21235f 100644 --- a/core/testing/runner.odin +++ b/core/testing/runner.odin @@ -28,6 +28,7 @@ import "core:terminal" import "core:terminal/ansi" import "core:thread" import "core:time" +import "core:path/slashpath" // Specify how many threads to use when running tests. TEST_THREADS : int : #config(ODIN_TEST_THREADS, 0) From cac90a84fcc8c587724494ab6cd0295b1b3aadc4 Mon Sep 17 00:00:00 2001 From: Harold Brenes Date: Wed, 11 Feb 2026 14:09:33 -0500 Subject: [PATCH 06/11] Update core/testing/runner.odin for each style loop Co-authored-by: Laytan --- core/testing/runner.odin | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/testing/runner.odin b/core/testing/runner.odin index a2e21235f..f4cab2815 100644 --- a/core/testing/runner.odin +++ b/core/testing/runner.odin @@ -205,8 +205,7 @@ Options :: struct { parse_cli_options :: proc(argv: []string, opts: ^Options, stdout, stderr: io.Writer) { test_names: strings.Builder - for i := 0; i < len(argv); i+=1 { - arg := argv[i] + for arg in argv { if strings.starts_with(arg, "-tests:") { tests := arg[strings.index(arg, ":")+1:] if len(tests) < 1 { From f50dd2f639bf92f746067dca891a4c6fbfe92650 Mon Sep 17 00:00:00 2001 From: Harold Brenes Date: Wed, 11 Feb 2026 14:13:49 -0500 Subject: [PATCH 07/11] Update core/testing/runner.odin Remove formatting `wprintln` variant where it is not needed Co-authored-by: Laytan --- core/testing/runner.odin | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/testing/runner.odin b/core/testing/runner.odin index f4cab2815..880c21327 100644 --- a/core/testing/runner.odin +++ b/core/testing/runner.odin @@ -225,11 +225,11 @@ parse_cli_options :: proc(argv: []string, opts: ^Options, stdout, stderr: io.Wri fmt.wprintfln(stdout, "Usage: %v [OPTIONS]", exe_name) fmt.wprintfln(stdout, "OPTIONS:") - fmt.wprintfln(stdout, " -help:") - fmt.wprintfln(stdout, " Display this help text and exit.") + fmt.wprintln(stdout, " -help:") + fmt.wprintln(stdout, " Display this help text and exit.") fmt.wprintln(stdout) - fmt.wprintfln(stdout, " -tests:") - fmt.wprintfln(stdout, " Specify a specific set of tests to run by name.\n" + + fmt.wprintln(stdout, " -tests:") + fmt.wprintln(stdout, " Specify a specific set of tests to run by name.\n" + " Each test is separated by a comma and may optionally include the package name.\n" + " This may be useful when running tests on multiple packages with `-all-packages`.\n" + " The format is: `package.test_name,test_name_only,...`") From 7ea074a2486084f5ea058d401af55fd73ca66c7a Mon Sep 17 00:00:00 2001 From: Harold Brenes Date: Wed, 11 Feb 2026 14:14:27 -0500 Subject: [PATCH 08/11] Update core/testing/runner.odin Write string directly Co-authored-by: Laytan --- core/testing/runner.odin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/testing/runner.odin b/core/testing/runner.odin index 880c21327..d4dc2408c 100644 --- a/core/testing/runner.odin +++ b/core/testing/runner.odin @@ -216,7 +216,7 @@ parse_cli_options :: proc(argv: []string, opts: ^Options, stdout, stderr: io.Wri if len(test_names.buf) > 0 { strings.write_byte(&test_names, ',') } - strings.write_bytes(&test_names, transmute([]u8)tests) + strings.write_string(&test_names, tests) } else if arg == "-help" { exe_name := "test" if path, err := os.get_executable_path(context.temp_allocator); err == nil { From 3b8cb7fc619bc857e6c98edc34326b46af92b6bc Mon Sep 17 00:00:00 2001 From: Harold Brenes Date: Wed, 11 Feb 2026 14:14:58 -0500 Subject: [PATCH 09/11] Update core/testing/runner.odin Use `strings.to_string` Co-authored-by: Laytan --- core/testing/runner.odin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/testing/runner.odin b/core/testing/runner.odin index d4dc2408c..b3ac03483 100644 --- a/core/testing/runner.odin +++ b/core/testing/runner.odin @@ -241,7 +241,7 @@ parse_cli_options :: proc(argv: []string, opts: ^Options, stdout, stderr: io.Wri } } - opts.test_names = string(test_names.buf[:]) + opts.test_names = strings.to_string(test_names) } runner :: proc(internal_tests: []Internal_Test) -> bool { From 2899d09003f3e56b31225c9761970ba3a7dea395 Mon Sep 17 00:00:00 2001 From: Harold Brenes Date: Wed, 11 Feb 2026 14:15:34 -0500 Subject: [PATCH 10/11] Use `strings.partition` instead of raw slicing Co-authored-by: Laytan --- core/testing/runner.odin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/testing/runner.odin b/core/testing/runner.odin index b3ac03483..020c40d0f 100644 --- a/core/testing/runner.odin +++ b/core/testing/runner.odin @@ -207,7 +207,7 @@ parse_cli_options :: proc(argv: []string, opts: ^Options, stdout, stderr: io.Wri for arg in argv { if strings.starts_with(arg, "-tests:") { - tests := arg[strings.index(arg, ":")+1:] + _, _, tests := strings.partition(arg, ":") if len(tests) < 1 { fmt.wprintln(stderr, "No test names specified for '-tests:'") os.exit(-1) From 0ee15453e7fbb0de59a2983343538f540c9e0fd2 Mon Sep 17 00:00:00 2001 From: Harold Brenes Date: Wed, 11 Feb 2026 14:16:25 -0500 Subject: [PATCH 11/11] Use `builder_len` instead of `len` on the underlying dynamic array Co-authored-by: Laytan --- core/testing/runner.odin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/testing/runner.odin b/core/testing/runner.odin index 020c40d0f..6fd243cd0 100644 --- a/core/testing/runner.odin +++ b/core/testing/runner.odin @@ -213,7 +213,7 @@ parse_cli_options :: proc(argv: []string, opts: ^Options, stdout, stderr: io.Wri os.exit(-1) } - if len(test_names.buf) > 0 { + if strings.builder_len(test_names) > 0 { strings.write_byte(&test_names, ',') } strings.write_string(&test_names, tests)