Skip to content

Commit 7b49471

Browse files
authored
feat: add a seed value to test runs (#13400)
1 parent 1f72803 commit 7b49471

File tree

34 files changed

+359
-15
lines changed

34 files changed

+359
-15
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
### Features
44

5+
- `[@jest/cli, jest-config]` A seed for the test run will be randomly generated, or set by a CLI option ([#13400](https://github.com/facebook/jest/pull/13400))
6+
- `[@jest/cli, jest-config]` `--show-seed` will display the seed value in the report, and can be set via a CLI flag or through the config file ([#13400](https://github.com/facebook/jest/pull/13400))
57
- `[jest-config]` Add `readInitialConfig` utility function ([#13356](https://github.com/facebook/jest/pull/13356))
68
- `[jest-core]` Enable testResultsProcessor to be async ([#13343](https://github.com/facebook/jest/pull/13343))
9+
- `[@jest/environment, jest-environment-node, jest-environment-jsdom, jest-runtime]` Add `getSeed()` to the `jest` object ([#13400](https://github.com/facebook/jest/pull/13400))
710
- `[expect, @jest/expect-utils]` Allow `isA` utility to take a type argument ([#13355](https://github.com/facebook/jest/pull/13355))
811
- `[expect]` Expose `AsyncExpectationResult` and `SyncExpectationResult` types ([#13411](https://github.com/facebook/jest/pull/13411))
912

docs/CLI.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,20 @@ The default regex matching works fine on small runs, but becomes slow if provide
350350

351351
:::
352352

353+
### `--seed=<num>`
354+
355+
Sets a seed value that can be retrieved in a test file via [`jest.getSeed()`](JestObjectAPI.md#jestgetseed). The seed value must be between `-0x80000000` and `0x7fffffff` inclusive (`-2147483648` (`-(2 ** 31)`) and `2147483647` (`2 ** 31 - 1`) in decimal).
356+
357+
```bash
358+
jest --seed=1324
359+
```
360+
361+
:::tip
362+
363+
If this option is not specified Jest will randomly generate the value. You can use the [`--showSeed`](#--showseed) flag to print the seed in the test report summary.
364+
365+
:::
366+
353367
### `--selectProjects <project1> ... <projectN>`
354368

355369
Run the tests of the specified projects. Jest uses the attribute `displayName` in the configuration to identify each project. If you use this option, you should provide a `displayName` to all your projects.
@@ -380,6 +394,12 @@ jest --shard=3/3
380394

381395
Print your Jest config and then exits.
382396

397+
### `--showSeed`
398+
399+
Prints the seed value in the test report summary. See [`--seed=<num>`](#--seednum) for the details.
400+
401+
Can also be set in configuration. See [`showSeed`](Configuration.md#showseed-boolean).
402+
383403
### `--silent`
384404

385405
Prevent tests from printing messages through the console.

docs/Configuration.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,12 @@ const config: Config = {
16141614
export default config;
16151615
```
16161616

1617+
### `showSeed` \[boolean]
1618+
1619+
Default: `false`
1620+
1621+
The equivalent of the [`--showSeed`](CLI.md#--showseed) flag to print the seed in the test report summary.
1622+
16171623
### `slowTestThreshold` \[number]
16181624

16191625
Default: `5`

docs/JestObjectAPI.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,16 @@ This function is not available when using legacy fake timers implementation.
908908

909909
## Misc
910910

911+
### `jest.getSeed()`
912+
913+
Every time Jest runs a seed value is randomly generated which you could use in a pseudorandom number generator or anywhere else.
914+
915+
:::tip
916+
917+
Use the [`--showSeed`](CLI.md#--showseed) flag to print the seed in the test report summary. To manually set the value of the seed use [`--seed=<num>`](CLI.md#--seednum) CLI argument.
918+
919+
:::
920+
911921
### `jest.setTimeout(timeout)`
912922

913923
Set the default timeout interval (in milliseconds) for all tests and before/after hooks in the test file. This only affects the test file from which this function is called. The default timeout interval is 5 seconds if this method is not called.

e2e/Utils.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ export const copyDir = (src: string, dest: string) => {
157157
}
158158
};
159159

160+
export const replaceSeed = (str: string) =>
161+
str.replace(/Seed: {8}(-?\d+)/g, 'Seed: <<REPLACED>>');
162+
160163
export const replaceTime = (str: string) =>
161164
str
162165
.replace(/\d*\.?\d+ m?s\b/g, '<<REPLACED>>')
@@ -201,7 +204,7 @@ export const extractSummary = (stdout: string) => {
201204
const match = stdout
202205
.replace(/(?:\\[rn])+/g, '\n')
203206
.match(
204-
/Test Suites:.*\nTests.*\nSnapshots.*\nTime.*(\nRan all test suites)*.*\n*$/gm,
207+
/(Seed:.*\n)?Test Suites:.*\nTests.*\nSnapshots.*\nTime.*(\nRan all test suites)*.*\n*$/gm,
205208
);
206209
if (!match) {
207210
throw new Error(dedent`
@@ -254,7 +257,7 @@ export const extractSummaries = (
254257
stdout: string,
255258
): Array<{rest: string; summary: string}> => {
256259
const regex =
257-
/Test Suites:.*\nTests.*\nSnapshots.*\nTime.*(\nRan all test suites)*.*\n*$/gm;
260+
/(Seed:.*\n)?Test Suites:.*\nTests.*\nSnapshots.*\nTime.*(\nRan all test suites)*.*\n*$/gm;
258261

259262
let match = regex.exec(stdout);
260263
const matches: Array<RegExpExecArray> = [];

e2e/__tests__/__snapshots__/showConfig.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ exports[`--showConfig outputs config info and exits 1`] = `
125125
"projects": [],
126126
"rootDir": "<<REPLACED_ROOT_DIR>>",
127127
"runTestsByPath": false,
128+
"seed": <<RANDOM_SEED>>,
128129
"skipFilter": false,
129130
"snapshotFormat": {
130131
"escapeString": false,

e2e/__tests__/jestObject.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import * as path from 'path';
9+
import runJest from '../runJest';
10+
11+
const dir = path.resolve(__dirname, '../jest-object');
12+
13+
test('passes with seed', () => {
14+
const result = runJest(dir, ['get-seed.test.js', '--seed', '1234']);
15+
expect(result.exitCode).toBe(0);
16+
});
17+
18+
test('fails with wrong seed', () => {
19+
const result = runJest(dir, ['get-seed.test.js', '--seed', '1111']);
20+
expect(result.exitCode).toBe(1);
21+
});
22+
23+
test('seed always exists', () => {
24+
const result = runJest(dir, ['any-seed.test.js']);
25+
expect(result.exitCode).toBe(0);
26+
});

e2e/__tests__/showConfig.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ test('--showConfig outputs config info and exits', () => {
3737
.replace(/"version": "(.+)"/g, '"version": "[version]"')
3838
.replace(/"maxWorkers": (\d+)/g, '"maxWorkers": "[maxWorkers]"')
3939
.replace(/"\S*show-config-test/gm, '"<<REPLACED_ROOT_DIR>>')
40-
.replace(/"\S*\/jest\/packages/gm, '"<<REPLACED_JEST_PACKAGES_DIR>>');
40+
.replace(/"\S*\/jest\/packages/gm, '"<<REPLACED_JEST_PACKAGES_DIR>>')
41+
.replace(/"seed": (-?\d+)/g, '"seed": <<RANDOM_SEED>>');
4142

4243
expect(stdout).toMatchSnapshot();
4344
});

e2e/__tests__/showSeed.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import * as path from 'path';
9+
import {extractSummary, replaceSeed} from '../Utils';
10+
import runJest from '../runJest';
11+
12+
const dir = path.resolve(__dirname, '../jest-object');
13+
14+
const randomSeedValueRegExp = /Seed:\s+<<REPLACED>>/;
15+
const seedValueRegExp = /Seed:\s+1234/;
16+
17+
test('--showSeed changes report to output seed', () => {
18+
const {stderr} = runJest(dir, ['--showSeed', '--no-cache']);
19+
20+
const {summary} = extractSummary(stderr);
21+
22+
expect(replaceSeed(summary)).toMatch(randomSeedValueRegExp);
23+
});
24+
25+
test('if --showSeed is not present the report will not show the seed', () => {
26+
const {stderr} = runJest(dir, ['--seed', '1234']);
27+
28+
const {summary} = extractSummary(stderr);
29+
30+
expect(replaceSeed(summary)).not.toMatch(randomSeedValueRegExp);
31+
});
32+
33+
test('if showSeed is present in the config the report will show the seed', () => {
34+
const {stderr} = runJest(dir, [
35+
'--seed',
36+
'1234',
37+
'--config',
38+
'different-config.json',
39+
]);
40+
41+
const {summary} = extractSummary(stderr);
42+
43+
expect(summary).toMatch(seedValueRegExp);
44+
});
45+
46+
test('--seed --showSeed will show the seed in the report', () => {
47+
const {stderr} = runJest(dir, ['--showSeed', '--seed', '1234']);
48+
49+
const {summary} = extractSummary(stderr);
50+
51+
expect(summary).toMatch(seedValueRegExp);
52+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
test('ensure seed exists', () => {
9+
expect(jest.getSeed()).toEqual(expect.any(Number));
10+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
test('getSeed', () => {
9+
expect(jest.getSeed()).toBe(1234);
10+
});

e2e/jest-object/different-config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"displayName": "Config from different-config.json file",
3+
"showSeed": true
4+
}

e2e/jest-object/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"jest": {
3+
"testEnvironment": "node"
4+
}
5+
}

packages/jest-cli/src/args.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,11 @@ export const options: {[key: string]: Options} = {
510510
"Allows to use a custom runner instead of Jest's default test runner.",
511511
type: 'string',
512512
},
513+
seed: {
514+
description:
515+
'Sets a seed value that can be retrieved in a tests file via `jest.getSeed()`. If this option is not specified Jest will randomly generate the value. The seed value must be between `-0x80000000` and `0x7fffffff` inclusive.',
516+
type: 'number',
517+
},
513518
selectProjects: {
514519
description:
515520
'Run the tests of the specified projects. ' +
@@ -541,6 +546,11 @@ export const options: {[key: string]: Options} = {
541546
description: 'Print your jest config and then exits.',
542547
type: 'boolean',
543548
},
549+
showSeed: {
550+
description:
551+
'Prints the seed value in the test report summary. See `--seed` for how to set this value',
552+
type: 'boolean',
553+
},
544554
silent: {
545555
description: 'Prevent tests from printing messages through the console.',
546556
type: 'boolean',

packages/jest-config/src/ValidConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ const initialOptions: Config.InitialOptions = {
137137
sandboxInjectedGlobals: [],
138138
setupFiles: ['<rootDir>/setup.js'],
139139
setupFilesAfterEnv: ['<rootDir>/testSetupFile.js'],
140+
showSeed: false,
140141
silent: true,
141142
skipFilter: false,
142143
skipNodeResolution: false,

packages/jest-config/src/__tests__/normalize.test.ts

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,12 @@ it('keeps custom ids based on the rootDir', async () => {
114114
});
115115

116116
it('minimal config is stable across runs', async () => {
117-
const firstNormalization = await normalize(
118-
{rootDir: '/root/path/foo'},
119-
{} as Config.Argv,
120-
);
121-
const secondNormalization = await normalize(
122-
{rootDir: '/root/path/foo'},
123-
{} as Config.Argv,
124-
);
117+
const firstNormalization = await normalize({rootDir: '/root/path/foo'}, {
118+
seed: 55555,
119+
} as Config.Argv);
120+
const secondNormalization = await normalize({rootDir: '/root/path/foo'}, {
121+
seed: 55555,
122+
} as Config.Argv);
125123

126124
expect(firstNormalization).toEqual(secondNormalization);
127125
expect(JSON.stringify(firstNormalization)).toBe(
@@ -2113,3 +2111,56 @@ it('parses workerIdleMemoryLimit', async () => {
21132111

21142112
expect(options.workerIdleMemoryLimit).toBe(47185920);
21152113
});
2114+
2115+
describe('seed', () => {
2116+
it('generates seed when not specified', async () => {
2117+
const {options} = await normalize({rootDir: '/root/'}, {} as Config.Argv);
2118+
expect(options.seed).toEqual(expect.any(Number));
2119+
});
2120+
2121+
it('uses seed specified', async () => {
2122+
const {options} = await normalize({rootDir: '/root/'}, {
2123+
seed: 4321,
2124+
} as Config.Argv);
2125+
expect(options.seed).toBe(4321);
2126+
});
2127+
2128+
it('throws if seed is too large or too small', async () => {
2129+
await expect(
2130+
normalize({rootDir: '/root/'}, {
2131+
seed: 2 ** 33,
2132+
} as Config.Argv),
2133+
).rejects.toThrow(
2134+
'seed value must be between `-0x80000000` and `0x7fffffff` inclusive - is 8589934592',
2135+
);
2136+
await expect(
2137+
normalize({rootDir: '/root/'}, {
2138+
seed: -(2 ** 33),
2139+
} as Config.Argv),
2140+
).rejects.toThrow(
2141+
'seed value must be between `-0x80000000` and `0x7fffffff` inclusive - is -8589934592',
2142+
);
2143+
});
2144+
});
2145+
2146+
describe('showSeed', () => {
2147+
test('showSeed is set when argv flag is set', async () => {
2148+
const {options} = await normalize({rootDir: '/root/'}, {
2149+
showSeed: true,
2150+
} as Config.Argv);
2151+
expect(options.showSeed).toBe(true);
2152+
});
2153+
2154+
test('showSeed is set when the config is set', async () => {
2155+
const {options} = await normalize(
2156+
{rootDir: '/root/', showSeed: true},
2157+
{} as Config.Argv,
2158+
);
2159+
expect(options.showSeed).toBe(true);
2160+
});
2161+
2162+
test('showSeed is false when neither is set', async () => {
2163+
const {options} = await normalize({rootDir: '/root/'}, {} as Config.Argv);
2164+
expect(options.showSeed).toBeFalsy();
2165+
});
2166+
});

packages/jest-config/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ const groupOptions = (
115115
reporters: options.reporters,
116116
rootDir: options.rootDir,
117117
runTestsByPath: options.runTestsByPath,
118+
seed: options.seed,
118119
shard: options.shard,
120+
showSeed: options.showSeed,
119121
silent: options.silent,
120122
skipFilter: options.skipFilter,
121123
snapshotFormat: options.snapshotFormat,

packages/jest-config/src/normalize.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,7 @@ export default async function normalize(
918918
case 'runTestsByPath':
919919
case 'sandboxInjectedGlobals':
920920
case 'silent':
921+
case 'showSeed':
921922
case 'skipFilter':
922923
case 'skipNodeResolution':
923924
case 'slowTestThreshold':
@@ -1021,6 +1022,24 @@ export default async function normalize(
10211022
newOptions.onlyChanged = newOptions.watch;
10221023
}
10231024

1025+
newOptions.showSeed = newOptions.showSeed || argv.showSeed;
1026+
1027+
const upperBoundSeedValue = 2 ** 31;
1028+
1029+
// xoroshiro128plus is used in v8 and is used here (at time of writing)
1030+
newOptions.seed =
1031+
argv.seed ??
1032+
Math.floor((2 ** 32 - 1) * Math.random() - upperBoundSeedValue);
1033+
if (
1034+
newOptions.seed < -upperBoundSeedValue ||
1035+
newOptions.seed > upperBoundSeedValue - 1
1036+
) {
1037+
throw new ValidationError(
1038+
'Validation Error',
1039+
`seed value must be between \`-0x80000000\` and \`0x7fffffff\` inclusive - is ${newOptions.seed}`,
1040+
);
1041+
}
1042+
10241043
if (!newOptions.onlyChanged) {
10251044
newOptions.onlyChanged = false;
10261045
}

packages/jest-core/src/lib/__tests__/__snapshots__/logDebugMessages.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ exports[`prints the config object 1`] = `
9595
"reporters": [],
9696
"rootDir": "/test_root_dir/",
9797
"runTestsByPath": false,
98+
"seed": 1234,
9899
"silent": false,
99100
"skipFilter": false,
100101
"snapshotFormat": {},

0 commit comments

Comments
 (0)