Skip to content

Commit

Permalink
Add option to call setup and teardown methods in workers
Browse files Browse the repository at this point in the history
  • Loading branch information
rafeca committed Sep 21, 2018
1 parent ee751bf commit deb30be
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 30 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- `[pretty-format]` Option to not escape strings in diff messages ([#5661](https://github.com/facebook/jest/pull/5661))
- `[jest-haste-map]` Add `getFileIterator` to `HasteFS` for faster file iteration ([#7010](https://github.com/facebook/jest/pull/7010)).
- `[jest-worker]` [**BREAKING**] Add functionality to call a `setup` method in the worker before the first call and a `teardown` method when ending the farm ([#7014](https://github.com/facebook/jest/pull/7014)).

### Fixes

Expand Down
11 changes: 11 additions & 0 deletions packages/jest-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ The callback you provide is called with the method name, plus all the rest of th

By default, no process is bound to any worker.

#### `setupArgs: Array<mixed>` (optional)

The arguments that will be passed to the `setup` method during initialization.

## Worker

The returned `Worker` instance has all the exposed methods, plus some additional ones to interact with the workers itself:
Expand All @@ -91,6 +95,13 @@ Finishes the workers by killing all workers. No further calls can be done to the

**Note:** Each worker has a unique id (index that starts with `1`) which is available on `process.env.JEST_WORKER_ID`

## Setting up and tearing down the child process

The child process can define two special methods (both of them can be asynchronous):

* `setup()`: If defined, it's executed before the first call to any method in the child.
* `teardown()`: If defined, it's executed when the farm ends.

# More examples

## Standard usage
Expand Down
61 changes: 55 additions & 6 deletions packages/jest-worker/src/__tests__/child.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,24 @@ const mockError = new TypeError('Booo');
const mockExtendedError = new ReferenceError('Booo extended');
const processExit = process.exit;
const processSend = process.send;
const uninitializedParam = {};
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

import {
CHILD_MESSAGE_INITIALIZE,
CHILD_MESSAGE_CALL,
CHILD_MESSAGE_END,
PARENT_MESSAGE_OK,
PARENT_MESSAGE_ERROR,
PARENT_MESSAGE_CLIENT_ERROR,
} from '../types';

let ended;
let mockCount;
let initializeParm = uninitializedParam;

beforeEach(() => {
mockCount = 0;
ended = false;

jest.mock(
'../my-fancy-worker',
Expand Down Expand Up @@ -68,6 +72,14 @@ beforeEach(() => {
fooWorks() {
return 1989;
},

setup(param) {
initializeParm = param;
},

teardown() {
ended = true;
},
};
},
{virtual: true},
Expand Down Expand Up @@ -116,6 +128,7 @@ it('lazily requires the file', () => {
]);

expect(mockCount).toBe(0);
expect(initializeParm).toBe(uninitializedParam); // Not called yet.

process.emit('message', [
CHILD_MESSAGE_CALL,
Expand All @@ -125,6 +138,27 @@ it('lazily requires the file', () => {
]);

expect(mockCount).toBe(1);
expect(initializeParm).toBe(undefined);
});

it('calls initialize with the correct arguments', () => {
expect(mockCount).toBe(0);

process.emit('message', [
CHILD_MESSAGE_INITIALIZE,
true, // Not really used here, but for flow type purity.
'./my-fancy-worker',
['foo'], // Pass empty initialize params so the initialize method is called.
]);

process.emit('message', [
CHILD_MESSAGE_CALL,
true, // Not really used here, but for flow type purity.
'fooWorks',
[],
]);

expect(initializeParm).toBe('foo');
});

it('returns results immediately when function is synchronous', () => {
Expand Down Expand Up @@ -153,7 +187,7 @@ it('returns results immediately when function is synchronous', () => {
]);

expect(process.send.mock.calls[1][0]).toEqual([
PARENT_MESSAGE_ERROR,
PARENT_MESSAGE_CLIENT_ERROR,
'TypeError',
'Booo',
mockError.stack,
Expand All @@ -168,7 +202,7 @@ it('returns results immediately when function is synchronous', () => {
]);

expect(process.send.mock.calls[2][0]).toEqual([
PARENT_MESSAGE_ERROR,
PARENT_MESSAGE_CLIENT_ERROR,
'Number',
void 0,
void 0,
Expand All @@ -183,7 +217,7 @@ it('returns results immediately when function is synchronous', () => {
]);

expect(process.send.mock.calls[3][0]).toEqual([
PARENT_MESSAGE_ERROR,
PARENT_MESSAGE_CLIENT_ERROR,
'ReferenceError',
'Booo extended',
mockExtendedError.stack,
Expand All @@ -197,7 +231,7 @@ it('returns results immediately when function is synchronous', () => {
[],
]);

expect(process.send.mock.calls[4][0][0]).toBe(PARENT_MESSAGE_ERROR);
expect(process.send.mock.calls[4][0][0]).toBe(PARENT_MESSAGE_CLIENT_ERROR);
expect(process.send.mock.calls[4][0][1]).toBe('Error');
expect(process.send.mock.calls[4][0][2]).toEqual(
'"null" or "undefined" thrown',
Expand Down Expand Up @@ -236,7 +270,7 @@ it('returns results when it gets resolved if function is asynchronous', async ()
await sleep(10);

expect(process.send.mock.calls[1][0]).toEqual([
PARENT_MESSAGE_ERROR,
PARENT_MESSAGE_CLIENT_ERROR,
'TypeError',
'Booo',
mockError.stack,
Expand Down Expand Up @@ -295,6 +329,21 @@ it('finishes the process with exit code 0 if requested', () => {
expect(process.exit.mock.calls[0]).toEqual([0]);
});

it('calls the teardown method ', () => {
process.emit('message', [
CHILD_MESSAGE_INITIALIZE,
true, // Not really used here, but for flow type purity.
'./my-fancy-worker',
]);

process.emit('message', [
CHILD_MESSAGE_END,
true, // Not really used here, but for flow type purity.
]);

expect(ended).toBe(true);
});

it('throws if an invalid message is detected', () => {
// Type 27 does not exist.
expect(() => {
Expand Down
1 change: 1 addition & 0 deletions packages/jest-worker/src/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ it('tries instantiating workers with the right options', () => {
expect(Worker.mock.calls[0][0]).toEqual({
forkOptions: {execArgv: []},
maxRetries: 6,
setupArgs: [],
workerId: 1,
workerPath: '/tmp/baz.js',
});
Expand Down
12 changes: 7 additions & 5 deletions packages/jest-worker/src/__tests__/worker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import EventEmitter from 'events';
import {
CHILD_MESSAGE_CALL,
CHILD_MESSAGE_INITIALIZE,
PARENT_MESSAGE_ERROR,
PARENT_MESSAGE_CLIENT_ERROR,
PARENT_MESSAGE_OK,
} from '../types';

Expand Down Expand Up @@ -86,13 +86,15 @@ it('initializes the child process with the given workerPath', () => {
new Worker({
forkOptions: {},
maxRetries: 3,
setupArgs: ['foo', 'bar'],
workerPath: '/tmp/foo/bar/baz.js',
});

expect(forkInterface.send.mock.calls[0][0]).toEqual([
CHILD_MESSAGE_INITIALIZE,
false,
'/tmp/foo/bar/baz.js',
['foo', 'bar'],
]);
});

Expand Down Expand Up @@ -201,7 +203,7 @@ it('relates replies to requests, in order', () => {

// and then the second call replies...
forkInterface.emit('message', [
PARENT_MESSAGE_ERROR,
PARENT_MESSAGE_CLIENT_ERROR,
'TypeError',
'foo',
'TypeError: foo',
Expand Down Expand Up @@ -287,7 +289,7 @@ it('creates error instances for known errors', () => {
worker.send([CHILD_MESSAGE_CALL, false, 'method', []], () => {}, callback1);

forkInterface.emit('message', [
PARENT_MESSAGE_ERROR,
PARENT_MESSAGE_CLIENT_ERROR,
'TypeError',
'bar',
'TypeError: bar',
Expand All @@ -303,7 +305,7 @@ it('creates error instances for known errors', () => {
worker.send([CHILD_MESSAGE_CALL, false, 'method', []], () => {}, callback2);

forkInterface.emit('message', [
PARENT_MESSAGE_ERROR,
PARENT_MESSAGE_CLIENT_ERROR,
'RandomCustomError',
'bar',
'RandomCustomError: bar',
Expand All @@ -320,7 +322,7 @@ it('creates error instances for known errors', () => {
worker.send([CHILD_MESSAGE_CALL, false, 'method', []], () => {}, callback3);

forkInterface.emit('message', [
PARENT_MESSAGE_ERROR,
PARENT_MESSAGE_CLIENT_ERROR,
'Number',
null,
null,
Expand Down
83 changes: 71 additions & 12 deletions packages/jest-worker/src/child.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ import {
CHILD_MESSAGE_CALL,
CHILD_MESSAGE_END,
CHILD_MESSAGE_INITIALIZE,
PARENT_MESSAGE_ERROR,
PARENT_MESSAGE_CLIENT_ERROR,
PARENT_MESSAGE_SETUP_ERROR,
PARENT_MESSAGE_OK,
} from './types';

import type {PARENT_MESSAGE_ERROR} from './types';

let file = null;
let setupArgs: Array<mixed> = [];
let initialized = false;

/**
* This file is a small bootstrapper for workers. It sets up the communication
Expand All @@ -36,14 +41,15 @@ process.on('message', (request: any /* Should be ChildMessage */) => {
switch (request[0]) {
case CHILD_MESSAGE_INITIALIZE:
file = request[2];
setupArgs = request[3];
break;

case CHILD_MESSAGE_CALL:
execMethod(request[2], request[3]);
break;

case CHILD_MESSAGE_END:
process.exit(0);
end();
break;

default:
Expand All @@ -61,7 +67,15 @@ function reportSuccess(result: any) {
process.send([PARENT_MESSAGE_OK, result]);
}

function reportError(error: Error) {
function reportClientError(error: Error) {
return reportError(error, PARENT_MESSAGE_CLIENT_ERROR);
}

function reportInitializeError(error: Error) {
return reportError(error, PARENT_MESSAGE_SETUP_ERROR);
}

function reportError(error: Error, type: PARENT_MESSAGE_ERROR) {
if (!process || !process.send) {
throw new Error('Child can only be used on a forked process');
}
Expand All @@ -71,7 +85,7 @@ function reportError(error: Error) {
}

process.send([
PARENT_MESSAGE_ERROR,
type,
error.constructor && error.constructor.name,
error.message,
error.stack,
Expand All @@ -80,25 +94,70 @@ function reportError(error: Error) {
]);
}

function end(): void {
// $FlowFixMe: This has to be a dynamic require.
const main = require(file);

if (!main.teardown) {
exitProcess();

return;
}

execFunction(main.teardown, main, [], exitProcess, exitProcess);
}

function exitProcess(): void {
process.exit(0);
}

function execMethod(method: string, args: $ReadOnlyArray<any>): void {
// $FlowFixMe: This has to be a dynamic require.
const main = require(file);

let fn;

if (method === 'default') {
fn = main.__esModule ? main['default'] : main;
} else {
fn = main[method];
}

function execHelper() {
execFunction(fn, main, args, reportSuccess, reportClientError);
}

if (initialized || !main.setup) {
execHelper();

return;
}

initialized = true;

execFunction(main.setup, main, setupArgs, execHelper, reportInitializeError);
}

function execFunction(
fn: (...args: $ReadOnlyArray<mixed>) => mixed,
ctx: mixed,
args: $ReadOnlyArray<mixed>,
onResult: (result: mixed) => void,
onError: (error: Error) => void,
): void {
let result;

try {
if (method === 'default') {
result = (main.__esModule ? main['default'] : main).apply(global, args);
} else {
result = main[method].apply(main, args);
}
result = fn.apply(ctx, args);
} catch (err) {
reportError(err);
onError(err);

return;
}

if (result && typeof result.then === 'function') {
result.then(reportSuccess, reportError);
result.then(onResult, onError);
} else {
reportSuccess(result);
onResult(result);
}
}
1 change: 1 addition & 0 deletions packages/jest-worker/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export default class {
const sharedWorkerOptions = {
forkOptions: options.forkOptions || {},
maxRetries: options.maxRetries || 3,
setupArgs: options.setupArgs || [],
workerPath,
};

Expand Down
Loading

0 comments on commit deb30be

Please sign in to comment.