Testing
Tests are written in the language itself, as test blocks, and run by the compiler. Reference: Tests.
Test blocks#
import testing
let full = ErrorCode.package(1)
pub struct Stack:
var items: i64[8]
var count: usize
pub func push(value: i64) -> !:
if self.count == self.items.length:
error(full, "the stack is full")
self.items[self.count] = value
self.count += 1
pub func pop() -> i64?:
if self.count == 0: return none
self.count -= 1
return self.items[self.count]
test "push then pop gives the value back":
var s = Stack()
try s.push(3)
assert(s.pop() == 3)
assert(s.pop() == none)
test "a full stack refuses":
var s = Stack()
for i in 0..<8: try s.push((i64)i)
var refused = false
s.push(9) catch failure:
refused = failure.code == full
testing.expect(refused, "the ninth push fails")luce-base test stack.lucbok push then pop gives the value back
ok a full stack refuses
2 passed- A test is a declaration,
test "description":followed by a block. It can sit next to the code it tests and use the module's private declarations. - A test fails, or the run stops. A test fails when an error leaves it (from
tryorerror(...)), or when anassertwritten in the test itself is false. It is reported asFAILwith a position and a message, and the other tests still run. Anything else that traps (an overflow, an index out of range, anassertinside a function the test calls, atesting.expect) stops the run there: the test is reported asFAIL, the trap's line follows, then the count of the tests run so far. luce-base buildleaves tests out of the program.
A failing run reports each failure under its test and counts them; the status is 1:
test "two and two":
assert(2 + 2 == 5, "arithmetic")
test "still runs":
assert(true)FAIL two and two
sums.lucb:2:5: assert failed: 2 + 2 == 5: arithmetic
ok still runs
1 passed
1 failedA failed assert is reported where it stands, with its condition; a test that fails by an error is reported at the test, with the error's message. Positions name the file relative to the project root, the directory with package.prisma, as in the rest of this guide.
A trap ends the run, as a trap ends a program. Like an unhandled exception in a pytest worker, it takes the test down with it, but the report still says which test and how far the run got:
func third(items: const i64[]) -> i64:
return items[2]
test "two and two":
assert(2 + 2 == 4)
test "reads past the end":
let pair: i64[2] = [1, 2]
_ = third(pair)
test "never reached":
assert(true)ok two and two
FAIL reads past the end
trap: sums.lucb:2:5: index out of bounds
1 passed
1 failedChecking values#
assert(condition) and assert(condition, "message") are the usual check: a failure names the line and the condition. The testing module adds checks that also print the values:
| Function | Fails when |
|---|---|
testing.expect(condition, message) | condition is false |
testing.expect_equal(actual, expected, message) | two integers differ |
testing.expect_text(actual, expected, message) | two strings differ |
A failure names the test's own line, for example stack.lucb:37: pop returns what was pushed: expected 2, got 1.
What a test runs with#
Each test runs with a fresh 1 MiB fixed-buffer allocator as its current allocator, so new in a test never touches the system heap and nothing leaks from one test into the next. testing.seed() gives a number that is the same on every run of the same test, and testing.random() a reproducible sequence from it, for tests that need random data.
A test that changes a global variable affects the tests after it; keep tests independent of globals, or reset them.
Which tests run#
luce-base test file.lucb runs the tests of the file and of every module of the same package that it imports, directly or not: module by module, each module's tests in the order they are declared, and an imported module before the module importing it. Tests in a dependency, another package, do not run; that package runs its own.
In a project, luc test runs the tests of every module of the project, imported or not: it tests the entry module with --package, which adds every other .lucb module under src/, in the order of their paths. For a project whose src/main.lucb imports helper and declares test "in main", whose src/helper.lucb declares test "in helper", and whose src/extra.lucb, which nothing imports, declares test "in extra", luc test prints:
ok in helper
ok in main
ok in extra
3 passedluce-base test src/main.lucb --package does the same outside luc. A library with no src/main.lucb and no module named after the package is tested the same way, from its first module. When the package also has Luce modules (.luc), luc test runs their tests with luce test --package as well, so a test in either language is never left out. luc test then runs the package's test programs (below) and ends with one total. A package with no test at all, no test block and no test program, fails with "no tests found".
Tests in their own files#
Tests can live outside the source, so a package's shipped code stays small. For the module src/parse.lucb, the directory tests/parse/ holds test files, listed in order in a TESTS file:
src/
parse.lucb
tests/
parse/
TESTS basic.lucb
errors.lucb
basic.lucb
errors.lucbWhen parse is tested, its test files are added to the module as if they were part of it, so they can use its private declarations. A build never reads them, and luc install leaves the tests/ directory out.
For a module that is a directory of files, src/mime/parse/, the tests go in tests/mime/parse/ in the same way.
Test programs#
A check that needs a process of its own, fixtures read from disk, a server, a window, the GPU or a comparison with another tool, is a program instead: a directory under tests/ holding a main.lucb (or a Luce main.luc) with pub func main.
tests/
roundtrip/
main.lucb pub func main(arguments: str[]) -> i32
sample.bin
expected what main prints, exactlyluc test finds every directory tests/<name>/ with a main, as pytest finds test_*.py files, builds it with the package's dependencies and runs it from the package root, as luc test itself runs; LUC_TEST_DIR names its own directory, for the fixtures beside it. It passes when it exits with status 0 and, if its directory has an expected file, when its standard output is exactly that file; one that exits 0 after printing a line skip: reason is skipped. Programs run in parallel, each with HOME and LUC_HOME pointing at a fresh scratch directory, removed afterwards, so a test never touches your own settings or keychain, and with LUCE and LUCE_BASE naming the compilers luc test uses. They build into build/luc-test/<name>/; build/tests/<name>/ is the program's own scratch space, which luc makes a directory and otherwise leaves alone.
ok push then pop gives the value back
1 passed
ok tests/roundtrip
total: 2 passed, 0 failed (1 test block, 1 program)A program imports any module of the package, private ones too, by its module name, import parse, as code inside src/ does, and a helper beside it by its own name. One with a package.prisma of its own is a separate package, for dependencies only the test needs, and imports the package's public modules as any dependent does. A directory under tests/ without a main is data, and a directory with a TESTS file holds test fragments, as above. luc test --list names the programs and the test blocks without running them.