@@ -0,0 +1,102 @@
export interface TestResult {
success : boolean ;
logs : string [ ] ;
}
// =============================================================================
// Reporting
export interface Reporter {
update ( path : string [ ] , result : TestResult ) : void ;
finalize ( ) : void ;
}
export interface Stream {
writeln ( s : string ) : void ;
}
const SEP = " ❯ " ;
export class StreamReporter implements Reporter {
stream : Stream ;
verbose : boolean ;
# failures : ( { path : string [ ] } & TestResult ) [ ] ;
counts : { successes : number , failures : number } ;
constructor ( stream : Stream , verbose : boolean = false ) {
this . stream = stream ;
this . verbose = verbose ;
this . # failures = [ ] ;
this . counts = { successes : 0 , failures : 0 } ;
}
update ( path : string [ ] , result : TestResult ) {
if ( path . length === 0 ) throw new RangeError ( "path is empty" ) ;
const pathStr = path . join ( SEP ) ;
if ( result . success ) {
this . counts . successes ++ ;
if ( this . verbose ) this . stream . writeln ( ` ✅️ ${ pathStr } ` ) ;
} else {
this . counts . failures ++ ;
this . stream . writeln ( ` ⚠️ ${ pathStr } ` ) ;
this . # failures . push ( { path , . . . result } ) ;
}
}
finalize() {
// Transform [{ path: ["a", "b", "c"] }, { path: ["a", "b", "d"] }]
// into { "a ❯ b": ["c", "d"] }.
let pathMap = new Map < string , ( { name : string } & TestResult ) [ ] > ( ) ;
for ( const f of this . # failures ) {
if ( f . path . length === 0 ) throw new RangeError ( "path is empty" ) ;
const key = f . path . slice ( 0 , - 1 ) . join ( SEP ) ;
if ( ! pathMap . has ( key ) ) pathMap . set ( key , [ ] ) ;
pathMap . get ( key ) ! . push ( { name : f.path.at ( - 1 ) ! , . . . f } ) ;
}
this . stream . writeln ( "" ) ;
this . stream . writeln ( "FAILURES" ) ;
this . stream . writeln ( "========" ) ;
for ( const [ path , tests ] of pathMap ) {
if ( tests . length === 1 ) {
this . # writeOutput ( tests [ 0 ] , path ? ` ${ path } ${ SEP } ` : "" , false ) ;
} else {
this . stream . writeln ( path ) ;
for ( const t of tests ) this . # writeOutput ( t , " - " , true ) ;
}
}
this . stream . writeln ( "" ) ;
const { successes , failures } = this . counts ;
this . stream . writeln ( ` ${ successes } succeeded, ${ failures } failed ` ) ;
}
# writeOutput ( test : { name : string } & TestResult , prefix : string , nested : boolean ) {
let output = "" ;
if ( test . logs . length ) {
// Individual logs might span multiple lines, so join them together
// then split it again.
const logStr = test . logs . join ( "\n" ) ;
const lines = logStr . split ( "\n" ) ;
if ( lines . length <= 1 ) {
output = ` : ${ logStr } ` ;
} else {
const padding = nested ? " " : " " ;
output = ":\n" + lines . map ( ( line ) = > padding + line ) . join ( "\n" ) ;
}
}
this . stream . writeln ( ` ${ prefix } ${ test . name } ${ output } ` ) ;
}
}
const r = new StreamReporter ( { writeln : console.log } , true ) ;
r . update ( [ "alien" , "can walk" ] , { success : false , logs : [ "assertion failed" ] } ) ;
r . update ( [ "alien" , "can speak" ] , { success : false , logs : [ "Uncaught ReferenceError: larynx is not defined" ] } ) ;
r . update ( [ "alien" , "sleep" ] , { success : true , logs : [ ] } ) ;
r . update ( [ "Tetromino" , "generate" , "tessellates" ] , { success : false , logs : [ "assertion failed: 1 != 2" ] } ) ;
r . update ( [ "Tetromino" , "solve" , "works" ] , { success : true , logs : [ ] } ) ;
r . update ( [ "discombobulate" , "english" ] , { success : false , logs : [ "hippopotomonstrosesquipedaliophobia\npneumonoultramicroscopicsilicovolcanoconiosis" , "supercalifragilisticexpialidocious" ] } ) ;
r . update ( [ "discombobulate" , "geography" ] , { success : false , logs : [ "Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu" ] } ) ;
r . update ( [ "recombobulate" ] , { success : true , logs : [ ] } ) ;
r . finalize ( ) ;