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",
|
"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
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'
|
#!/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,25 +13,27 @@ 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 = []
|
||||||
|
|
||||||
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')) {
|
if (rootStats.isFile() && path.basename(root).endsWith('.test.ts')) {
|
||||||
collectedHere.push(root)
|
collectedHere.push(root)
|
||||||
} else if (rootStats.isDirectory()) {
|
} else if (rootStats.isDirectory()) {
|
||||||
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)
|
||||||
return acc
|
return acc
|
||||||
}, [] as Array<string>)
|
}, [] as 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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
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 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) => {
|
||||||
|
|
|
@ -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": [
|
||||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -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"
|
||||||
|
|
Reference in a new issue