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:
Marc 2023-04-15 21:09:04 -04:00 committed by GitHub
parent 6a9e9ee3fc
commit 912261443a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 96 additions and 117 deletions

View file

@ -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)

View file

@ -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

View file

@ -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)
})

View file

@ -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

View file

@ -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', {

View file

@ -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

View file

@ -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
View 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
}
}

View file

@ -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)

View file

@ -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,
} }