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:
parent
b34766244f
commit
57f2757adf
5 changed files with 129 additions and 98 deletions
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
16
src/utils.ts
Normal 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`
|
||||
}
|
|
@ -1,56 +1,60 @@
|
|||
import assert from 'assert'
|
||||
import { test, expect } from '../src/testCaseUtils'
|
||||
import { describe, test, expect } from '../src/testCaseUtils'
|
||||
|
||||
test('Equality (number)', () => {
|
||||
describe('Equality', () => {
|
||||
test('Equality (number)', () => {
|
||||
assert.doesNotThrow(() => expect(1).toEqual(1))
|
||||
})
|
||||
})
|
||||
|
||||
test('Equality (string)', () => {
|
||||
test('Equality (string)', () => {
|
||||
assert.doesNotThrow(() => expect('expectations').toEqual('expectations'))
|
||||
})
|
||||
})
|
||||
|
||||
test('Equality (boolean)', () => {
|
||||
test('Equality (boolean)', () => {
|
||||
assert.doesNotThrow(() => expect(true).toEqual(true))
|
||||
})
|
||||
})
|
||||
|
||||
test('Equality (failed - number)', () => {
|
||||
test('Equality (failed - number)', () => {
|
||||
assert.throws(() => expect(1).toEqual(2))
|
||||
})
|
||||
})
|
||||
|
||||
test('Equality (failed - string)', () => {
|
||||
test('Equality (failed - string)', () => {
|
||||
assert.throws(() => expect('expectation').toEqual('something else'))
|
||||
})
|
||||
})
|
||||
|
||||
test('Equality (failed - boolean)', () => {
|
||||
test('Equality (failed - boolean)', () => {
|
||||
assert.throws(() => expect(true).toEqual(false))
|
||||
})
|
||||
})
|
||||
|
||||
test('Identity comparison (number)', () => {
|
||||
describe('Identity', () => {
|
||||
test('Identity comparison (number)', () => {
|
||||
assert.doesNotThrow(() => expect(1).toBe(1))
|
||||
})
|
||||
})
|
||||
|
||||
test('Identity comparison (boolean)', () => {
|
||||
test('Identity comparison (boolean)', () => {
|
||||
assert.doesNotThrow(() => expect(true).toBe(true))
|
||||
})
|
||||
})
|
||||
|
||||
test('Identity comparison (string)', () => {
|
||||
test('Identity comparison (string)', () => {
|
||||
assert.doesNotThrow(() => expect('identity').toBe('identity'))
|
||||
})
|
||||
})
|
||||
|
||||
test('Identity comparison (failed - number)', () => {
|
||||
test('Identity comparison (failed - number)', () => {
|
||||
assert.throws(() => expect(1).toEqual(2))
|
||||
})
|
||||
})
|
||||
|
||||
test('Identity comparison (failed - boolean)', () => {
|
||||
test('Identity comparison (failed - boolean)', () => {
|
||||
assert.throws(() => expect(false).toBe(true))
|
||||
})
|
||||
})
|
||||
|
||||
test('Identity comparison (failed - string)', () => {
|
||||
test('Identity comparison (failed - string)', () => {
|
||||
assert.throws(() => expect('yes').toBe('no'))
|
||||
})
|
||||
})
|
||||
|
||||
test('Equality negation', () => {
|
||||
test('Equality negation', () => {
|
||||
assert.doesNotThrow(() => expect('yes').not.toEqual('no'))
|
||||
})
|
||||
})
|
||||
|
||||
test('Stacked equality negation', () => {
|
||||
|
|
Reference in a new issue