From be1930779eb202c88632137627967ae2cde2da24 Mon Sep 17 00:00:00 2001 From: rowan Date: Wed, 9 Apr 2025 18:43:37 -0500 Subject: [PATCH] add a default terminal test runner --- src/ansi.js | 55 +++++++++++++++++++++++++++++ src/fn.js | 22 ++++++++++++ src/runner.js | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 src/ansi.js create mode 100644 src/fn.js create mode 100644 src/runner.js diff --git a/src/ansi.js b/src/ansi.js new file mode 100644 index 0000000..eadb60c --- /dev/null +++ b/src/ansi.js @@ -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)) + diff --git a/src/fn.js b/src/fn.js new file mode 100644 index 0000000..e74fdbd --- /dev/null +++ b/src/fn.js @@ -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] +} diff --git a/src/runner.js b/src/runner.js new file mode 100644 index 0000000..000eb9d --- /dev/null +++ b/src/runner.js @@ -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')), + ] +}) +