Skip to content

btest: Introduce globbing for targets and tests #180

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jul 5, 2025
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ RSS=\
$(SRC)/b.rs \
$(SRC)/crust.rs \
$(SRC)/flag.rs \
$(SRC)/glob.rs \
$(SRC)/lexer.rs \
$(SRC)/nob.rs \
$(SRC)/targets.rs \
Expand All @@ -38,6 +39,7 @@ RSS=\
POSIX_OBJS=\
$(BUILD)/nob.posix.o \
$(BUILD)/flag.posix.o \
$(BUILD)/glob.posix.o \
$(BUILD)/libc.posix.o \
$(BUILD)/arena.posix.o \
$(BUILD)/fake6502.posix.o \
Expand All @@ -47,6 +49,7 @@ POSIX_OBJS=\
MINGW32_OBJS=\
$(BUILD)/nob.mingw32.o \
$(BUILD)/flag.mingw32.o \
$(BUILD)/glob.mingw32.o \
$(BUILD)/libc.mingw32.o \
$(BUILD)/arena.mingw32.o \
$(BUILD)/fake6502.mingw32.o \
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,36 +44,42 @@ It doesn't crash when it encounters errors, it just collects the statuses of the

### Slicing the Test Matrix

If you want to test only on a specific platform you can supply the flag `-t`
If you want to test only on a specific platform you can supply the flag `-t`.

```console
$ ./build/btest -t fasm-x86_64-linux
```

You can supply several platforms
You can supply several platforms.

```console
$ ./build/btest -t fasm-x86_64-linux -t uxn
```

If you want to run a specific test case you can supply flag `-c`
If you want to run a specific test case you can supply flag `-c`.

```console
$ ./build/btest -c upper
```

You can do several tests
You can do several tests.

```console
$ ./build/btest -c upper -c vector
```

And of course you can combine both `-c` and `-t` flags to slice the Test Matrix however you want
And of course you can combine both `-c` and `-t` flags to slice the Test Matrix however you want.

```console
$ ./build/btest -c upper -c vector -t fasm-x86_64-linux -t uxn
```

Both flags accept [glob](https://en.wikipedia.org/wiki/Glob_(programming)) patterns.

```console
$ ./build/btest -t *linux -c *linux
```

## References

- https://en.wikipedia.org/wiki/B_(programming_language)
Expand Down
126 changes: 83 additions & 43 deletions src/btest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod nob;
pub mod targets;
pub mod runner;
pub mod flag;
pub mod glob;
pub mod jim;
pub mod jimp;

Expand All @@ -23,6 +24,7 @@ use nob::*;
use targets::*;
use runner::mos6502::{Config, DEFAULT_LOAD_OFFSET};
use flag::*;
use glob::*;
use jim::*;
use jimp::*;

Expand Down Expand Up @@ -275,6 +277,25 @@ pub unsafe fn print_bottom_labels(targets: *const [Target], stats_by_target: *co
}
}

pub unsafe fn matches_glob(pattern: *const c_char, text: *const c_char) -> Option<bool> {
let mark = temp_save();
let result = match glob_utf8(pattern, text) {
Glob_Result::MATCHED => Ok(true),
Glob_Result::UNMATCHED => Ok(false),
Glob_Result::OOM_ERROR => Err(c!("out of memory")),
Glob_Result::ENCODING_ERROR => Err(c!("encoding error")),
Glob_Result::SYNTAX_ERROR => Err(c!("syntax error")),
};
temp_rewind(mark);
match result {
Ok(result) => Some(result),
Err(error) => {
fprintf(stderr(), c!("ERROR: while matching pattern `%s`: %s\n"), pattern, error);
None
}
}
}

