| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent e800f00 commit cb5f671
26 files changed
| 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 | |
| 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 | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -392,6 +392,9 @@ | |||
| 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 | }, | |
| 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 | . | |
| 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 | } | |
| 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 | } | |
| 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 | }; | |
| 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", | |
| 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"; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,13 @@ | |||
| 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 | + }); | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments