feat: runner without dynamic import (#5)

* feat: runner without dynamic import

* refactor: case collection count

* feat: handle failures

* revert: restore test
This commit is contained in:
Marc 2023-03-31 00:32:45 -04:00
parent b34766244f
commit 57f2757adf
Signed by: marc
GPG key ID: 048E042F22B5DC79
5 changed files with 129 additions and 98 deletions

View file

@ -1,18 +1,9 @@
import Context from './context'
import { greenText, redText, exec, generateCachedCollectedPathFromActual } from './utils'
import util from 'util'
import childProcess from 'child_process'
import { promises as fs, type Dirent, type PathLike } from 'fs'
import path from 'path'
function greenText(text: string): string {
return `\x1b[32m${text}\x1b[0m`
}
function redText(text: string): string {
return `\x1b[31m${text}\x1b[0m`
}
/*
* Collects test files recursively starting from the provided root
* path.
@ -41,48 +32,47 @@ async function collectTests(root: string): Promise<Array<string>> {
return collectedHere
}
/*
* Collects then executes test cases based on provided test files.
*/
async function runTests(collectedPaths: Array<string>) {
/*
* Test files are imported dynamically so the `test` functions
* defined in them are run. Running the functions doesn't actually
* run the test, but instead builds the catalog of
* known cases, which are executed in the next step.
*/
await Promise.all(
collectedPaths.map(async (collectedPath) => {
await import(path.resolve(collectedPath))
}),
for await (const collectedPath of collectedPaths) {
// FIXME: This should just use `node` and transform if TS is present instead.
const result = await exec(`ts-node ${collectedPath}`, {})
console.log(result.stdout)
}
}
async function collectCases(collectedPaths: Array<string>) {
let collectedCount = 0
for await (const collectedPath of collectedPaths) {
// FIXME: This should just use `node` and transform if TS is present instead.
const result = await exec(`COLLECT=1 ts-node ${collectedPath}`, {})
const collectedCases = await fs.readFile(
`.womm-cache/${generateCachedCollectedPathFromActual(path.resolve(collectedPath))}`,
{ encoding: 'utf8' },
)
console.log(greenText(`Collected ${Context.collectedTests.size} cases.`))
/*
* Each case collected is executed and can fail independently
* of its peers.
*/
for (let entry of Context.collectedTests.entries()) {
const [testLabel, testCase] = entry
console.group(greenText(`Test: ${testLabel}`))
try {
testCase.call()
} catch (e) {
console.error(redText(`FAIL ${testLabel}`))
console.error(e)
}
console.groupEnd()
collectedCount += collectedCases.split('\n').length
}
console.log(greenText(`Collected ${collectedCount} cases`))
} /*
* Logic executed when running the test runner CLI.
*/
;(async () => {
console.group('Test run')
const collectionRoot = process.argv[2]
const [, , collectionRoot, ...omit] = process.argv
try {
await fs.mkdir('.womm-cache')
const collectedTests = await collectTests(collectionRoot)
runTests(collectedTests)
await collectCases(collectedTests)
await runTests(collectedTests)
} catch (e) {
console.group(redText('Test run failed'))
console.log(redText(String(e)))
console.groupEnd()
} finally {
await fs.rm('.womm-cache', { force: true, recursive: true })
}
})().catch((e) => {
throw e
})

View file

@ -1,18 +1,38 @@
import Context from './context'
import { promises as fs } from 'fs'
import expect from './expect'
import { greenText, redText, generateCachedCollectedPathFromActual } from './utils'
import { type TestCaseLabel, type TestCaseFunction, type TestCaseGroup } from './types'
function test(label: string, testCase: any) {
// Hack to get the file that contains the test definition.
const _prepareStackTrace = Error.prepareStackTrace
Error.prepareStackTrace = (_, stack) => stack
const stack = new Error().stack?.slice(1)
Error.prepareStackTrace = _prepareStackTrace
function describe(label: TestCaseLabel, testGroup: TestCaseGroup) {
if (process.env.COLLECT) {
testGroup()
return
}
const testCaseLocation = String(stack?.[0])?.match(/\(.*\)/)?.[0] ?? 'unknown'
Context.collectedTests.set(`${testCaseLocation}:${label}`, testCase)
console.group(greenText(label))
testGroup()
console.groupEnd()
}
function test(label: TestCaseLabel, testCase: TestCaseFunction): void {
if (process.env.COLLECT) {
fs.appendFile(`.womm-cache/${generateCachedCollectedPathFromActual(process.argv[1])}`, `${label}\n`)
return
}
try {
testCase()
console.log(greenText(`[PASSED] ${label}`))
} catch (e) {
console.group(redText(`[FAILED] ${label}`))
console.log(redText(String(e)))
console.groupEnd()
}
}
const it = test
export { it, test, expect }
export { it, test, expect, describe }

View file

@ -1,6 +1,7 @@
export type TestCaseLabel = string
export type TestFilePath = string
export type TestCaseFunction = () => void
export type TestCaseGroup = () => void
export interface IContext {
collectedTests: Map<TestFilePath, any>

16
src/utils.ts Normal file
View file

@ -0,0 +1,16 @@
import util from 'util'
import childProcess from 'child_process'
export const exec = util.promisify(childProcess.exec)
export function generateCachedCollectedPathFromActual(path: string): string {
return path.replace(/[\/.]/g, '_')
}
export function greenText(text: string): string {
return `\x1b[32m${text}\x1b[0m`
}
export function redText(text: string): string {
return `\x1b[31m${text}\x1b[0m`
}

View file

@ -1,6 +1,7 @@
import assert from 'assert'
import { test, expect } from '../src/testCaseUtils'
import { describe, test, expect } from '../src/testCaseUtils'
describe('Equality', () => {
test('Equality (number)', () => {
assert.doesNotThrow(() => expect(1).toEqual(1))
})
@ -24,7 +25,9 @@ test('Equality (failed - string)', () => {
test('Equality (failed - boolean)', () => {
assert.throws(() => expect(true).toEqual(false))
})
})
describe('Identity', () => {
test('Identity comparison (number)', () => {
assert.doesNotThrow(() => expect(1).toBe(1))
})
@ -52,6 +55,7 @@ test('Identity comparison (failed - string)', () => {
test('Equality negation', () => {
assert.doesNotThrow(() => expect('yes').not.toEqual('no'))
})
})
test('Stacked equality negation', () => {
assert.doesNotThrow(() => expect('yes').not.not.toEqual('yes'))