pub unsafe fn record_tests(
// Inputs
test_folder: *const c_char, cases: *const [*const c_char], targets: *const [Target], bat: *mut Bat,
Expand Down Expand Up @@ -554,17 +575,17 @@ pub unsafe fn replay_tests(
}

pub unsafe fn main(argc: i32, argv: *mut*mut c_char) -> Option<()> {
let target_flags = flag_list(c!("t"), c!("Compilation targets to test on"));
let exclude_target_flags = flag_list(c!("xt"), c!("Compilation targets to exclude from testing"));
let list_targets = flag_bool(c!("tlist"), false, c!("Print the list of compilation targets"));
let cases_flags = flag_list(c!("c"), c!("Test cases"));
let list_cases = flag_bool(c!("clist"), false, c!("Print the list of test cases"));
let target_flags = flag_list(c!("t"), c!("Compilation targets to select for testing. Can be a glob pattern."));
let exclude_target_flags = flag_list(c!("xt"), c!("Compilation targets to exclude from selected ones. Can be a glob pattern"));
let list_targets = flag_bool(c!("tlist"), false, c!("Print the list of selected compilation targets."));

let cases_flags = flag_list(c!("c"), c!("Test cases to select for testing. Can be a glob pattern."));
// TODO: introduce -xc flag similar to -xt
let list_cases = flag_bool(c!("clist"), false, c!("Print the list of selected test cases"));

let test_folder = flag_str(c!("dir"), c!("./tests/"), c!("Test folder"));
let help = flag_bool(c!("help"), false, c!("Print this help message"));
let record = flag_bool(c!("record"), false, c!("Record test cases instead of replaying them"));
// TODO: introduce -xc flag
// TODO: select test cases and targets by a glob pattern
// See if https://github.com/tsoding/glob.h can be used here

if !flag_parse(argc, argv) {
usage();
Expand All @@ -585,54 +606,73 @@ pub unsafe fn main(argc: i32, argv: *mut*mut c_char) -> Option<()> {
let mut reports: Array<Report> = zeroed();
let mut stats_by_target: Array<ReportStats> = zeroed();

let mut exclude_targets: Array<Target> = zeroed();
for j in 0..(*exclude_target_flags).count {
let target_name = *(*exclude_target_flags).items.add(j);
if let Some(target) = Target::by_name(target_name) {
da_append(&mut exclude_targets, target)
} else {
fprintf(stderr(), c!("ERROR: unknown target `%s`\n"), target_name);
return None;
}
}
let mut targets: Array<Target> = zeroed();
let mut selected_targets: Array<Target> = zeroed();
if (*target_flags).count == 0 {
for j in 0..TARGET_ORDER.len() {
let target = (*TARGET_ORDER)[j];
if !slice_contains(da_slice(exclude_targets), &target) {
da_append(&mut targets, target)
}
}
da_append_many(&mut selected_targets, TARGET_ORDER);
} else {
for j in 0..(*target_flags).count {
let target_name = *(*target_flags).items.add(j);
if let Some(target) = Target::by_name(target_name) {
if !slice_contains(da_slice(exclude_targets), &target) {
da_append(&mut targets, target)
let mut added_anything = false;
let pattern = *(*target_flags).items.add(j);
for j in 0..TARGET_ORDER.len() {
let target = (*TARGET_ORDER)[j];
let name = target.name();
if matches_glob(pattern, name)? {
da_append(&mut selected_targets, target);
added_anything = true;
}
} else {
fprintf(stderr(), c!("ERROR: unknown target `%s`\n"), target_name);
}
if !added_anything {
fprintf(stderr(), c!("ERROR: unknown target `%s`\n"), pattern);
return None;
}
}
}
let mut targets: Array<Target> = zeroed();
for i in 0..selected_targets.count {
let target = *selected_targets.items.add(i);
let mut matches_any = false;
'exclude: for j in 0..(*exclude_target_flags).count {
let pattern = *(*exclude_target_flags).items.add(j);
if matches_glob(pattern, target.name())? {
matches_any = true;
break 'exclude;
}
}
if !matches_any {
da_append(&mut targets, target);
}
}

let mut all_cases: Array<*const c_char> = zeroed();

let mut test_files: File_Paths = zeroed();
if !read_entire_dir(*test_folder, &mut test_files) { return None; }
qsort(test_files.items as *mut c_void, test_files.count, size_of::<*const c_char>(), compar_cstr);

for i in 0..test_files.count {
let test_file = *test_files.items.add(i);
if *test_file == '.' as c_char { continue; }
let Some(case_name) = temp_strip_suffix(test_file, c!(".b")) else { continue; };
da_append(&mut all_cases, case_name);
}

let mut cases: Array<*const c_char> = zeroed();
if (*cases_flags).count == 0 {
let mut case_files: File_Paths = zeroed();
if !read_entire_dir(*test_folder, &mut case_files) { return None; } // TODO: memory leak. The file names are strduped to temp, but the File_Paths dynamic array itself is still allocated on the heap
qsort(case_files.items as *mut c_void, case_files.count, size_of::<*const c_char>(), compar_cstr);

for i in 0..case_files.count {
let case_file = *case_files.items.add(i);
if *case_file == '.' as c_char { continue; }
let Some(case_name) = temp_strip_suffix(case_file, c!(".b")) else { continue; };
da_append(&mut cases, case_name);
}
cases = all_cases;
} else {
for i in 0..(*cases_flags).count {
let case_name = *(*cases_flags).items.add(i);
da_append(&mut cases, case_name);
let saved_count = cases.count;
let pattern = *(*cases_flags).items.add(i);
for i in 0..all_cases.count {
let case_name = *all_cases.items.add(i);
if matches_glob(pattern, case_name)? {
da_append(&mut cases, case_name);
}
}
if cases.count == saved_count {
fprintf(stderr(), c!("ERROR: unknown test case `%s`\n"), pattern);
return None;
}
}
}

Expand Down
14 changes: 14 additions & 0 deletions src/glob.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use core::ffi::*;

#[repr(C)]
pub enum Glob_Result {
OOM_ERROR = -4,
ENCODING_ERROR = -3,
SYNTAX_ERROR = -2,
UNMATCHED = -1,
MATCHED = 0,
}

extern "C" {
pub fn glob_utf8(pattern: *const c_char, text: *const c_char) -> Glob_Result;
}
4 changes: 4 additions & 0 deletions src/nob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ pub struct Cmd_Redirect {
extern "C" {
#[link_name = "nob_temp_sprintf"]
pub fn temp_sprintf(format: *const c_char, ...) -> *mut c_char;
#[link_name = "nob_temp_save"]
pub fn temp_save() -> usize;
#[link_name = "nob_temp_rewind"]
pub fn temp_rewind(checkpoint: usize);
#[link_name = "nob_sb_appendf"]
pub fn sb_appendf(sb: *mut String_Builder, fmt: *const c_char, ...) -> c_int;
#[link_name = "nob_sv_from_cstr"]
Expand Down
5 changes: 5 additions & 0 deletions thirdparty/glob.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#include "nob.h"
#define GLOB_IMPLEMENTATION
#define GLOB_MALLOC nob_temp_alloc
#define GLOB_FREE(...)
#include "glob.h"
Loading