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 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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
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 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', () => {
|
||||||
|
|
Reference in a new issue