Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jest-runner): improve typings by exposing TestRunner abstract classes #12646

Merged
merged 17 commits into from Apr 8, 2022
24 changes: 11 additions & 13 deletions packages/jest-core/src/TestScheduler.ts
Expand Up @@ -29,7 +29,7 @@ import {
import {createScriptTransformer} from '@jest/transform';
import type {Config} from '@jest/types';
import {formatExecError} from 'jest-message-util';
import type TestRunner from 'jest-runner';
import type {JestTestRunner, TestRunnerContext} from 'jest-runner';
import type {Context} from 'jest-runtime';
import {
buildSnapshotResolver,
Expand All @@ -40,6 +40,11 @@ import ReporterDispatcher from './ReporterDispatcher';
import type TestWatcher from './TestWatcher';
import {shouldRunInBand} from './testSchedulerHelper';

type TestRunnerConstructor = new (
globalConfig: Config.GlobalConfig,
context: TestRunnerContext,
) => JestTestRunner;

export type TestSchedulerOptions = {
startRun: (globalConfig: Config.GlobalConfig) => void;
};
Expand Down Expand Up @@ -206,14 +211,14 @@ class TestScheduler {
showStatus: !runInBand,
});

const testRunners: {[key: string]: TestRunner} = Object.create(null);
const contextsByTestRunner = new WeakMap<TestRunner, Context>();
const testRunners: Record<string, JestTestRunner> = Object.create(null);
const contextsByTestRunner = new WeakMap<JestTestRunner, Context>();
await Promise.all(
Array.from(contexts).map(async context => {
const {config} = context;
if (!testRunners[config.runner]) {
const transformer = await createScriptTransformer(config);
const Runner: typeof TestRunner =
const Runner: TestRunnerConstructor =
await transformer.requireAndTranspileModule(config.runner);
const runner = new Runner(this._globalConfig, {
changedFiles: this._context.changedFiles,
Expand Down Expand Up @@ -262,14 +267,7 @@ class TestScheduler {
),
];

await testRunner.runTests(
tests,
watcher,
undefined,
undefined,
undefined,
testRunnerOptions,
);
await testRunner.runTests(tests, watcher, testRunnerOptions);

unsubscribes.forEach(sub => sub());
} else {
Expand Down Expand Up @@ -310,7 +308,7 @@ class TestScheduler {
}

private _partitionTests(
testRunners: Record<string, TestRunner>,
testRunners: Record<string, JestTestRunner>,
tests: Array<Test>,
): Record<string, Array<Test>> | null {
if (Object.keys(testRunners).length > 1) {
Expand Down
63 changes: 63 additions & 0 deletions packages/jest-runner/__typetests__/jest-runner.test.ts
@@ -0,0 +1,63 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import type {UnsubscribeFn} from 'emittery';
import {expectType} from 'tsd-lite';
import type {Test, TestEvents} from '@jest/test-result';
import type {Config} from '@jest/types';
import {CallbackTestRunner, EmittingTestRunner} from 'jest-runner';
import type {
OnTestFailure,
OnTestStart,
OnTestSuccess,
TestRunnerContext,
TestRunnerOptions,
TestWatcher,
} from 'jest-runner';

const globalConfig = {} as Config.GlobalConfig;
const runnerContext = {} as TestRunnerContext;

class CallbackRunner extends CallbackTestRunner {
override async runTests(
tests: Array<Test>,
watcher: TestWatcher,
onStart: OnTestStart,
onResult: OnTestSuccess,
onFailure: OnTestFailure,
options: TestRunnerOptions,
): Promise<void> {
return;
}
}

const callbackRunner = new CallbackRunner(globalConfig, runnerContext);

expectType<boolean | undefined>(callbackRunner.isSerial);
expectType<false>(callbackRunner.supportsEventEmitters);

class EmittingRunner extends EmittingTestRunner {
override on<Name extends keyof TestEvents>(
eventName: string,
listener: (eventData: TestEvents[Name]) => void | Promise<void>,
): UnsubscribeFn {
return () => {};
}

override async runTests(
tests: Array<Test>,
watcher: TestWatcher,
options: TestRunnerOptions,
): Promise<void> {
return;
}
}

const emittingRunner = new EmittingRunner(globalConfig, runnerContext);

expectType<boolean | undefined>(emittingRunner.isSerial);
expectType<true>(emittingRunner.supportsEventEmitters);
11 changes: 11 additions & 0 deletions packages/jest-runner/__typetests__/tsconfig.json
@@ -0,0 +1,11 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"noUnusedLocals": false,
"noUnusedParameters": false,
"skipLibCheck": true,

"types": []
},
"include": ["./**/*"]
}
3 changes: 2 additions & 1 deletion packages/jest-runner/package.json
Expand Up @@ -39,10 +39,11 @@
"throat": "^6.0.1"
},
"devDependencies": {
"@tsd/typescript": "~4.6.2",
"@types/exit": "^0.1.30",
"@types/graceful-fs": "^4.1.2",
"@types/source-map-support": "^0.5.0",
"jest-jasmine2": "^28.0.0-alpha.8"
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
"tsd-lite": "^0.5.1"
},
"engines": {
"node": "^12.13.0 || ^14.15.0 || ^16.13.0 || >=17.0.0"
Expand Down
6 changes: 0 additions & 6 deletions packages/jest-runner/src/__tests__/testRunner.test.ts
Expand Up @@ -42,9 +42,6 @@ test('injects the serializable module map into each worker in watch mode', async
{context, path: './file2.test.js'},
],
new TestWatcher({isWatchMode: globalConfig.watch}),
undefined,
undefined,
undefined,
{serial: false},
);

Expand Down Expand Up @@ -76,9 +73,6 @@ test('assign process.env.JEST_WORKER_ID = 1 when in runInBand mode', async () =>
await new TestRunner(globalConfig, {}).runTests(
[{context, path: './file.test.js'}],
new TestWatcher({isWatchMode: globalConfig.watch}),
undefined,
undefined,
undefined,
{serial: true},
);

Expand Down
62 changes: 20 additions & 42 deletions packages/jest-runner/src/index.ts
Expand Up @@ -14,64 +14,43 @@ import type {
TestFileEvent,
TestResult,
} from '@jest/test-result';
import type {Config} from '@jest/types';
import {deepCyclicCopy} from 'jest-util';
import {PromiseWithCustomMessage, Worker} from 'jest-worker';
import runTest from './runTest';
import type {SerializableResolver, worker} from './testWorker';
import type {
OnTestFailure,
OnTestStart,
OnTestSuccess,
TestRunnerContext,
TestRunnerOptions,
TestWatcher,
} from './types';
import {EmittingTestRunner, TestRunnerOptions, TestWatcher} from './types';

const TEST_WORKER_PATH = require.resolve('./testWorker');

interface WorkerInterface extends Worker {
worker: typeof worker;
}

export {CallbackTestRunner, EmittingTestRunner} from './types';
SimenB marked this conversation as resolved.
Show resolved Hide resolved
export type {
OnTestFailure,
OnTestStart,
OnTestSuccess,
TestWatcher,
TestRunnerContext,
TestRunnerOptions,
JestTestRunner,
} from './types';

export default class TestRunner {
private readonly _globalConfig: Config.GlobalConfig;
private readonly _context: TestRunnerContext;
private readonly eventEmitter = new Emittery<TestEvents>();
readonly supportsEventEmitters: boolean = true;

readonly isSerial?: boolean;

constructor(globalConfig: Config.GlobalConfig, context: TestRunnerContext) {
this._globalConfig = globalConfig;
this._context = context;
}
export default class TestRunner extends EmittingTestRunner {
readonly #eventEmitter = new Emittery<TestEvents>();

async runTests(
tests: Array<Test>,
watcher: TestWatcher,
// keep these three as they're still passed and should be in the types,
// even if this particular runner doesn't use them
_onStart: OnTestStart | undefined,
_onResult: OnTestSuccess | undefined,
_onFailure: OnTestFailure | undefined,
options: TestRunnerOptions,
): Promise<void> {
return await (options.serial
? this._createInBandTestRun(tests, watcher)
: this._createParallelTestRun(tests, watcher));
? this.#createInBandTestRun(tests, watcher)
: this.#createParallelTestRun(tests, watcher));
}

private async _createInBandTestRun(tests: Array<Test>, watcher: TestWatcher) {
async #createInBandTestRun(tests: Array<Test>, watcher: TestWatcher) {
process.env.JEST_WORKER_ID = '1';
const mutex = throat(1);
return tests.reduce(
Expand All @@ -85,12 +64,12 @@ export default class TestRunner {

// `deepCyclicCopy` used here to avoid mem-leak
const sendMessageToJest: TestFileEvent = (eventName, args) =>
this.eventEmitter.emit(
this.#eventEmitter.emit(
eventName,
deepCyclicCopy(args, {keepPrototype: false}),
);

await this.eventEmitter.emit('test-file-start', [test]);
await this.#eventEmitter.emit('test-file-start', [test]);

return runTest(
test.path,
Expand All @@ -103,18 +82,16 @@ export default class TestRunner {
})
.then(
result =>
this.eventEmitter.emit('test-file-success', [test, result]),
err => this.eventEmitter.emit('test-file-failure', [test, err]),
this.#eventEmitter.emit('test-file-success', [test, result]),
error =>
this.#eventEmitter.emit('test-file-failure', [test, error]),
),
),
Promise.resolve(),
);
}

private async _createParallelTestRun(
tests: Array<Test>,
watcher: TestWatcher,
) {
async #createParallelTestRun(tests: Array<Test>, watcher: TestWatcher) {
const resolvers: Map<string, SerializableResolver> = new Map();
for (const test of tests) {
if (!resolvers.has(test.context.config.name)) {
Expand Down Expand Up @@ -146,7 +123,7 @@ export default class TestRunner {
return Promise.reject();
}

await this.eventEmitter.emit('test-file-start', [test]);
await this.#eventEmitter.emit('test-file-start', [test]);

const promise = worker.worker({
config: test.context.config,
Expand All @@ -166,7 +143,7 @@ export default class TestRunner {
if (promise.UNSTABLE_onCustomMessage) {
// TODO: Get appropriate type for `onCustomMessage`
promise.UNSTABLE_onCustomMessage(([event, payload]: any) =>
this.eventEmitter.emit(event, payload),
this.#eventEmitter.emit(event, payload),
);
}

Expand All @@ -184,8 +161,9 @@ export default class TestRunner {
const runAllTests = Promise.all(
tests.map(test =>
runTestInWorker(test).then(
result => this.eventEmitter.emit('test-file-success', [test, result]),
error => this.eventEmitter.emit('test-file-failure', [test, error]),
result =>
this.#eventEmitter.emit('test-file-success', [test, result]),
error => this.#eventEmitter.emit('test-file-failure', [test, error]),
),
),
);
Expand All @@ -211,7 +189,7 @@ export default class TestRunner {
eventName: Name,
listener: (eventData: TestEvents[Name]) => void | Promise<void>,
): Emittery.UnsubscribeFn {
return this.eventEmitter.on(eventName, listener);
return this.#eventEmitter.on(eventName, listener);
}
}

Expand Down
43 changes: 43 additions & 0 deletions packages/jest-runner/src/types.ts
Expand Up @@ -10,6 +10,7 @@ import type {JestEnvironment} from '@jest/environment';
import type {
SerializableError,
Test,
TestEvents,
TestFileEvent,
TestResult,
} from '@jest/test-result';
Expand All @@ -19,10 +20,12 @@ import type RuntimeType from 'jest-runtime';
export type ErrorWithCode = Error & {code?: string};

export type OnTestStart = (test: Test) => Promise<void>;

export type OnTestFailure = (
test: Test,
serializableError: SerializableError,
) => Promise<void>;

export type OnTestSuccess = (
test: Test,
testResult: TestResult,
Expand Down Expand Up @@ -60,3 +63,43 @@ export interface TestWatcher extends Emittery<{change: WatcherState}> {
isInterrupted(): boolean;
isWatchMode(): boolean;
}

abstract class BaseTestRunner {
readonly isSerial?: boolean;
abstract readonly supportsEventEmitters: boolean;

constructor(
protected readonly _globalConfig: Config.GlobalConfig,
protected readonly _context: TestRunnerContext,
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
) {}
}

export abstract class CallbackTestRunner extends BaseTestRunner {
readonly supportsEventEmitters = false;

abstract runTests(
tests: Array<Test>,
watcher: TestWatcher,
onStart: OnTestStart,
onResult: OnTestSuccess,
onFailure: OnTestFailure,
options: TestRunnerOptions,
): Promise<void>;
}

export abstract class EmittingTestRunner extends BaseTestRunner {
readonly supportsEventEmitters = true;

abstract runTests(
tests: Array<Test>,
watcher: TestWatcher,
options: TestRunnerOptions,
): Promise<void>;

abstract on<Name extends keyof TestEvents>(
eventName: Name,
listener: (eventData: TestEvents[Name]) => void | Promise<void>,
): Emittery.UnsubscribeFn;
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
}

export type JestTestRunner = CallbackTestRunner | EmittingTestRunner;