Skip to content

test_runner: add experimental tag-based test filtering#63054

Open
atlowChemi wants to merge 1 commit intonodejs:mainfrom
atlowChemi:test_runner-tag-filter
Open

test_runner: add experimental tag-based test filtering#63054
atlowChemi wants to merge 1 commit intonodejs:mainfrom
atlowChemi:test_runner-tag-filter

Conversation

@atlowChemi
Copy link
Copy Markdown
Member

Summary

Adds an experimental tag-filter feature to node:test, mirroring the
boolean-expression filter shape that other test runners have established.

  • Authoring: a tags: string[] option on test() / it() / suite() /
    describe(). Tags inherit from suites to nested tests by union.
  • Filtering: --experimental-test-tag-filter='<expr>' CLI flag and
    testTagFilters: string | string[] option on run(). Repeatable;
    multiple expressions AND together. Composes by AND with
    --test-name-pattern, --test-skip-pattern, and .only.
  • Expression grammar: and/&&, or/||, not/!, parentheses,
    * wildcards. Standard precedence (not > and > or).
  • Reporter: tags: string[] field added to the six testId-keyed
    events. Built-in reporters ignore the field for v1; custom reporters
    can read it.
  • Context: t.tags accessor on TestContext exposes the test's
    flattened tag set as a frozen array.

Prior art

Tag filtering is well-trodden ground in the JS testing ecosystem. The
expression syntax here is closest to Vitest's, which itself follows the
boolean/wildcard pattern other tools converged on:

Vitest additionally lets a tag carry per-test config overrides such as
timeout or retry (with priority resolution between overlapping
tags). That's intentionally out of scope for this PR β€” landing it
would commit to a separate tags: [{ name, timeout, retry, priority }]
config surface that's orthogonal to filtering. It can be layered on
later without breaking the union-inheritance semantics this PR
establishes.

@nodejs-github-bot
Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/config
  • @nodejs/test_runner

@nodejs-github-bot nodejs-github-bot added 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 Apr 30, 2026
@atlowChemi atlowChemi added the test_runner Issues and PRs related to the test runner subsystem. label Apr 30, 2026
Comment thread doc/api/cli.md
declare tags via the `tags` option on `test()`, `it()`, `suite()`, or
`describe()`. Tags inherit from suites to nested tests by union.

The expression supports boolean operators (`and`/`&&`, `or`/`||`,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

isnt there a better way then this tag filtering syntax?.
i.e accept in the run method a string[] or function(string) => boolean and in case you want some complex logic - use the run api without the node cli - That makes much more sense to me

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

that is more consistent to our name filter flag

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Without the boolean syntax this feature mostly collapses into name-pattern...

The goal (I had in mind) of this feature is to introduce a easy (and common, see prior art) way to filter without the need for a programmatic API. Sure, you could already achieve complex filtering via run() API, but the goal here is to add a built-in logic for this.

Vitest, mocha-tags, and jest-runner-groups all expose such a boolean composition syntax.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Before I even saw Moshe's comment, I was thinking the same thing. But after a minute, I can see the use of it.

I know && and || align with javascript, but it seems a little long, and the simpler & and | look pretty straightforward to me (and aligns with others, like query params and old skool forum query syntax). What happens when you combine them though?

--experimental-test-tag-filter=foo&bar&qux|zed

Is that "foo and bar and (qux or zed)" or "(foo and bar and qux) or zed"? For some reason, my eyes see the former, but syntactically, it's usually the latter. IMO the original spec for syntax made a huge mistake not requiring parentheses when combining operators. If we support combining operators, I think parentheses should be required.

--experimental-test-tag-filter=(foo&bar&qux)|zed
--experimental-test-tag-filter=foo&bar&(qux|zed)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@JakobJingleheimer I feel that & vs && is not really "a little long", and I feel using & and | could be confusing with bitwise operators, while removing just a single character...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I considered that before posting: I think nobody would be confused here because bitwise is not appropriate.

&& vs & is a thought musing.

My bigger concern would be parens for combined operators.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

thats why I think we should release a initial version that does not include this new langauge

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 90.06%. Comparing base (1fd4949) to head (853f715).

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #63054      +/-   ##
==========================================
+ Coverage   90.03%   90.06%   +0.02%     
==========================================
  Files         713      714       +1     
  Lines      224699   225345     +646     
  Branches    42473    42623     +150     
==========================================
+ Hits       202310   202959     +649     
+ Misses      14179    14172       -7     
- Partials     8210     8214       +4     
Files with missing lines Coverage Ξ”
lib/internal/test_runner/harness.js 86.42% <100.00%> (+0.27%) ⬆️
lib/internal/test_runner/runner.js 93.93% <100.00%> (+0.26%) ⬆️
lib/internal/test_runner/tag_filter.js 100.00% <100.00%> (ΓΈ)
lib/internal/test_runner/test.js 96.99% <100.00%> (+0.08%) ⬆️
lib/internal/test_runner/tests_stream.js 88.65% <100.00%> (+0.42%) ⬆️
lib/internal/test_runner/utils.js 65.19% <100.00%> (+1.60%) ⬆️
src/node_options.cc 76.63% <100.00%> (+0.02%) ⬆️
src/node_options.h 97.98% <ΓΈ> (ΓΈ)

... and 23 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.

@JakobJingleheimer
Copy link
Copy Markdown
Member

Sweet! I'll try to carve out time on Saturday to reciew

Comment thread test/parallel/test-runner-tag-filter-cli.mjs Outdated
@atlowChemi atlowChemi force-pushed the test_runner-tag-filter branch 3 times, most recently from 3591324 to 31a7737 Compare May 5, 2026 16:47
Copy link
Copy Markdown
Member

@JakobJingleheimer JakobJingleheimer left a comment

Choose a reason for hiding this comment

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

Sorry for the delay. GitHub is having a bit of a stroke. Every time I try to add a comment to the review "Something went wrong" and I have to copy it, reload, paste, and try a couple more times.

I'll try to finish tomorrow (I got through 10 of 18 files, which doesn't include tag_filter.js).

