refactor: build context and run tests after gathering facts (#26)
* refactor: build context and run tests after gathering facts * test: update integration test to not check for case collection * refactor: further cleanup * refactor: leftover log statements * refactor: further cleaning * refactor: no more collector
This commit is contained in:
parent
6a9e9ee3fc
commit
912261443a
10 changed files with 96 additions and 117 deletions
|
@ -36,7 +36,6 @@ async function requires_ts_node_for_ts_tests() {
|
||||||
|
|
||||||
assert.ok(stdout.includes('sample.test.ts is not supported without --ts and will be ignored'), 'Unsupported test notice not found')
|
assert.ok(stdout.includes('sample.test.ts is not supported without --ts and will be ignored'), 'Unsupported test notice not found')
|
||||||
assert.ok(stdout.includes('Collected 0 test files'), 'Did not find notice of no test collected')
|
assert.ok(stdout.includes('Collected 0 test files'), 'Did not find notice of no test collected')
|
||||||
assert.ok(stdout.includes('Collected 0 test cases'), 'Did not find notice of no cases collected')
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
await tearDown()
|
await tearDown()
|
||||||
console.log(e)
|
console.log(e)
|
||||||
|
|
|
@ -14,5 +14,4 @@ addHashbang() {
|
||||||
}
|
}
|
||||||
|
|
||||||
addHashbang dist/cli.js
|
addHashbang dist/cli.js
|
||||||
addHashbang dist/collector.js
|
|
||||||
addHashbang dist/worker.js
|
addHashbang dist/worker.js
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { type Context } from './types'
|
|
||||||
import { getContext, spawnProcess } from './utils'
|
|
||||||
|
|
||||||
async function collectCases(context: Context, collectedPaths: Array<string>): Promise<number> {
|
|
||||||
const extraArgs: Array<string> = []
|
|
||||||
|
|
||||||
if (context.ts) extraArgs.push('--transpile-only')
|
|
||||||
|
|
||||||
const runtime = context.ts ? 'ts-node' : 'node'
|
|
||||||
|
|
||||||
let totalCases = 0
|
|
||||||
for await (const collectedPath of collectedPaths) {
|
|
||||||
const collectedCount = await new Promise<number>((resolve, reject) => {
|
|
||||||
let count = 0
|
|
||||||
spawnProcess(runtime, [...extraArgs, collectedPath], {
|
|
||||||
extraEnv: { COLLECT: '1' },
|
|
||||||
|
|
||||||
onClose: (code) => {
|
|
||||||
resolve(count)
|
|
||||||
},
|
|
||||||
onStdoutData: (message) => {
|
|
||||||
count += message.split('\n').filter((caseLabel: string) => caseLabel.length > 0).length
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
totalCases += collectedCount
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalCases
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Collector worker runtime
|
|
||||||
*/
|
|
||||||
async function work() {
|
|
||||||
const [, workerRuntime, ...assignedTestFiles] = process.argv
|
|
||||||
const tsMode = Boolean(process.env.TS === '1')
|
|
||||||
|
|
||||||
const context = getContext(workerRuntime, tsMode)
|
|
||||||
const collectedCount = await collectCases(context, assignedTestFiles)
|
|
||||||
if (process.send) process.send(JSON.stringify({ total: collectedCount }))
|
|
||||||
}
|
|
||||||
|
|
||||||
work().catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { forkWorker, greenText, yellowText, redText, boldText, splitIntoBatches } from './utils'
|
import { forkWorker, yellowText, boldText, splitIntoBatches } from './utils'
|
||||||
import { type Args, type Context, type WorkerReport, type CollectorReport } from './types'
|
import { type Args, type Context, type WorkerReport } from './types'
|
||||||
|
|
||||||
import { promises as fs } from 'fs'
|
import { promises as fs } from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
@ -35,34 +35,6 @@ async function collectTests(roots: Array<string>): Promise<Array<string>> {
|
||||||
return collectedHere
|
return collectedHere
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectCases(context: Context, collectedPaths: Array<string>, workerCount: number = 1): Promise<number> {
|
|
||||||
const batchedCollectedPaths = splitIntoBatches(collectedPaths, workerCount)
|
|
||||||
|
|
||||||
const batchResults = await Promise.all(
|
|
||||||
batchedCollectedPaths.map(
|
|
||||||
(batch): Promise<CollectorReport> =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const collectorReport: CollectorReport = { totalCases: 0 }
|
|
||||||
forkWorker(context.collectorRuntime, batch, {
|
|
||||||
onClose: (code) => {
|
|
||||||
resolve(collectorReport)
|
|
||||||
},
|
|
||||||
onMessage: (message: string) => {
|
|
||||||
collectorReport.totalCases += JSON.parse(message).total
|
|
||||||
},
|
|
||||||
extraEnv: { TS: context.nodeRuntime === 'ts-node' ? '1' : '0' },
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const collectedCount = batchResults.reduce((total, batchResult) => {
|
|
||||||
return total + batchResult.totalCases
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
return collectedCount
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Splits the list of collected test files into `workerCount` batches and starts
|
* Splits the list of collected test files into `workerCount` batches and starts
|
||||||
* worker processes.
|
* worker processes.
|
||||||
|
@ -132,11 +104,6 @@ async function run(args: Args, context: Context) {
|
||||||
|
|
||||||
console.log(`Collected ${supportedTests.length} test files in ${boldText((testCollectTime / 1000).toFixed(3))}s`)
|
console.log(`Collected ${supportedTests.length} test files in ${boldText((testCollectTime / 1000).toFixed(3))}s`)
|
||||||
|
|
||||||
performance.mark('case-collect:start')
|
|
||||||
const collectedCaseCount = await collectCases(context, supportedTests)
|
|
||||||
performance.mark('case-collect:end')
|
|
||||||
const caseCollectTime = performance.measure('case-collect', 'case-collect:start', 'case-collect:end').duration
|
|
||||||
console.log(`Collected ${collectedCaseCount} test cases in ${boldText((caseCollectTime / 1000).toFixed(3))}s`)
|
|
||||||
const summary = await assignTestsToWorkers(context, supportedTests, args.workers)
|
const summary = await assignTestsToWorkers(context, supportedTests, args.workers)
|
||||||
|
|
||||||
const hasFailed = Object.values(summary).filter((workerReport) => !workerReport.pass).length > 0
|
const hasFailed = Object.values(summary).filter((workerReport) => !workerReport.pass).length > 0
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { promises as fs } from 'fs'
|
import { promises as fs } from 'fs'
|
||||||
|
import { performance } from 'perf_hooks'
|
||||||
|
|
||||||
import { greenText, redText } from '../utils'
|
import { getTestContext, setContext } from '../testContext'
|
||||||
import { type TestCaseLabel, type TestCaseFunction, type TestCaseGroup } from '../types'
|
import { type TestCaseLabel, type TestCaseFunction, type TestCaseGroup } from '../types'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -15,13 +16,19 @@ import { type TestCaseLabel, type TestCaseFunction, type TestCaseGroup } from '.
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
function describe(label: TestCaseLabel, testGroup: TestCaseGroup) {
|
function describe(label: TestCaseLabel, testGroup: TestCaseGroup) {
|
||||||
if (process.env.COLLECT) {
|
const parentContext = getTestContext()
|
||||||
testGroup()
|
const currentContext = parentContext.addChildContext(label)
|
||||||
return
|
|
||||||
}
|
setContext(currentContext)
|
||||||
|
|
||||||
console.log(greenText(label))
|
|
||||||
testGroup()
|
testGroup()
|
||||||
|
|
||||||
|
setContext(parentContext)
|
||||||
|
|
||||||
|
if (parentContext.isRootContext) {
|
||||||
|
parentContext.runTests()
|
||||||
|
setContext(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(describe, 'each', {
|
Object.defineProperty(describe, 'each', {
|
||||||
|
|
|
@ -104,7 +104,7 @@ function toNotThrow(func: () => unknown, negated: boolean = false): MatcherRepor
|
||||||
function toHaveLength(value: unknown, length: unknown, negated: boolean = false): MatcherReport {
|
function toHaveLength(value: unknown, length: unknown, negated: boolean = false): MatcherReport {
|
||||||
let valueLength = 0
|
let valueLength = 0
|
||||||
|
|
||||||
const typedLength = length as number
|
const typedLength = length as number
|
||||||
const typedValue = value as WithLength
|
const typedValue = value as WithLength
|
||||||
|
|
||||||
if (typeof typedValue === 'string' || typeof typedValue.length === 'number') valueLength = typedValue.length as number
|
if (typeof typedValue === 'string' || typeof typedValue.length === 'number') valueLength = typedValue.length as number
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { promises as fs } from 'fs'
|
import { type TestCaseLabel, type TestCaseFunction } from '../types'
|
||||||
import { performance } from 'perf_hooks'
|
|
||||||
import { greenText, redText } from '../utils'
|
import { getTestContext, setContext } from '../testContext'
|
||||||
import { type TestCaseLabel, type TestCaseFunction, type TestCaseGroup } from '../types'
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* `test` defines a single test case.
|
* `test` defines a single test case.
|
||||||
|
@ -13,24 +12,13 @@ import { type TestCaseLabel, type TestCaseFunction, type TestCaseGroup } from '.
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
function test(label: TestCaseLabel, testCase: TestCaseFunction): void {
|
function test(label: TestCaseLabel, testCase: TestCaseFunction): void {
|
||||||
performance.mark(`test-${label}:start`)
|
const context = getTestContext()
|
||||||
if (process.env.COLLECT) {
|
|
||||||
console.log(label)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasFailed = false
|
context.addTest(label, testCase)
|
||||||
try {
|
if (context.isRootContext) {
|
||||||
testCase()
|
context.runTests()
|
||||||
} catch (e) {
|
setContext(null)
|
||||||
hasFailed = true
|
|
||||||
console.log(redText(String(e)))
|
|
||||||
}
|
}
|
||||||
performance.mark(`test-${label}:end`)
|
|
||||||
const testDuration = performance.measure(`test-${label}`, `test-${label}:start`, `test-${label}:end`).duration
|
|
||||||
|
|
||||||
if (hasFailed) console.log(redText(`❌ [FAILED] ${label} (${(testDuration / 1000).toFixed(3)}s)`))
|
|
||||||
else console.log(greenText(`✅ [PASS] ${label} (${(testDuration / 1000).toFixed(3)}s)`))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(test, 'each', {
|
Object.defineProperty(test, 'each', {
|
||||||
|
|
72
src/testContext.ts
Normal file
72
src/testContext.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { performance } from 'perf_hooks'
|
||||||
|
import { redText, greenText } from './utils'
|
||||||
|
import { type TestCaseLabel, type TestCaseFunction } from './types'
|
||||||
|
|
||||||
|
let _testContext: TestContext | undefined | null
|
||||||
|
|
||||||
|
export function getTestContext(): TestContext {
|
||||||
|
if (!_testContext) _testContext = new TestContext()
|
||||||
|
|
||||||
|
return _testContext
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setContext(context: TestContext | null) {
|
||||||
|
_testContext = context
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestContext {
|
||||||
|
children: Map<string, TestContext>
|
||||||
|
tests: Map<TestCaseLabel, TestCaseFunction>
|
||||||
|
parentContext?: TestContext | null
|
||||||
|
|
||||||
|
constructor(parentContext: TestContext | null = null) {
|
||||||
|
this.tests = new Map()
|
||||||
|
this.children = new Map()
|
||||||
|
this.parentContext = parentContext
|
||||||
|
}
|
||||||
|
|
||||||
|
addTest(testLabel: TestCaseLabel, testFunction: TestCaseFunction) {
|
||||||
|
this.tests.set(testLabel, testFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
addChildContext(label: string): TestContext {
|
||||||
|
const childContext = new TestContext(this)
|
||||||
|
this.children.set(label, childContext)
|
||||||
|
return childContext
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest(label: TestCaseLabel, test: TestCaseFunction) {
|
||||||
|
performance.mark(`test-${label}:start`)
|
||||||
|
let hasFailed = false
|
||||||
|
try {
|
||||||
|
test()
|
||||||
|
} catch (e) {
|
||||||
|
hasFailed = true
|
||||||
|
console.log(redText(String(e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
performance.mark(`test-${label}:end`)
|
||||||
|
const testDuration = performance.measure(`test-${label}`, `test-${label}:start`, `test-${label}:end`).duration
|
||||||
|
|
||||||
|
if (hasFailed) console.log(redText(`❌ [FAILED] ${label} (${(testDuration / 1000).toFixed(3)}s)`))
|
||||||
|
else console.log(greenText(`✅ [PASS] ${label} (${(testDuration / 1000).toFixed(3)}s)`))
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests() {
|
||||||
|
for (const test of this.tests) {
|
||||||
|
const [label, testFunction] = test
|
||||||
|
this.runTest(label, testFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of this.children) {
|
||||||
|
const [label, childContext] = child
|
||||||
|
console.group(greenText(label))
|
||||||
|
childContext.runTests()
|
||||||
|
console.groupEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isRootContext() {
|
||||||
|
return this.parentContext === null
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ export type TestCaseGroup = (...args: Array<unknown>) => void
|
||||||
export interface Context {
|
export interface Context {
|
||||||
workerRuntime: string
|
workerRuntime: string
|
||||||
runnerRuntime: string
|
runnerRuntime: string
|
||||||
collectorRuntime: string
|
|
||||||
nodeRuntime: 'ts-node' | 'node'
|
nodeRuntime: 'ts-node' | 'node'
|
||||||
ts: boolean
|
ts: boolean
|
||||||
}
|
}
|
||||||
|
@ -58,10 +57,6 @@ export interface WorkerReport {
|
||||||
runtime: number | null
|
runtime: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectorReport {
|
|
||||||
totalCases: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WithLength {
|
export interface WithLength {
|
||||||
length?: number | (() => number)
|
length?: number | (() => number)
|
||||||
size?: number | (() => number)
|
size?: number | (() => number)
|
||||||
|
|
|
@ -54,7 +54,6 @@ export function getContext(runnerPath: string, ts: boolean = false): Context {
|
||||||
return {
|
return {
|
||||||
workerRuntime: path.join(installDirectory, `worker${runnerExtension}`),
|
workerRuntime: path.join(installDirectory, `worker${runnerExtension}`),
|
||||||
runnerRuntime: runnerPath,
|
runnerRuntime: runnerPath,
|
||||||
collectorRuntime: path.join(installDirectory, `collector${runnerExtension}`),
|
|
||||||
nodeRuntime,
|
nodeRuntime,
|
||||||
ts,
|
ts,
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue