add a default terminal test runner

This commit is contained in:
Rowan 2025-04-09 18:43:37 -05:00
parent b130e6fd18
commit be1930779e
3 changed files with 172 additions and 0 deletions

55
src/ansi.js Normal file
View file

@ -0,0 +1,55 @@
import { mapObj, pipe } from './fn.js'
export const Reset = Object.freeze({
All: 0,
Intensity: 22,
Italic: 23,
Underline: 24,
Blink: 25,
Reverse: 27,
Conceal: 28,
Strike: 29
})
export const Style = Object.freeze({
Bold: 1,
Faint: 2,
Italic: 3,
Underline: 4,
SlowBlink: 5,
RapidBlink: 6,
ReverseVideo: 7,
Conceal: 8,
Strike: 9
})
const ForegroundColor = Object.freeze({
Black: 30,
Red: 31,
Green: 32,
Yellow: 33,
Blue: 34,
Magenta: 35,
Cyan: 36,
White: 37,
Default: 39
})
const BackgroundColor = mapObj(
([k, v]) => [k, v + 10]
)(ForegroundColor)
const sgr = args => `\x1b[${args.join(';')}m`
export const Color = Object.freeze({
Foreground: ForegroundColor,
Background: BackgroundColor,
get fg() { return this.Foreground },
get bg() { return this.Background }
})
export const prepend = args => s => `${sgr(args)}${s}`
export const append = args => s => `${s}${sgr(args)}`
export const wrap = (start, end) => pipe(prepend(start), append(end))

22
src/fn.js Normal file
View file

@ -0,0 +1,22 @@
export const exec = f => f()
export const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x)
export const map = f => xs => xs.map(f)
export const mapObj = f => pipe(Object.entries, map(f), Object.fromEntries)
export const partition = (predicate, xs) => {
const passed = []
const failed = []
const len = xs.length
for (let i = 0; i < len; i++) {
const value = xs[i]
if (predicate(value)) {
passed.push(value)
} else {
failed.push(value)
}
}
return [passed, failed]
}

95
src/runner.js Normal file
View file

@ -0,0 +1,95 @@
import { Color, Style, Reset, wrap } from './ansi.js'
import { exec, partition } from './fn.js'
const { Yellow, Green, Red } = Color.Foreground
const { Bold } = Style
const reset = [Reset.All]
const style = (s, ...args) => wrap(args, reset)(s)
const fail = wrap([Red, Bold], reset)
const process = ([name, tests]) => {
const results = tests.map(exec)
const [passed, failed] = partition(test => test.success, results)
return {
name,
passed,
failed,
results,
toString: () => {
const len = results.length
const passLen = passed.length
const failLen = failed.length
let color = Yellow
if (passLen === len) {
color = Green
} else if (failLen === len) {
color = Red
}
const header = `${style(name, Bold, color)} (${passLen}/${len})`
const text = results.map(result => {
const status = result.success ? 'pass' : fail('fail')
return ` ${result.description}... ${status}`
}).join('\n')
return `${header}\n${text}\n`
}
}
}
const makeSummary = results => {
const [passed, failed] = results.reduce(
([pass, fail], result) => ([
pass + result.passed.length,
fail + result.failed.length,
]),
[0, 0]
)
return {
passed,
failed,
toString() {
const failures = results
.filter(result => result.failed.length)
.flatMap(result => {
return result.failed.map(({ description, error }) => {
return ` ${result.name}::${description}\n${error.stack}`
}).join('\n\n')
}).join('\n\n')
const summary = `test result: ${failed}. ${passed} passed; ${failed} failed`
return `failures:\n${failures}\n\n${summary}`
}
}
}
export const term = tests => {
const suites = Object.entries(tests).map(process)
suites.forEach(s => console.log(s.toString()))
console.log(makeSummary(suites).toString())
}
const mock = (success, description, error) => () => ({ success, description, error })
term({
SuccessTest: [
mock(true, 'Unit 1'),
mock(true, 'Unit 2')
],
MixedTest: [
mock(false, 'Unit 3', new Error('bb')),
mock(true, 'Unit 4'),
mock(true, 'Unit 5')
],
FailTest: [
mock(false, 'Unit 6', new Error('aa')),
mock(false, 'Unit 7', new Error('cc')),
]
})