From 57f2757adfdcb9a8c4571f55281fb541fde91be5 Mon Sep 17 00:00:00 2001 From: Marc Cataford Date: Fri, 31 Mar 2023 00:32:45 -0400 Subject: [PATCH] feat: runner without dynamic import (#5) * feat: runner without dynamic import * refactor: case collection count * feat: handle failures * revert: restore test --- src/runner.ts | 82 ++++++++++++++++++---------------------- src/testCaseUtils.ts | 38 ++++++++++++++----- src/types.ts | 1 + src/utils.ts | 16 ++++++++ tests/expect.test.ts | 90 +++++++++++++++++++++++--------------------- 5 files changed, 129 insertions(+), 98 deletions(-) create mode 100644 src/utils.ts diff --git a/src/runner.ts b/src/runner.ts index 8dbfaa3..b9a3f68 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -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> { return collectedHere } -/* - * Collects then executes test cases based on provided test files. - */ async function runTests(collectedPaths: Array) { - /* - * 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)) - }), - ) - - 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() + 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) { + 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. */ ;(async () => { - console.group('Test run') - const collectionRoot = process.argv[2] - const collectedTests = await collectTests(collectionRoot) - runTests(collectedTests) - console.groupEnd() + const [, , collectionRoot, ...omit] = process.argv + try { + await fs.mkdir('.womm-cache') + + 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) => { throw e }) diff --git a/src/testCaseUtils.ts b/src/testCaseUtils.ts index 26d827e..e84e434 100644 --- a/src/testCaseUtils.ts +++ b/src/testCaseUtils.ts @@ -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 } diff --git a/src/types.ts b/src/types.ts index add379f..8e021d0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..ca35e30 --- /dev/null +++ b/src/utils.ts @@ -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` +} diff --git a/tests/expect.test.ts b/tests/expect.test.ts index 633aeb0..b488f5b 100644 --- a/tests/expect.test.ts +++ b/tests/expect.test.ts @@ -1,56 +1,60 @@ import assert from 'assert' -import { test, expect } from '../src/testCaseUtils' +import { describe, test, expect } from '../src/testCaseUtils' -test('Equality (number)', () => { - assert.doesNotThrow(() => expect(1).toEqual(1)) +describe('Equality', () => { + 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)', () => { - assert.doesNotThrow(() => expect('expectations').toEqual('expectations')) -}) +describe('Identity', () => { + test('Identity comparison (number)', () => { + assert.doesNotThrow(() => expect(1).toBe(1)) + }) -test('Equality (boolean)', () => { - assert.doesNotThrow(() => expect(true).toEqual(true)) -}) + test('Identity comparison (boolean)', () => { + assert.doesNotThrow(() => expect(true).toBe(true)) + }) -test('Equality (failed - number)', () => { - assert.throws(() => expect(1).toEqual(2)) -}) + test('Identity comparison (string)', () => { + assert.doesNotThrow(() => expect('identity').toBe('identity')) + }) -test('Equality (failed - string)', () => { - assert.throws(() => expect('expectation').toEqual('something else')) -}) + test('Identity comparison (failed - number)', () => { + assert.throws(() => expect(1).toEqual(2)) + }) -test('Equality (failed - boolean)', () => { - assert.throws(() => expect(true).toEqual(false)) -}) + test('Identity comparison (failed - boolean)', () => { + assert.throws(() => expect(false).toBe(true)) + }) -test('Identity comparison (number)', () => { - assert.doesNotThrow(() => expect(1).toBe(1)) -}) + test('Identity comparison (failed - string)', () => { + assert.throws(() => expect('yes').toBe('no')) + }) -test('Identity comparison (boolean)', () => { - assert.doesNotThrow(() => expect(true).toBe(true)) -}) - -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('Equality negation', () => { + assert.doesNotThrow(() => expect('yes').not.toEqual('no')) + }) }) test('Stacked equality negation', () => {