feat: cli options, help (#10)

* feat: cli options, help

* refactor: resolve tsconfig kerfuffle

* wip: clean up

* refactor: trim messaging on failure
This commit is contained in:
Marc 2023-04-02 01:13:57 -04:00
parent e6aa1a16c3
commit 40cc9fa664
Signed by: marc
GPG key ID: 048E042F22B5DC79
8 changed files with 128 additions and 47 deletions

View file

@ -1,13 +1,19 @@
{ {
"name": "testrunner", "name": "works-on-my-machine",
"version": "1.0.0", "description": "A no-dependency test runner",
"main": "index.js", "version": "0.0.0",
"main": "dist/runner.js",
"license": "MIT", "license": "MIT",
"packageManager": "yarn@3.5.0", "packageManager": "yarn@3.5.0",
"files": [
"dist/**/*.js"
],
"bin": { "bin": {
"womm": "./dist/runner.js" "womm": "dist/runner.js"
}, },
"scripts": { "scripts": {
"prepack": "yarn build",
"prebuild": "rm -rf dist",
"lint": "rome check src tests && rome format src tests", "lint": "rome check src tests && rome format src tests",
"lint:fix": "rome check src tests --apply && rome format src tests --write", "lint:fix": "rome check src tests --apply && rome format src tests --write",
"test": "ts-node ./src/runner.ts ./tests", "test": "ts-node ./src/runner.ts ./tests",

12
src/help.ts Normal file
View file

@ -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] ...<test-files-or-directories>
Flags:
--help, -h: Prints this message
`

View file

@ -1,5 +1,9 @@
import { getContext, greenText, redText, exec, generateCachedCollectedPathFromActual, splitIntoBatches } from './utils' #!/usr/bin/env node
import { type IContext, type TestServer } from './types'
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 { promises as fs } from 'fs'
import path from 'path' import path from 'path'
@ -9,9 +13,10 @@ import net from 'net'
* Collects test files recursively starting from the provided root * Collects test files recursively starting from the provided root
* path. * path.
*/ */
async function collectTests(root: string): Promise<Array<string>> { async function collectTests(roots: Array<string>): Promise<Array<string>> {
const collectedHere = [] const collectedHere = []
for (const root of roots) {
const rootStats = await fs.stat(root) const rootStats = await fs.stat(root)
if (rootStats.isFile() && path.basename(root).endsWith('.test.ts')) { if (rootStats.isFile() && path.basename(root).endsWith('.test.ts')) {
@ -20,7 +25,7 @@ async function collectTests(root: string): Promise<Array<string>> {
const content = await fs.readdir(root, { encoding: 'utf8' }) const content = await fs.readdir(root, { encoding: 'utf8' })
const segmentedCollectedPaths = await Promise.all( const segmentedCollectedPaths = await Promise.all(
content.map((item: string) => collectTests(path.join(root, item))), content.map((item: string) => collectTests([path.join(root, item)])),
) )
const collectedPaths = segmentedCollectedPaths.reduce((acc: Array<string>, collectedSegment: Array<string>) => { const collectedPaths = segmentedCollectedPaths.reduce((acc: Array<string>, collectedSegment: Array<string>) => {
acc.push(...collectedSegment) acc.push(...collectedSegment)
@ -29,6 +34,7 @@ async function collectTests(root: string): Promise<Array<string>> {
collectedHere.push(...collectedPaths) collectedHere.push(...collectedPaths)
} }
}
return collectedHere return collectedHere
} }
@ -75,8 +81,8 @@ function setUpSocket(socketPath: string): TestServer {
server.workersRegistered = (server.workersRegistered ?? 0) + 1 server.workersRegistered = (server.workersRegistered ?? 0) + 1
console.log(`Worker ${workerId} registered.`) console.log(`Worker ${workerId} registered.`)
s.on('data', (d) => { s.on('data', (rawMessage: Buffer) => {
const workerReport: any = JSON.parse(d.toString('utf8')) const workerReport: { results: string; failed: boolean } = JSON.parse(rawMessage.toString('utf8'))
console.log(workerReport.results) console.log(workerReport.results)
if (workerReport.failed) server.failure = true if (workerReport.failed) server.failure = true
@ -84,25 +90,62 @@ function setUpSocket(socketPath: string): TestServer {
}) })
return server return server
}
function parseArgs(args: Array<string>): Args {
const [, runtimePath, ...userArgs] = args
const {
argsWithoutFlags,
shortFlags,
longFlags,
}: { argsWithoutFlags: Array<string>; longFlags: Array<string>; shortFlags: Array<string> } = (
userArgs as Array<string>
).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<string>
longFlags: Array<string>
shortFlags: Array<string>
},
)
return {
runtimePath,
targets: argsWithoutFlags,
help: longFlags.includes('--help') || shortFlags.includes('-h'),
}
} /* } /*
* Logic executed when running the test runner CLI. * Logic executed when running the test runner CLI.
*/ */
;(async () => { ;(async () => {
const [, runnerPath, collectionRoot, ...omit] = process.argv const args = parseArgs(process.argv)
const context = getContext(runnerPath)
if (args.help) {
console.log(helpText)
return
}
const context = getContext(args.runtimePath)
let server let server
try { try {
server = setUpSocket(context.runnerSocket) server = setUpSocket(context.runnerSocket)
const collectedTests = await collectTests(collectionRoot) const collectedTests = await collectTests(args.targets)
await collectCases(context, collectedTests) await collectCases(context, collectedTests)
await assignTestsToWorkers(context, collectedTests) await assignTestsToWorkers(context, collectedTests)
if (server.failure) throw new Error('test') if (server.failure) throw new Error()
} catch (e) { } catch (e) {
console.group(redText('Test run failed')) console.log(redText('Test run failed'))
console.log(redText(String(e)))
console.groupEnd()
} finally { } finally {
server?.close() server?.close()
} }

View file

@ -1,7 +1,7 @@
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import expect from './expect' import expect from './expect'
import { greenText, redText, generateCachedCollectedPathFromActual } from './utils' import { greenText, redText } from './utils'
import { type TestCaseLabel, type TestCaseFunction, type TestCaseGroup } from './types' import { type TestCaseLabel, type TestCaseFunction, type TestCaseGroup } from './types'
function describe(label: TestCaseLabel, testGroup: TestCaseGroup) { function describe(label: TestCaseLabel, testGroup: TestCaseGroup) {

View file

@ -17,3 +17,9 @@ export interface IContext {
nodeRuntime: 'ts-node' | 'node' nodeRuntime: 'ts-node' | 'node'
runnerSocket: string runnerSocket: string
} }
export interface Args {
targets: Array<string>
runtimePath: string
help: boolean
}

View file

@ -5,8 +5,11 @@ import { type IContext } from './types'
export const exec = util.promisify(childProcess.exec) 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 { export function greenText(text: string): string {
@ -17,6 +20,12 @@ export function redText(text: string): string {
return `\x1b[31m${text}\x1b[0m` 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 { export function getContext(runnerPath: string): IContext {
const installDirectory = path.dirname(runnerPath) const installDirectory = path.dirname(runnerPath)
const runnerExtension = path.extname(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<T>(data: Array<T>, desiredBatchCount: number = 1): Array<Array<T>> { export function splitIntoBatches<T>(data: Array<T>, desiredBatchCount: number = 1): Array<Array<T>> {
const desiredBatchSize = Math.max(data.length / desiredBatchCount, 1) const desiredBatchSize = Math.max(data.length / desiredBatchCount, 1)
return data.reduce((acc, item: T) => { return data.reduce((acc, item: T) => {

View file

@ -7,12 +7,13 @@
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./", "rootDir": "./src",
"removeComments": true, "removeComments": true,
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
}, },
"include": ["src"], "include": ["src"],
"exclude": [ "exclude": [

View file

@ -188,19 +188,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "ts-node@npm:^10.9.1":
version: 10.9.1 version: 10.9.1
resolution: "ts-node@npm:10.9.1" resolution: "ts-node@npm:10.9.1"
@ -266,6 +253,19 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "yn@npm:3.1.1":
version: 3.1.1 version: 3.1.1
resolution: "yn@npm:3.1.1" resolution: "yn@npm:3.1.1"