Skip to content

The language

Testing

A test "name": block is a function with no parameters that passes when it finishes and fails when it panics. Put tests in the same file as the code they test, and run them all with brine test. Each test runs in its own process, in parallel with the others.

stats.br
fn mean(xs: [float]) -> float:
return sum(x for x in xs) / float(len(xs))
fn main():
print(mean([1.0, 2.0, 6.0]))
test "mean of three numbers":
assert mean([1.0, 2.0, 6.0]) == 3.0
test "mean of one number":
assert mean([4.5]) == 4.5
Terminal window
brine test stats.br
Output
Built tests [dev] in 0.09s
test mean of three numbers ... ok
test mean of one number ... ok
test result: ok. 2 passed; 0 failed; finished in 0.02s
  • test "name": goes at the top level of any .br file, next to the functions it tests. The name is any string.
  • The body is ordinary code. It can call any function in the file or in imported modules, use vars, loops and par, and read files.
  • assert cond panics when cond is false. assert cond, message adds a message (any str, often an f-string).
  • Any panic fails the test: a failed assert, an out-of-range index, a missing map key, an unwrap() of None, or panic(...).
  • Tests aren’t part of the program: brine run and brine build leave them out.

assert is not only for tests. In normal code it’s a runtime check that always runs, and the prover treats the condition as a known fact after it. See contracts.

stats.br
fn mean(xs: [float]) -> float:
return sum(x for x in xs) / float(len(xs))
fn main():
print(mean([1.0, 2.0, 6.0]))
test "mean of three numbers":
assert mean([1.0, 2.0, 6.0]) == 3.0
test "mean of one number":
assert mean([4.5]) == 4.5
test "mean of nothing":
let empty: [float] = []
print("about to divide by", len(empty))
let m = mean(empty)
assert m == 0.0, f"mean([]) was {m}"
brine test stats.br
Built tests [dev] in 0.09s
test mean of three numbers ... ok
test mean of one number ... ok
test mean of nothing ... FAILED
---- mean of nothing ----
about to divide by 0
panic: assertion failed: m == 0.0
message: mean([]) was nan
--> stats.br:17:5
test result: FAILED. 2 passed; 1 failed; finished in 0.02s
  • What a test prints is captured. It’s shown only when the test fails, in that test’s section, followed by the panic message, the failed condition and its location.
  • brine test exits with 0 when every test passes and 1 when any fails.
  • Results are listed in the order the tests appear, whichever finishes first.
Terminal window
brine test # every test in the project
brine test "mean" # only tests whose name contains "mean"
brine test stats.br # the tests in one file (and the modules it imports)
brine test stats.br "nothing" # one file, filtered
brine test --release # build the tests with optimizations
brine test --json # one JSON object per test
  • In a project, brine test builds src/main.br and every module it imports, and runs all their tests. A module’s tests are named with the module first: geometry: a 2 by 3 rectangle. See modules.
  • The filter is a case-sensitive substring of the test name (including the module: part). When nothing matches, brine test says so and exits with 0.
  • Tests run in parallel, as many at a time as you have cores. Each one is a separate process, so a panic in one test can’t affect another. Each test process runs par code on 2 threads.

While tests run, the environment variable BRINE holds the path of the brine that is running them. A test can use it to check that a program compiles, or that it doesn’t:

compiler_test.br
# Check that a program with a known mistake is rejected with the right error.
fn check_output(source: str) -> Result[str, str]:
let path = os.temp_dir() + "/brine-test-" + str(os.pid()) + ".br"
write_file(path, source)?
let cmd = [getenv("BRINE").unwrap(), "check", path, "--color", "never"]
let p = os.spawn(cmd, {}, true, false)?
os.wait(p)
let (out, err) = os.output(p)
os.remove_file(path)?
return Ok(out + err)
test "reading and changing in one expression is rejected":
let src = "fn main():\n var xs = [1, 2]\n print(xs.pop(), xs)\n"
let out = check_output(src).unwrap()
assert "error[E0405]" in out, out
brine test compiler_test.br
Built tests [dev] in 0.10s
test reading and changing in one expression is rejected ... ok
test result: ok. 1 passed; 0 failed; finished in 0.04s

The file name includes os.pid() because tests run at the same time; each test process has its own id. See the os module for os.spawn.

With --json, brine test prints one JSON object per test, in order, and nothing else:

brine test stats.br --json
{"test":"mean of three numbers","ok":true,"seconds":0.022,"output":""}
{"test":"mean of one number","ok":true,"seconds":0.026,"output":""}
{"test":"mean of nothing","ok":false,"seconds":0.029,"output":"about to divide by 0\n\npanic: assertion failed: m == 0.0\n message: mean([]) was nan\n --> stats.br:17:5\n"}
Field Type Meaning
test string the test’s name
ok bool whether it passed
seconds number how long its process ran
output string everything it printed to stdout and stderr, including the panic message

The exit code is the same as without --json. Agents and CI scripts can read the lines one at a time. See the CLI.

  • Tests live next to the code, in test "name": blocks.
  • A test fails if it panics; assert cond, message is the usual way.
  • brine test [file] [filter] runs every matching test, each in its own process, in parallel.
  • Output is shown only for failing tests (and always in --json).
  • $BRINE is the compiler running the tests.

Related: modules, error handling, built-in functions, the CLI.