Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test_runner: add global setup and teardown functionality #57438

Open
wants to merge 34 commits into
base: main
Choose a base branch
from

Conversation

pmarchini
Copy link
Member

I'm opening this PR as a draft to gather some feedback regarding global setup/teardown.

The idea is to leverage the new configuration file to allow users to manage this scenario.

Personally, I'm currently using a "wrapper" file that directly runs the run function, preceded and followed by setup/teardown when it's necessary to launch containers or other resources.

At the moment, I've added support for a possible shared context object between the two hooks.

However, I'm not fully convinced about this approach.

I think some crucial improvements might include adding events into the test stream to notify about the success or failure of global setup and teardown.

Note: this PR is still missing run new option documentation and test to cover the feature via run function.

@nodejs-github-bot
Copy link
Collaborator

nodejs-github-bot commented Mar 13, 2025

Review requested:

  • @nodejs/config
  • @nodejs/test_runner

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Mar 13, 2025
@pmarchini
Copy link
Member Author

Although it's still a draft, would anyone be willing to spend some time providing feedback / ideas? @nodejs/test_runner

Thaaaanks 🚀

@cjihrig
Copy link
Contributor

cjihrig commented Mar 13, 2025

I looked at this very quickly, so I may have missed something. There are some additional things that I think need to be tested:

  • Running without --test. If someone wants to run a single file that contains tests like node foo.js, that should work like the other test runner flags do.
  • This should verify that no user code can run before these new hooks. This is especially important for describe(), which builds the suites before the test runner starts executing things.
  • How does this interact with --import and --require since this is duplicating functionality that they provide.
  • How does this interact with watch mode.

@pmarchini
Copy link
Member Author

pmarchini commented Mar 13, 2025

Hey @cjihrig, thanks for the feedback!

Point by point:

  1. I think it would make sense to limit the functionality to --test only or direct run invocation.

  2. I agree, I will cover that case.

  3. Currently, --require and --import clearly act at startup, and in the case of isolation process, they cause a rerun for each individual test file.
    I wonder if, from a developer experience perspective, it would make sense to evaluate whether overlapping multiple flags could lead to unintended side effects for the user.
    At the same time, having potential overlap seems reasonable to me...what do you think?

  4. Regarding watch, I'm torn. Initially, I was thinking of not supporting it, but reconsidering, especially in scenarios involving container creation/teardown or similar cases, it seems important to provide this functionality.
    My only concern is whether it would be possible to break down the implementation incrementally (also to ease the review process).

@cjihrig
Copy link
Contributor

cjihrig commented Mar 13, 2025

I think it would make sense to limit the functionality to --test only or direct run invocation.

Regarding watch, I'm torn.

I feel pretty strongly that these need to be supported. For people working on the test runner, I understand that it can be annoying to support the various modes of operation, but that isn't a good reason not to do it. IMO, it only makes sense to omit something if there is a good reason from the end user's perspective (for example --test-force-exit not being compatible with watch mode).

Regarding interop with --require/--import, I don't really have strong feelings, but I think we should nail down what the behavior will be. If those always run before this new functionality, that's totally fine. I think we should have a test for the happy path. We probably should test the case where someone uses one of these flags and invokes the test runner from there while also having a global setup.

@pmarchini
Copy link
Member Author

Sounds good to me, I see your reasons and you obviously have more vision and experience than me so I'm more than glad to follow your suggestions! 🚀
I'll update the PR ASAP

@pmarchini pmarchini marked this pull request as ready for review March 27, 2025 12:31
@pmarchini
Copy link
Member Author

hey @cjihrig, @atlowChemi

I've added the following tests :

  • Basic setup and teardown functionality
  • Setup-only modules
  • Teardown-only modules
  • Multiple test files with shared setup/teardown
  • Context sharing between setup and teardown
  • Async setup and teardown
  • Error handling in setup
  • TypeScript support for setup/teardown functions
  • ESM module support for setup/teardown functions
  • Ensuring setup runs only once per test run
  • Ensuring teardown runs only once per test run
  • Interop with --require flag (required module before globalSetup)
  • Interop with --import flag (behavior differs between run modes)
  • Tests in imported modules with setup/teardown
  • Watch mode interaction with global setup/teardown
  • Direct invocation vs. --test flag behavior
  • Edge cases when combining multiple flags

