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 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 { promises as fs, type Dirent, type PathLike } from 'fs'
import path from 'path' 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 * Collects test files recursively starting from the provided root
* path. * path.
@ -41,48 +32,47 @@ async function collectTests(root: string): Promise<Array<string>> {
return collectedHere return collectedHere
} }
/*
* Collects then executes test cases based on provided test files.
*/
async function runTests(collectedPaths: Array<string>) { async function runTests(collectedPaths: Array<string>) {
/* for await (const collectedPath of collectedPaths) {
* Test files are imported dynamically so the `test` functions // FIXME: This should just use `node` and transform if TS is present instead.
* defined in them are run. Running the functions doesn't actually const result = await exec(`ts-node ${collectedPath}`, {})
* run the test, but instead builds the catalog of console.log(result.stdout)
* known cases, which are executed in the next step.
*/
await Promise.all(
collectedPaths.map(async (collectedPath) => {
await import(path.resolve(collectedPath))
}),
)
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()
} }
}
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' },
)
collectedCount += collectedCases.split('\n').length
}
console.log(greenText(`Collected ${collectedCount} cases`))
} /* } /*
* Logic executed when running the test runner CLI. * Logic executed when running the test runner CLI.
*/ */
;(async () => { ;(async () => {
console.group('Test run') const [, , collectionRoot, ...omit] = process.argv
const collectionRoot = process.argv[2] try {
const collectedTests = await collectTests(collectionRoot) await fs.mkdir('.womm-cache')
runTests(collectedTests)
console.groupEnd() const collectedTests = await collectTests(collectionRoot)
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) => { })().catch((e) => {
throw e throw e
}) })

View file

@ -1,18 +1,38 @@
import Context from './context' import Context from './context'
import { promises as fs } from 'fs'
import expect from './expect' 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) { function describe(label: TestCaseLabel, testGroup: TestCaseGroup) {
// Hack to get the file that contains the test definition. if (process.env.COLLECT) {
const _prepareStackTrace = Error.prepareStackTrace testGroup()
Error.prepareStackTrace = (_, stack) => stack return
const stack = new Error().stack?.slice(1) }
Error.prepareStackTrace = _prepareStackTrace
const testCaseLocation = String(stack?.[0])?.match(/\(.*\)/)?.[0] ?? 'unknown' console.group(greenText(label))
Context.collectedTests.set(`${testCaseLocation}:${label}`, testCase) 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 const it = test
export { it, test, expect } export { it, test, expect, describe }

View file

@ -1,6 +1,7 @@
export type TestCaseLabel = string export type TestCaseLabel = string
export type TestFilePath = string export type TestFilePath = string
export type TestCaseFunction = () => void export type TestCaseFunction = () => void
export type TestCaseGroup = () => void
export interface IContext { export interface IContext {
collectedTests: Map<TestFilePath, any> 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,56 +1,60 @@
import assert from 'assert' import assert from 'assert'
import { test, expect } from '../src/testCaseUtils' import { describe, test, expect } from '../src/testCaseUtils'
test('Equality (number)', () => { describe('Equality', () => {
assert.doesNotThrow(() => expect(1).toEqual(1)) test('Equality (number)', () => {
assert.doesNotThrow(() => expect(1).toEqual(1))
})
test('Equality (string)', () => {
assert.doesNotThrow(() => expect('expectations').toEqual('expectations'))
})
test('Equality (boolean)', () => {
assert.doesNotThrow(() => expect(true).toEqual(true))
})
test('Equality (failed - number)', () => {
assert.throws(() => expect(1).toEqual(2))
})
test('Equality (failed - string)', () => {
assert.throws(() => expect('expectation').toEqual('something else'))
})
test('Equality (failed - boolean)', () => {
assert.throws(() => expect(true).toEqual(false))
})
}) })
test('Equality (string)', () => { describe('Identity', () => {
assert.doesNotThrow(() => expect('expectations').toEqual('expectations')) test('Identity comparison (number)', () => {
}) assert.doesNotThrow(() => expect(1).toBe(1))
})
test('Equality (boolean)', () => { test('Identity comparison (boolean)', () => {
assert.doesNotThrow(() => expect(true).toEqual(true)) assert.doesNotThrow(() => expect(true).toBe(true))
}) })
test('Equality (failed - number)', () => { test('Identity comparison (string)', () => {
assert.throws(() => expect(1).toEqual(2)) assert.doesNotThrow(() => expect('identity').toBe('identity'))
}) })
test('Equality (failed - string)', () => { test('Identity comparison (failed - number)', () => {
assert.throws(() => expect('expectation').toEqual('something else')) assert.throws(() => expect(1).toEqual(2))
}) })
test('Equality (failed - boolean)', () => { test('Identity comparison (failed - boolean)', () => {
assert.throws(() => expect(true).toEqual(false)) assert.throws(() => expect(false).toBe(true))
}) })
test('Identity comparison (number)', () => { test('Identity comparison (failed - string)', () => {
assert.doesNotThrow(() => expect(1).toBe(1)) assert.throws(() => expect('yes').toBe('no'))
}) })
test('Identity comparison (boolean)', () => { test('Equality negation', () => {
assert.doesNotThrow(() => expect(true).toBe(true)) assert.doesNotThrow(() => expect('yes').not.toEqual('no'))
}) })
test('Identity comparison (string)', () => {
assert.doesNotThrow(() => expect('identity').toBe('identity'))
})
test('Identity comparison (failed - number)', () => {
assert.throws(() => expect(1).toEqual(2))
})
test('Identity comparison (failed - boolean)', () => {
assert.throws(() => expect(false).toBe(true))
})
test('Identity comparison (failed - string)', () => {
assert.throws(() => expect('yes').toBe('no'))
})
test('Equality negation', () => {
assert.doesNotThrow(() => expect('yes').not.toEqual('no'))
}) })
test('Stacked equality negation', () => { test('Stacked equality negation', () => {