A small CLI runner that compiles every .pp / .pas file under testfiles/, optionally executes the produced binary, and reports pass / fail with timings. Written in {$mode unleashed} so that building the tool itself exercises the compiler under test.
lazbuild unleashed/tests/testtool/testtool.lpi
The built binary lands in unleashed/tests/testtool.exe. By default it invokes whatever fpc is on PATH. Override with --fpc=PATH (see below).
unleashed/tests/
testtool/ tool source (testtool.lpr, testtool.lpi)
testtool.exe built binary
testfiles/ test sources, one .pp per test
<feature>/ tests grouped by language feature
sanity_*.pp self-tests for the runner
tests.log one line per test, written every run
fail.log failures only, written only when at least one fails
.tmp/ transient build artifacts, removed at end of run
The testfiles/ root may also be moved with --path=DIR.
testtool run the whole suite, parallel
testtool --filter=autofree run only tests whose path contains "autofree"
testtool --only-failed rerun what failed in the previous run
testtool --fail-fast stop at the first failure
testtool --list just print discovered tests, do not run
testtool --parallel=1 force sequential execution
testtool --help full option list
The runner walks testfiles/ recursively, picks up each .pp / .pas file, and for each one:
- extracts the first
{ ... }comment in the file and parses any%FLAGtokens out of it - compiles the file with
fpc(extra args injected per%OPT=and per--fpcconfig) - depending on the flags, either expects the compile to fail, or runs the produced exe and inspects its exit code
Default verdict rules, in priority order:
| Situation | Verdict |
|---|---|
%CPU set and target cpu not in the list |
SKIP, phase skip (nothing compiled) |
| Compiler hung past the timeout | FAIL, phase compile-timeout |
%FAIL set and compiler exited non-zero |
PASS, phase expected-fail |
%FAIL set and compiler exited zero |
FAIL, phase compile |
Compile failed without %FAIL |
FAIL, phase compile, notes = compiler output |
%NORUN (or --norun) and compile succeeded |
PASS, phase compile |
| Run hung past the timeout | FAIL, phase run-timeout |
| Run exited 0 | PASS, phase run |
| Run exited non-zero | FAIL, phase run, notes = program output |
A test is a single .pp (or .pas) file. The body uses Halt(N) to signal verdict at runtime, where N = 0 (or natural fall-through) means pass and any other value means fail. Different Halt(N) calls at different points in the body let you identify the failing assertion site from the exit code recorded in fail.log.
Minimal example:
program inline_vars_inferred_01;
{$mode unleashed}
var s: string;
begin
var n := 42;
if n <> 42 then Halt(1);
s := 'hello';
if Length(s) <> 5 then Halt(2);
end.When this test fails with exit=2, the author knows it was the second check.
Flags live in the FIRST { ... } comment in the file. The comment must come before any code and must not be a compiler directive ({$...}). Unknown flags are silently ignored. Flag names are case-insensitive (%norun works the same as %NORUN).
{ %FLAG1 %FLAG2=value }
program tname;
{$mode unleashed}
...
A value containing whitespace must be quoted with "...", otherwise the tokenizer splits on the first space and only the head ends up as the value:
{ %OPT="-O3 -OoDEADSTORE" %NORUN } <- correct: OPT receives both args
{ %OPT=-O3 -OoDEADSTORE %NORUN } <- WRONG: OPT receives only `-O3`
| Flag | Effect |
|---|---|
%NORUN |
Compile only; do not run the produced exe. Pass = compiler exits 0. |
%FAIL |
The test must NOT compile. Pass = compiler exits non-zero. |
%OPT=ARGS |
Extra arguments passed verbatim to the compiler. Quote the value if it contains spaces: %OPT="-O2 -Cr". |
%TIMEOUT=N |
Per-test timeout in seconds, overrides --timeout. |
%CHECKBIN_HAS=L |
Comma-separated list of substrings; each MUST appear in the produced binary. After successful compile (and run, if any), the runner reads the exe as bytes and asserts every entry is found. |
%CHECKBIN_LACKS=L |
Same shape, opposite assertion: every entry MUST NOT appear. Useful for verifying RTTI stripping, dead-code elimination, etc. |
%CPU=L |
Comma-separated list of target cpus (as reported by fpc -iTP, e.g. x86_64,aarch64) the test applies to. When the compiler targets a cpu not in the list, the test is reported as SKIP instead of being compiled. Use for tests that exercise 64-bit-only behavior such as Int64 for parallel loop variables. |
When either %CHECKBIN_* flag is set, the runner adds -Xs -XX -CX to the compile to keep dead code and debug-section noise out of the byte search. The check happens whether or not the test runs (so it composes with %NORUN). On violation the verdict is FAIL with phase checkbin and a note naming the offending substring.
There is no per-file modeswitch flag: a test that needs a specific modeswitch should put {$modeswitch NAME} directly in its source. To compile the SAME suite under different mode / modeswitch combinations from the command line, use --mode= and --modeswitch= (see CLI flags).
Mixing %NORUN and %FAIL is not meaningful; %FAIL takes precedence.
<feature>_<variant>_NN.pp, zero-padded number, lowercase. Examples:
inline_vars_inferred_01.pp
autofree_in_try_except_classical_var_01.pp
match_returning_tuple_03.pp
Tests are grouped into per-feature folders under testfiles/. The runner descends recursively, so any depth works.
| Flag | Effect |
|---|---|
--path=DIR |
Override the testfiles/ root. Useful for pointing the runner at another suite, e.g. --path=tests/webtbs to run the FPC tracker tests with the configured compiler. |
--filter=SUBSTR |
Run only tests whose absolute path contains SUBSTR. |
--exclude=SUBSTR |
Drop tests whose path contains SUBSTR. Applied after --filter. |
--limit=N |
Cap the test count to N after filter / exclude. Takes the first N from the sorted list. |
--only-failed |
Rerun only tests listed in the previous fail.log. Exits 0 with a message if no fail.log exists. |
--list |
Print discovered test paths and exit; do not compile or run anything. |
| Flag | Effect |
|---|---|
--fpc=PATH |
Path to the fpc binary. Default: fpc resolved through PATH. |
--norun |
Force %NORUN semantics on every test. Compile-only run; useful when the run side is too slow or unavailable. |
--timeout=N |
Default per-test timeout in seconds. 0 disables the timeout entirely. Default: 30. Overridden per-test by %TIMEOUT=N. |
--parallel=N |
Number of worker threads. Each worker uses its own .tmp/W<i>/ subdir so artifacts never collide. Default: half the CPU core count. Use --parallel=1 to force sequential. |
--fail-fast |
Stop dispatching new tests at the first failure. In parallel mode, workers already mid-test finish normally; only the unscheduled tail is skipped. |
When set, these patch the test source on the fly into a temporary copy under .tmp/; the on-disk test file is never modified.
| Flag | Effect |
|---|---|
--mode=NAME |
Override the {$mode} directive. If the source contains {$mode X}, that line is rewritten to {$mode NAME} in the temp copy. If the source has no {$mode} directive at all, -MNAME is appended to the compiler args instead (which has the same effect). |
--modeswitch=LIST |
Inject one or more {$modeswitch} directives. LIST is comma-separated; each entry is a name with an optional + (enable, the default) or - (disable) suffix. Examples: --modeswitch=advancedrecords enables one switch; --modeswitch=helpers+,multihelpers- enables helpers and explicitly disables multihelpers. |
Injection point for --modeswitch directives, first match wins:
- immediately after the LAST existing
{$modeswitch ...}directive in the source - immediately after the
{$mode ...}directive - immediately after the first non-directive
{...}comment - at the very top of the file
This ordering guarantees the injected directives are not silently overridden by a {$mode} directive that appears below them (which would reset all modeswitches to the new mode's defaults).
| Flag | Effect |
|---|---|
--list |
(also a selection flag, see above) |
--no-color |
Disable ANSI colors. Auto-applied when stdout is not a console (e.g. piped to a file). |
--keep-temp |
Keep .tmp/W<i>/ artifacts of failing tests (.exe, .o, .ppu, etc.) so they can be inspected after the run. Passing tests are still cleaned. Without this flag the entire .tmp/ tree is wiped at end of run. |
--help, -h |
Print the option list and exit. |
Both files are written next to testtool.exe (unleashed/tests/).
One line per test, header form:
[2026-05-13 22:08:55] [PASS] autofree/autofree_basic_01.pp phase=run exit=0
[2026-05-13 22:08:55] [FAIL] match/match_subject_01.pp phase=run exit=2 (%TIMEOUT=5)
Suffix tags in parens echo the flags that were active for that test.
Only written when at least one test failed. Each failure entry repeats the header from tests.log, followed by indented compiler / runner output. Banner lines (Free Pascal Compiler ..., Copyright ..., Target OS: ...) and noisy absolute paths are stripped so the log stays readable. The leading Compiling X:\full\path\testfiles\... is shortened to Compiling ...\testfiles\....
If a subsequent run has zero failures, the previous fail.log is deleted.
The runner spawns N worker threads, each pulling tests off a shared queue protected by a critical section. Each worker has its own .tmp/W<i>/ subdirectory and sets its own GTempDir thread variable, so per-test .exe / .o / .ppu paths never collide even when two tests share the same source basename in different folders.
Test output is serialized through another critical section, so the per-test progress line [N/M] path ... PASS is printed atomically. Since workers finish out of order, the N is a completion counter (1, 2, 3, ...), not the dispatch index.
--fail-fast is honored cooperatively: when any worker observes a failure, it flips a flag that prevents further tests from being dispatched. Workers already mid-test let that test complete (so its result is recorded), then exit.
Default parallelism is TThread.ProcessorCount div 2, with a floor of 1.
There is no built-in diff mode; the same effect is achieved with two runs:
testtool --fpc=our-ppc # against the local build
testtool --fpc=trunk-ppc # against upstream trunk
diff fail.log fail.log.trunk
--path=DIR complements this for cross-suite comparisons:
testtool --fpc=our-ppc --path=../tests/webtbs # FPC tracker suite, our compiler
testtool --fpc=trunk-ppc --path=../tests/webtbs # same suite, upstream compiler
The runner exits 0 when every test passed, 1 when at least one failed, 2 for a usage / config error (e.g. testfiles directory does not exist).