My main concern at the moment is this change in behaviour:
https://github.com/nodejs/node/pull/57438/files#diff-196bc4ec67eaf2b311f953b5282702f42d7c55a5e6ab5599fdf83f51fdf3c23cR197-R210

  it('should include test type in enqueue, dequeue events', async (t) => {
    const stream = await run({
      files: [join(testFixtures, 'default-behavior/test/suite_and_test.cjs')],
    });
    t.plan(4);
    stream.on('test:enqueue', common.mustCall((data) => {
      if (data.name === 'this is a suite') {
        t.assert.strictEqual(data.type, 'suite');
      }
      if (data.name === 'this is a test') {
        t.assert.strictEqual(data.type, 'test');
      }
    }, 3)); // TODO(pmarchini): this should be 2 but after `await root.harness.waitForGlobalSetup()` it's 3

I'd really appreciate any feedback or suggestions regarding how I've implemented the feature 😁


testResources.set(reporterScope.asyncId(), reporterScope);

async function setupGlobalSetupTeardownFunctions(globalSetupPath, cwd) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsdoc would be nice

@pmarchini
Copy link
Member Author

Looking after the failing test!

Copy link
Contributor

@cjihrig cjihrig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a round of comments. I'll look in more detail once they are addressed and the CI is passing.

const esmLoader = require('internal/modules/esm/loader');
const { kEmptyObject } = require('internal/util');
const { validateFunction } = require('internal/validators');
const { getOptionValue } = require('internal/options');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be using this function in this file. All of the getOptionValue() calls should have already happened in parseCommandLine(), and the value should already be available here.


testResources.set(reporterScope.asyncId(), reporterScope);

async function setupGlobalSetupTeardownFunctions(globalSetupPath, cwd) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this might fit better in utils.js, and called from parseCommandLine(), similar to how some other flags like --test-name-pattern are processed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding having it as part of utils.js: I agree, it definitely makes more sense.
As for parseCommandLine: at the moment, it is not "cwd-aware", and I'm not sure whether it should have that dependency.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, it may need to be cwd aware since it already deals with loading of reporters.

Copy link

codecov bot commented Mar 27, 2025

Codecov Report

Attention: Patch coverage is 98.66667% with 1 line in your changes missing coverage. Please review.

Project coverage is 90.25%. Comparing base (67786c1) to head (19ca427).
Report is 24 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/test_runner/runner.js 92.85% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #57438      +/-   ##
==========================================
+ Coverage   90.21%   90.25%   +0.03%     
==========================================
  Files         630      630              
  Lines      185524   185737     +213     
  Branches    36387    36412      +25     
==========================================
+ Hits       167378   167639     +261     
+ Misses      11037    10995      -42     
+ Partials     7109     7103       -6     
Files with missing lines Coverage Δ
lib/internal/test_runner/harness.js 93.35% <100.00%> (+0.43%) ⬆️
lib/internal/test_runner/utils.js 60.46% <100.00%> (+1.94%) ⬆️
src/node_options.cc 85.23% <100.00%> (+0.01%) ⬆️
src/node_options.h 98.88% <ø> (ø)
lib/internal/test_runner/runner.js 89.19% <92.85%> (+0.03%) ⬆️

... and 42 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@nodejs-github-bot
Copy link
Collaborator

doc/api/test.md Outdated
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 should export either:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"either" seems like the user can only specify one or the other.

@@ -94,7 +126,7 @@ function createTestTree(rootTestOptions, globalOptions) {

function createProcessEventHandler(eventName, rootTest) {
return (err) => {
if (rootTest.harness.bootstrapPromise) {
if (rootTest.harness.bootstrapPromise || rootTest.harness.globalSetupPromise) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels wrong to need to check for both of these. bootstrapPromise should be the only thing we need to check in order to know if the test runner is still bootstrapping. bootstrapPromise should always be truthy when globalSetupPromise is truthy because the global setup hook should be considered part of the bootstrap process IMO.

Copy link
Member Author

@pmarchini pmarchini Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, my biggest concern at the moment is that we're manually setting harness.bootstrapPromise to different values based on the type of run (isolation/watch/lazy bootstrap), and for this reason, I have the impression that it makes it more complex to understand and maintain the code.

root.harness.bootstrapPromise = null;

root.harness.bootstrapPromise = null;

root.harness.bootstrapPromise = promise;

globalRoot.harness.bootstrapPromise = globalOptions.setup(globalRoot.reporter);

subtest.root.harness.bootstrapPromise = null;

Having both bootstrap and globalSetup allows, from a code perspective, the decoupling of the existing bootstrapPromise logic (shared across the different run modes), with the goal of reducing it to just two distinct flows where the global setup is handled: lazyBootstrap and run bootstrap.

This is the rationale behind the implementation.

Note: I might be missing something, like an easy way to centralise the logic in a single place

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there is anything wrong with having a separate promise. I was referring to the lifetime of the promises. If I am understanding the changes, the global setup is considered part of the bootstrapping process. So, the global setup should always be finished by the time the bootstrapping is considered finished (if it isn't, then things are likely to go wrong). I don't want to get to a state where we have a bunch of different promises that we need to check all over the place.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just pushed a refactor to take advantage of rootTest.harness.bootstrapPromise to encapsulate the global setup while reducing the feature's footprint.

WDYT?

Copy link
Contributor

@cjihrig cjihrig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@pmarchini pmarchini force-pushed the test_runner/49732-global-setup-teardown branch from 3ddcf93 to 46a2340 Compare April 9, 2025 18:47
@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants