char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
eae10ac
main
1import { 2ArrayPrototypePush , 3RangeError , 4TypeError , 5} from "ext:core/primordials" ; 6import { flag , optionsOf , registry , type Definition } from "./shared.js" ; 7import { required , usv } from "ext:web/idl.js" ; 8// The runner captures intrinsics before guest evaluation can poison globals. 9import "./runner.js" ; 10 11const DEFAULT_TIMEOUT_MS = 5000 ; 12 13/** Context passed to each test function; also the type of nested step contexts. */ 14export interface TestContext { 15readonly name :string ; 16/** 17* Runs a substep sequentially and reports it indented under its parent. 18* Resolves true when the step passed, false when it was ignored or 19* failed; a failing step never throws, but any failed step still fails 20* its ancestors up to the test. Steps share their test's timeout; 21* `only` and `timeout` options are rejected. Only top-level tests count 22* toward file totals. 23*/ 24step ( name :string , fn :( t :TestContext ) => void | Promise < void >) :Promise < boolean >; 25step ( options :{ 26name :string ; 27fn ?:( t :TestContext ) => void | Promise < void >; 28ignore ?:boolean ; 29}) :Promise < boolean >; 30} 31export interface TestOptions { 32name :string ; 33/** Required unless `ignore` is true. */ 34fn ?:( t :TestContext ) => void | Promise < void >; 35ignore ?:boolean ; 36/** When any test in a file sets this, the rest of the file is filtered out. */ 37only ?:boolean ; 38/** 39* Milliseconds before the test fails; defaults to 5000, 0 requires the 40* body to settle synchronously, past 2147483647 is rejected, and 41* Infinity disables the per-test timeout (the file-level 60s progress 42* budget still applies). Covers the test body and all of its steps 43* together, including steps never awaited. 44*/ 45timeout ?:number ; 46} 47 48function checkTimeout ( value :unknown ) :number { 49if ( typeof value !== "number" || value !== value ) { 50throw new TypeError ( "Test timeout must be a number of milliseconds" ); 51} 52if ( value < 0 ) { 53throw new RangeError ( "Test timeout must be zero or more milliseconds" ); 54} 55// Timers truncate to 32 bits, so larger finite values would fire immediately. 56if ( value !== Infinity && value > 2147483647 ) { 57throw new RangeError ( 58"Test timeout must be at most 2147483647ms; use Infinity for no timeout" , 59); 60} 61return value ; 62} 63 64function parseTest ( nameOrOptions :unknown , fn :unknown , only :boolean , ignore :boolean , count :number ) :Definition { 65required ( count , 1 ); 66let name , func = fn , timeout = DEFAULT_TIMEOUT_MS ; 67if ( typeof nameOrOptions === "string" ) { 68// An undefined function falls through to the missing-function check so 69// that test.ignore('name') reads naturally. 70if ( count > 2 || ( count === 2 && fn !== undefined && typeof fn !== "function" )) { 71throw new TypeError ( "Expected test(name, fn)" ); 72} 73name = usv ( nameOrOptions ); 74} else { 75if ( count > 1 ) throw new TypeError ( "Expected test(options)" ); 76const options = optionsOf ( nameOrOptions , [ "name" , "fn" , "ignore" , "only" , "timeout" ], "test" ); 77if ( options . name === undefined ) throw new TypeError ( "Test requires a name" ); 78name = usv ( options . name ); 79func = options . fn ; 80ignore = flag ( options , "ignore" , "Test" ) || ignore ; 81only = flag ( options , "only" , "Test" ) || only ; 82if ( options . timeout !== undefined ) timeout = checkTimeout ( options . timeout ); 83} 84if ( func === undefined ) { 85if ( ! ignore ) throw new TypeError ( `Test " ${ name } " requires a function` ); 86} else if ( typeof func !== "function" ) { 87throw new TypeError ( `Test " ${ name } " requires a function` ); 88} 89return { name, fn :func , ignore, only, timeout} as Definition ; 90} 91 92function push ( nameOrOptions :unknown , fn :unknown , only :boolean , ignore :boolean , count :number ) :void { 93// Registrations after collection would silently never run. 94if ( registry . sealed ) throw new TypeError ( "test() must be called while the test file evaluates" ); 95ArrayPrototypePush ( registry . definitions , parseTest ( nameOrOptions , fn , only , ignore , count )); 96} 97 98const registration = function test ( nameOrOptions :unknown , fn ?:unknown ) :void { 99push ( nameOrOptions , fn , false , false , arguments . length ); 100}; 101registration . only = function only ( nameOrOptions :unknown , fn ?:unknown ) :void { 102push ( nameOrOptions , fn , true , false , arguments . length ); 103}; 104registration . ignore = function ignore ( nameOrOptions :unknown , fn ?:unknown ) :void { 105push ( nameOrOptions , fn , false , true , arguments . length ); 106}; 107 108 109/** 110* Registers a test in the current file. Only `avril test` runs registered 111* tests; under `run` and `serve` registration is inert. Each file runs in 112* its own fresh isolate, tests within a file run sequentially in 113* registration order, and a `pledge()` inside one test narrows every test 114* after it in the same file. There are no resource or timer sanitizers. 115*/ 116export const test :( 117(( name :string , fn :( t :TestContext ) => void | Promise < void >) => void ) & 118(( options :TestOptions ) => void ) 119) & { 120/** Registers a test that runs only when some test in its file uses `only`. */ 121only :(( name :string , fn :( t :TestContext ) => void | Promise < void >) => void ) & 122(( options :TestOptions ) => void ); 123/** Registers a test that is collected but never runs. */ 124ignore :(( name :string , fn ?:( t :TestContext ) => void | Promise < void >) => void ) & 125(( options :TestOptions ) => void ); 126} = registration ;