Skip to content

Commit

Permalink
Concurrency limit (jestjs#7770)
Browse files Browse the repository at this point in the history
  • Loading branch information
arcanis authored and captain-yossarian committed Jul 18, 2019
1 parent f38e1e6 commit efe8fa6
Show file tree
Hide file tree
Showing 19 changed files with 94 additions and 21 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
- `[jest-resolve]`: Pass default resolver into custom resolvers ([#7714](https://github.com/facebook/jest/pull/7714))
- `[jest-cli]`: `global{Setup,Teardown}` use default export with es modules ([#7750](https://github.com/facebook/jest/pull/7750))
- `[jest-runtime]` Better error messages when the jest environment is used after teardown by async code ([#7756](https://github.com/facebook/jest/pull/7756))
- `[jest-jasmine2]` Will now only execute at most 5 concurrent tests _within the same testsuite_ when using `test.concurrent` ([#7770](https://github.com/facebook/jest/pull/7770))
- `[jest-circus]` Same as `[jest-jasmine2]`, only 5 tests will run concurrently by default ([#7770](https://github.com/facebook/jest/pull/7770))
- `[jest-config]` A new `maxConcurrency` option allows to change the number of tests allowed to run concurrently ([#7770](https://github.com/facebook/jest/pull/7770))

### Fixes

Expand Down
1 change: 1 addition & 0 deletions TestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
lastCommit: false,
listTests: false,
logHeapUsage: false,
maxConcurrency: 5,
maxWorkers: 2,
noSCM: null,
noStackTrace: false,
Expand Down
4 changes: 4 additions & 0 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ Lists all tests as JSON that Jest will run given the arguments, and exits. This

Logs the heap usage after every test. Useful to debug memory leaks. Use together with `--runInBand` and `--expose-gc` in node.

### `--maxConcurrency=<num>`

Prevents Jest from executing more than the specified amount of tests at the same time. Only affects tests that use `test.concurrent`.

### `--maxWorkers=<num>|<string>`

Alias: `-w`. Specifies the maximum number of workers the worker-pool will spawn for running tests. This defaults to the number of the cores available on your machine. It may be useful to adjust this in resource limited environments like CIs but the default should be adequate for most use-cases.
Expand Down
6 changes: 6 additions & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,12 @@ _Note: A global teardown module configured in a project (using multi-project run

_Node: The same caveat concerning transformation of `node_modules_ as for `globalSetup` applies to `globalTeardown`.

### `maxConcurrency` [number]

Default: `5`

A number limiting the number of tests that are allowed to run at the same time when using `test.concurrent`. Any test above this limit will be queued and executed once a slot is released.

### `moduleDirectories` [array<string>]

Default: `["node_modules"]`
Expand Down
1 change: 1 addition & 0 deletions e2e/__tests__/__snapshots__/showConfig.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ exports[`--showConfig outputs config info and exits 1`] = `
"globalSetup": null,
"globalTeardown": null,
"listTests": false,
"maxConcurrency": 5,
"maxWorkers": "[maxWorkers]",
"noStackTrace": false,
"nonFlagArgs": [],
Expand Down
9 changes: 9 additions & 0 deletions e2e/__tests__/jasmineAsync.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ describe('async jasmine', () => {
expect(json.testResults[0].message).toMatch(/concurrent test fails/);
});

it("doesn't execute more than 5 tests simultaneously", () => {
const result = runWithJson('jasmine-async', ['concurrent-many.test.js']);
const json = result.json;
expect(json.numTotalTests).toBe(10);
expect(json.numPassedTests).toBe(10);
expect(json.numFailedTests).toBe(0);
expect(json.numPendingTests).toBe(0);
});

it('async test fails', () => {
const result = runWithJson('jasmine-async', ['asyncTestFails.test.js']);

Expand Down
26 changes: 26 additions & 0 deletions e2e/jasmine-async/__tests__/concurrent-many.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* 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.
*/

'use strict';

const sleep = duration => new Promise(resolve => setTimeout(resolve, duration));
let current = 0;

for (let t = 0; t < 10; ++t) {
it.concurrent(`#${t}`, () => {
current += 1;

if (current > 5) {
current -= 1;
throw new Error(`Too many processes ran simultaneously`);
} else {
return sleep(20).then(() => {
current -= 1;
});
}
});
}
3 changes: 2 additions & 1 deletion packages/jest-circus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"jest-snapshot": "^24.0.0",
"jest-util": "^24.0.0",
"pretty-format": "^24.0.0",
"stack-utils": "^1.0.1"
"stack-utils": "^1.0.1",
"throat": "^4.0.0"
},
"devDependencies": {
"execa": "^1.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
addSerializer,
buildSnapshotResolver,
} from 'jest-snapshot';
import throat from 'throat';
import {addEventHandler, dispatch, ROOT_DESCRIBE_BLOCK_NAME} from '../state';
import {getTestID} from '../utils';
import run from '../run';
Expand All @@ -41,6 +42,8 @@ export const initialize = ({
testPath: Path,
parentProcess: Process,
}) => {
const mutex = throat(globalConfig.maxConcurrency);

Object.assign(global, globals);

global.xit = global.it.skip;
Expand All @@ -60,7 +63,7 @@ export const initialize = ({
// Unfortunately at this stage there's no way to know if there are any `.only` tests in the suite
// that will result in this test to be skipped, so we'll be executing the promise function anyway,
// even if it ends up being skipped.
const promise = testFn();
const promise = mutex(() => testFn());
global.test(testName, () => promise, timeout);
};

Expand All @@ -69,7 +72,7 @@ export const initialize = ({
testFn: () => Promise<any>,
timeout?: number,
) => {
const promise = testFn();
const promise = mutex(() => testFn());
global.test.only(testName, () => promise, timeout);
};

Expand Down
7 changes: 7 additions & 0 deletions packages/jest-cli/src/cli/args.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,13 @@ export const options = {
'when transformers supply source maps.\n\nDEPRECATED',
type: 'boolean',
},
maxConcurrency: {
default: 5,
description:
'Specifies the maximum number of tests that are allowed to run' +
'concurrently. This only affects tests using `test.concurrent`.',
type: 'number',
},
maxWorkers: {
alias: 'w',
description:
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/Defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default ({
computeSha1: false,
providesModuleNodeModules: [],
},
maxConcurrency: 5,
moduleDirectories: ['node_modules'],
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'],
moduleNameMapper: {},
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/ValidConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export default ({
json: false,
lastCommit: false,
logHeapUsage: true,
maxConcurrency: 5,
moduleDirectories: ['node_modules'],
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'],
moduleLoader: '<rootDir>',
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ const groupOptions = (
lastCommit: options.lastCommit,
listTests: options.listTests,
logHeapUsage: options.logHeapUsage,
maxConcurrency: options.maxConcurrency,
maxWorkers: options.maxWorkers,
noSCM: undefined,
noStackTrace: options.noStackTrace,
Expand Down
2 changes: 2 additions & 0 deletions packages/jest-config/src/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,7 @@ export default function normalize(
case 'lastCommit':
case 'listTests':
case 'logHeapUsage':
case 'maxConcurrency':
case 'mapCoverage':
case 'name':
case 'noStackTrace':
Expand Down Expand Up @@ -803,6 +804,7 @@ export default function normalize(
? 'all'
: 'new';

newOptions.maxConcurrency = parseInt(newOptions.maxConcurrency, 10);
newOptions.maxWorkers = getMaxWorkers(argv);

if (newOptions.testRegex.length && options.testMatch) {
Expand Down
3 changes: 2 additions & 1 deletion packages/jest-jasmine2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"jest-message-util": "^24.0.0",
"jest-snapshot": "^24.0.0",
"jest-util": "^24.0.0",
"pretty-format": "^24.0.0"
"pretty-format": "^24.0.0",
"throat": "^4.0.0"
},
"devDependencies": {
"jest-diff": "^24.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-jasmine2/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ async function jasmine2(
};
}

jasmineAsyncInstall(environment.global);
jasmineAsyncInstall(globalConfig, environment.global);

installEach(environment);

Expand Down
30 changes: 19 additions & 11 deletions packages/jest-jasmine2/src/jasmineAsyncInstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
*/

import type {Global} from 'types/Global';
import type {GlobalConfig} from 'types/Config';

import isGeneratorFn from 'is-generator-fn';
import co from 'co';
import isGeneratorFn from 'is-generator-fn';
import throat from 'throat';
import isError from './isError';

function isPromise(obj) {
Expand Down Expand Up @@ -125,21 +127,23 @@ function promisifyIt(originalFn, env, jasmine) {
};
}

function makeConcurrent(originalFn: Function, env) {
function makeConcurrent(originalFn: Function, env, mutex) {
return function(specName, fn, timeout) {
if (env != null && !env.specFilter({getFullName: () => specName || ''})) {
return originalFn.call(env, specName, () => Promise.resolve(), timeout);
}

let promise;

try {
promise = fn();
if (!isPromise(promise)) {
promise = mutex(() => {
const promise = fn();
if (isPromise(promise)) {
return promise;
}
throw new Error(
`Jest: concurrent test "${specName}" must return a Promise.`,
);
}
});
} catch (error) {
return originalFn.call(env, specName, () => Promise.reject(error));
}
Expand All @@ -148,16 +152,20 @@ function makeConcurrent(originalFn: Function, env) {
};
}

export default function jasmineAsyncInstall(global: Global) {
export default function jasmineAsyncInstall(
globalConfig: GlobalConfig,
global: Global,
) {
const jasmine = global.jasmine;
const mutex = throat(globalConfig.maxConcurrency);

const env = jasmine.getEnv();
env.it = promisifyIt(env.it, env, jasmine);
env.fit = promisifyIt(env.fit, env, jasmine);
global.it.concurrent = makeConcurrent(env.it, env);
global.it.concurrent.only = makeConcurrent(env.fit, env);
global.it.concurrent.skip = makeConcurrent(env.xit, env);
global.fit.concurrent = makeConcurrent(env.fit);
global.it.concurrent = makeConcurrent(env.it, env, mutex);
global.it.concurrent.only = makeConcurrent(env.fit, env, mutex);
global.it.concurrent.skip = makeConcurrent(env.xit, env, mutex);
global.fit.concurrent = makeConcurrent(env.fit, env, mutex);
env.afterAll = promisifyLifeCycleFunction(env.afterAll, env);
env.afterEach = promisifyLifeCycleFunction(env.afterEach, env);
env.beforeAll = promisifyLifeCycleFunction(env.beforeAll, env);
Expand Down
3 changes: 3 additions & 0 deletions types/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type DefaultOptions = {|
globalSetup: ?string,
globalTeardown: ?string,
haste: HasteConfig,
maxConcurrency: number,
moduleDirectories: Array<string>,
moduleFileExtensions: Array<string>,
moduleNameMapper: {[key: string]: string},
Expand Down Expand Up @@ -124,6 +125,7 @@ export type InitialOptions = {
lastCommit?: boolean,
listTests?: boolean,
mapCoverage?: boolean,
maxConcurrency?: number,
moduleDirectories?: Array<string>,
moduleFileExtensions?: Array<string>,
moduleLoader?: Path,
Expand Down Expand Up @@ -213,6 +215,7 @@ export type GlobalConfig = {|
lastCommit: boolean,
logHeapUsage: boolean,
listTests: boolean,
maxConcurrency: number,
maxWorkers: number,
noStackTrace: boolean,
nonFlagArgs: Array<string>,
Expand Down
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3553,11 +3553,6 @@ core-js@^2.2.0, core-js@^2.2.2, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.7:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.3.tgz#4b70938bdffdaf64931e66e2db158f0892289c49"
integrity sha512-l00tmFFZOBHtYhN4Cz7k32VM7vTn3rE2ANjQDxdEN6zmXZ/xq1jQuutnmHvMG1ZJ7xd72+TA5YpUK8wz3rWsfQ==

core-js@^2.5.0:
version "2.6.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.3.tgz#4b70938bdffdaf64931e66e2db158f0892289c49"
integrity sha512-l00tmFFZOBHtYhN4Cz7k32VM7vTn3rE2ANjQDxdEN6zmXZ/xq1jQuutnmHvMG1ZJ7xd72+TA5YpUK8wz3rWsfQ==

core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
Expand Down

0 comments on commit efe8fa6

Please sign in to comment.