Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1499,7 +1499,7 @@ Enable module mocking in the test runner.

This feature requires `--allow-worker` if used with the [Permission Model][].

### `--experimental-test-tag-filter=<tag>`
### `--experimental-test-tag-filter='<expr>'`

<!-- YAML
added:
Expand All @@ -1509,14 +1509,20 @@ added:

> Stability: 1.0 - Early development

Run only tests whose tag set contains `<tag>`. Tests declare tags via the
`tags` option on `test()`, `it()`, `suite()`, or `describe()`; tags
inherit from suites to nested tests by union. Filtering is
case-insensitive.
Run only tests that match the provided boolean tag-filter expression. Tests
declare tags via the `tags` option on `test()`, `it()`, `suite()`, or
`describe()`. Tags inherit from suites to nested tests by union.

The flag may be specified more than once; tests must contain **every**
filter value to run. See [Test tags][] for details on declaring and
inheriting tags.
The expression supports boolean operators (`and`/`&&`, `or`/`||`,
`not`/`!`), parentheses for grouping, and `*` wildcards inside identifiers.
Standard precedence applies: `not` binds tighter than `and`, which binds
tighter than `or`. See [Test tags][] for the full grammar and behavior.

The flag may be specified more than once; multiple expressions are combined
with AND, so a test must satisfy every expression to run.

A malformed expression causes the test runner to exit with a non-zero status
before running any tests.

### `--experimental-vfs`

Expand Down
51 changes: 51 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,18 @@ returns an empty iterator. The prepared statement [parameters are bound][] using
the values in `namedParameters` and `anonymousParameters`. See
[Binding parameters][].

### `statement.resetStats()`

<!-- YAML
added: REPLACEME
-->

Resets every counter reported by [`statement.stat()`][] back to zero, except
`memused`, which reports current memory usage and cannot be reset. This
method is a wrapper around [`sqlite3_stmt_status()`][] and is useful for
measuring a specific workload without the counts accumulated by earlier
executions of the same prepared statement.

### `statement.run([namedParameters][, ...anonymousParameters])`

<!-- YAML
Expand Down Expand Up @@ -1322,6 +1334,43 @@ added: REPLACEME
Finalizes the prepared statement. If the prepared statement is already
finalized, then this is a no-op.

### `statement.stat(counter)`

<!-- YAML
added: REPLACEME
-->

* `counter` {string} The name of the counter to read. One of:

* `'fullscanStep'` The number of times SQLite has stepped forward in a table
as part of a full table scan.
* `'sort'` The number of sort operations that have occurred.
* `'autoindex'` The number of rows inserted into transient indices that were
created automatically to help joins run faster.
* `'vmStep'` The number of virtual machine operations executed by the
prepared statement.
* `'reprepare'` The number of times the statement has been automatically
reprepared due to schema changes or changes to bound parameters.
* `'run'` The number of execution cycles started by the prepared statement.
* `'filterMiss'` The number of times the Bloom filter returned a result that
required the join step to be processed as normal.
* `'filterHit'` The number of times a join step was bypassed because a Bloom
filter returned not-found.
* `'memused'` The approximate number of bytes of heap memory used to store
the prepared statement.

* Returns: {number} The current value of the requested counter.

Returns one of the runtime counters that SQLite tracks for this prepared
statement. This method is a wrapper around [`sqlite3_stmt_status()`][] and does
not reset the counter. Asserting that a statement does not perform a full table
scan (`statement.stat('fullscanStep') === 0`) is a useful check to guard
against degenerate performance.

The `'filterMiss'` and `'filterHit'` counters require SQLite 3.38.0 or later.
Builds linked against an older SQLite with `--shared-sqlite` do not expose them,
and passing either name throws `ERR_INVALID_ARG_VALUE`.

## Class: `SQLTagStore`

