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:
parent
e6aa1a16c3
commit
40cc9fa664
8 changed files with 128 additions and 47 deletions
14
package.json
14
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",
|
||||
|
|
12
src/help.ts
Normal file
12
src/help.ts
Normal 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
|
||||
`
|
|
@ -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,9 +13,10 @@ import net from 'net'
|
|||
* Collects test files recursively starting from the provided root
|
||||
* path.
|
||||
*/
|
||||
async function collectTests(root: string): Promise<Array<string>> {
|
||||
async function collectTests(roots: Array<string>): Promise<Array<string>> {
|
||||
const collectedHere = []
|
||||
|
||||
for (const root of roots) {
|
||||
const rootStats = await fs.stat(root)
|
||||
|
||||
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 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>) => {
|
||||
acc.push(...collectedSegment)
|
||||
|
@ -29,6 +34,7 @@ async function collectTests(root: string): Promise<Array<string>> {
|
|||
|
||||
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<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.
|
||||
*/
|
||||
;(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()
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -17,3 +17,9 @@ export interface IContext {
|
|||
nodeRuntime: 'ts-node' | 'node'
|
||||
runnerSocket: string
|
||||
}
|
||||
|
||||
export interface Args {
|
||||
targets: Array<string>
|
||||
runtimePath: string
|
||||
help: boolean
|
||||
}
|
||||
|
|
17
src/utils.ts
17
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<T>(data: Array<T>, desiredBatchCount: number = 1): Array<Array<T>> {
|
||||
const desiredBatchSize = Math.max(data.length / desiredBatchCount, 1)
|
||||
return data.reduce((acc, item: T) => {
|
||||
|
|
|
@ -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": [
|
||||
|
|
26
yarn.lock
26
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"
|
||||
|
|
Reference in a new issue