Test v0.1.0

Source
Unit tests: checks with readable failures, and the runner that #Test() functions are gathered into

Installation

Add Test to the [Dependencies] table of your package's Rux.toml:

[Dependencies]
Test = { Namespace = "Rux", Version = "0.1.0" }

Standard packages are declared by hand: the registry does not serve them, so rux add Rux/Test cannot add them.

Dependencies

Guide

Unit tests: functions marked #Test(), checks whose failures say what was expected and what was found, and the runner a test build gathers them into.

What it provides

ModuleCovers
ExpectExpect, ExpectEqual, ExpectNotEqual, ExpectNear, ExpectText, ExpectSome, ExpectNone, ExpectSuccess and ExpectFailure
FailureTestFailure, a failure with its message or a skip with its reason
RunnerTestCase, TestOptions, RunTests, which reads its filters from the command line, and RunTestsWith
BenchmarkBenchmarkCase, BenchmarkOptions, BenchmarkResult, Measure, Consume, RunBenchmarks and RunBenchmarksWith

The module names describe where each declaration comes from; they are not part of an import path.

Example

import Test::{ ExpectEqual, ExpectText, TestFailure };

func Greeting(name: char8[..]) -> char8[..] {
    return name.length == 0 ? "hello" : "hello, friend";
}

#Test()
func GreetsAnyone() -> ! TestFailure {
    ExpectText(Greeting(""), "hello")?;
    ExpectEqual(Greeting("ann").length, 13u)?;
}

rux test builds the package in test mode and runs it, reporting each test with its time, and rux test -- Greets runs only the tests whose names hold Greets.

Design

A test is a function marked #Test() that takes nothing and returns nothing or ! TestFailure. It is declared at the top level of a source file or inside a module block, which then prefixes its name, as in Parser::ReadsNumbers. Each check returns ! TestFailure, so a test passes a failed check on with ? and stops there. The failure says what was expected and what was found: ExpectText names the byte where two texts part and shows each around it. A test ends with fail TestFailure::Skip("reason") to be counted as skipped rather than failed. A failure keeps its message inline, so failing never allocates and never fails itself.

A test build of a package with #Test() functions generates its Main: a table of TestCase values handed to RunTests. This works for libraries too, so a SourceLibrary can hold its own tests beside its code. The package must depend on Test and must not declare a Main of its own. rux test runs every executable package under Tests/ and every package whose sources mark tests, and passes the arguments after -- to each. The runner reads each plain argument as a filter on test names and runs the tests any of them matches, with --exact to match whole names, --skip <text> to leave some out, --list to list them and --quiet to report only failures. A filter that matches no test fails the run rather than passing with nothing run. When arguments are given, rux test shows each package's report even when it passed, so rux test -- --list lists the tests.

A benchmark is a BenchmarkCase: a name and a function that repeats the measured work as many times as it is told. Measure times the whole call, so setup before the loop is counted too, spread over the iterations; keep it small next to the work, or do it once outside the function. Measure runs it with a count that grows from one, at least twofold and at most a hundredfold a round, aiming past the minimum time from the rate so far, until a run lasts 200 milliseconds or reaches its cap, and reports that run; RunBenchmarks measures a table of them and prints the time per iteration of each, with a name filter and --quick from the command line. Consume(@result) keeps a result the compiler would otherwise see as unused, and the work that made it, from being optimized away.

Tests run one after another in one process. A failed check ends only its own test. A panic or a failed Assert ends the whole run, which rux test then reports with the output up to that point.

Index

Types

NameSummary
BenchmarkCaseA benchmark: its name, which the runner reports and filters on, and the function that repeats the measured work as many times as it is told.
BenchmarkResultWhat a measurement found: how many iterations its final run made and how long that run took.
BenchmarkOptionsWhich benchmarks a run chooses and how long each is measured.
TestFailureWhy a test did not pass.
TestCaseA test: its name, which the runner reports and filters on, and the function that runs it.
TestOptionsWhich tests a run chooses and how it reports them.

Functions

NameSummary
Consume<T>Marks value as used, so the work that computed it is not removed as having no effect.
MeasureRuns run with a growing iteration count until one run lasts at least options.minimumTime or reaches options.maximumIterations, and reports that run.
RunBenchmarksWithMeasures the benchmarks that options choose, in order, and reports each as its time per iteration and the iterations it was measured over, then how many were measured.
RunBenchmarksMeasures the benchmarks the process's arguments choose.
ExpectFails unless condition holds.
ExpectEqual<T>Fails unless actual equals expected, saying both.
ExpectNotEqual<T>Fails if actual equals unexpected.
ExpectNearFails unless actual is within tolerance of expected.
ExpectTextFails unless actual is the same text as expected, saying at which byte they part and showing each around it.
ExpectSome<T>The value of value, failing when there is none.
ExpectNone<T>Fails when value is present.
ExpectSuccess<T, E>The success of outcome, failing when it failed.
ExpectFailure<T, E>The failure of outcome, failing the test when it succeeded.
RunTestsWithRuns the tests that options choose, in order, and reports each as ok, FAILED with its message, or skipped with its reason, then a summary of the counts and the time taken.
RunTestsRuns the tests the process's arguments choose.