char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
7abfdd9
main
Testing
avril test discovers test files, runs each in a fresh isolate, and reports
one human-readable summary. There is no bench mode, no JSON reporter, and no
Deno global: tests import their API from avril:test.
import { test } from "avril:test" ; test ( "adds" , () => { if ( 1 + 1 !== 2 ) throw new Error ( "math broke" ); }); test ( "steps nest" , async ( t ) => { await t . step ( "one" , () => {}); await t . step ( "two" , async ( t ) => { await t . step ( "deeper" , () => {}); }); });
avril testavril test tests/ ./extra_test.tsavril test--filter=adds avril test--no-run avril test-0 --allow-net=example.test --allow-read=./fixtures/
Permission, package (--offline, --frozen, --jsr-registry, --cache-dir,
--lock, --vendor), --include, and --env-file options behave exactly as
for avril run. Each file loads with a child of the CLI capabilities plus
its own packages, store, and graph, so a pledge() in one file never narrows
another; each discovered file gets an implicit js-exec grant like an entry
module, so avril test -0 <file> works without extra flags.
Discovery
With no arguments, the working directory is walked. Explicit directories are
walked; explicit files run even when their names match nothing; explicit
http(s) URLs run as remote modules.
Walked files must look like tests: test.<ext>, *_test.<ext>, or
*.test.<ext>, where <ext> is one of js, mjs, cjs, cts, jsx, ts, mts, tsx.
(d.ts files are excluded because they have no runtime code.)
Dot-directories, node_modules, vendor, target, and __pycache__ are
skipped, as are dot-files; symlinks are never followed while walking (an
explicit symlinked directory walks its target), and results are sorted and
deduplicated. An empty result is an error (No test files found).
API
test ( name , fn ); test ({ name, fn ?, ignore ?, only ?, timeout ?}); test . only ( name , fn ); test . only ( options ); test . ignore ( name , fn ); test . ignore ( options );
fnreceives aTestContextand may be sync or async. It is required unlessignoreis true (test.ignore('name')needs no function); unknown option keys throw at registration, which fails the file, as does callingtest()after collection (for example inside a test body).timeoutis milliseconds before the test fails (default5000).timeout: 0means the body must settle synchronously; values past2147483647are rejected at registration (timers truncate to 32 bits);Infinitydisables the per-test timeout, but the file-level 60s progress budget below still applies. The timeout covers the body plus all of its steps, including steps the body never awaited.- When any test in a file sets
only, every other test in that file is filtered out. Filtered-out tests never fail the run: a file whose focused tests pass exits 0.--filter=PATTERNkeeps only tests whose top-level name contains the substring; steps inherit their test's selection, and filtered tests are counted, not run. t.step(name, fn)/t.step({ name, fn?, ignore? })runs a substep sequentially in call order and reports it indented. It resolvestruewhen the step passed andfalsewhen it was ignored or failed — a failing step never throws. Any failed step still fails its ancestors up to the test, which reportsdue to N failed step(s)when its own body passed. Steps share their test's timeout; step-levelonlyandtimeoutare rejected. Starting a step while another runs on the same context fails the newcomer (Started step ... while another step was running) without failing its victim; steps must be awaited one at a time. The parent waits out steps its body never awaited, and steps completing after their test settled (a timed-out body waking up) are dropped, never reported under a later test. Only top-level tests count toward file totals; steps are detail.- There is deliberately no
(name, options, fn)third form and no compat shim beyond this surface.
Isolation and authority
Each file runs in its own fresh isolate, sequentially, in registration order.
Global state is shared between tests in one file but never leaks across files.
Each file also gets a child of the CLI capabilities with its own packages,
store, and graph: a pledge() call inside a test narrows its whole file
isolate irreversibly — including that file's own loading, exactly as under
avril run — while later files still load and run under the un-narrowed
parent, so tests after it in the same file inherit the restriction; prefer
CLI flags for authority. There are no resource, timer, or op sanitizers: a
test that leaves an interval running still passes, and the harness does not
wait for idle handles before moving to the next file. Rejections nobody
handles during the run are collected under the running test and fail the file
with the full list instead of aborting silently; strays surfacing after the
run resolves are not observed.
Reporter and exit codes
Check ./math_test.ts
ok - adds (0ms)
FAILED - subtracts (1ms)
Error: boom
at Object.fn (/tmp/proj/math_test.ts:18:29)
ignored - skipped
ok - ./math_test.ts (1 passed, 1 failed, 1 ignored, 0 filtered)
test result: FAILED. 1 passed; 1 failed; 1 ignored; 0 filtered; 1 file.
Failure stacks use source-mapped locations with the runner's own frames
trimmed. Files that fail to prepare or evaluate (syntax errors, import
failures, registration TypeErrors) print their detail in the file footer.
Guest console output passes through without per-test capture. A test that
spins synchronously past its timeout never yields to the runner's own timers,
so a watchdog thread terminates the isolate (own timeout plus a small slack,
or the file budget for Infinity): the spinning test reports a timeout and
the rest of its file fails with it. A file that makes no progress at all for
60s — top-level evaluation or a stall between tests — is terminated with that
message in its footer. The exit code is nonzero when any test fails or any
file errors. --no-run still evaluates each file's top level to collect
registrations, then lists test - name entries instead of running them.
1# Testing 2 3`avril test` discovers test files, runs each in a fresh isolate, and reports 4one human-readable summary. There is no bench mode, no JSON reporter, and no 5`Deno` global: tests import their API from `avril:test`. 6 7``` ts 8import { test } from "avril:test"; 9 10test("adds", () => { 11if (1 + 1 !== 2) throw new Error("math broke"); 12}); 13 14test("steps nest", async (t) => { 15await t.step("one", () => {}); 16await t.step("two", async (t) => { 17await t.step("deeper", () => {}); 18}); 19}); 20``` 21 22``` sh 23avril test 24avril test tests/ ./extra_test.ts 25avril test --filter=adds 26avril test --no-run 27avril test -0 --allow-net=example.test --allow-read=./fixtures/ 28``` 29 30Permission, package (`--offline`, `--frozen`, `--jsr-registry`, `--cache-dir`, 31`--lock`, `--vendor`), `--include`, and `--env-file` options behave exactly as 32for `avril run`. Each file loads with a child of the CLI capabilities plus 33its own packages, store, and graph, so a `pledge()` in one file never narrows 34another; each discovered file gets an implicit js-exec grant like an entry 35module, so `avril test -0 <file>` works without extra flags. 36 37## Discovery 38 39With no arguments, the working directory is walked. Explicit directories are 40walked; explicit files run even when their names match nothing; explicit 41`http(s)` URLs run as remote modules. 42 43Walked files must look like tests: `test.<ext>`, `*_test.<ext>`, or 44`*.test.<ext>`, where `<ext>` is one of `js`, `mjs`, `cjs`, `cts`, `jsx`, `ts`, `mts`, `tsx`. 45(`d.ts` files are excluded because they have no runtime code.) 46Dot-directories, `node_modules`, `vendor`, `target`, and `__pycache__` are 47skipped, as are dot-files; symlinks are never followed while walking (an 48explicit symlinked directory walks its target), and results are sorted and 49deduplicated. An empty result is an error (`No test files found`). 50 51## API 52 53``` ts 54test(name, fn); 55test({ name, fn?, ignore?, only?, timeout? }); 56test.only(name, fn); test.only(options); 57test.ignore(name, fn); test.ignore(options); 58``` 59 60- `fn` receives a `TestContext` and may be sync or async. It is required 61unless `ignore` is true (`test.ignore('name')` needs no function); unknown 62option keys throw at registration, which fails the file, as does calling 63`test()` after collection (for example inside a test body). 64- `timeout` is milliseconds before the test fails (default `5000`). 65`timeout: 0` means the body must settle synchronously; values past 66`2147483647` are rejected at registration (timers truncate to 32 bits); 67`Infinity` disables the per-test timeout, but the file-level 60s progress 68budget below still applies. The timeout covers the body plus all of its 69steps, including steps the body never awaited. 70- When any test in a file sets `only`, every other test in that file is 71filtered out. Filtered-out tests never fail the run: a file whose focused 72tests pass exits 0. `--filter=PATTERN` keeps only tests whose top-level 73name contains the substring; steps inherit their test's selection, and 74filtered tests are counted, not run. 75- `t.step(name, fn)` / `t.step({ name, fn?, ignore? })` runs a substep 76sequentially in call order and reports it indented. It resolves `true` 77when the step passed and `false` when it was ignored or failed — a failing 78step never throws. Any failed step still fails its ancestors up to the 79test, which reports `due to N failed step(s)` when its own body passed. 80Steps share their test's timeout; step-level `only` and `timeout` are 81rejected. Starting a step while another runs on the same context fails 82the newcomer (`Started step ... while another step was running`) without 83failing its victim; steps must be awaited one at a time. The parent waits 84out steps its body never awaited, and steps completing after their test 85settled (a timed-out body waking up) are dropped, never reported under a 86later test. Only top-level tests count toward file totals; steps are 87detail. 88- There is deliberately no `(name, options, fn)` third form and no compat 89shim beyond this surface. 90 91## Isolation and authority 92 93Each file runs in its own fresh isolate, sequentially, in registration order. 94Global state is shared between tests in one file but never leaks across files. 95Each file also gets a child of the CLI capabilities with its own packages, 96store, and graph: a `pledge()` call inside a test narrows its whole file 97isolate irreversibly — including that file's own loading, exactly as under 98`avril run` — while later files still load and run under the un-narrowed 99parent, so tests after it in the same file inherit the restriction; prefer 100CLI flags for authority. There are no resource, timer, or op sanitizers: a 101test that leaves an interval running still passes, and the harness does not 102wait for idle handles before moving to the next file. Rejections nobody 103handles during the run are collected under the running test and fail the file 104with the full list instead of aborting silently; strays surfacing after the 105run resolves are not observed. 106 107## Reporter and exit codes 108 109``` 110Check ./math_test.ts 111ok - adds (0ms) 112FAILED - subtracts (1ms) 113Error: boom 114at Object.fn (/tmp/proj/math_test.ts:18:29) 115ignored - skipped 116ok - ./math_test.ts (1 passed, 1 failed, 1 ignored, 0 filtered) 117test result: FAILED. 1 passed; 1 failed; 1 ignored; 0 filtered; 1 file. 118``` 119 120Failure stacks use source-mapped locations with the runner's own frames 121trimmed. Files that fail to prepare or evaluate (syntax errors, import 122failures, registration `TypeError`s) print their detail in the file footer. 123Guest `console` output passes through without per-test capture. A test that 124spins synchronously past its timeout never yields to the runner's own timers, 125so a watchdog thread terminates the isolate (own timeout plus a small slack, 126or the file budget for `Infinity`): the spinning test reports a timeout and 127the rest of its file fails with it. A file that makes no progress at all for 12860s — top-level evaluation or a stall between tests — is terminated with that 129message in its footer. The exit code is nonzero when any test fails or any 130file errors. `--no-run` still evaluates each file's top level to collect 131registrations, then lists `test - name` entries instead of running them.