GeezerVison is showing github.com — test_runner: support test order randomization · nodejs/node@a21ae17
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/cli.md +35Lines changed: 35 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2780,6 +2780,38 @@ changes: | 2780 | 2780 | Configures the test runner to only execute top level tests that have the `only` | 2781 | 2781 | option set. This flag is not necessary when test isolation is disabled. | 2782 | 2782 |
| | 2783 | +### `--test-random-seed` | | 2784 | + | | 2785 | +<!-- YAML | | 2786 | +added: REPLACEME | | 2787 | +--> | | 2788 | + | | 2789 | +Set the seed used to randomize test execution order. This applies to both test | | 2790 | +file execution order and queued tests within each file. Providing this flag | | 2791 | +enables randomization implicitly, even without `--test-randomize`. | | 2792 | + | | 2793 | +The value must be an integer between `0` and `4294967295`. | | 2794 | + | | 2795 | +This flag cannot be used with `--watch` or `--test-rerun-failures`. | | 2796 | + | | 2797 | +### `--test-randomize` | | 2798 | + | | 2799 | +<!-- YAML | | 2800 | +added: REPLACEME | | 2801 | +--> | | 2802 | + | | 2803 | +Randomize test execution order. This applies to both test file execution order | | 2804 | +and queued tests within each file. This can help detect tests that rely on | | 2805 | +shared state or execution order. | | 2806 | + | | 2807 | +The seed used for randomization is printed in the test summary and can be | | 2808 | +reused with `--test-random-seed`. | | 2809 | + | | 2810 | +For detailed behavior and examples, see | | 2811 | +[randomizing tests execution order][]. | | 2812 | + | | 2813 | +This flag cannot be used with `--watch` or `--test-rerun-failures`. | | 2814 | + | 2783 | 2815 | ### `--test-reporter` | 2784 | 2816 |
| 2785 | 2817 | <!-- YAML | @@ -3698,6 +3730,8 @@ one is included in the list below. | 3698 | 3730 | * `--test-isolation` | 3699 | 3731 | * `--test-name-pattern` | 3700 | 3732 | * `--test-only` | | 3733 | +* `--test-random-seed` | | 3734 | +* `--test-randomize` | 3701 | 3735 | * `--test-reporter-destination` | 3702 | 3736 | * `--test-reporter` | 3703 | 3737 | * `--test-rerun-failures` | @@ -4282,6 +4316,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 | 4282 | 4316 | [libuv threadpool documentation]: https://docs.libuv.org/en/latest/threadpool.html | 4283 | 4317 | [module compile cache]: module.md#module-compile-cache | 4284 | 4318 | [preloading asynchronous module customization hooks]: module.md#registration-of-asynchronous-customization-hooks | | 4319 | +[randomizing tests execution order]: test.md#randomizing-tests-execution-order | 4285 | 4320 | [remote code execution]: https://www.owasp.org/index.php/Code_Injection | 4286 | 4321 | [running tests from the command line]: test.md#running-tests-from-the-command-line | 4287 | 4322 | [scavenge garbage collector]: https://v8.dev/blog/orinoco-parallel-scavenger | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/test.md +100Lines changed: 100 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -627,6 +627,94 @@ prevent shell expansion, which can reduce portability across systems. | 627 | 627 | node --test "**/*.test.js" "**/*.spec.js" | 628 | 628 | ``` | 629 | 629 |
| | 630 | +### Randomizing tests execution order | | 631 | + | | 632 | +<!-- YAML | | 633 | +added: REPLACEME | | 634 | +--> | | 635 | + | | 636 | +> Stability: 1.0 - Early development | | 637 | +
| | 638 | +The test runner can randomize execution order to help detect | | 639 | +order-dependent tests. When enabled, the runner randomizes both discovered | | 640 | +test files and queued tests within each file. Use `--test-randomize` to | | 641 | +enable this mode. | | 642 | + | | 643 | +```bash | | 644 | +node --test --test-randomize | | 645 | +``` | | 646 | + | | 647 | +When randomization is enabled, the test runner prints the seed used for the run | | 648 | +as a diagnostic message: | | 649 | + | | 650 | +```text | | 651 | +Randomized test order seed: 12345 | | 652 | +``` | | 653 | + | | 654 | +Use `--test-random-seed=<number>` to replay the same randomized order | | 655 | +deterministically. Supplying `--test-random-seed` also enables randomization, | | 656 | +so `--test-randomize` is optional when a seed is provided: | | 657 | + | | 658 | +```bash | | 659 | +node --test --test-random-seed=12345 | | 660 | +``` | | 661 | + | | 662 | +In most test files, randomization works automatically. One important exception | | 663 | +is when subtests are awaited one by one. In that pattern, each subtest starts | | 664 | +only after the previous one finishes, so the runner keeps declaration order | | 665 | +instead of randomizing it. | | 666 | + | | 667 | +Example: this runs sequentially and is **not** randomized. | | 668 | + | | 669 | +```mjs | | 670 | +import test from 'node:test'; | | 671 | + | | 672 | +test('math', async (t) => { | | 673 | + for (const name of ['adds', 'subtracts', 'multiplies']) { | | 674 | + // Sequentially awaiting each subtest preserves declaration order. | | 675 | + await t.test(name, async () => {}); | | 676 | + } | | 677 | +}); | | 678 | +``` | | 679 | + | | 680 | +```cjs | | 681 | +const test = require('node:test'); | | 682 | + | | 683 | +test('math', async (t) => { | | 684 | + for (const name of ['adds', 'subtracts', 'multiplies']) { | | 685 | + // Sequentially awaiting each subtest preserves declaration order. | | 686 | + await t.test(name, async () => {}); | | 687 | + } | | 688 | +}); | | 689 | +``` | | 690 | + | | 691 | +Using suite-style APIs such as `describe()`/`it()` or `suite()`/`test()` | | 692 | +still allows randomization, because sibling tests are enqueued together. | | 693 | + | | 694 | +Example: this remains eligible for randomization. | | 695 | + | | 696 | +```mjs | | 697 | +import { describe, it } from 'node:test'; | | 698 | + | | 699 | +describe('math', () => { | | 700 | + it('adds', () => {}); | | 701 | + it('subtracts', () => {}); | | 702 | + it('multiplies', () => {}); | | 703 | +}); | | 704 | +``` | | 705 | + | | 706 | +```cjs | | 707 | +const { describe, it } = require('node:test'); | | 708 | + | | 709 | +describe('math', () => { | | 710 | + it('adds', () => {}); | | 711 | + it('subtracts', () => {}); | | 712 | + it('multiplies', () => {}); | | 713 | +}); | | 714 | +``` | | 715 | + | | 716 | +`--test-randomize` and `--test-random-seed` are not supported with `--watch` mode. | | 717 | + | 630 | 718 | Matching files are executed as test files. | 631 | 719 | More information on the test file execution can be found | 632 | 720 | in the [test runner execution model][] section. | @@ -667,6 +755,10 @@ test runner functionality: | 667 | 755 | * `--test-reporter` - Reporting is managed by the parent process | 668 | 756 | * `--test-reporter-destination` - Output destinations are controlled by the parent | 669 | 757 | * `--experimental-config-file` - Config file paths are managed by the parent | | 758 | +* `--test-randomize` - Randomization is managed by the parent process and | | 759 | + propagated to child processes | | 760 | +* `--test-random-seed` - Randomization seed is managed by the parent process and | | 761 | + propagated to child processes | 670 | 762 |
| 671 | 763 | All other Node.js options from command line arguments, environment variables, | 672 | 764 | and configuration files are inherited by the child processes. | @@ -1575,6 +1667,14 @@ changes: | 1575 | 1667 | that specifies the index of the shard to run. This option is _required_. | 1576 | 1668 | * `total` {number} is a positive integer that specifies the total number | 1577 | 1669 | of shards to split the test files to. This option is _required_. | | 1670 | + * `randomize` {boolean} Randomize execution order for test files and queued tests. | | 1671 | + This option is not supported with `watch: true`. | | 1672 | + **Default:** `false`. | | 1673 | + * `randomSeed` {number} Seed used when randomizing execution order. If this | | 1674 | + option is set, runs can replay the same randomized order deterministically, | | 1675 | + and setting this option also enables randomization. The value must be an | | 1676 | + integer between `0` and `4294967295`. | | 1677 | + **Default:** `undefined`. | 1578 | 1678 | * `rerunFailuresFilePath` {string} A file path where the test runner will | 1579 | 1679 | store the state of the tests to allow rerunning only the failed tests on a next run. | 1580 | 1680 | see \[Rerunning failed tests]\[] for more information. | |
Collapse file Copy file name to clipboardExpand all lines: doc/node-config-schema.json +16Lines changed: 16 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 524 | 524 | "type": "boolean", | 525 | 525 | "description": "run tests with 'only' option set" | 526 | 526 | }, | | 527 | + "test-random-seed": { | | 528 | + "type": "number", | | 529 | + "description": "seed used to randomize test execution order" | | 530 | + }, | | 531 | + "test-randomize": { | | 532 | + "type": "boolean", | | 533 | + "description": "run tests in a random order" | | 534 | + }, | 527 | 535 | "test-reporter": { | 528 | 536 | "oneOf": [ | 529 | 537 | { | | 906 | 914 | "type": "boolean", | 907 | 915 | "description": "run tests with 'only' option set" | 908 | 916 | }, | | 917 | + "test-random-seed": { | | 918 | + "type": "number", | | 919 | + "description": "seed used to randomize test execution order" | | 920 | + }, | | 921 | + "test-randomize": { | | 922 | + "type": "boolean", | | 923 | + "description": "run tests in a random order" | | 924 | + }, | 909 | 925 | "test-reporter": { | 910 | 926 | "oneOf": [ | 911 | 927 | { | |
Collapse file Copy file name to clipboardExpand all lines: doc/node.1 +20Lines changed: 20 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1363,6 +1363,22 @@ tests must satisfy \fBboth\fR requirements in order to be executed. | 1363 | 1363 | Configures the test runner to only execute top level tests that have the \fBonly\fR | 1364 | 1364 | option set. This flag is not necessary when test isolation is disabled. | 1365 | 1365 | . | | 1366 | +.It Fl -test-random-seed | | 1367 | +Set the seed used to randomize test execution order. | | 1368 | +This applies to both test file execution order and queued tests within each file. | | 1369 | +Providing this flag enables randomization implicitly, even without | | 1370 | +\fB--test-randomize\fR. | | 1371 | +The value must be an integer between 0 and 4294967295. | | 1372 | +This flag cannot be used with \fB--watch\fR or \fB--test-rerun-failures\fR. | | 1373 | +. | | 1374 | +.It Fl -test-randomize | | 1375 | +Randomize test execution order. | | 1376 | +This applies to both test file execution order and queued tests within each file. | | 1377 | +This can help detect tests that rely on shared state or execution order. | | 1378 | +The seed used for randomization is printed in the test summary and can be | | 1379 | +reused with \fB--test-random-seed\fR. | | 1380 | +This flag cannot be used with \fB--watch\fR or \fB--test-rerun-failures\fR. | | 1381 | +. | 1366 | 1382 | .It Fl -test-reporter | 1367 | 1383 | A test reporter to use when running tests. See the documentation on | 1368 | 1384 | test reporters for more details. | @@ -2040,6 +2056,10 @@ one is included in the list below. | 2040 | 2056 | .It | 2041 | 2057 | \fB--test-reporter-destination\fR | 2042 | 2058 | .It | | 2059 | +\fB--test-randomize\fR | | 2060 | +.It | | 2061 | +\fB--test-random-seed\fR | | 2062 | +.It | 2043 | 2063 | \fB--test-reporter\fR | 2044 | 2064 | .It | 2045 | 2065 | \fB--test-rerun-failures\fR | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/runner.js +73-4Lines changed: 73 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -59,6 +59,7 @@ const { | 59 | 59 | validateObject, | 60 | 60 | validateOneOf, | 61 | 61 | validateInteger, | | 62 | + validateUint32, | 62 | 63 | validateString, | 63 | 64 | validateStringArray, | 64 | 65 | } = require('internal/validators'); | @@ -84,6 +85,7 @@ const { | 84 | 85 | const { FastBuffer } = require('internal/buffer'); | 85 | 86 |
| 86 | 87 | const { | | 88 | + createRandomSeed, | 87 | 89 | convertStringToRegExp, | 88 | 90 | countCompletedTest, | 89 | 91 | kDefaultPattern, | @@ -105,12 +107,14 @@ const kIsolatedProcessName = Symbol('kIsolatedProcessName'); | 105 | 107 | const kFilterArgs = [ | 106 | 108 | '--test', | 107 | 109 | '--experimental-test-coverage', | | 110 | + '--test-randomize', | 108 | 111 | '--watch', | 109 | 112 | '--experimental-default-config-file', | 110 | 113 | ]; | 111 | 114 | const kFilterArgValues = [ | 112 | 115 | '--test-reporter', | 113 | 116 | '--test-reporter-destination', | | 117 | + '--test-random-seed', | 114 | 118 | '--experimental-config-file', | 115 | 119 | ]; | 116 | 120 | const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms']; | @@ -168,6 +172,8 @@ function getRunArgs(path, { forceExit, | 168 | 172 | argv: suppliedArgs, | 169 | 173 | execArgv, | 170 | 174 | rerunFailuresFilePath, | | 175 | + randomize, | | 176 | + randomSeed, | 171 | 177 | root: { timeout }, | 172 | 178 | cwd }) { | 173 | 179 | const processNodeOptions = getOptionsAsFlagsFromBinding(); | @@ -209,6 +215,12 @@ function getRunArgs(path, { forceExit, | 209 | 215 | if (rerunFailuresFilePath) { | 210 | 216 | ArrayPrototypePush(runArgs, `--test-rerun-failures=${rerunFailuresFilePath}`); | 211 | 217 | } | | 218 | + if (randomize) { | | 219 | + ArrayPrototypePush(runArgs, '--test-randomize'); | | 220 | + } | | 221 | + if (randomSeed != null) { | | 222 | + ArrayPrototypePush(runArgs, `--test-random-seed=${randomSeed}`); | | 223 | + } | 212 | 224 |
| 213 | 225 | ArrayPrototypePushApply(runArgs, execArgv); | 214 | 226 |
| @@ -646,6 +658,8 @@ function run(options = kEmptyObject) { | 646 | 658 | lineCoverage = 0, | 647 | 659 | branchCoverage = 0, | 648 | 660 | functionCoverage = 0, | | 661 | + randomize: suppliedRandomize, | | 662 | + randomSeed: suppliedRandomSeed, | 649 | 663 | execArgv = [], | 650 | 664 | argv = [], | 651 | 665 | cwd = process.cwd(), | @@ -674,6 +688,56 @@ function run(options = kEmptyObject) { | 674 | 688 | if (globPatterns != null) { | 675 | 689 | validateArray(globPatterns, 'options.globPatterns'); | 676 | 690 | } | | 691 | + if (suppliedRandomize != null) { | | 692 | + validateBoolean(suppliedRandomize, 'options.randomize'); | | 693 | + } | | 694 | + if (suppliedRandomSeed != null) { | | 695 | + validateUint32(suppliedRandomSeed, 'options.randomSeed'); | | 696 | + } | | 697 | + let randomize = suppliedRandomize; | | 698 | + let randomSeed = suppliedRandomSeed; | | 699 | + | | 700 | + if (randomSeed != null) { | | 701 | + randomize = true; | | 702 | + } | | 703 | + if (watch) { | | 704 | + if (randomSeed != null) { | | 705 | + throw new ERR_INVALID_ARG_VALUE( | | 706 | + 'options.randomSeed', | | 707 | + randomSeed, | | 708 | + 'is not supported with watch mode', | | 709 | + ); | | 710 | + } | | 711 | + if (randomize) { | | 712 | + throw new ERR_INVALID_ARG_VALUE( | | 713 | + 'options.randomize', | | 714 | + randomize, | | 715 | + 'is not supported with watch mode', | | 716 | + ); | | 717 | + } | | 718 | + } | | 719 | + if (rerunFailuresFilePath) { | | 720 | + validatePath(rerunFailuresFilePath, 'options.rerunFailuresFilePath'); | | 721 | + // TODO(pmarchini): Support rerun-failures with randomization by | | 722 | + // persisting the randomization seed in the rerun state file. | | 723 | + if (randomSeed != null) { | | 724 | + throw new ERR_INVALID_ARG_VALUE( | | 725 | + 'options.randomSeed', | | 726 | + randomSeed, | | 727 | + 'is not supported with rerun failures mode', | | 728 | + ); | | 729 | + } | | 730 | + if (randomize) { | | 731 | + throw new ERR_INVALID_ARG_VALUE( | | 732 | + 'options.randomize', | | 733 | + randomize, | | 734 | + 'is not supported with rerun failures mode', | | 735 | + ); | | 736 | + } | | 737 | + } | | 738 | + if (randomize) { | | 739 | + randomSeed ??= createRandomSeed(); | | 740 | + } | 677 | 741 |
| 678 | 742 | validateString(cwd, 'options.cwd'); | 679 | 743 |
| @@ -683,10 +747,6 @@ function run(options = kEmptyObject) { | 683 | 747 | ); | 684 | 748 | } | 685 | 749 |
| 686 | | - if (rerunFailuresFilePath) { | 687 | | - validatePath(rerunFailuresFilePath, 'options.rerunFailuresFilePath'); | 688 | | - } | 689 | | - | 690 | 750 | if (shard != null) { | 691 | 751 | validateObject(shard, 'options.shard'); | 692 | 752 | // Avoid re-evaluating the shard object in case it's a getter | @@ -783,11 +843,18 @@ function run(options = kEmptyObject) { | 783 | 843 | functionCoverage: functionCoverage, | 784 | 844 | cwd, | 785 | 845 | globalSetupPath, | | 846 | + randomize, | | 847 | + randomSeed, | 786 | 848 | }; | | 849 | + | 787 | 850 | const root = createTestTree(rootTestOptions, globalOptions); | 788 | 851 | let testFiles = files ?? createTestFileList(globPatterns, cwd); | 789 | 852 | const { isTestRunner } = globalOptions; | 790 | 853 |
| | 854 | + if (randomize) { | | 855 | + root.diagnostic(`Randomized test order seed: ${randomSeed}`); | | 856 | + } | | 857 | + | 791 | 858 | if (shard) { | 792 | 859 | testFiles = ArrayPrototypeFilter(testFiles, (_, index) => index % shard.total === shard.index - 1); | 793 | 860 | } | @@ -833,6 +900,8 @@ function run(options = kEmptyObject) { | 833 | 900 | rerunFailuresFilePath, | 834 | 901 | env, | 835 | 902 | workerIdPool: isolation === 'process' ? workerIdPool : null, | | 903 | + randomize, | | 904 | + randomSeed, | 836 | 905 | }; | 837 | 906 |
| 838 | 907 | if (isolation === 'process') { | |
|