Comment thread doc/api/cli.md
declare tags via the `tags` option on `test()`, `it()`, `suite()`, or
`describe()`. Tags inherit from suites to nested tests by union.

The expression supports boolean operators (`and`/`&&`, `or`/`||`,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Before I even saw Moshe's comment, I was thinking the same thing. But after a minute, I can see the use of it.

I know && and || align with javascript, but it seems a little long, and the simpler & and | look pretty straightforward to me (and aligns with others, like query params and old skool forum query syntax). What happens when you combine them though?

--experimental-test-tag-filter=foo&bar&qux|zed

Is that "foo and bar and (qux or zed)" or "(foo and bar and qux) or zed"? For some reason, my eyes see the former, but syntactically, it's usually the latter. IMO the original spec for syntax made a huge mistake not requiring parentheses when combining operators. If we support combining operators, I think parentheses should be required.

--experimental-test-tag-filter=(foo&bar&qux)|zed
--experimental-test-tag-filter=foo&bar&(qux|zed)

Comment thread doc/api/test.md Outdated
### Authoring tagged tests

Pass a `tags` array on any of `test()`, `it()`, `suite()`, or `describe()`.
Tags inherit from a suite to its child tests by union β€” a test inside a
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
Tags inherit from a suite to its child tests by union β€” a test inside a
Tags inherit from a suite to its child tests by unionβ€”a test inside a

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Why? (genuinely asking, English is not my first language)

Copy link
Copy Markdown
Member

@JakobJingleheimer JakobJingleheimer May 6, 2026

Choose a reason for hiding this comment

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

The second part is an interjection, like you're interrupting yourself to clarify something. That's separated by an emdash. Basically what you did is correct except you used the wrong character (and an emdash is not padded by whitespaceβ€”it spears the two phrases together like a kebab).

I used it there as another example of how it works πŸ™‚

Basically:

  • Hyphen combines words (after-thought)
  • Endash combines numbers (2–3)
  • Emdash combines phrases (but not alwaysβ€”sometimes a colon or semicolon is more appropriate)

Comment thread doc/api/test.md Outdated

Untagged tests behave as if they have an empty tag set. As a result:

* Any include expression (a tag, wildcard, `and`, or `or`) is **false**
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think

Suggested change
* Any include expression (a tag, wildcard, `and`, or `or`) is **false**
* Any included expression (a tag, wildcard, `and`, or `or`) is **false**

Otherwise, I don't understand what it intends.

Comment thread doc/api/test.md Outdated
Comment on lines +576 to +580
* Any include expression (a tag, wildcard, `and`, or `or`) is **false**
for an untagged test, so untagged tests are excluded under any positive
filter.
* `not X` is **true** for an untagged test, so excluding tags does not
accidentally remove untagged tests.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is generally difficult to follow. I think a table of examples would be more straightforward.

Comment thread lib/internal/test_runner/test.js Outdated
Comment on lines +601 to +604
if (!mergedSet.has(ownTags[i])) {
ArrayPrototypePush(merged, ownTags[i]);
mergedSet.add(ownTags[i]);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: This is a bit unexpectedβ€”Set is already unique (SafeSet surely already handles this "doesn't have"). I would expect something like two (Safe)Sets, one full and one subset. If context.tags is accessed, then pay the Array.from cost JIT.

Comment on lines -37 to +38
fail(nesting, loc, testNumber, name, details, directive, testId) {
fail(nesting, loc, testNumber, name, details, directive, testId, tags) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

General comment: this is getting to be a bit of a junk-drawer of arguments 😬

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I agree, perhaps we could open a good first issue Issues that are suitable for first-time contributors. issue to handle this

@atlowChemi
Copy link
Copy Markdown
Member Author

GitHub is having a bit of a stroke.

@JakobJingleheimer github having issues? no way πŸ˜‚

Add a `tags` option on `test()` / `it()` / `suite()` / `describe()` and
the `--experimental-test-tag-filter` CLI flag (plus the `testTagFilters`
option on `run()`) that accepts a Vitest-style boolean expression with
`and`/`&&`, `or`/`||`, `not`/`!`, parentheses, and `*` wildcards.

Tags inherit from suite to child tests by union. Filtering composes by
AND with name patterns, skip patterns, and `.only`. Untagged tests are
filtered out under any include expression; `not X` is true against an
untagged test. Tag matching is case-insensitive with lowercase canonical
storage.

Signed-off-by: atlowChemi <chemi@atlow.co.il>
@atlowChemi atlowChemi force-pushed the test_runner-tag-filter branch from 31a7737 to 853f715 Compare May 7, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. test_runner Issues and PRs related to the test runner subsystem.

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants