mirror of
https://github.com/nodejs/node.git
synced 2025-04-28 05:25:19 +00:00
test_runner: add global setup and teardown functionality
Some checks are pending
Coverage Linux (without intl) / coverage-linux-without-intl (push) Waiting to run
Coverage Linux / coverage-linux (push) Waiting to run
Coverage Windows / coverage-windows (push) Waiting to run
Test and upload documentation to artifacts / build-docs (push) Waiting to run
Linters / lint-addon-docs (push) Waiting to run
Linters / lint-cpp (push) Waiting to run
Linters / format-cpp (push) Waiting to run
Linters / lint-js-and-md (push) Waiting to run
Linters / lint-py (push) Waiting to run
Linters / lint-yaml (push) Waiting to run
Linters / lint-sh (push) Waiting to run
Linters / lint-codeowners (push) Waiting to run
Linters / lint-pr-url (push) Waiting to run
Linters / lint-readme (push) Waiting to run
Notify on Push / Notify on Force Push on `main` (push) Waiting to run
Notify on Push / Notify on Push on `main` that lacks metadata (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Some checks are pending
Coverage Linux (without intl) / coverage-linux-without-intl (push) Waiting to run
Coverage Linux / coverage-linux (push) Waiting to run
Coverage Windows / coverage-windows (push) Waiting to run
Test and upload documentation to artifacts / build-docs (push) Waiting to run
Linters / lint-addon-docs (push) Waiting to run
Linters / lint-cpp (push) Waiting to run
Linters / format-cpp (push) Waiting to run
Linters / lint-js-and-md (push) Waiting to run
Linters / lint-py (push) Waiting to run
Linters / lint-yaml (push) Waiting to run
Linters / lint-sh (push) Waiting to run
Linters / lint-codeowners (push) Waiting to run
Linters / lint-pr-url (push) Waiting to run
Linters / lint-readme (push) Waiting to run
Notify on Push / Notify on Force Push on `main` (push) Waiting to run
Notify on Push / Notify on Push on `main` that lacks metadata (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
PR-URL: https://github.com/nodejs/node/pull/57438 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
This commit is contained in:
parent
e800f009f6
commit
cb5f671a34
@ -2452,6 +2452,19 @@ added:
|
||||
Configures the test runner to exit the process once all known tests have
|
||||
finished executing even if the event loop would otherwise remain active.
|
||||
|
||||
### `--test-global-setup=module`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1.0 - Early development
|
||||
|
||||
Specify a module that will be evaluated before all tests are executed and
|
||||
can be used to setup global state or fixtures for tests.
|
||||
|
||||
See the documentation on [global setup and teardown][] for more details.
|
||||
|
||||
### `--test-isolation=mode`
|
||||
|
||||
<!-- YAML
|
||||
@ -3347,6 +3360,7 @@ one is included in the list below.
|
||||
* `--test-coverage-functions`
|
||||
* `--test-coverage-include`
|
||||
* `--test-coverage-lines`
|
||||
* `--test-global-setup`
|
||||
* `--test-isolation`
|
||||
* `--test-name-pattern`
|
||||
* `--test-only`
|
||||
@ -3898,6 +3912,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
|
||||
[emit_warning]: process.md#processemitwarningwarning-options
|
||||
[environment_variables]: #environment-variables
|
||||
[filtering tests by name]: test.md#filtering-tests-by-name
|
||||
[global setup and teardown]: test.md#global-setup-and-teardown
|
||||
[jitless]: https://v8.dev/blog/jitless
|
||||
[libuv threadpool documentation]: https://docs.libuv.org/en/latest/threadpool.html
|
||||
[module compile cache]: module.md#module-compile-cache
|
||||
|
@ -397,6 +397,60 @@ their dependencies. When a change is detected, the test runner will
|
||||
rerun the tests affected by the change.
|
||||
The test runner will continue to run until the process is terminated.
|
||||
|
||||
## Global setup and teardown
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1.0 - Early development
|
||||
|
||||
The test runner supports specifying a module that will be evaluated before all tests are executed and
|
||||
can be used to setup global state or fixtures for tests. This is useful for preparing resources or setting up
|
||||
shared state that is required by multiple tests.
|
||||
|
||||
This module can export any of the following:
|
||||
|
||||
* A `globalSetup` function which runs once before all tests start
|
||||
* A `globalTeardown` function which runs once after all tests complete
|
||||
|
||||
The module is specified using the `--test-global-setup` flag when running tests from the command line.
|
||||
|
||||
```cjs
|
||||
// setup-module.js
|
||||
async function globalSetup() {
|
||||
// Setup shared resources, state, or environment
|
||||
console.log('Global setup executed');
|
||||
// Run servers, create files, prepare databases, etc.
|
||||
}
|
||||
|
||||
async function globalTeardown() {
|
||||
// Clean up resources, state, or environment
|
||||
console.log('Global teardown executed');
|
||||
// Close servers, remove files, disconnect from databases, etc.
|
||||
}
|
||||
|
||||
module.exports = { globalSetup, globalTeardown };
|
||||
```
|
||||
|
||||
```mjs
|
||||
// setup-module.mjs
|
||||
export async function globalSetup() {
|
||||
// Setup shared resources, state, or environment
|
||||
console.log('Global setup executed');
|
||||
// Run servers, create files, prepare databases, etc.
|
||||
}
|
||||
|
||||
export async function globalTeardown() {
|
||||
// Clean up resources, state, or environment
|
||||
console.log('Global teardown executed');
|
||||
// Close servers, remove files, disconnect from databases, etc.
|
||||
}
|
||||
```
|
||||
|
||||
If the global setup function throws an error, no tests will be run and the process will exit with a non-zero exit code.
|
||||
The global teardown function will not be called in this case.
|
||||
|
||||
## Running tests from the command line
|
||||
|
||||
The Node.js test runner can be invoked from the command line by passing the
|
||||
|
@ -392,6 +392,9 @@
|
||||
"test-coverage-lines": {
|
||||
"type": "number"
|
||||
},
|
||||
"test-global-setup": {
|
||||
"type": "string"
|
||||
},
|
||||
"test-isolation": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -464,6 +464,9 @@ Require a minimum threshold for line coverage (0 - 100).
|
||||
Configures the test runner to exit the process once all known tests have
|
||||
finished executing even if the event loop would otherwise remain active.
|
||||
.
|
||||
.It Fl -test-global-setup
|
||||
Specifies a module containing global setup and teardown functions for the test runner.
|
||||
.
|
||||
.It Fl -test-isolation Ns = Ns Ar mode
|
||||
Configures the type of test isolation used in the test runner.
|
||||
.
|
||||
|
@ -25,21 +25,22 @@ const {
|
||||
parseCommandLine,
|
||||
reporterScope,
|
||||
shouldColorizeTestFiles,
|
||||
setupGlobalSetupTeardownFunctions,
|
||||
} = require('internal/test_runner/utils');
|
||||
const { queueMicrotask } = require('internal/process/task_queues');
|
||||
const { TIMEOUT_MAX } = require('internal/timers');
|
||||
const { clearInterval, setInterval } = require('timers');
|
||||
const { bigint: hrtime } = process.hrtime;
|
||||
const resolvedPromise = PromiseResolve();
|
||||
const testResources = new SafeMap();
|
||||
let globalRoot;
|
||||
let globalSetupExecuted = false;
|
||||
|
||||
testResources.set(reporterScope.asyncId(), reporterScope);
|
||||
|
||||
function createTestTree(rootTestOptions, globalOptions) {
|
||||
const buildPhaseDeferred = PromiseWithResolvers();
|
||||
const isFilteringByName = globalOptions.testNamePatterns ||
|
||||
globalOptions.testSkipPatterns;
|
||||
globalOptions.testSkipPatterns;
|
||||
const isFilteringByOnly = (globalOptions.isolation === 'process' || process.env.NODE_TEST_CONTEXT) ?
|
||||
globalOptions.only : true;
|
||||
const harness = {
|
||||
@ -47,7 +48,6 @@ function createTestTree(rootTestOptions, globalOptions) {
|
||||
buildPromise: buildPhaseDeferred.promise,
|
||||
buildSuites: [],
|
||||
isWaitingForBuildPhase: false,
|
||||
bootstrapPromise: resolvedPromise,
|
||||
watching: false,
|
||||
config: globalOptions,
|
||||
coverage: null,
|
||||
@ -71,6 +71,21 @@ function createTestTree(rootTestOptions, globalOptions) {
|
||||
snapshotManager: null,
|
||||
isFilteringByName,
|
||||
isFilteringByOnly,
|
||||
async runBootstrap() {
|
||||
if (globalSetupExecuted) {
|
||||
return PromiseResolve();
|
||||
}
|
||||
globalSetupExecuted = true;
|
||||
const globalSetupFunctions = await setupGlobalSetupTeardownFunctions(
|
||||
globalOptions.globalSetupPath,
|
||||
globalOptions.cwd,
|
||||
);
|
||||
harness.globalTeardownFunction = globalSetupFunctions.globalTeardownFunction;
|
||||
if (typeof globalSetupFunctions.globalSetupFunction === 'function') {
|
||||
return globalSetupFunctions.globalSetupFunction();
|
||||
}
|
||||
return PromiseResolve();
|
||||
},
|
||||
async waitForBuildPhase() {
|
||||
if (harness.buildSuites.length > 0) {
|
||||
await SafePromiseAllReturnVoid(harness.buildSuites);
|
||||
@ -81,6 +96,7 @@ function createTestTree(rootTestOptions, globalOptions) {
|
||||
};
|
||||
|
||||
harness.resetCounters();
|
||||
harness.bootstrapPromise = harness.runBootstrap();
|
||||
globalRoot = new Test({
|
||||
__proto__: null,
|
||||
...rootTestOptions,
|
||||
@ -232,6 +248,11 @@ function setupProcessState(root, globalOptions) {
|
||||
'Promise resolution is still pending but the event loop has already resolved',
|
||||
kCancelledByParent));
|
||||
|
||||
if (root.harness.globalTeardownFunction) {
|
||||
await root.harness.globalTeardownFunction();
|
||||
root.harness.globalTeardownFunction = null;
|
||||
}
|
||||
|
||||
hook.disable();
|
||||
process.removeListener('uncaughtException', exceptionHandler);
|
||||
process.removeListener('unhandledRejection', rejectionHandler);
|
||||
@ -278,7 +299,10 @@ function lazyBootstrapRoot() {
|
||||
process.exitCode = kGenericUserError;
|
||||
}
|
||||
});
|
||||
globalRoot.harness.bootstrapPromise = globalOptions.setup(globalRoot.reporter);
|
||||
globalRoot.harness.bootstrapPromise = SafePromiseAllReturnVoid([
|
||||
globalRoot.harness.bootstrapPromise,
|
||||
globalOptions.setup(globalRoot.reporter),
|
||||
]);
|
||||
}
|
||||
return globalRoot;
|
||||
}
|
||||
|
@ -87,6 +87,7 @@ const {
|
||||
} = require('internal/test_runner/utils');
|
||||
const { Glob } = require('internal/fs/glob');
|
||||
const { once } = require('events');
|
||||
const { validatePath } = require('internal/fs/utils');
|
||||
const {
|
||||
triggerUncaughtException,
|
||||
exitCodes: { kGenericUserError },
|
||||
@ -559,6 +560,7 @@ function run(options = kEmptyObject) {
|
||||
isolation = 'process',
|
||||
watch,
|
||||
setup,
|
||||
globalSetupPath,
|
||||
only,
|
||||
globPatterns,
|
||||
coverage = false,
|
||||
@ -668,6 +670,10 @@ function run(options = kEmptyObject) {
|
||||
validateStringArray(argv, 'options.argv');
|
||||
validateStringArray(execArgv, 'options.execArgv');
|
||||
|
||||
if (globalSetupPath != null) {
|
||||
validatePath(globalSetupPath, 'options.globalSetupPath');
|
||||
}
|
||||
|
||||
const rootTestOptions = { __proto__: null, concurrency, timeout, signal };
|
||||
const globalOptions = {
|
||||
__proto__: null,
|
||||
@ -682,6 +688,7 @@ function run(options = kEmptyObject) {
|
||||
branchCoverage: branchCoverage,
|
||||
functionCoverage: functionCoverage,
|
||||
cwd,
|
||||
globalSetupPath,
|
||||
};
|
||||
const root = createTestTree(rootTestOptions, globalOptions);
|
||||
let testFiles = files ?? createTestFileList(globPatterns, cwd);
|
||||
@ -754,7 +761,9 @@ function run(options = kEmptyObject) {
|
||||
const cascadedLoader = esmLoader.getOrInitializeCascadedLoader();
|
||||
let topLevelTestCount = 0;
|
||||
|
||||
root.harness.bootstrapPromise = promise;
|
||||
root.harness.bootstrapPromise = root.harness.bootstrapPromise ?
|
||||
SafePromiseAllReturnVoid([root.harness.bootstrapPromise, promise]) :
|
||||
promise;
|
||||
|
||||
const userImports = getOptionValue('--import');
|
||||
for (let i = 0; i < userImports.length; i++) {
|
||||
@ -799,12 +808,15 @@ function run(options = kEmptyObject) {
|
||||
debug('beginning test execution');
|
||||
root.entryFile = null;
|
||||
finishBootstrap();
|
||||
root.processPendingSubtests();
|
||||
return root.processPendingSubtests();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const runChain = async () => {
|
||||
if (root.harness?.bootstrapPromise) {
|
||||
await root.harness.bootstrapPromise;
|
||||
}
|
||||
if (typeof setup === 'function') {
|
||||
await setup(root.reporter);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ const {
|
||||
} = primordials;
|
||||
|
||||
const { AsyncResource } = require('async_hooks');
|
||||
const { relative, sep } = require('path');
|
||||
const { relative, sep, resolve } = require('path');
|
||||
const { createWriteStream } = require('fs');
|
||||
const { pathToFileURL } = require('internal/url');
|
||||
const { getOptionValue } = require('internal/options');
|
||||
@ -41,7 +41,12 @@ const {
|
||||
kIsNodeError,
|
||||
} = require('internal/errors');
|
||||
const { compose } = require('stream');
|
||||
const { validateInteger } = require('internal/validators');
|
||||
const {
|
||||
validateInteger,
|
||||
validateFunction,
|
||||
} = require('internal/validators');
|
||||
const { validatePath } = require('internal/fs/utils');
|
||||
const { kEmptyObject } = require('internal/util');
|
||||
|
||||
const coverageColors = {
|
||||
__proto__: null,
|
||||
@ -199,6 +204,7 @@ function parseCommandLine() {
|
||||
const timeout = getOptionValue('--test-timeout') || Infinity;
|
||||
const isChildProcess = process.env.NODE_TEST_CONTEXT === 'child';
|
||||
const isChildProcessV8 = process.env.NODE_TEST_CONTEXT === 'child-v8';
|
||||
let globalSetupPath;
|
||||
let concurrency;
|
||||
let coverageExcludeGlobs;
|
||||
let coverageIncludeGlobs;
|
||||
@ -223,6 +229,7 @@ function parseCommandLine() {
|
||||
} else {
|
||||
destinations = getOptionValue('--test-reporter-destination');
|
||||
reporters = getOptionValue('--test-reporter');
|
||||
globalSetupPath = getOptionValue('--test-global-setup');
|
||||
if (reporters.length === 0 && destinations.length === 0) {
|
||||
ArrayPrototypePush(reporters, kDefaultReporter);
|
||||
}
|
||||
@ -328,6 +335,7 @@ function parseCommandLine() {
|
||||
only,
|
||||
reporters,
|
||||
setup,
|
||||
globalSetupPath,
|
||||
shard,
|
||||
sourceMaps,
|
||||
testNamePatterns,
|
||||
@ -597,6 +605,27 @@ function getCoverageReport(pad, summary, symbol, color, table) {
|
||||
return report;
|
||||
}
|
||||
|
||||
async function setupGlobalSetupTeardownFunctions(globalSetupPath, cwd) {
|
||||
let globalSetupFunction;
|
||||
let globalTeardownFunction;
|
||||
if (globalSetupPath) {
|
||||
validatePath(globalSetupPath, 'options.globalSetupPath');
|
||||
const fileURL = pathToFileURL(resolve(cwd, globalSetupPath));
|
||||
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
|
||||
const globalSetupModule = await cascadedLoader
|
||||
.import(fileURL, pathToFileURL(cwd + sep).href, kEmptyObject);
|
||||
if (globalSetupModule.globalSetup) {
|
||||
validateFunction(globalSetupModule.globalSetup, 'globalSetupModule.globalSetup');
|
||||
globalSetupFunction = globalSetupModule.globalSetup;
|
||||
}
|
||||
if (globalSetupModule.globalTeardown) {
|
||||
validateFunction(globalSetupModule.globalTeardown, 'globalSetupModule.globalTeardown');
|
||||
globalTeardownFunction = globalSetupModule.globalTeardown;
|
||||
}
|
||||
}
|
||||
return { __proto__: null, globalSetupFunction, globalTeardownFunction };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
convertStringToRegExp,
|
||||
countCompletedTest,
|
||||
@ -607,4 +636,5 @@ module.exports = {
|
||||
reporterScope,
|
||||
shouldColorizeTestFiles,
|
||||
getCoverageReport,
|
||||
setupGlobalSetupTeardownFunctions,
|
||||
};
|
||||
|
@ -761,6 +761,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
|
||||
"exclude files from coverage report that match this glob pattern",
|
||||
&EnvironmentOptions::coverage_exclude_pattern,
|
||||
kAllowedInEnvvar);
|
||||
AddOption("--test-global-setup",
|
||||
"specifies the path to the global setup file",
|
||||
&EnvironmentOptions::test_global_setup_path,
|
||||
kAllowedInEnvvar);
|
||||
AddOption("--test-udp-no-try-send", "", // For testing only.
|
||||
&EnvironmentOptions::test_udp_no_try_send);
|
||||
AddOption("--throw-deprecation",
|
||||
|
@ -197,6 +197,7 @@ class EnvironmentOptions : public Options {
|
||||
std::vector<std::string> test_name_pattern;
|
||||
std::vector<std::string> test_reporter;
|
||||
std::vector<std::string> test_reporter_destination;
|
||||
std::string test_global_setup_path;
|
||||
bool test_only = false;
|
||||
bool test_udp_no_try_send = false;
|
||||
std::string test_isolation = "process";
|
||||
|
13
test/fixtures/test-runner/global-setup-teardown/another-test-file.js
vendored
Normal file
13
test/fixtures/test-runner/global-setup-teardown/another-test-file.js
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('Another test that verifies setup flag existance', (t) => {
|
||||
const setupFlagPath = process.env.SETUP_FLAG_PATH;
|
||||
assert.ok(fs.existsSync(setupFlagPath), 'Setup flag file should exist');
|
||||
|
||||
const content = fs.readFileSync(setupFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Setup was executed');
|
||||
});
|
26
test/fixtures/test-runner/global-setup-teardown/async-setup-teardown.js
vendored
Normal file
26
test/fixtures/test-runner/global-setup-teardown/async-setup-teardown.js
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
const { setTimeout } = require('node:timers/promises');
|
||||
|
||||
const asyncFlagPath = process.env.ASYNC_FLAG_PATH;
|
||||
|
||||
async function globalSetup() {
|
||||
console.log('Async setup starting');
|
||||
|
||||
await setTimeout(500);
|
||||
|
||||
fs.writeFileSync(asyncFlagPath, 'Setup part');
|
||||
console.log('Async setup completed');
|
||||
}
|
||||
|
||||
async function globalTeardown() {
|
||||
console.log('Async teardown starting');
|
||||
|
||||
await setTimeout(100);
|
||||
|
||||
fs.appendFileSync(asyncFlagPath, ', Teardown part');
|
||||
console.log('Async teardown completed');
|
||||
}
|
||||
|
||||
module.exports = { globalSetup, globalTeardown };
|
20
test/fixtures/test-runner/global-setup-teardown/basic-setup-teardown.js
vendored
Normal file
20
test/fixtures/test-runner/global-setup-teardown/basic-setup-teardown.js
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
|
||||
// Path for temporary file to track execution
|
||||
const setupFlagPath = process.env.SETUP_FLAG_PATH;
|
||||
const teardownFlagPath = process.env.TEARDOWN_FLAG_PATH;
|
||||
|
||||
async function globalSetup() {
|
||||
console.log('Global setup executed');
|
||||
fs.writeFileSync(setupFlagPath, 'Setup was executed');
|
||||
}
|
||||
|
||||
async function globalTeardown() {
|
||||
console.log('Global teardown executed');
|
||||
fs.writeFileSync(teardownFlagPath, 'Teardown was executed');
|
||||
fs.rmSync(setupFlagPath, { force: true });
|
||||
}
|
||||
|
||||
module.exports = { globalSetup, globalTeardown };
|
18
test/fixtures/test-runner/global-setup-teardown/basic-setup-teardown.mjs
vendored
Normal file
18
test/fixtures/test-runner/global-setup-teardown/basic-setup-teardown.mjs
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
// Path for temporary file to track execution
|
||||
const setupFlagPath = process.env.SETUP_FLAG_PATH;
|
||||
const teardownFlagPath = process.env.TEARDOWN_FLAG_PATH;
|
||||
|
||||
async function globalSetup() {
|
||||
console.log('Global setup executed');
|
||||
fs.writeFileSync(setupFlagPath, 'Setup was executed');
|
||||
}
|
||||
|
||||
async function globalTeardown() {
|
||||
console.log('Global teardown executed');
|
||||
fs.writeFileSync(teardownFlagPath, 'Teardown was executed');
|
||||
fs.rmSync(setupFlagPath, { force: true });
|
||||
}
|
||||
|
||||
export { globalSetup, globalTeardown };
|
24
test/fixtures/test-runner/global-setup-teardown/basic-setup-teardown.ts
vendored
Normal file
24
test/fixtures/test-runner/global-setup-teardown/basic-setup-teardown.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
// Path for temporary file to track execution
|
||||
const setupFlagPath = process.env.SETUP_FLAG_PATH;
|
||||
const teardownFlagPath = process.env.TEARDOWN_FLAG_PATH;
|
||||
|
||||
async function globalSetup(): Promise<void> {
|
||||
console.log('Global setup executed');
|
||||
if (setupFlagPath) {
|
||||
fs.writeFileSync(setupFlagPath, 'Setup was executed');
|
||||
}
|
||||
}
|
||||
|
||||
async function globalTeardown(): Promise<void> {
|
||||
console.log('Global teardown executed');
|
||||
if (teardownFlagPath) {
|
||||
fs.writeFileSync(teardownFlagPath, 'Teardown was executed');
|
||||
}
|
||||
if (setupFlagPath) {
|
||||
fs.rmSync(setupFlagPath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export { globalSetup, globalTeardown };
|
11
test/fixtures/test-runner/global-setup-teardown/error-in-setup.js
vendored
Normal file
11
test/fixtures/test-runner/global-setup-teardown/error-in-setup.js
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
async function globalSetup() {
|
||||
throw new Error('Deliberate error in global setup');
|
||||
}
|
||||
|
||||
async function globalTeardown() {
|
||||
console.log('Teardown should not run if setup fails');
|
||||
}
|
||||
|
||||
module.exports = { globalSetup, globalTeardown };
|
4
test/fixtures/test-runner/global-setup-teardown/imported-module-with-test.mjs
vendored
Normal file
4
test/fixtures/test-runner/global-setup-teardown/imported-module-with-test.mjs
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
import { test } from 'node:test';
|
||||
|
||||
test('Imported module Ok', () => {});
|
||||
test('Imported module Fail', () => { throw new Error('fail'); });
|
1
test/fixtures/test-runner/global-setup-teardown/imported-module.mjs
vendored
Normal file
1
test/fixtures/test-runner/global-setup-teardown/imported-module.mjs
vendored
Normal file
@ -0,0 +1 @@
|
||||
console.log('Imported module executed');
|
1
test/fixtures/test-runner/global-setup-teardown/required-module.cjs
vendored
Normal file
1
test/fixtures/test-runner/global-setup-teardown/required-module.cjs
vendored
Normal file
@ -0,0 +1 @@
|
||||
console.log('Required module executed');
|
12
test/fixtures/test-runner/global-setup-teardown/setup-only.js
vendored
Normal file
12
test/fixtures/test-runner/global-setup-teardown/setup-only.js
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
|
||||
const setupFlagPath = process.env.SETUP_ONLY_FLAG_PATH;
|
||||
|
||||
async function globalSetup() {
|
||||
console.log('Setup-only module executed');
|
||||
fs.writeFileSync(setupFlagPath, 'Setup-only was executed');
|
||||
}
|
||||
|
||||
module.exports = { globalSetup };
|
12
test/fixtures/test-runner/global-setup-teardown/teardown-only.js
vendored
Normal file
12
test/fixtures/test-runner/global-setup-teardown/teardown-only.js
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
|
||||
const teardownFlagPath = process.env.TEARDOWN_ONLY_FLAG_PATH;
|
||||
|
||||
async function globalTeardown() {
|
||||
console.log('Teardown-only module executed');
|
||||
fs.writeFileSync(teardownFlagPath, 'Teardown-only was executed');
|
||||
}
|
||||
|
||||
module.exports = { globalTeardown };
|
17
test/fixtures/test-runner/global-setup-teardown/test-file.js
vendored
Normal file
17
test/fixtures/test-runner/global-setup-teardown/test-file.js
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('verify setup was executed', (t) => {
|
||||
const setupFlagPath = process.env.SETUP_FLAG_PATH;
|
||||
assert.ok(fs.existsSync(setupFlagPath), 'Setup flag file should exist');
|
||||
|
||||
const content = fs.readFileSync(setupFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Setup was executed');
|
||||
});
|
||||
|
||||
test('another simple test', (t) => {
|
||||
assert.ok(true);
|
||||
});
|
35
test/fixtures/test-runner/test-runner-global-hooks.mjs
vendored
Normal file
35
test/fixtures/test-runner/test-runner-global-hooks.mjs
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
import { run } from 'node:test';
|
||||
import { spec } from 'node:test/reporters';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
const options = {
|
||||
file: {
|
||||
type: 'string',
|
||||
},
|
||||
globalSetup: {
|
||||
type: 'string',
|
||||
},
|
||||
isolation: {
|
||||
type: 'string',
|
||||
},
|
||||
};
|
||||
|
||||
const {
|
||||
values,
|
||||
} = parseArgs({ args: process.argv.slice(2), options });
|
||||
|
||||
let files;
|
||||
let globalSetupPath;
|
||||
|
||||
if (values.file) {
|
||||
files = [values.file];
|
||||
}
|
||||
|
||||
if (values.globalSetup) {
|
||||
globalSetupPath = values.globalSetup;
|
||||
}
|
||||
|
||||
run({
|
||||
files,
|
||||
globalSetupPath,
|
||||
}).compose(spec).pipe(process.stdout);
|
568
test/parallel/test-runner-global-setup-teardown.mjs
Normal file
568
test/parallel/test-runner-global-setup-teardown.mjs
Normal file
@ -0,0 +1,568 @@
|
||||
import '../common/index.mjs';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
import { describe, it, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import fs from 'node:fs';
|
||||
import { spawn } from 'node:child_process';
|
||||
import tmpdir from '../common/tmpdir.js';
|
||||
import { once } from 'node:events';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const testFixtures = fixtures.path('test-runner');
|
||||
|
||||
async function runTest(
|
||||
{
|
||||
isolation,
|
||||
globalSetupFile,
|
||||
testFiles = ['test-file.js'],
|
||||
env = {},
|
||||
additionalFlags = [],
|
||||
runnerEnabled = true,
|
||||
}
|
||||
) {
|
||||
const globalSetupPath = join(testFixtures, 'global-setup-teardown', globalSetupFile);
|
||||
|
||||
const args = [];
|
||||
|
||||
if (runnerEnabled) {
|
||||
args.push('--test');
|
||||
}
|
||||
|
||||
if (isolation) {
|
||||
args.push(`--test-isolation=${isolation}`);
|
||||
}
|
||||
|
||||
args.push(
|
||||
'--test-reporter=spec',
|
||||
`--test-global-setup=${globalSetupPath}`
|
||||
);
|
||||
|
||||
if (additionalFlags.length > 0) {
|
||||
args.push(...additionalFlags);
|
||||
}
|
||||
|
||||
const testFilePaths = testFiles.map((file) => join(testFixtures, 'global-setup-teardown', file));
|
||||
args.push(...testFilePaths);
|
||||
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
args,
|
||||
{
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
...env
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
await once(child, 'exit');
|
||||
|
||||
return { stdout, stderr };
|
||||
}
|
||||
|
||||
[
|
||||
{
|
||||
isolation: 'none',
|
||||
runnerEnabled: true
|
||||
},
|
||||
{
|
||||
isolation: 'process',
|
||||
runnerEnabled: true
|
||||
},
|
||||
{
|
||||
isolation: undefined,
|
||||
runnerEnabled: false
|
||||
},
|
||||
].forEach((testCase) => {
|
||||
const { isolation, runnerEnabled } = testCase;
|
||||
describe(`test runner global hooks with isolation=${isolation} and --test: ${runnerEnabled}`, { concurrency: false }, () => {
|
||||
beforeEach(() => {
|
||||
tmpdir.refresh();
|
||||
});
|
||||
|
||||
it('should run globalSetup and globalTeardown functions', async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-executed.tmp');
|
||||
const teardownFlagPath = tmpdir.resolve('teardown-executed.tmp');
|
||||
|
||||
const { stdout, stderr } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'basic-setup-teardown.js',
|
||||
env: {
|
||||
SETUP_FLAG_PATH: setupFlagPath,
|
||||
TEARDOWN_FLAG_PATH: teardownFlagPath
|
||||
},
|
||||
runnerEnabled
|
||||
});
|
||||
|
||||
assert.match(stdout, /pass 2/);
|
||||
assert.match(stdout, /fail 0/);
|
||||
assert.match(stdout, /Global setup executed/);
|
||||
assert.match(stdout, /Global teardown executed/);
|
||||
assert.strictEqual(stderr.length, 0);
|
||||
|
||||
// After all tests complete, the teardown should have run
|
||||
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
|
||||
const content = fs.readFileSync(teardownFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Teardown was executed');
|
||||
|
||||
// Setup flag should have been removed by teardown
|
||||
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
|
||||
});
|
||||
|
||||
it('should run setup-only module', async () => {
|
||||
const setupOnlyFlagPath = tmpdir.resolve('setup-only-executed.tmp');
|
||||
|
||||
const { stdout } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'setup-only.js',
|
||||
env: {
|
||||
SETUP_ONLY_FLAG_PATH: setupOnlyFlagPath,
|
||||
SETUP_FLAG_PATH: setupOnlyFlagPath
|
||||
},
|
||||
runnerEnabled
|
||||
});
|
||||
|
||||
assert.match(stdout, /Setup-only module executed/);
|
||||
|
||||
assert.ok(fs.existsSync(setupOnlyFlagPath), 'Setup-only flag file should exist');
|
||||
const content = fs.readFileSync(setupOnlyFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Setup-only was executed');
|
||||
});
|
||||
|
||||
it('should run teardown-only module', async () => {
|
||||
const teardownOnlyFlagPath = tmpdir.resolve('teardown-only-executed.tmp');
|
||||
const setupFlagPath = tmpdir.resolve('setup-for-teardown-only.tmp');
|
||||
|
||||
// Create a setup file for test-file.js to find
|
||||
fs.writeFileSync(setupFlagPath, 'Setup was executed');
|
||||
|
||||
const { stdout } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'teardown-only.js',
|
||||
env: {
|
||||
TEARDOWN_ONLY_FLAG_PATH: teardownOnlyFlagPath,
|
||||
SETUP_FLAG_PATH: setupFlagPath
|
||||
},
|
||||
runnerEnabled
|
||||
});
|
||||
|
||||
assert.match(stdout, /pass 2/);
|
||||
assert.match(stdout, /fail 0/);
|
||||
assert.match(stdout, /Teardown-only module executed/);
|
||||
|
||||
assert.ok(fs.existsSync(teardownOnlyFlagPath), 'Teardown-only flag file should exist');
|
||||
const content = fs.readFileSync(teardownOnlyFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Teardown-only was executed');
|
||||
});
|
||||
|
||||
// Create a file in globalSetup and delete it in globalTeardown
|
||||
// two test files should both verify that the file exists
|
||||
// This works as the globalTeardown removes the setupFlag
|
||||
it('should run globalTeardown only after all tests are done in case of more than one test file', async () => {
|
||||
const teardownOnlyFlagPath = tmpdir.resolve('teardown-only-executed.tmp');
|
||||
const setupFlagPath = tmpdir.resolve('setup-for-teardown-only.tmp');
|
||||
|
||||
// Create a setup file for test-file.js to find
|
||||
fs.writeFileSync(setupFlagPath, 'Setup was executed');
|
||||
|
||||
const { stdout } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'teardown-only.js',
|
||||
env: {
|
||||
TEARDOWN_ONLY_FLAG_PATH: teardownOnlyFlagPath,
|
||||
SETUP_FLAG_PATH: setupFlagPath
|
||||
},
|
||||
runnerEnabled,
|
||||
testFiles: ['test-file.js', 'another-test-file.js']
|
||||
});
|
||||
|
||||
if (runnerEnabled) {
|
||||
assert.match(stdout, /pass 3/);
|
||||
} else {
|
||||
assert.match(stdout, /pass 2/);
|
||||
}
|
||||
|
||||
assert.match(stdout, /fail 0/);
|
||||
assert.match(stdout, /Teardown-only module executed/);
|
||||
|
||||
assert.ok(fs.existsSync(teardownOnlyFlagPath), 'Teardown-only flag file should exist');
|
||||
const content = fs.readFileSync(teardownOnlyFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Teardown-only was executed');
|
||||
});
|
||||
|
||||
// TODO(pmarchini): We should be able to share context between setup and teardown
|
||||
it.todo('should share context between setup and teardown');
|
||||
|
||||
it('should handle async setup and teardown', async () => {
|
||||
const asyncFlagPath = tmpdir.resolve('async-executed.tmp');
|
||||
const setupFlagPath = tmpdir.resolve('setup-for-async.tmp');
|
||||
|
||||
// Create a setup file for test-file.js to find
|
||||
fs.writeFileSync(setupFlagPath, 'Setup was executed');
|
||||
|
||||
const { stdout } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'async-setup-teardown.js',
|
||||
env: {
|
||||
ASYNC_FLAG_PATH: asyncFlagPath,
|
||||
SETUP_FLAG_PATH: setupFlagPath
|
||||
},
|
||||
runnerEnabled
|
||||
});
|
||||
|
||||
assert.match(stdout, /pass 2/);
|
||||
assert.match(stdout, /fail 0/);
|
||||
assert.match(stdout, /Async setup starting/);
|
||||
assert.match(stdout, /Async setup completed/);
|
||||
assert.match(stdout, /Async teardown starting/);
|
||||
assert.match(stdout, /Async teardown completed/);
|
||||
|
||||
assert.ok(fs.existsSync(asyncFlagPath), 'Async flag file should exist');
|
||||
const content = fs.readFileSync(asyncFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Setup part, Teardown part');
|
||||
});
|
||||
|
||||
it('should handle error in setup', async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-for-error.tmp');
|
||||
|
||||
const { stdout, stderr } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'error-in-setup.js',
|
||||
env: {
|
||||
SETUP_FLAG_PATH: setupFlagPath
|
||||
},
|
||||
runnerEnabled
|
||||
});
|
||||
|
||||
// Verify that the error is reported properly
|
||||
const errorReported = stderr.includes('Deliberate error in global setup');
|
||||
assert.ok(errorReported, 'Should report the error from global setup');
|
||||
|
||||
// Verify the teardown wasn't executed
|
||||
assert.ok(!stdout.includes('Teardown should not run if setup fails'),
|
||||
'Teardown should not run after setup fails');
|
||||
});
|
||||
|
||||
it('should run TypeScript globalSetup and globalTeardown functions', async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-executed-ts.tmp');
|
||||
const teardownFlagPath = tmpdir.resolve('teardown-executed-ts.tmp');
|
||||
|
||||
const { stdout, stderr } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'basic-setup-teardown.ts',
|
||||
env: {
|
||||
SETUP_FLAG_PATH: setupFlagPath,
|
||||
TEARDOWN_FLAG_PATH: teardownFlagPath
|
||||
},
|
||||
additionalFlags: ['--no-warnings'],
|
||||
runnerEnabled
|
||||
});
|
||||
|
||||
assert.match(stdout, /pass 2/);
|
||||
assert.match(stdout, /fail 0/);
|
||||
assert.match(stdout, /Global setup executed/);
|
||||
assert.match(stdout, /Global teardown executed/);
|
||||
assert.strictEqual(stderr.length, 0);
|
||||
|
||||
// After all tests complete, the teardown should have run
|
||||
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
|
||||
const content = fs.readFileSync(teardownFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Teardown was executed');
|
||||
|
||||
// Setup flag should have been removed by teardown
|
||||
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
|
||||
});
|
||||
|
||||
it('should run ESM globalSetup and globalTeardown functions', async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-executed-esm.tmp');
|
||||
const teardownFlagPath = tmpdir.resolve('teardown-executed-esm.tmp');
|
||||
|
||||
const { stdout, stderr } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'basic-setup-teardown.mjs',
|
||||
env: {
|
||||
SETUP_FLAG_PATH: setupFlagPath,
|
||||
TEARDOWN_FLAG_PATH: teardownFlagPath
|
||||
},
|
||||
runnerEnabled
|
||||
});
|
||||
|
||||
assert.match(stdout, /pass 2/);
|
||||
assert.match(stdout, /fail 0/);
|
||||
assert.match(stdout, /Global setup executed/);
|
||||
assert.match(stdout, /Global teardown executed/);
|
||||
assert.strictEqual(stderr.length, 0);
|
||||
|
||||
// After all tests complete, the teardown should have run
|
||||
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
|
||||
const content = fs.readFileSync(teardownFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Teardown was executed');
|
||||
|
||||
// Setup flag should have been removed by teardown
|
||||
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
|
||||
});
|
||||
|
||||
it('should run globalSetup only once for run', async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-executed-once.tmp');
|
||||
const teardownFlagPath = tmpdir.resolve('teardown-executed-once.tmp');
|
||||
|
||||
const { stdout } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'basic-setup-teardown.js',
|
||||
env: {
|
||||
SETUP_FLAG_PATH: setupFlagPath,
|
||||
TEARDOWN_FLAG_PATH: teardownFlagPath
|
||||
},
|
||||
runnerEnabled
|
||||
});
|
||||
|
||||
const GlobalSetupOccurrences = (stdout.match(/Global setup executed/g) || []).length;
|
||||
|
||||
// Global setup should run only once
|
||||
assert.strictEqual(GlobalSetupOccurrences, 1);
|
||||
});
|
||||
|
||||
it('should run globalSetup and globalTeardown only once for run', async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-executed-once.tmp');
|
||||
const teardownFlagPath = tmpdir.resolve('teardown-executed-once.tmp');
|
||||
|
||||
const { stdout } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'basic-setup-teardown.js',
|
||||
env: {
|
||||
SETUP_FLAG_PATH: setupFlagPath,
|
||||
TEARDOWN_FLAG_PATH: teardownFlagPath
|
||||
},
|
||||
runnerEnabled
|
||||
});
|
||||
|
||||
const GlobalTeardownOccurrences = (stdout.match(/Global teardown executed/g) || []).length;
|
||||
|
||||
// Global teardown should run only once
|
||||
assert.strictEqual(GlobalTeardownOccurrences, 1);
|
||||
});
|
||||
|
||||
it('should run globalSetup and globalTeardown only once for run with multiple test files',
|
||||
{
|
||||
skip: !runnerEnabled ? 'Skipping test as --test is not enabled' : false
|
||||
},
|
||||
async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-executed-once.tmp');
|
||||
const teardownFlagPath = tmpdir.resolve('teardown-executed-once.tmp');
|
||||
const testFiles = ['test-file.js', 'another-test-file.js'];
|
||||
const { stdout } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'basic-setup-teardown.js',
|
||||
env: {
|
||||
SETUP_FLAG_PATH: setupFlagPath,
|
||||
TEARDOWN_FLAG_PATH: teardownFlagPath
|
||||
},
|
||||
runnerEnabled,
|
||||
testFiles
|
||||
});
|
||||
const GlobalSetupOccurrences = (stdout.match(/Global setup executed/g) || []).length;
|
||||
const GlobalTeardownOccurrences = (stdout.match(/Global teardown executed/g) || []).length;
|
||||
|
||||
assert.strictEqual(GlobalSetupOccurrences, 1);
|
||||
assert.strictEqual(GlobalTeardownOccurrences, 1);
|
||||
|
||||
assert.match(stdout, /pass 3/);
|
||||
assert.match(stdout, /fail 0/);
|
||||
}
|
||||
);
|
||||
|
||||
describe('interop with --require and --import', () => {
|
||||
const cjsPath = join(testFixtures, 'global-setup-teardown', 'required-module.cjs');
|
||||
const esmpFile = fixtures.fileURL('test-runner', 'global-setup-teardown', 'imported-module.mjs');
|
||||
|
||||
it('should run required module before globalSetup', async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-for-required.tmp');
|
||||
const teardownFlagPath = tmpdir.resolve('teardown-for-required.tmp');
|
||||
|
||||
// Create a setup file for test-file.js to find
|
||||
fs.writeFileSync(setupFlagPath, '');
|
||||
|
||||
const { stdout } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'basic-setup-teardown.js',
|
||||
requirePath: './required-module.js',
|
||||
env: {
|
||||
SETUP_FLAG_PATH: setupFlagPath,
|
||||
TEARDOWN_FLAG_PATH: teardownFlagPath
|
||||
},
|
||||
additionalFlags: [
|
||||
`--require=${cjsPath}`,
|
||||
],
|
||||
runnerEnabled
|
||||
});
|
||||
|
||||
assert.match(stdout, /pass 2/);
|
||||
assert.match(stdout, /fail 0/);
|
||||
assert.match(stdout, /Required module executed/);
|
||||
assert.match(stdout, /Global setup executed/);
|
||||
assert.match(stdout, /Global teardown executed/);
|
||||
|
||||
// Verify that the required module was executed before the global setup
|
||||
const requiredExecutedPosition = stdout.indexOf('Required module executed');
|
||||
const globalSetupExecutedPosition = stdout.indexOf('Global setup executed');
|
||||
assert.ok(requiredExecutedPosition < globalSetupExecutedPosition,
|
||||
'Required module should have been executed before global setup');
|
||||
|
||||
// After all tests complete, the teardown should have run
|
||||
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
|
||||
const content = fs.readFileSync(teardownFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Teardown was executed');
|
||||
|
||||
// Setup flag should have been removed by teardown
|
||||
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
|
||||
});
|
||||
|
||||
// This difference in behavior is due to the way --import is being handled by
|
||||
// run_main entry point or test_runner entry point
|
||||
if (runnerEnabled) {
|
||||
it('should run imported module after globalSetup', async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-for-imported.tmp');
|
||||
const teardownFlagPath = tmpdir.resolve('teardown-for-imported.tmp');
|
||||
|
||||
// Create a setup file for test-file.js to find
|
||||
fs.writeFileSync(setupFlagPath, 'non-empty');
|
||||
|
||||
const { stdout } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'basic-setup-teardown.mjs',
|
||||
importPath: './imported-module.js',
|
||||
env: {
|
||||
SETUP_FLAG_PATH: setupFlagPath,
|
||||
TEARDOWN_FLAG_PATH: teardownFlagPath
|
||||
},
|
||||
additionalFlags: [
|
||||
`--import=${esmpFile}`,
|
||||
],
|
||||
runnerEnabled
|
||||
});
|
||||
|
||||
assert.match(stdout, /pass 2/);
|
||||
assert.match(stdout, /fail 0/);
|
||||
assert.match(stdout, /Imported module executed/);
|
||||
assert.match(stdout, /Global setup executed/);
|
||||
assert.match(stdout, /Global teardown executed/);
|
||||
|
||||
// Verify that the imported module was executed after the global setup
|
||||
const globalSetupExecutedPosition = stdout.indexOf('Global setup executed');
|
||||
const importedExecutedPosition = stdout.indexOf('Imported module executed');
|
||||
assert.ok(globalSetupExecutedPosition < importedExecutedPosition,
|
||||
'Imported module should be executed after global setup');
|
||||
|
||||
// After all tests complete, the teardown should have run
|
||||
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
|
||||
const content = fs.readFileSync(teardownFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Teardown was executed');
|
||||
|
||||
// Setup flag should have been removed by teardown
|
||||
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
|
||||
});
|
||||
} else {
|
||||
it('should run imported module before globalSetup', async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-for-imported.tmp');
|
||||
const teardownFlagPath = tmpdir.resolve('teardown-for-imported.tmp');
|
||||
|
||||
// Create a setup file for test-file.js to find
|
||||
fs.writeFileSync(setupFlagPath, 'non-empty');
|
||||
|
||||
const { stdout } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'basic-setup-teardown.mjs',
|
||||
importPath: './imported-module.js',
|
||||
env: {
|
||||
SETUP_FLAG_PATH: setupFlagPath,
|
||||
TEARDOWN_FLAG_PATH: teardownFlagPath
|
||||
},
|
||||
additionalFlags: [
|
||||
`--import=${esmpFile}`,
|
||||
],
|
||||
runnerEnabled
|
||||
});
|
||||
|
||||
assert.match(stdout, /pass 2/);
|
||||
assert.match(stdout, /fail 0/);
|
||||
assert.match(stdout, /Imported module executed/);
|
||||
assert.match(stdout, /Global setup executed/);
|
||||
assert.match(stdout, /Global teardown executed/);
|
||||
|
||||
// Verify that the imported module was executed before the global setup
|
||||
const importedExecutedPosition = stdout.indexOf('Imported module executed');
|
||||
const globalSetupExecutedPosition = stdout.indexOf('Global setup executed');
|
||||
assert.ok(importedExecutedPosition < globalSetupExecutedPosition,
|
||||
'Imported module should be executed before global setup');
|
||||
|
||||
// After all tests complete, the teardown should have run
|
||||
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
|
||||
const content = fs.readFileSync(teardownFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Teardown was executed');
|
||||
|
||||
// Setup flag should have been removed by teardown
|
||||
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
|
||||
});
|
||||
}
|
||||
|
||||
it('should execute globalSetup and globalTeardown correctly with imported module containing tests', async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-executed.tmp');
|
||||
const teardownFlagPath = tmpdir.resolve('teardown-executed.tmp');
|
||||
const importedModuleWithTestFile = fixtures.fileURL(
|
||||
'test-runner',
|
||||
'global-setup-teardown',
|
||||
'imported-module-with-test.mjs'
|
||||
);
|
||||
// Create a setup file for test-file.js to find
|
||||
fs.writeFileSync(setupFlagPath, 'non-empty');
|
||||
|
||||
const { stdout } = await runTest({
|
||||
isolation,
|
||||
globalSetupFile: 'basic-setup-teardown.js',
|
||||
env: {
|
||||
SETUP_FLAG_PATH: setupFlagPath,
|
||||
TEARDOWN_FLAG_PATH: teardownFlagPath
|
||||
},
|
||||
additionalFlags: [
|
||||
`--import=${importedModuleWithTestFile}`,
|
||||
],
|
||||
runnerEnabled
|
||||
});
|
||||
|
||||
assert.match(stdout, /Global setup executed/);
|
||||
assert.match(stdout, /Imported module Ok/);
|
||||
assert.match(stdout, /Imported module Fail/);
|
||||
assert.match(stdout, /verify setup was executed/);
|
||||
assert.match(stdout, /another simple test/);
|
||||
assert.match(stdout, /Global teardown executed/);
|
||||
assert.match(stdout, /tests 4/);
|
||||
assert.match(stdout, /suites 0/);
|
||||
assert.match(stdout, /pass 3/);
|
||||
assert.match(stdout, /fail 1/);
|
||||
assert.match(stdout, /cancelled 0/);
|
||||
assert.match(stdout, /skipped 0/);
|
||||
assert.match(stdout, /todo 0/);
|
||||
|
||||
// After all tests complete, the teardown should have run
|
||||
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
|
||||
const content = fs.readFileSync(teardownFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Teardown was executed');
|
||||
// Setup flag should have been removed by teardown
|
||||
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
125
test/parallel/test-runner-global-setup-watch-mode.mjs
Normal file
125
test/parallel/test-runner-global-setup-watch-mode.mjs
Normal file
@ -0,0 +1,125 @@
|
||||
import * as common from '../common/index.mjs';
|
||||
import { beforeEach, describe, it } from 'node:test';
|
||||
import { once } from 'node:events';
|
||||
import assert from 'node:assert';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import tmpdir from '../common/tmpdir.js';
|
||||
|
||||
if (common.isIBMi)
|
||||
common.skip('IBMi does not support `fs.watch()`');
|
||||
|
||||
if (common.isAIX)
|
||||
common.skip('folder watch capability is limited in AIX.');
|
||||
|
||||
let fixturePaths;
|
||||
|
||||
// This test updates these files repeatedly,
|
||||
// Reading them from disk is unreliable due to race conditions.
|
||||
const fixtureContent = {
|
||||
'test.js': `
|
||||
const test = require('node:test');
|
||||
test('test with global hooks', (t) => {
|
||||
t.assert.ok('test passed');
|
||||
});
|
||||
`,
|
||||
'global-setup-teardown.js': `
|
||||
async function globalSetup() {
|
||||
console.log('Global setup executed');
|
||||
process.on('message', (message) => {
|
||||
if (message === 'exit') {
|
||||
process.kill(process.pid, 'SIGINT');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function globalTeardown() {
|
||||
console.log('Global teardown executed');
|
||||
}
|
||||
|
||||
module.exports = { globalSetup, globalTeardown };
|
||||
`
|
||||
};
|
||||
|
||||
function refresh() {
|
||||
tmpdir.refresh();
|
||||
fixturePaths = Object.keys(fixtureContent)
|
||||
.reduce((acc, file) => ({ ...acc, [file]: tmpdir.resolve(file) }), {});
|
||||
Object.entries(fixtureContent)
|
||||
.forEach(([file, content]) => writeFileSync(fixturePaths[file], content));
|
||||
}
|
||||
|
||||
describe('test runner watch mode with global setup hooks', () => {
|
||||
beforeEach(refresh);
|
||||
for (const isolation of ['none', 'process']) {
|
||||
describe(`isolation: ${isolation}`, () => {
|
||||
it(`should run global setup/teardown hooks with each test run in watch mode`,
|
||||
// TODO(pmarchini): Skip test on Windows as the VS2022 build
|
||||
// has issues handling SIGTERM and SIGINT signals correctly.
|
||||
// See: https://github.com/nodejs/node/issues/46097
|
||||
{ todo: common.isWindows },
|
||||
async () => {
|
||||
const globalSetupFileFixture = fixturePaths['global-setup-teardown.js'];
|
||||
const ran1 = Promise.withResolvers();
|
||||
const ran2 = Promise.withResolvers();
|
||||
|
||||
const child = spawn(process.execPath,
|
||||
[
|
||||
'--watch',
|
||||
'--test',
|
||||
'--test-reporter=spec',
|
||||
`--test-isolation=${isolation}`,
|
||||
'--test-global-setup=' + globalSetupFileFixture,
|
||||
fixturePaths['test.js'],
|
||||
],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
||||
cwd: tmpdir.path,
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let currentRun = '';
|
||||
const runs = [];
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
currentRun += data.toString();
|
||||
const testRuns = stdout.match(/duration_ms\s\d+/g);
|
||||
if (testRuns?.length >= 1) ran1.resolve();
|
||||
if (testRuns?.length >= 2) ran2.resolve();
|
||||
});
|
||||
|
||||
await ran1.promise;
|
||||
runs.push(currentRun);
|
||||
currentRun = '';
|
||||
|
||||
const content = fixtureContent['test.js'];
|
||||
const path = fixturePaths['test.js'];
|
||||
writeFileSync(path, content);
|
||||
|
||||
await ran2.promise;
|
||||
runs.push(currentRun);
|
||||
|
||||
currentRun = '';
|
||||
child.send('exit');
|
||||
await once(child, 'exit');
|
||||
|
||||
assert.match(runs[0], /Global setup executed/);
|
||||
assert.match(runs[0], /tests 1/);
|
||||
assert.match(runs[0], /pass 1/);
|
||||
assert.match(runs[0], /fail 0/);
|
||||
|
||||
assert.doesNotMatch(runs[1], /Global setup executed/);
|
||||
assert.doesNotMatch(runs[1], /Global teardown executed/);
|
||||
assert.match(runs[1], /tests 1/);
|
||||
assert.match(runs[1], /pass 1/);
|
||||
assert.match(runs[1], /fail 0/);
|
||||
|
||||
// Verify stdout after killing the child
|
||||
assert.doesNotMatch(currentRun, /Global setup executed/);
|
||||
assert.match(currentRun, /Global teardown executed/);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
227
test/parallel/test-runner-run-global-hooks.mjs
Normal file
227
test/parallel/test-runner-run-global-hooks.mjs
Normal file
@ -0,0 +1,227 @@
|
||||
import '../common/index.mjs';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
import { describe, it, beforeEach, run } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import fs from 'node:fs';
|
||||
import tmpdir from '../common/tmpdir.js';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
|
||||
const testFixtures = fixtures.path('test-runner', 'global-setup-teardown');
|
||||
const runnerFixture = fixtures.path('test-runner', 'test-runner-global-hooks.mjs');
|
||||
|
||||
describe('require(\'node:test\').run with global hooks', { concurrency: false }, () => {
|
||||
beforeEach(() => {
|
||||
tmpdir.refresh();
|
||||
});
|
||||
|
||||
async function runTestWithGlobalHooks({
|
||||
globalSetupFile,
|
||||
testFile = 'test-file.js',
|
||||
runnerEnv = {},
|
||||
isolation = 'process'
|
||||
}) {
|
||||
const testFilePath = path.join(testFixtures, testFile);
|
||||
const globalSetupPath = path.join(testFixtures, globalSetupFile);
|
||||
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
runnerFixture,
|
||||
'--file', testFilePath,
|
||||
'--globalSetup', globalSetupPath,
|
||||
'--isolation', isolation,
|
||||
],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe',
|
||||
env: {
|
||||
...runnerEnv,
|
||||
...process.env,
|
||||
AVOID_PRINT_LOGS: 'true',
|
||||
NODE_OPTIONS: '--no-warnings',
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
await once(child, 'exit');
|
||||
|
||||
// Assert in order to print a detailed error message if the test fails
|
||||
assert.partialDeepStrictEqual(stderr, '');
|
||||
assert.match(stdout, /pass (\d+)/);
|
||||
assert.match(stdout, /fail (\d+)/);
|
||||
|
||||
const results = {
|
||||
passed: parseInt((stdout.match(/pass (\d+)/) || [])[1] || '0', 10),
|
||||
failed: parseInt((stdout.match(/fail (\d+)/) || [])[1] || '0', 10)
|
||||
};
|
||||
|
||||
return { results };
|
||||
}
|
||||
|
||||
for (const isolation of ['none', 'process']) {
|
||||
describe(`with isolation : ${isolation}`, () => {
|
||||
it('should run globalSetup and globalTeardown functions', async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-executed.tmp');
|
||||
const teardownFlagPath = tmpdir.resolve('teardown-executed.tmp');
|
||||
|
||||
const { results } = await runTestWithGlobalHooks({
|
||||
globalSetupFile: 'basic-setup-teardown.js',
|
||||
runnerEnv: {
|
||||
SETUP_FLAG_PATH: setupFlagPath,
|
||||
TEARDOWN_FLAG_PATH: teardownFlagPath
|
||||
},
|
||||
isolation
|
||||
});
|
||||
|
||||
assert.strictEqual(results.passed, 2);
|
||||
assert.strictEqual(results.failed, 0);
|
||||
// After all tests complete, the teardown should have run
|
||||
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
|
||||
const content = fs.readFileSync(teardownFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Teardown was executed');
|
||||
// Setup flag should have been removed by teardown
|
||||
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
|
||||
});
|
||||
|
||||
it('should run setup-only module', async () => {
|
||||
const setupOnlyFlagPath = tmpdir.resolve('setup-only-executed.tmp');
|
||||
|
||||
const { results } = await runTestWithGlobalHooks({
|
||||
globalSetupFile: 'setup-only.js',
|
||||
runnerEnv: {
|
||||
SETUP_ONLY_FLAG_PATH: setupOnlyFlagPath,
|
||||
SETUP_FLAG_PATH: setupOnlyFlagPath
|
||||
},
|
||||
isolation
|
||||
});
|
||||
|
||||
assert.strictEqual(results.passed, 1);
|
||||
assert.strictEqual(results.failed, 1);
|
||||
assert.ok(fs.existsSync(setupOnlyFlagPath), 'Setup-only flag file should exist');
|
||||
const content = fs.readFileSync(setupOnlyFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Setup-only was executed');
|
||||
});
|
||||
|
||||
it('should run teardown-only module', async () => {
|
||||
const teardownOnlyFlagPath = tmpdir.resolve('teardown-only-executed.tmp');
|
||||
const setupFlagPath = tmpdir.resolve('setup-for-teardown-only.tmp');
|
||||
|
||||
// Create a setup file for test-file.js to find
|
||||
fs.writeFileSync(setupFlagPath, 'Setup was executed');
|
||||
|
||||
const { results } = await runTestWithGlobalHooks({
|
||||
globalSetupFile: 'teardown-only.js',
|
||||
runnerEnv: {
|
||||
TEARDOWN_ONLY_FLAG_PATH: teardownOnlyFlagPath,
|
||||
SETUP_FLAG_PATH: setupFlagPath
|
||||
},
|
||||
isolation
|
||||
});
|
||||
|
||||
assert.strictEqual(results.passed, 2);
|
||||
assert.strictEqual(results.failed, 0);
|
||||
assert.ok(fs.existsSync(teardownOnlyFlagPath), 'Teardown-only flag file should exist');
|
||||
const content = fs.readFileSync(teardownOnlyFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Teardown-only was executed');
|
||||
});
|
||||
|
||||
// TODO(pmarchini): We should be able to share context between setup and teardown
|
||||
it.todo('should share context between setup and teardown');
|
||||
|
||||
it('should handle async setup and teardown', async () => {
|
||||
const asyncFlagPath = tmpdir.resolve('async-executed.tmp');
|
||||
const setupFlagPath = tmpdir.resolve('setup-for-async.tmp');
|
||||
|
||||
// Create a setup file for test-file.js to find
|
||||
fs.writeFileSync(setupFlagPath, 'Setup was executed');
|
||||
|
||||
const { results } = await runTestWithGlobalHooks({
|
||||
globalSetupFile: 'async-setup-teardown.js',
|
||||
runnerEnv: {
|
||||
ASYNC_FLAG_PATH: asyncFlagPath,
|
||||
SETUP_FLAG_PATH: setupFlagPath
|
||||
},
|
||||
isolation
|
||||
});
|
||||
|
||||
assert.strictEqual(results.passed, 2);
|
||||
assert.strictEqual(results.failed, 0);
|
||||
assert.ok(fs.existsSync(asyncFlagPath), 'Async flag file should exist');
|
||||
const content = fs.readFileSync(asyncFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Setup part, Teardown part');
|
||||
});
|
||||
|
||||
it('should run TypeScript globalSetup and globalTeardown functions', async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-executed-ts.tmp');
|
||||
const teardownFlagPath = tmpdir.resolve('teardown-executed-ts.tmp');
|
||||
|
||||
const { results } = await runTestWithGlobalHooks({
|
||||
globalSetupFile: 'basic-setup-teardown.ts',
|
||||
runnerEnv: {
|
||||
SETUP_FLAG_PATH: setupFlagPath,
|
||||
TEARDOWN_FLAG_PATH: teardownFlagPath
|
||||
},
|
||||
isolation
|
||||
});
|
||||
|
||||
assert.strictEqual(results.passed, 2);
|
||||
assert.strictEqual(results.failed, 0);
|
||||
// After all tests complete, the teardown should have run
|
||||
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
|
||||
const content = fs.readFileSync(teardownFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Teardown was executed');
|
||||
|
||||
// Setup flag should have been removed by teardown
|
||||
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
|
||||
});
|
||||
|
||||
it('should run ESM globalSetup and globalTeardown functions', async () => {
|
||||
const setupFlagPath = tmpdir.resolve('setup-executed-esm.tmp');
|
||||
const teardownFlagPath = tmpdir.resolve('teardown-executed-esm.tmp');
|
||||
|
||||
const { results } = await runTestWithGlobalHooks({
|
||||
globalSetupFile: 'basic-setup-teardown.mjs',
|
||||
runnerEnv: {
|
||||
SETUP_FLAG_PATH: setupFlagPath,
|
||||
TEARDOWN_FLAG_PATH: teardownFlagPath
|
||||
},
|
||||
isolation
|
||||
});
|
||||
|
||||
assert.strictEqual(results.passed, 2);
|
||||
assert.strictEqual(results.failed, 0);
|
||||
// After all tests complete, the teardown should have run
|
||||
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
|
||||
const content = fs.readFileSync(teardownFlagPath, 'utf8');
|
||||
assert.strictEqual(content, 'Teardown was executed');
|
||||
// Setup flag should have been removed by teardown
|
||||
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('should validate that globalSetupPath is a string', () => {
|
||||
[123, {}, [], true, false].forEach((invalidValue) => {
|
||||
assert.throws(() => {
|
||||
run({
|
||||
files: [path.join(testFixtures, 'test-file.js')],
|
||||
globalSetupPath: invalidValue
|
||||
});
|
||||
}, {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
message: /The "options\.globalSetupPath" property must be of type string/
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -206,7 +206,7 @@ describe('require(\'node:test\').run', { concurrency: true }, () => {
|
||||
if (data.name === 'this is a test') {
|
||||
t.assert.strictEqual(data.type, 'test');
|
||||
}
|
||||
}, 2));
|
||||
}, 3));
|
||||
stream.on('test:dequeue', common.mustCall((data) => {
|
||||
if (data.name === 'this is a suite') {
|
||||
t.assert.strictEqual(data.type, 'suite');
|
||||
@ -214,7 +214,7 @@ describe('require(\'node:test\').run', { concurrency: true }, () => {
|
||||
if (data.name === 'this is a test') {
|
||||
t.assert.strictEqual(data.type, 'test');
|
||||
}
|
||||
}, 2));
|
||||
}, 3));
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for await (const _ of stream);
|
||||
|
Loading…
Reference in New Issue
Block a user