<!-- YAML
Expand Down Expand Up @@ -1840,6 +1889,7 @@ callback function to indicate what type of operation is being authorized.
[`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html
[`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
[`sqlite3_stmt_status()`]: https://www.sqlite.org/c3ref/stmt_status.html
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
[`sqlite3session_attach()`]: https://www.sqlite.org/session/sqlite3session_attach.html
[`sqlite3session_changeset()`]: https://www.sqlite.org/session/sqlite3session_changeset.html
Expand All @@ -1848,6 +1898,7 @@ callback function to indicate what type of operation is being authorized.
[`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html
[`statement.setAllowBareNamedParameters()`]: #statementsetallowbarenamedparametersenabled
[`statement.setAllowUnknownNamedParameters()`]: #statementsetallowunknownnamedparametersenabled
[`statement.stat()`]: #statementstatcounter
[busy timeout]: https://sqlite.org/c3ref/busy_timeout.html
[connection]: https://www.sqlite.org/c3ref/sqlite3.html
[data types]: https://www.sqlite.org/datatype3.html
Expand Down
99 changes: 76 additions & 23 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,8 +491,8 @@ added:

Tags annotate tests and suites with arbitrary string labels. The
[`--experimental-test-tag-filter`][] CLI flag (or the `testTagFilters`
option on [`run()`][]) selects tests whose tag set contains every
provided filter value.
option on [`run()`][]) selects tests by a boolean expression over those
labels.

Tags are an alternative to encoding metadata into test names. They are
useful for cross-cutting axes such as subsystem, speed bucket, flakiness,
Expand Down Expand Up @@ -525,37 +525,89 @@ describe('database', { tags: ['db'] }, () => {
});
```

Tag values must be non-empty strings. Tags are matched case-insensitively;
the canonical form is lowercase. Duplicates within a single `tags` array
are collapsed on the lowercased form, preserving the first-seen
declaration order.
Tag values must be non-empty strings that contain no whitespace, no
operator characters (`& | ! ( ) *`), and are not the reserved words
`'and'`, `'or'`, or `'not'` in any casing. Tags are matched
case-insensitively; the canonical form is lowercase. Duplicates within a
single `tags` array are collapsed on the lowercased form, preserving the
first-seen declaration order.

Hooks (`before`, `after`, `beforeEach`, `afterEach`) do not declare their
own tags. They run as part of their owning suite, which carries the
suite's tags.

### Filtering by tag
### Filtering syntax

Each [`--experimental-test-tag-filter`][] value is a literal tag name. A
test runs only when its tag set contains that name. The flag may be
specified more than once; tests must match **every** filter to run. The
same applies to the `testTagFilters` array on [`run()`][]. Filters are
case-insensitive and AND'd with [`--test-name-pattern`][],
[`--test-skip-pattern`][], and `.only` filtering.
The filter expression supports:

Untagged tests are excluded under any non-empty filter, since the filter
requires the tag to be present.
* Identifiers鈥攁ny non-whitespace, non-operator characters. A literal
identifier matches a tag of the same value (case-insensitive).
* `*` wildcards inside an identifier match any sequence of characters.
A bare `*` matches any tagged test.
* Boolean operators with two equivalent forms:
* `and` / `&&`
* `or` / `||`
* `not` / `!`
* Parentheses for grouping.

### Reading tags from inside a test
The word forms (`and`, `or`, `not`) require whitespace separation; the
punctuation forms do not.

#### Operator precedence

The expression is evaluated with the standard precedence
`not > and > or`. Binary operators are left-associative.

| Expression | Equivalent grouping |
| -------------- | ------------------- |
| `a or b and c` | `a or (b and c)` |
| `not a and b` | `(not a) and b` |

Use parentheses to override:

| Expression | Selects |
| ------------------------------ | ------------------------------------------ |
| `(unit or smoke) and not slow` | unit-or-smoke tests that are not also slow |
| `db && !flaky` | db tests that are not flaky |
| `*` | every tagged test |

#### Untagged tests

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

| Filter expression | Untagged test | Why |
| ------------------------ | ------------- | ------------------------------------------------ |
| `db` | excluded | Positive match against an empty tag set is false |
| `*` | excluded | The bare wildcard requires at least one tag |
| `db or unit` | excluded | Both branches are false against an empty tag set |
| `not flaky` | included | Negation against an empty tag set is true |
| `not flaky and not slow` | included | Both negations are true against an empty tag set |
| `db or not flaky` | included | The negated branch is true |

For example, `--experimental-test-tag-filter='not flaky'` runs every test
that is not tagged `flaky`, including all untagged tests.

#### Composing multiple filters

[`--experimental-test-tag-filter`][] may be specified more than once on the
command line. Multiple expressions compose by AND鈥攁 test must satisfy
every expression to run. The same applies to passing an array to
`testTagFilters` on [`run()`][]. The tag filter is also AND'd with
[`--test-name-pattern`][], [`--test-skip-pattern`][], and `.only`
filtering.

#### Reading tags from inside a test

The [`TestContext`][] object exposes the test's tags as a frozen array
through [`context.tags`][], so tests can branch on their own metadata.

### Errors
#### Errors

A tag value that violates the validation rules above throws
`ERR_INVALID_ARG_VALUE` at the registration site, before any test runs.
A non-array `tags` value throws `ERR_INVALID_ARG_TYPE`.
A non-array `tags` value throws `ERR_INVALID_ARG_TYPE`. A malformed
filter expression on the CLI causes the test runner to exit with a
non-zero status before running any test files.

## Extraneous asynchronous activity

Expand Down Expand Up @@ -830,7 +882,7 @@ test runner functionality:

* `--test` - Prevented to avoid recursive test execution
* `--experimental-test-coverage` - Managed by the test runner
* `--experimental-test-tag-filter` - Filter values are validated by the parent
* `--experimental-test-tag-filter` - Filter expressions are validated by the parent
process and re-emitted to child processes
* `--watch` - Watch mode is handled at the parent level
* `--experimental-default-config-file` - Config file loading is handled by the parent
Expand Down Expand Up @@ -1746,10 +1798,11 @@ changes:
For each test that is executed, any corresponding test hooks, such as
`beforeEach()`, are also run.
**Default:** `undefined`.
* `testTagFilters` {string|string\[]} A tag name, or an array of tag names,
used to filter tests by their declared tags. Tests must contain every
listed tag to run. Equivalent to passing [`--experimental-test-tag-filter`][]
on the command line. See [Test tags][]. **Default:** `undefined`.
* `testTagFilters` {string|string\[]} A boolean expression, or an array of
boolean expressions, used to filter tests by their declared tags.
Multiple expressions compose by AND. Equivalent to passing
[`--experimental-test-tag-filter`][] on the command line. See
[Test tags][]. **Default:** `undefined`.
* `timeout` {number} A number of milliseconds the test execution will
fail after.
If unspecified, subtests inherit this value from their parent.
Expand Down
20 changes: 12 additions & 8 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -814,14 +814,18 @@ collecting code coverage from tests for more details.
Enable module mocking in the test runner.
This feature requires \fB--allow-worker\fR if used with the Permission Model.
.
.It Fl -experimental-test-tag-filter Ns = Ns Ar <tag>
Run only tests whose tag set contains \fB<tag>\fR. Tests declare tags via the
\fBtags\fR option on \fBtest()\fR, \fBit()\fR, \fBsuite()\fR, or \fBdescribe()\fR; tags
inherit from suites to nested tests by union. Filtering is
case-insensitive.
The flag may be specified more than once; tests must contain \fBevery\fR
filter value to run. See Test tags for details on declaring and
inheriting tags.
.It Fl -experimental-test-tag-filter Ns = Ns Ar '<expr>'
Run only tests that match the provided boolean tag-filter expression. Tests
declare tags via the \fBtags\fR option on \fBtest()\fR, \fBit()\fR, \fBsuite()\fR, or
\fBdescribe()\fR. Tags inherit from suites to nested tests by union.
The expression supports boolean operators (\fBand\fR/\fB&&\fR, \fBor\fR/\fB||\fR,
\fBnot\fR/\fB!\fR), parentheses for grouping, and \fB*\fR wildcards inside identifiers.
Standard precedence applies: \fBnot\fR binds tighter than \fBand\fR, which binds
tighter than \fBor\fR. See Test tags for the full grammar and behavior.
The flag may be specified more than once; multiple expressions are combined
with AND, so a test must satisfy every expression to run.
A malformed expression causes the test runner to exit with a non-zero status
before running any tests.
.
.It Fl -experimental-vfs
Enable the experimental \fBnode:vfs\fR module.
Expand Down
25 changes: 18 additions & 7 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ const {

const {
FSReqCallback,
statValues,
} = binding;
const { toPathIfFileURL } = require('internal/url');
const {
Expand Down Expand Up @@ -3254,6 +3253,11 @@ function realpathSync(p, options) {
const seenLinks = new SafeMap();
const knownHard = new SafeSet();
const original = p;
// Whether the symlink this walk resolved last pointed at a pipe or a
// socket, which is where the walk stops. It cannot be read back from the
// shared stat buffer, which holds the last stat made anywhere in the
// process rather than the last one made here.
let reachedPipeOrSocket = false;

// Current character position in p
let pos;
Expand Down Expand Up @@ -3297,8 +3301,7 @@ function realpathSync(p, options) {

// Continue if not a symlink, break if a pipe/socket
if (knownHard.has(base) || cache?.get(base) === base) {
if (isFileType(statValues, S_IFIFO) ||
isFileType(statValues, S_IFSOCK)) {
if (reachedPipeOrSocket) {
break;
}
continue;
Expand Down Expand Up @@ -3336,7 +3339,9 @@ function realpathSync(p, options) {
}
}
if (linkTarget === null) {
binding.stat(base, false, undefined, true);
const targetStats = binding.stat(base, false, undefined, true);
reachedPipeOrSocket = isFileType(targetStats, S_IFIFO) ||
isFileType(targetStats, S_IFSOCK);
linkTarget = binding.readlink(base, undefined);
}
resolvedLink = pathModule.resolve(previous, linkTarget);
Expand Down Expand Up @@ -3418,6 +3423,11 @@ function realpath(p, options, callback) {

const seenLinks = new SafeMap();
const knownHard = new SafeSet();
// Whether the symlink this walk resolved last pointed at a pipe or a
// socket, which is where the walk stops. It cannot be read back from the
// shared stat buffer, which holds the last stat made anywhere in the
// process rather than the last one made here.
let reachedPipeOrSocket = false;

// Current character position in p
let pos;
Expand Down Expand Up @@ -3466,8 +3476,7 @@ function realpath(p, options, callback) {

// Continue if not a symlink, break if a pipe/socket
if (knownHard.has(base)) {
if (isFileType(statValues, S_IFIFO) ||
isFileType(statValues, S_IFSOCK)) {
if (reachedPipeOrSocket) {
return callback(null, encodeRealpathResult(p, options));
}
return process.nextTick(LOOP);
Expand Down Expand Up @@ -3497,9 +3506,11 @@ function realpath(p, options, callback) {
return gotTarget(null, seenLinks.get(id));
}
}
fs.stat(base, (err) => {
fs.stat(base, (err, targetStats) => {
if (err) return callback(err);

reachedPipeOrSocket = targetStats.isFIFO() || targetStats.isSocket();

fs.readlink(base, (err, target) => {
if (!isWindows) seenLinks.set(id, target);
gotTarget(err, target);
Expand Down
Loading
Loading