From 40cc9fa664a06e19a40c5f672a1a88eac1b46aab Mon Sep 17 00:00:00 2001 From: Marc Cataford Date: Sun, 2 Apr 2023 01:13:57 -0400 Subject: [PATCH] feat: cli options, help (#10) * feat: cli options, help * refactor: resolve tsconfig kerfuffle * wip: clean up * refactor: trim messaging on failure --- package.json | 14 +++++-- src/help.ts | 12 ++++++ src/runner.ts | 93 ++++++++++++++++++++++++++++++++------------ src/testCaseUtils.ts | 2 +- src/types.ts | 6 +++ src/utils.ts | 17 +++++++- tsconfig.json | 5 ++- yarn.lock | 26 ++++++------- 8 files changed, 128 insertions(+), 47 deletions(-) create mode 100644 src/help.ts diff --git a/package.json b/package.json index 9aa4fec..f11cde1 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,19 @@ { - "name": "testrunner", - "version": "1.0.0", - "main": "index.js", + "name": "works-on-my-machine", + "description": "A no-dependency test runner", + "version": "0.0.0", + "main": "dist/runner.js", "license": "MIT", "packageManager": "yarn@3.5.0", + "files": [ + "dist/**/*.js" + ], "bin": { - "womm": "./dist/runner.js" + "womm": "dist/runner.js" }, "scripts": { + "prepack": "yarn build", + "prebuild": "rm -rf dist", "lint": "rome check src tests && rome format src tests", "lint:fix": "rome check src tests --apply && rome format src tests --write", "test": "ts-node ./src/runner.ts ./tests", diff --git a/src/help.ts b/src/help.ts new file mode 100644 index 0000000..c51716b --- /dev/null +++ b/src/help.ts @@ -0,0 +1,12 @@ +import { boldText } from './utils' + +export default ` +${boldText('Works on my machine v0.0.0')} + +A no-dependency test runner +--- +womm [--help] [-h] ... + +Flags: +--help, -h: Prints this message +` diff --git a/src/runner.ts b/src/runner.ts index e346050..4a4fd97 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -1,5 +1,9 @@ -import { getContext, greenText, redText, exec, generateCachedCollectedPathFromActual, splitIntoBatches } from './utils' -import { type IContext, type TestServer } from './types' +#!/usr/bin/env node + +import { getContext, greenText, redText, exec, splitIntoBatches } from './utils' +import helpText from './help' +import { type Args, type IContext, type TestServer } from './types' +import { type Buffer } from 'buffer' import { promises as fs } from 'fs' import path from 'path' @@ -9,25 +13,27 @@ import net from 'net' * Collects test files recursively starting from the provided root * path. */ -async function collectTests(root: string): Promise> { +async function collectTests(roots: Array): Promise> { const collectedHere = [] - const rootStats = await fs.stat(root) + for (const root of roots) { + const rootStats = await fs.stat(root) - if (rootStats.isFile() && path.basename(root).endsWith('.test.ts')) { - collectedHere.push(root) - } else if (rootStats.isDirectory()) { - const content = await fs.readdir(root, { encoding: 'utf8' }) + if (rootStats.isFile() && path.basename(root).endsWith('.test.ts')) { + collectedHere.push(root) + } else if (rootStats.isDirectory()) { + const content = await fs.readdir(root, { encoding: 'utf8' }) - const segmentedCollectedPaths = await Promise.all( - content.map((item: string) => collectTests(path.join(root, item))), - ) - const collectedPaths = segmentedCollectedPaths.reduce((acc: Array, collectedSegment: Array) => { - acc.push(...collectedSegment) - return acc - }, [] as Array) + const segmentedCollectedPaths = await Promise.all( + content.map((item: string) => collectTests([path.join(root, item)])), + ) + const collectedPaths = segmentedCollectedPaths.reduce((acc: Array, collectedSegment: Array) => { + acc.push(...collectedSegment) + return acc + }, [] as Array) - collectedHere.push(...collectedPaths) + collectedHere.push(...collectedPaths) + } } return collectedHere @@ -75,8 +81,8 @@ function setUpSocket(socketPath: string): TestServer { server.workersRegistered = (server.workersRegistered ?? 0) + 1 console.log(`Worker ${workerId} registered.`) - s.on('data', (d) => { - const workerReport: any = JSON.parse(d.toString('utf8')) + s.on('data', (rawMessage: Buffer) => { + const workerReport: { results: string; failed: boolean } = JSON.parse(rawMessage.toString('utf8')) console.log(workerReport.results) if (workerReport.failed) server.failure = true @@ -84,25 +90,62 @@ function setUpSocket(socketPath: string): TestServer { }) return server +} + +function parseArgs(args: Array): Args { + const [, runtimePath, ...userArgs] = args + + const { + argsWithoutFlags, + shortFlags, + longFlags, + }: { argsWithoutFlags: Array; longFlags: Array; shortFlags: Array } = ( + userArgs as Array + ).reduce( + (acc, arg: string) => { + if (arg.startsWith('--')) acc.longFlags.push(arg) + else if (arg.startsWith('-')) acc.shortFlags.push(arg) + else acc.argsWithoutFlags.push(arg) + + return acc + }, + { argsWithoutFlags: [], longFlags: [], shortFlags: [] } as { + argsWithoutFlags: Array + longFlags: Array + shortFlags: Array + }, + ) + + return { + runtimePath, + targets: argsWithoutFlags, + help: longFlags.includes('--help') || shortFlags.includes('-h'), + } } /* * Logic executed when running the test runner CLI. */ ;(async () => { - const [, runnerPath, collectionRoot, ...omit] = process.argv - const context = getContext(runnerPath) + const args = parseArgs(process.argv) + + if (args.help) { + console.log(helpText) + return + } + + const context = getContext(args.runtimePath) let server try { server = setUpSocket(context.runnerSocket) - const collectedTests = await collectTests(collectionRoot) + const collectedTests = await collectTests(args.targets) await collectCases(context, collectedTests) + await assignTestsToWorkers(context, collectedTests) - if (server.failure) throw new Error('test') + if (server.failure) throw new Error() + } catch (e) { - console.group(redText('Test run failed')) - console.log(redText(String(e))) - console.groupEnd() + console.log(redText('Test run failed')) } finally { server?.close() } diff --git a/src/testCaseUtils.ts b/src/testCaseUtils.ts index d7b3462..a5e6f84 100644 --- a/src/testCaseUtils.ts +++ b/src/testCaseUtils.ts @@ -1,7 +1,7 @@ import { promises as fs } from 'fs' import expect from './expect' -import { greenText, redText, generateCachedCollectedPathFromActual } from './utils' +import { greenText, redText } from './utils' import { type TestCaseLabel, type TestCaseFunction, type TestCaseGroup } from './types' function describe(label: TestCaseLabel, testGroup: TestCaseGroup) { diff --git a/src/types.ts b/src/types.ts index 62fd072..503c962 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,3 +17,9 @@ export interface IContext { nodeRuntime: 'ts-node' | 'node' runnerSocket: string } + +export interface Args { + targets: Array + runtimePath: string + help: boolean +} diff --git a/src/utils.ts b/src/utils.ts index 4cd91b0..e81ee91 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,8 +5,11 @@ import { type IContext } from './types' export const exec = util.promisify(childProcess.exec) -export function generateCachedCollectedPathFromActual(path: string): string { - return path.replace(/[\/.]/g, '_') +/* + * Terminal text style + */ +export function boldText(text: string): string { + return `\x1b[1m${text}\x1b[0m` } export function greenText(text: string): string { @@ -17,6 +20,12 @@ export function redText(text: string): string { return `\x1b[31m${text}\x1b[0m` } +/* + * Generates a context object that contains general information + * about the test runner. The parameter here should always be + * `process.argv[1]`, which will allow all the other paths + * to be set properly. + */ export function getContext(runnerPath: string): IContext { const installDirectory = path.dirname(runnerPath) const runnerExtension = path.extname(runnerPath) @@ -32,6 +41,10 @@ export function getContext(runnerPath: string): IContext { } } +/* + * Divides the given list into `desiredBatchCount` batches, returning + * an array of arrays which add up to the given list. + */ export function splitIntoBatches(data: Array, desiredBatchCount: number = 1): Array> { const desiredBatchSize = Math.max(data.length / desiredBatchCount, 1) return data.reduce((acc, item: T) => { diff --git a/tsconfig.json b/tsconfig.json index ae4610d..348dc91 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,12 +7,13 @@ "declarationMap": true, "sourceMap": true, "outDir": "./dist", - "rootDir": "./", + "rootDir": "./src", "removeComments": true, "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true }, "include": ["src"], "exclude": [ diff --git a/yarn.lock b/yarn.lock index d970f9a..d809335 100644 --- a/yarn.lock +++ b/yarn.lock @@ -188,19 +188,6 @@ __metadata: languageName: node linkType: hard -"testrunner@workspace:.": - version: 0.0.0-use.local - resolution: "testrunner@workspace:." - dependencies: - "@types/node": ^18.15.10 - rome: ^12.0.0 - ts-node: ^10.9.1 - typescript: ^4.0.0 - bin: - womm: ./dist/runner.js - languageName: unknown - linkType: soft - "ts-node@npm:^10.9.1": version: 10.9.1 resolution: "ts-node@npm:10.9.1" @@ -266,6 +253,19 @@ __metadata: languageName: node linkType: hard +"works-on-my-machine@workspace:.": + version: 0.0.0-use.local + resolution: "works-on-my-machine@workspace:." + dependencies: + "@types/node": ^18.15.10 + rome: ^12.0.0 + ts-node: ^10.9.1 + typescript: ^4.0.0 + bin: + womm: dist/runner.js + languageName: unknown + linkType: soft + "yn@npm:3.1.1": version: 3.1.1 resolution: "yn@npm:3.1.1"