GeezerVison is showing github.com — test_runner: add global setup and teardown functionality · nodejs/node@13dee58
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 +15Lines changed: 15 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2452,6 +2452,19 @@ added: | 2452 | 2452 | Configures the test runner to exit the process once all known tests have | 2453 | 2453 | finished executing even if the event loop would otherwise remain active. | 2454 | 2454 |
| | 2455 | +### `--test-global-setup=module` | | 2456 | + | | 2457 | +<!-- YAML | | 2458 | +added: REPLACEME | | 2459 | +--> | | 2460 | + | | 2461 | +> Stability: 1.0 - Early development | | 2462 | +
| | 2463 | +Specify a module that will be evaluated before all tests are executed and | | 2464 | +can be used to setup global state or fixtures for tests. | | 2465 | + | | 2466 | +See the documentation on [global setup and teardown][] for more details. | | 2467 | + | 2455 | 2468 | ### `--test-isolation=mode` | 2456 | 2469 |
| 2457 | 2470 | <!-- YAML | @@ -3347,6 +3360,7 @@ one is included in the list below. | 3347 | 3360 | * `--test-coverage-functions` | 3348 | 3361 | * `--test-coverage-include` | 3349 | 3362 | * `--test-coverage-lines` | | 3363 | +* `--test-global-setup` | 3350 | 3364 | * `--test-isolation` | 3351 | 3365 | * `--test-name-pattern` | 3352 | 3366 | * `--test-only` | @@ -3898,6 +3912,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 | 3898 | 3912 | [emit_warning]: process.md#processemitwarningwarning-options | 3899 | 3913 | [environment_variables]: #environment-variables | 3900 | 3914 | [filtering tests by name]: test.md#filtering-tests-by-name | | 3915 | +[global setup and teardown]: test.md#global-setup-and-teardown | 3901 | 3916 | [jitless]: https://v8.dev/blog/jitless | 3902 | 3917 | [libuv threadpool documentation]: https://docs.libuv.org/en/latest/threadpool.html | 3903 | 3918 | [module compile cache]: module.md#module-compile-cache | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/test.md +54Lines changed: 54 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -397,6 +397,60 @@ their dependencies. When a change is detected, the test runner will | 397 | 397 | rerun the tests affected by the change. | 398 | 398 | The test runner will continue to run until the process is terminated. | 399 | 399 |
| | 400 | +## Global setup and teardown | | 401 | + | | 402 | +<!-- YAML | | 403 | +added: REPLACEME | | 404 | +--> | | 405 | + | | 406 | +> Stability: 1.0 - Early development | | 407 | +
| | 408 | +The test runner supports specifying a module that will be evaluated before all tests are executed and | | 409 | +can be used to setup global state or fixtures for tests. This is useful for preparing resources or setting up | | 410 | +shared state that is required by multiple tests. | | 411 | + | | 412 | +This module can export any of the following: | | 413 | + | | 414 | +* A `globalSetup` function which runs once before all tests start | | 415 | +* A `globalTeardown` function which runs once after all tests complete | | 416 | + | | 417 | +The module is specified using the `--test-global-setup` flag when running tests from the command line. | | 418 | + | | 419 | +```cjs | | 420 | +// setup-module.js | | 421 | +async function globalSetup() { | | 422 | + // Setup shared resources, state, or environment | | 423 | + console.log('Global setup executed'); | | 424 | + // Run servers, create files, prepare databases, etc. | | 425 | +} | | 426 | + | | 427 | +async function globalTeardown() { | | 428 | + // Clean up resources, state, or environment | | 429 | + console.log('Global teardown executed'); | | 430 | + // Close servers, remove files, disconnect from databases, etc. | | 431 | +} | | 432 | + | | 433 | +module.exports = { globalSetup, globalTeardown }; | | 434 | +``` | | 435 | + | | 436 | +```mjs | | 437 | +// setup-module.mjs | | 438 | +export async function globalSetup() { | | 439 | + // Setup shared resources, state, or environment | | 440 | + console.log('Global setup executed'); | | 441 | + // Run servers, create files, prepare databases, etc. | | 442 | +} | | 443 | + | | 444 | +export async function globalTeardown() { | | 445 | + // Clean up resources, state, or environment | | 446 | + console.log('Global teardown executed'); | | 447 | + // Close servers, remove files, disconnect from databases, etc. | | 448 | +} | | 449 | +``` | | 450 | + | | 451 | +If the global setup function throws an error, no tests will be run and the process will exit with a non-zero exit code. | | 452 | +The global teardown function will not be called in this case. | | 453 | + | 400 | 454 | ## Running tests from the command line | 401 | 455 |
| 402 | 456 | The Node.js test runner can be invoked from the command line by passing the | |
Collapse file Copy file name to clipboardExpand all lines: doc/node-config-schema.json +3Lines changed: 3 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 392 | 392 | "test-coverage-lines": { | 393 | 393 | "type": "number" | 394 | 394 | }, | | 395 | + "test-global-setup": { | | 396 | + "type": "string" | | 397 | + }, | 395 | 398 | "test-isolation": { | 396 | 399 | "type": "string" | 397 | 400 | }, | |
Collapse file Copy file name to clipboardExpand all lines: doc/node.1 +3Lines changed: 3 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -464,6 +464,9 @@ Require a minimum threshold for line coverage (0 - 100). | 464 | 464 | Configures the test runner to exit the process once all known tests have | 465 | 465 | finished executing even if the event loop would otherwise remain active. | 466 | 466 | . | | 467 | +.It Fl -test-global-setup | | 468 | +Specifies a module containing global setup and teardown functions for the test runner. | | 469 | +. | 467 | 470 | .It Fl -test-isolation Ns = Ns Ar mode | 468 | 471 | Configures the type of test isolation used in the test runner. | 469 | 472 | . | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/harness.js +28-4Lines changed: 28 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -25,29 +25,29 @@ const { | 25 | 25 | parseCommandLine, | 26 | 26 | reporterScope, | 27 | 27 | shouldColorizeTestFiles, | | 28 | + setupGlobalSetupTeardownFunctions, | 28 | 29 | } = require('internal/test_runner/utils'); | 29 | 30 | const { queueMicrotask } = require('internal/process/task_queues'); | 30 | 31 | const { TIMEOUT_MAX } = require('internal/timers'); | 31 | 32 | const { clearInterval, setInterval } = require('timers'); | 32 | 33 | const { bigint: hrtime } = process.hrtime; | 33 | | -const resolvedPromise = PromiseResolve(); | 34 | 34 | const testResources = new SafeMap(); | 35 | 35 | let globalRoot; | | 36 | +let globalSetupExecuted = false; | 36 | 37 |
| 37 | 38 | testResources.set(reporterScope.asyncId(), reporterScope); | 38 | 39 |
| 39 | 40 | function createTestTree(rootTestOptions, globalOptions) { | 40 | 41 | const buildPhaseDeferred = PromiseWithResolvers(); | 41 | 42 | const isFilteringByName = globalOptions.testNamePatterns || | 42 | | - globalOptions.testSkipPatterns; | | 43 | + globalOptions.testSkipPatterns; | 43 | 44 | const isFilteringByOnly = (globalOptions.isolation === 'process' || process.env.NODE_TEST_CONTEXT) ? | 44 | 45 | globalOptions.only : true; | 45 | 46 | const harness = { | 46 | 47 | __proto__: null, | 47 | 48 | buildPromise: buildPhaseDeferred.promise, | 48 | 49 | buildSuites: [], | 49 | 50 | isWaitingForBuildPhase: false, | 50 | | - bootstrapPromise: resolvedPromise, | 51 | 51 | watching: false, | 52 | 52 | config: globalOptions, | 53 | 53 | coverage: null, | @@ -71,6 +71,21 @@ function createTestTree(rootTestOptions, globalOptions) { | 71 | 71 | snapshotManager: null, | 72 | 72 | isFilteringByName, | 73 | 73 | isFilteringByOnly, | | 74 | + async runBootstrap() { | | 75 | + if (globalSetupExecuted) { | | 76 | + return PromiseResolve(); | | 77 | + } | | 78 | + globalSetupExecuted = true; | | 79 | + const globalSetupFunctions = await setupGlobalSetupTeardownFunctions( | | 80 | + globalOptions.globalSetupPath, | | 81 | + globalOptions.cwd, | | 82 | + ); | | 83 | + harness.globalTeardownFunction = globalSetupFunctions.globalTeardownFunction; | | 84 | + if (typeof globalSetupFunctions.globalSetupFunction === 'function') { | | 85 | + return globalSetupFunctions.globalSetupFunction(); | | 86 | + } | | 87 | + return PromiseResolve(); | | 88 | + }, | 74 | 89 | async waitForBuildPhase() { | 75 | 90 | if (harness.buildSuites.length > 0) { | 76 | 91 | await SafePromiseAllReturnVoid(harness.buildSuites); | @@ -81,6 +96,7 @@ function createTestTree(rootTestOptions, globalOptions) { | 81 | 96 | }; | 82 | 97 |
| 83 | 98 | harness.resetCounters(); | | 99 | + harness.bootstrapPromise = harness.runBootstrap(); | 84 | 100 | globalRoot = new Test({ | 85 | 101 | __proto__: null, | 86 | 102 | ...rootTestOptions, | @@ -232,6 +248,11 @@ function setupProcessState(root, globalOptions) { | 232 | 248 | 'Promise resolution is still pending but the event loop has already resolved', | 233 | 249 | kCancelledByParent)); | 234 | 250 |
| | 251 | + if (root.harness.globalTeardownFunction) { | | 252 | + await root.harness.globalTeardownFunction(); | | 253 | + root.harness.globalTeardownFunction = null; | | 254 | + } | | 255 | + | 235 | 256 | hook.disable(); | 236 | 257 | process.removeListener('uncaughtException', exceptionHandler); | 237 | 258 | process.removeListener('unhandledRejection', rejectionHandler); | @@ -278,7 +299,10 @@ function lazyBootstrapRoot() { | 278 | 299 | process.exitCode = kGenericUserError; | 279 | 300 | } | 280 | 301 | }); | 281 | | - globalRoot.harness.bootstrapPromise = globalOptions.setup(globalRoot.reporter); | | 302 | + globalRoot.harness.bootstrapPromise = SafePromiseAllReturnVoid([ | | 303 | + globalRoot.harness.bootstrapPromise, | | 304 | + globalOptions.setup(globalRoot.reporter), | | 305 | + ]); | 282 | 306 | } | 283 | 307 | return globalRoot; | 284 | 308 | } | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/runner.js +14-2Lines changed: 14 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -87,6 +87,7 @@ const { | 87 | 87 | } = require('internal/test_runner/utils'); | 88 | 88 | const { Glob } = require('internal/fs/glob'); | 89 | 89 | const { once } = require('events'); | | 90 | +const { validatePath } = require('internal/fs/utils'); | 90 | 91 | const { | 91 | 92 | triggerUncaughtException, | 92 | 93 | exitCodes: { kGenericUserError }, | @@ -559,6 +560,7 @@ function run(options = kEmptyObject) { | 559 | 560 | isolation = 'process', | 560 | 561 | watch, | 561 | 562 | setup, | | 563 | + globalSetupPath, | 562 | 564 | only, | 563 | 565 | globPatterns, | 564 | 566 | coverage = false, | @@ -668,6 +670,10 @@ function run(options = kEmptyObject) { | 668 | 670 | validateStringArray(argv, 'options.argv'); | 669 | 671 | validateStringArray(execArgv, 'options.execArgv'); | 670 | 672 |
| | 673 | + if (globalSetupPath != null) { | | 674 | + validatePath(globalSetupPath, 'options.globalSetupPath'); | | 675 | + } | | 676 | + | 671 | 677 | const rootTestOptions = { __proto__: null, concurrency, timeout, signal }; | 672 | 678 | const globalOptions = { | 673 | 679 | __proto__: null, | @@ -682,6 +688,7 @@ function run(options = kEmptyObject) { | 682 | 688 | branchCoverage: branchCoverage, | 683 | 689 | functionCoverage: functionCoverage, | 684 | 690 | cwd, | | 691 | + globalSetupPath, | 685 | 692 | }; | 686 | 693 | const root = createTestTree(rootTestOptions, globalOptions); | 687 | 694 | let testFiles = files ?? createTestFileList(globPatterns, cwd); | @@ -754,7 +761,9 @@ function run(options = kEmptyObject) { | 754 | 761 | const cascadedLoader = esmLoader.getOrInitializeCascadedLoader(); | 755 | 762 | let topLevelTestCount = 0; | 756 | 763 |
| 757 | | - root.harness.bootstrapPromise = promise; | | 764 | + root.harness.bootstrapPromise = root.harness.bootstrapPromise ? | | 765 | + SafePromiseAllReturnVoid([root.harness.bootstrapPromise, promise]) : | | 766 | + promise; | 758 | 767 |
| 759 | 768 | const userImports = getOptionValue('--import'); | 760 | 769 | for (let i = 0; i < userImports.length; i++) { | @@ -799,12 +808,15 @@ function run(options = kEmptyObject) { | 799 | 808 | debug('beginning test execution'); | 800 | 809 | root.entryFile = null; | 801 | 810 | finishBootstrap(); | 802 | | - root.processPendingSubtests(); | | 811 | + return root.processPendingSubtests(); | 803 | 812 | }; | 804 | 813 | } | 805 | 814 | } | 806 | 815 |
| 807 | 816 | const runChain = async () => { | | 817 | + if (root.harness?.bootstrapPromise) { | | 818 | + await root.harness.bootstrapPromise; | | 819 | + } | 808 | 820 | if (typeof setup === 'function') { | 809 | 821 | await setup(root.reporter); | 810 | 822 | } | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/utils.js +32-2Lines changed: 32 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -27,7 +27,7 @@ const { | 27 | 27 | } = primordials; | 28 | 28 |
| 29 | 29 | const { AsyncResource } = require('async_hooks'); | 30 | | -const { relative, sep } = require('path'); | | 30 | +const { relative, sep, resolve } = require('path'); | 31 | 31 | const { createWriteStream } = require('fs'); | 32 | 32 | const { pathToFileURL } = require('internal/url'); | 33 | 33 | const { getOptionValue } = require('internal/options'); | @@ -41,7 +41,12 @@ const { | 41 | 41 | kIsNodeError, | 42 | 42 | } = require('internal/errors'); | 43 | 43 | const { compose } = require('stream'); | 44 | | -const { validateInteger } = require('internal/validators'); | | 44 | +const { | | 45 | + validateInteger, | | 46 | + validateFunction, | | 47 | +} = require('internal/validators'); | | 48 | +const { validatePath } = require('internal/fs/utils'); | | 49 | +const { kEmptyObject } = require('internal/util'); | 45 | 50 |
| 46 | 51 | const coverageColors = { | 47 | 52 | __proto__: null, | @@ -199,6 +204,7 @@ function parseCommandLine() { | 199 | 204 | const timeout = getOptionValue('--test-timeout') || Infinity; | 200 | 205 | const isChildProcess = process.env.NODE_TEST_CONTEXT === 'child'; | 201 | 206 | const isChildProcessV8 = process.env.NODE_TEST_CONTEXT === 'child-v8'; | | 207 | + let globalSetupPath; | 202 | 208 | let concurrency; | 203 | 209 | let coverageExcludeGlobs; | 204 | 210 | let coverageIncludeGlobs; | @@ -223,6 +229,7 @@ function parseCommandLine() { | 223 | 229 | } else { | 224 | 230 | destinations = getOptionValue('--test-reporter-destination'); | 225 | 231 | reporters = getOptionValue('--test-reporter'); | | 232 | + globalSetupPath = getOptionValue('--test-global-setup'); | 226 | 233 | if (reporters.length === 0 && destinations.length === 0) { | 227 | 234 | ArrayPrototypePush(reporters, kDefaultReporter); | 228 | 235 | } | @@ -328,6 +335,7 @@ function parseCommandLine() { | 328 | 335 | only, | 329 | 336 | reporters, | 330 | 337 | setup, | | 338 | + globalSetupPath, | 331 | 339 | shard, | 332 | 340 | sourceMaps, | 333 | 341 | testNamePatterns, | @@ -597,6 +605,27 @@ function getCoverageReport(pad, summary, symbol, color, table) { | 597 | 605 | return report; | 598 | 606 | } | 599 | 607 |
| | 608 | +async function setupGlobalSetupTeardownFunctions(globalSetupPath, cwd) { | | 609 | + let globalSetupFunction; | | 610 | + let globalTeardownFunction; | | 611 | + if (globalSetupPath) { | | 612 | + validatePath(globalSetupPath, 'options.globalSetupPath'); | | 613 | + const fileURL = pathToFileURL(resolve(cwd, globalSetupPath)); | | 614 | + const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); | | 615 | + const globalSetupModule = await cascadedLoader | | 616 | + .import(fileURL, pathToFileURL(cwd + sep).href, kEmptyObject); | | 617 | + if (globalSetupModule.globalSetup) { | | 618 | + validateFunction(globalSetupModule.globalSetup, 'globalSetupModule.globalSetup'); | | 619 | + globalSetupFunction = globalSetupModule.globalSetup; | | 620 | + } | | 621 | + if (globalSetupModule.globalTeardown) { | | 622 | + validateFunction(globalSetupModule.globalTeardown, 'globalSetupModule.globalTeardown'); | | 623 | + globalTeardownFunction = globalSetupModule.globalTeardown; | | 624 | + } | | 625 | + } | | 626 | + return { __proto__: null, globalSetupFunction, globalTeardownFunction }; | | 627 | +} | | 628 | + | 600 | 629 | module.exports = { | 601 | 630 | convertStringToRegExp, | 602 | 631 | countCompletedTest, | @@ -607,4 +636,5 @@ module.exports = { | 607 | 636 | reporterScope, | 608 | 637 | shouldColorizeTestFiles, | 609 | 638 | getCoverageReport, | | 639 | + setupGlobalSetupTeardownFunctions, | 610 | 640 | }; |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.cc +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -761,6 +761,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { | 761 | 761 | "exclude files from coverage report that match this glob pattern", | 762 | 762 | &EnvironmentOptions::coverage_exclude_pattern, | 763 | 763 | kAllowedInEnvvar); | | 764 | + AddOption("--test-global-setup", | | 765 | + "specifies the path to the global setup file", | | 766 | + &EnvironmentOptions::test_global_setup_path, | | 767 | + kAllowedInEnvvar); | 764 | 768 | AddOption("--test-udp-no-try-send", "", // For testing only. | 765 | 769 | &EnvironmentOptions::test_udp_no_try_send); | 766 | 770 | AddOption("--throw-deprecation", | |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.h +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -197,6 +197,7 @@ class EnvironmentOptions : public Options { | 197 | 197 | std::vector<std::string> test_name_pattern; | 198 | 198 | std::vector<std::string> test_reporter; | 199 | 199 | std::vector<std::string> test_reporter_destination; | | 200 | + std::string test_global_setup_path; | 200 | 201 | bool test_only = false; | 201 | 202 | bool test_udp_no_try_send = false; | 202 | 203 | std::string test_isolation = "process"; | |
Collapse file Copy file name to clipboard +13Lines changed: 13 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +const test = require('node:test'); | | 4 | +const assert = require('node:assert'); | | 5 | +const fs = require('node:fs'); | | 6 | + | | 7 | +test('Another test that verifies setup flag existance', (t) => { | | 8 | + const setupFlagPath = process.env.SETUP_FLAG_PATH; | | 9 | + assert.ok(fs.existsSync(setupFlagPath), 'Setup flag file should exist'); | | 10 | + | | 11 | + const content = fs.readFileSync(setupFlagPath, 'utf8'); | | 12 | + assert.strictEqual(content, 'Setup was executed'); | | 13 | +}); |
|