test-inventory 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/dist/bin.js +1 -0
- package/dist/cli.d.ts +4 -2
- package/dist/cli.js +23 -2
- package/dist/config-dependencies.d.ts +62 -0
- package/dist/config-dependencies.js +231 -0
- package/dist/config-runs.d.ts +47 -5
- package/dist/config-runs.js +205 -42
- package/dist/config.js +65 -3
- package/dist/dependency-hash.d.ts +250 -0
- package/dist/dependency-hash.js +1533 -0
- package/dist/diagnostics.js +1 -0
- package/dist/extract.d.ts +12 -0
- package/dist/extract.js +19 -6
- package/dist/filter.d.ts +6 -0
- package/dist/filter.js +11 -1
- package/dist/frameworks/common.d.ts +5 -0
- package/dist/frameworks/common.js +11 -0
- package/dist/frameworks/playwright.js +10 -2
- package/dist/frameworks/vitest.d.ts +1 -1
- package/dist/frameworks/vitest.js +36 -6
- package/dist/hash.d.ts +1 -1
- package/dist/model.d.ts +14 -2
- package/dist/parse.d.ts +6 -0
- package/dist/parse.js +204 -0
- package/dist/records.d.ts +6 -1
- package/dist/records.js +10 -2
- package/dist/resolve.js +28 -8
- package/dist/scan-file.d.ts +8 -0
- package/dist/scan-file.js +11 -2
- package/dist/scan.js +73 -2
- package/dist/types.d.ts +11 -1
- package/package.json +1 -1
- package/schema/v1.json +21 -1
package/dist/diagnostics.js
CHANGED
|
@@ -26,6 +26,7 @@ export const LEVELS = {
|
|
|
26
26
|
'config-value-unknown': 'warning',
|
|
27
27
|
'file-not-run': 'warning',
|
|
28
28
|
'forbidden-only': 'error',
|
|
29
|
+
'dependency-not-followed': 'warning',
|
|
29
30
|
'symlink-ignored': 'warning',
|
|
30
31
|
'invalid-import': 'error',
|
|
31
32
|
'globals-not-enabled': 'warning',
|
package/dist/extract.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface Scope {
|
|
|
18
18
|
hookRun: RunSettings;
|
|
19
19
|
/** Fixtures of the hooks in the scope that run around each test. */
|
|
20
20
|
hookFixtures: string[];
|
|
21
|
+
/** Fixtures of every hook in the scope, `beforeAll` and `afterAll` too, which set up what its tests run. */
|
|
22
|
+
setupFixtures: string[];
|
|
23
|
+
/** A hook in the scope takes its fixtures whole, so it may use any. */
|
|
24
|
+
takesAllFixtures: boolean;
|
|
21
25
|
suite: PendingSuite | null;
|
|
22
26
|
}
|
|
23
27
|
/** Where a test or describe registered through an imported helper is written, in the helper's module. */
|
|
@@ -33,6 +37,10 @@ export interface PendingSuite {
|
|
|
33
37
|
/** For a describe declared in an imported helper, the call in the scanned file that registers it; null otherwise. */
|
|
34
38
|
site: Span | null;
|
|
35
39
|
declaredAt: DeclaredAt | null;
|
|
40
|
+
/** The file the describe is written in. */
|
|
41
|
+
frame: Frame;
|
|
42
|
+
/** The outermost helper call in the scanned file that declares the describe; null when the file declares it. */
|
|
43
|
+
registeredBy: CallExpression | null;
|
|
36
44
|
/** Where the scanned file registers the describe: its own call, or the outermost helper call that declares it. */
|
|
37
45
|
order: number;
|
|
38
46
|
/** Statements of the tests and describes directly inside, left out of the describe's body hash. */
|
|
@@ -48,6 +56,10 @@ export interface PendingTest {
|
|
|
48
56
|
/** For a test declared in an imported helper, the call in the scanned file that registers it; null otherwise. */
|
|
49
57
|
site: Span | null;
|
|
50
58
|
declaredAt: DeclaredAt | null;
|
|
59
|
+
/** The file the test is written in. */
|
|
60
|
+
frame: Frame;
|
|
61
|
+
/** The outermost helper call in the scanned file that declares the test; null when the file declares it. */
|
|
62
|
+
registeredBy: CallExpression | null;
|
|
51
63
|
/** Where the scanned file registers the test: its own call, or the outermost helper call that declares it. */
|
|
52
64
|
order: number;
|
|
53
65
|
/** States from the declaration, then from calls in the body such as `test.skip(condition)`. */
|
package/dist/extract.js
CHANGED
|
@@ -140,6 +140,8 @@ class Walker {
|
|
|
140
140
|
hooks: noHooks(),
|
|
141
141
|
hookRun: {},
|
|
142
142
|
hookFixtures: [],
|
|
143
|
+
setupFixtures: [],
|
|
144
|
+
takesAllFixtures: false,
|
|
143
145
|
suite: null,
|
|
144
146
|
};
|
|
145
147
|
}
|
|
@@ -158,7 +160,7 @@ class Walker {
|
|
|
158
160
|
helper: null,
|
|
159
161
|
declaredIn: null,
|
|
160
162
|
quiet: false,
|
|
161
|
-
|
|
163
|
+
registeredBy: null,
|
|
162
164
|
});
|
|
163
165
|
// Functions defined inside these are deferred in turn; the loop reaches them too, as they are added to the end.
|
|
164
166
|
for (const entry of this.deferred)
|
|
@@ -418,6 +420,8 @@ class Walker {
|
|
|
418
420
|
context.scope.hooks[api.name] += 1;
|
|
419
421
|
if (EACH_HOOKS.has(api.name))
|
|
420
422
|
context.scope.hookFixtures.push(...api.fixtures);
|
|
423
|
+
context.scope.setupFixtures.push(...api.fixtures);
|
|
424
|
+
context.scope.takesAllFixtures ||= api.takesAllFixtures;
|
|
421
425
|
}
|
|
422
426
|
if (api.fn) {
|
|
423
427
|
if (!call.arguments.includes(api.fn))
|
|
@@ -628,7 +632,7 @@ class Walker {
|
|
|
628
632
|
...context,
|
|
629
633
|
isInFunction: true,
|
|
630
634
|
declaredIn: null,
|
|
631
|
-
|
|
635
|
+
registeredBy: context.registeredBy ?? call,
|
|
632
636
|
});
|
|
633
637
|
});
|
|
634
638
|
this.callStack.pop();
|
|
@@ -749,13 +753,16 @@ class Walker {
|
|
|
749
753
|
mergeRun(call.target.hookRun, settings);
|
|
750
754
|
}
|
|
751
755
|
}
|
|
752
|
-
/**
|
|
756
|
+
/**
|
|
757
|
+
* Counts `expect()`, `expect.soft()`, `expect.poll()` and `expect.element()`, and reads `expect.assertions(n)` and
|
|
758
|
+
* `expect.hasAssertions()`.
|
|
759
|
+
*/
|
|
753
760
|
expectCall(call, test) {
|
|
754
761
|
const callee = unwrap(call.callee);
|
|
755
762
|
const [root, member] = callee.type === 'MemberExpression' ? [unwrap(callee.object), propertyName(callee)] : [callee, 'call'];
|
|
756
763
|
if (root.type !== 'Identifier' || root.name !== 'expect')
|
|
757
764
|
return;
|
|
758
|
-
if (member === 'call' || member === 'soft' || member === 'poll')
|
|
765
|
+
if (member === 'call' || member === 'soft' || member === 'poll' || member === 'element')
|
|
759
766
|
test.expectCount += 1;
|
|
760
767
|
if (member === 'hasAssertions')
|
|
761
768
|
test.expectsAssertions = true;
|
|
@@ -926,7 +933,9 @@ class Walker {
|
|
|
926
933
|
call,
|
|
927
934
|
site: this.site,
|
|
928
935
|
declaredAt: this.declaredAt(call),
|
|
929
|
-
|
|
936
|
+
frame: this.frame,
|
|
937
|
+
registeredBy: context.registeredBy,
|
|
938
|
+
order: (context.registeredBy ?? call).start,
|
|
930
939
|
states: [...declaration.states],
|
|
931
940
|
annotations: [...declaration.details.annotations],
|
|
932
941
|
isInLoop: context.isInLoop,
|
|
@@ -976,6 +985,8 @@ class Walker {
|
|
|
976
985
|
hooks: noHooks(),
|
|
977
986
|
hookRun: {},
|
|
978
987
|
hookFixtures: [],
|
|
988
|
+
setupFixtures: [],
|
|
989
|
+
takesAllFixtures: false,
|
|
979
990
|
suite: null,
|
|
980
991
|
};
|
|
981
992
|
const suite = {
|
|
@@ -984,7 +995,9 @@ class Walker {
|
|
|
984
995
|
call,
|
|
985
996
|
site: this.site,
|
|
986
997
|
declaredAt: this.declaredAt(call),
|
|
987
|
-
|
|
998
|
+
frame: this.frame,
|
|
999
|
+
registeredBy: context.registeredBy,
|
|
1000
|
+
order: (context.registeredBy ?? call).start,
|
|
988
1001
|
cuts: [],
|
|
989
1002
|
isInCondition: context.isInCondition,
|
|
990
1003
|
isInFunction: context.isInFunction,
|
package/dist/filter.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export interface InventoryFilter {
|
|
|
11
11
|
only?: boolean;
|
|
12
12
|
/** Keeps tests that don't run and give no reason. Tests in a file the runner refuses are left out. */
|
|
13
13
|
withoutReason?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Keeps the tests a change to one of these files can affect: those whose `dependencies` include it, and those that
|
|
16
|
+
* reach code the scan can't follow (`hasUnfollowedDependencies`). Paths are from the project root, as
|
|
17
|
+
* `dependencies` writes them. An empty list keeps only the tests the scan can't follow.
|
|
18
|
+
*/
|
|
19
|
+
related?: string[];
|
|
14
20
|
}
|
|
15
21
|
/** An inventory whose test records have only `id` and the keys picked with `pickTestFields()`. */
|
|
16
22
|
export type PickedInventory<K extends keyof TestRecord> = Omit<Inventory, 'tests'> & {
|
package/dist/filter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { checkObject, OptionsError } from "./options.js";
|
|
2
3
|
import { summarize } from "./summary.js";
|
|
3
4
|
const STATES = ['active', 'skip', 'fixme', 'todo', 'expectedToFail', 'notLoaded'];
|
|
@@ -7,11 +8,15 @@ const FILTER_KEYS = [
|
|
|
7
8
|
'grep',
|
|
8
9
|
'only',
|
|
9
10
|
'withoutReason',
|
|
11
|
+
'related',
|
|
10
12
|
];
|
|
11
13
|
// Typed as a record of every key, so a key added to TestRecord fails to compile until it is listed here.
|
|
12
14
|
const TEST_KEYS = Object.keys({
|
|
13
15
|
id: true,
|
|
14
16
|
bodyHash: true,
|
|
17
|
+
dependencyHash: true,
|
|
18
|
+
dependencies: true,
|
|
19
|
+
hasUnfollowedDependencies: true,
|
|
15
20
|
suiteId: true,
|
|
16
21
|
suitePath: true,
|
|
17
22
|
relativeFilePath: true,
|
|
@@ -117,11 +122,16 @@ export function matcher(filter) {
|
|
|
117
122
|
const grep = pattern(checked.grep);
|
|
118
123
|
const only = flag(checked.only, 'filter.only');
|
|
119
124
|
const withoutReason = flag(checked.withoutReason, 'filter.withoutReason');
|
|
125
|
+
// Written as a shell or Windows may write them, as `./pages/order-ticket.ts` or `pages\\order-ticket.ts`.
|
|
126
|
+
const related = checked.related === undefined
|
|
127
|
+
? null
|
|
128
|
+
: new Set(list(checked.related, 'filter.related', null).map((file) => path.posix.normalize(file.replaceAll('\\', '/'))));
|
|
120
129
|
return (test) => (states.length === 0 || states.includes(test.state)) &&
|
|
121
130
|
(tags.length === 0 || test.tags.some((tag) => tags.includes(tag))) &&
|
|
122
131
|
(grep === null || grep.test(test.fullName)) &&
|
|
123
132
|
(!only || test.isOnly) &&
|
|
124
|
-
(!withoutReason || (!test.isActive && !test.isNotLoaded && test.reason === null))
|
|
133
|
+
(!withoutReason || (!test.isActive && !test.isNotLoaded && test.reason === null)) &&
|
|
134
|
+
(related === null || test.hasUnfollowedDependencies || test.dependencies.some((file) => related.has(file)));
|
|
125
135
|
}
|
|
126
136
|
/**
|
|
127
137
|
* Keeps the tests that pass every condition of the filter; a list keeps a test that has any of its values. The
|
|
@@ -116,6 +116,11 @@ export declare function readOptions(file: FileContext, scopes: readonly Node[],
|
|
|
116
116
|
export declare function unresolvedOptions(file: FileContext, options: Options, result: string): Problem[];
|
|
117
117
|
/** The names a function takes from the object pattern of one of its parameters, in source order. */
|
|
118
118
|
export declare function destructuredNames(fn: FunctionNode | null, index: number | null): string[];
|
|
119
|
+
/**
|
|
120
|
+
* Whether a function takes one of its parameters whole, as in `(fixtures) => ...`, or keeps the rest of its object
|
|
121
|
+
* pattern, so that any key of it may be used.
|
|
122
|
+
*/
|
|
123
|
+
export declare function takesWhole(fn: FunctionNode | null, index: number | null): boolean;
|
|
119
124
|
/** A whole number, such as a timeout in milliseconds; null, with a warning, when it can't be worked out. */
|
|
120
125
|
export declare function readCount(file: FileContext, name: string, option: OptionValue, problems: Problem[]): number | null;
|
|
121
126
|
/** A warning for one option whose value could not be worked out. */
|
|
@@ -287,6 +287,17 @@ export function destructuredNames(fn, index) {
|
|
|
287
287
|
const names = parameter.properties.map((property) => (property.type === 'Property' ? propertyKey(property) : null));
|
|
288
288
|
return names.filter((name) => name !== null);
|
|
289
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Whether a function takes one of its parameters whole, as in `(fixtures) => ...`, or keeps the rest of its object
|
|
292
|
+
* pattern, so that any key of it may be used.
|
|
293
|
+
*/
|
|
294
|
+
export function takesWhole(fn, index) {
|
|
295
|
+
const parameter = index === null ? undefined : fn?.params[index];
|
|
296
|
+
if (!parameter)
|
|
297
|
+
return false;
|
|
298
|
+
return (parameter.type !== 'ObjectPattern' ||
|
|
299
|
+
parameter.properties.some((property) => property.type === 'RestElement' || property.computed));
|
|
300
|
+
}
|
|
290
301
|
/** A whole number, such as a timeout in milliseconds; null, with a warning, when it can't be worked out. */
|
|
291
302
|
export function readCount(file, name, option, problems) {
|
|
292
303
|
const { node, resolved } = option;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { childNodes, isFunctionNode, propertyKey, propertyName, unwrap } from "../ast.js";
|
|
2
2
|
import { snippet } from "../source.js";
|
|
3
3
|
import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
|
|
4
|
-
import { atLeast, findFunction, hookFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, destructuredNames, readCondition, readCount, readOptions, readReason, readStrings, readTitle, refused, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
|
|
4
|
+
import { atLeast, findFunction, hookFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, destructuredNames, takesWhole, readCondition, readCount, readOptions, readReason, readStrings, readTitle, refused, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
|
|
5
5
|
const MISMATCHED_SOURCES = new Set(['vitest', '@jest/globals']);
|
|
6
6
|
// Maps rather than object literals, so names such as `constructor` don't match inherited properties.
|
|
7
7
|
const DESCRIBES = new Map([
|
|
@@ -259,7 +259,14 @@ export class PlaywrightAdapter {
|
|
|
259
259
|
return this.configure(call, context);
|
|
260
260
|
if (HOOKS.has(key)) {
|
|
261
261
|
const fn = hookFunction(this.file, context.scopes, call.arguments);
|
|
262
|
-
return {
|
|
262
|
+
return {
|
|
263
|
+
kind: 'hook',
|
|
264
|
+
name: key,
|
|
265
|
+
fn,
|
|
266
|
+
testContext: null,
|
|
267
|
+
fixtures: destructuredNames(fn, 0),
|
|
268
|
+
takesAllFixtures: takesWhole(fn, 0),
|
|
269
|
+
};
|
|
263
270
|
}
|
|
264
271
|
if (RUN_MEMBERS.has(key))
|
|
265
272
|
return this.runSetting(call, key, context);
|
|
@@ -475,6 +482,7 @@ export class PlaywrightAdapter {
|
|
|
475
482
|
caseCount: null,
|
|
476
483
|
testContext: null,
|
|
477
484
|
fixtures: destructuredNames(hashed, 0),
|
|
485
|
+
takesAllFixtures: takesWhole(hashed, 0),
|
|
478
486
|
usesBench: false,
|
|
479
487
|
problems,
|
|
480
488
|
};
|
|
@@ -75,7 +75,7 @@ export declare class VitestAdapter implements Adapter {
|
|
|
75
75
|
/** Vitest's order: `only` wins over `skip`, which wins over `todo`. */
|
|
76
76
|
private states;
|
|
77
77
|
private readDeclarationOptions;
|
|
78
|
-
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }
|
|
78
|
+
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`. */
|
|
79
79
|
private retry;
|
|
80
80
|
private meta;
|
|
81
81
|
private caseCount;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { childNodes, isFunctionNode, isImportMetaVitest, propertyKey, propertyName, unwrap, } from "../ast.js";
|
|
2
2
|
import { snippet } from "../source.js";
|
|
3
3
|
import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
|
|
4
|
-
import { atLeast, hookFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, destructuredNames, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
|
|
4
|
+
import { atLeast, hookFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, destructuredNames, takesWhole, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
|
|
5
5
|
/** What a test function chosen at run time, as in `isCI ? it.skip : it`, is either way: only the links both have. */
|
|
6
6
|
function shared(a, b) {
|
|
7
7
|
if (!a || a.kind !== b?.kind)
|
|
@@ -27,6 +27,7 @@ const MODIFIERS = {
|
|
|
27
27
|
};
|
|
28
28
|
const TABLES = new Set(['each', 'for']);
|
|
29
29
|
const CONDITIONS = new Set(['skipIf', 'runIf']);
|
|
30
|
+
const STATE_FLAGS = ['only', 'todo', 'fails'];
|
|
30
31
|
const DERIVATIONS = new Set(['extend', 'override', 'scoped']);
|
|
31
32
|
/** Hooks, with the position of the test context among their callback's parameters. */
|
|
32
33
|
const HOOKS = new Map([
|
|
@@ -528,6 +529,7 @@ export class VitestAdapter {
|
|
|
528
529
|
fn,
|
|
529
530
|
testContext: index === null ? null : contextNames(fn, index),
|
|
530
531
|
fixtures: fixturesOf(fn, index, this.file.version),
|
|
532
|
+
takesAllFixtures: takesWhole(fn, index),
|
|
531
533
|
};
|
|
532
534
|
}
|
|
533
535
|
contextSkip(call, context) {
|
|
@@ -633,6 +635,17 @@ export class VitestAdapter {
|
|
|
633
635
|
const chain = new Set(links.filter((link) => MODIFIERS[kind].has(link.name)).map((link) => link.name));
|
|
634
636
|
const flags = new Set(chain);
|
|
635
637
|
let conditions = links.filter((link) => CONDITIONS.has(link.name)).map((link) => this.linkCondition(link, context));
|
|
638
|
+
// From Vitest 5.0.5 a test's `skip`, `skipIf` and `runIf` each set its `skip`, so the last of them decides, and a
|
|
639
|
+
// `skipIf` or `runIf` replaces the skip of a tag or of a describe around. The scan applies that to all of Vitest 5.
|
|
640
|
+
const lastSkip = links.filter((link) => link.name === 'skip' || CONDITIONS.has(link.name)).at(-1);
|
|
641
|
+
const skipsByChain = kind === 'test' && lastSkip !== undefined && atLeast(this.file.version, 5, 0);
|
|
642
|
+
const conditionSetsSkip = skipsByChain && CONDITIONS.has(lastSkip.name);
|
|
643
|
+
if (skipsByChain) {
|
|
644
|
+
flags.delete('skip');
|
|
645
|
+
conditions = conditionSetsSkip ? [this.linkCondition(lastSkip, context)] : [];
|
|
646
|
+
if (!conditionSetsSkip)
|
|
647
|
+
flags.add('skip');
|
|
648
|
+
}
|
|
636
649
|
// A test's own options win over its chain, so `test.skip('title', { skip: false }, fn)` runs. The chain wins for a
|
|
637
650
|
// test's mode, and for everything on a describe.
|
|
638
651
|
for (const [flag, value] of read.flags) {
|
|
@@ -652,7 +665,7 @@ export class VitestAdapter {
|
|
|
652
665
|
// win. A `skip` passed down skips them, and turns todo into skip, as skip goes before todo.
|
|
653
666
|
const line = this.file.source.line(call.start);
|
|
654
667
|
const around = context.passedDown;
|
|
655
|
-
const passed = read.skip ? null : around;
|
|
668
|
+
const passed = read.skip || conditionSetsSkip ? null : around;
|
|
656
669
|
const skips = passed?.condition.applies === 'always' && !flags.has('only');
|
|
657
670
|
const table = links.find((link) => TABLES.has(link.name)) ?? null;
|
|
658
671
|
const isTodo = flags.has('todo') || body === undefined;
|
|
@@ -701,7 +714,12 @@ export class VitestAdapter {
|
|
|
701
714
|
...base,
|
|
702
715
|
testContext: contextNames(base.hashed, index),
|
|
703
716
|
fixtures: fixturesOf(base.hashed, index, this.file.version),
|
|
717
|
+
takesAllFixtures: takesWhole(base.hashed, index),
|
|
704
718
|
usesBench: atLeast(this.file.version, 5, 0) && destructuredNames(base.hashed, index).includes('bench'),
|
|
719
|
+
ownStateOptions: [
|
|
720
|
+
...(read.skip || conditionSetsSkip ? ['skip'] : []),
|
|
721
|
+
...STATE_FLAGS.filter((flag) => read.flags.has(flag)),
|
|
722
|
+
],
|
|
705
723
|
};
|
|
706
724
|
}
|
|
707
725
|
return refusal ? refused(call, refusal, context, () => declared) : declared;
|
|
@@ -761,8 +779,14 @@ export class VitestAdapter {
|
|
|
761
779
|
return result;
|
|
762
780
|
const options = readOptions(this.file, context.scopes, node);
|
|
763
781
|
problems.push(...unresolvedOptions(this.file, options, 'its flags, tags and meta are missing'));
|
|
782
|
+
// From Vitest 5.0.5 an option set to `undefined` replaces nothing; the scan applies that to all of Vitest 5.
|
|
783
|
+
const given = (key) => {
|
|
784
|
+
const option = options.values.get(key);
|
|
785
|
+
const isUndefined = option?.resolved.ok === true && option.resolved.value === undefined;
|
|
786
|
+
return isUndefined && atLeast(this.file.version, 5, 0) ? undefined : option;
|
|
787
|
+
};
|
|
764
788
|
for (const flag of ['only', 'todo', 'fails', 'concurrent', 'sequential', 'shuffle']) {
|
|
765
|
-
const option =
|
|
789
|
+
const option = given(flag);
|
|
766
790
|
if (option && !option.resolved.ok) {
|
|
767
791
|
const { importedFrom } = option.resolved;
|
|
768
792
|
problems.push(unresolvedOption(this.file, flag, option.node, importedFrom, 'it is not applied'));
|
|
@@ -774,7 +798,7 @@ export class VitestAdapter {
|
|
|
774
798
|
result.flags.set(flag, Boolean(option.resolved.value));
|
|
775
799
|
}
|
|
776
800
|
}
|
|
777
|
-
const skip =
|
|
801
|
+
const skip = given('skip');
|
|
778
802
|
if (skip) {
|
|
779
803
|
result.skip = skip.resolved.ok
|
|
780
804
|
? { applies: skip.resolved.value ? 'always' : 'never', text: null }
|
|
@@ -804,7 +828,7 @@ export class VitestAdapter {
|
|
|
804
828
|
this.retry(retry, details, context, problems);
|
|
805
829
|
return result;
|
|
806
830
|
}
|
|
807
|
-
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }
|
|
831
|
+
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`. */
|
|
808
832
|
retry(option, details, context, problems) {
|
|
809
833
|
const node = unwrap(option.node);
|
|
810
834
|
if (node.type !== 'ObjectExpression') {
|
|
@@ -813,8 +837,14 @@ export class VitestAdapter {
|
|
|
813
837
|
details.run.retry = { count, delay: null, condition: null };
|
|
814
838
|
return;
|
|
815
839
|
}
|
|
816
|
-
if (!atLeast(this.file.version, 4, 1))
|
|
840
|
+
if (!atLeast(this.file.version, 4, 1)) {
|
|
841
|
+
// Before 4.1 the runner compares the attempt number with the object, so it never runs the test.
|
|
842
|
+
const source = snippet(text(this.file, node));
|
|
843
|
+
const message = `Option retry: ${source} is an object, which Vitest reads from 4.1; before, it never runs the test.`;
|
|
844
|
+
problems.push(problem('unresolved-option', node, message, source));
|
|
845
|
+
details.run.retry = null;
|
|
817
846
|
return;
|
|
847
|
+
}
|
|
818
848
|
const values = readOptions(this.file, context.scopes, node);
|
|
819
849
|
problems.push(...unresolvedOptions(this.file, values, 'the retry is not applied'));
|
|
820
850
|
const read = (key) => {
|
package/dist/hash.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Node } from 'oxc-parser';
|
|
2
2
|
import type { Span } from './model.ts';
|
|
3
|
-
export declare function shortHash(input: string): string;
|
|
3
|
+
export declare function shortHash(input: string | Uint8Array): string;
|
|
4
4
|
/**
|
|
5
5
|
* Hashes code by its syntax tree, so formatting, comments, quote style, trailing commas and line endings don't change
|
|
6
6
|
* the hash. Nodes at the `cut` spans, such as the tests inside a describe, are left out. Returns null when no code is
|
package/dist/model.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { CallExpression, Expression, Node } from 'oxc-parser';
|
|
|
2
2
|
import type { FunctionNode } from './ast.ts';
|
|
3
3
|
import type { Annotation, DiagnosticCode, DiagnosticLevel, HookCounts, MetaValue } from './types.ts';
|
|
4
4
|
export type State = 'skip' | 'fixme' | 'todo' | 'expectedToFail';
|
|
5
|
+
/** The Vitest options that set a test's state. */
|
|
6
|
+
export type StateOption = 'skip' | 'only' | 'todo' | 'fails';
|
|
5
7
|
export type Mode = 'parallel' | 'serial' | 'default';
|
|
6
8
|
/** A state set by a declaration, a describe or a call such as `test.skip(condition)`. */
|
|
7
9
|
export interface StateSetting {
|
|
@@ -51,12 +53,15 @@ export interface Details {
|
|
|
51
53
|
/** Run settings written in one place; a key that is missing is not set there. */
|
|
52
54
|
export interface RunSettings {
|
|
53
55
|
timeout?: number;
|
|
54
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* A retry setting replaces the whole `retry` of the describes around, delay and condition included; null when it is
|
|
58
|
+
* set to a value the runner can't use.
|
|
59
|
+
*/
|
|
55
60
|
retry?: {
|
|
56
61
|
count: number;
|
|
57
62
|
delay: number | null;
|
|
58
63
|
condition: string | null;
|
|
59
|
-
};
|
|
64
|
+
} | null;
|
|
60
65
|
repeats?: number;
|
|
61
66
|
/** Playwright `test.slow()`: the conditions of the calls, any of which makes the test slow; null when one always does. */
|
|
62
67
|
slow?: {
|
|
@@ -86,8 +91,12 @@ export interface TestDeclaration extends Declaration {
|
|
|
86
91
|
testContext: unknown;
|
|
87
92
|
/** Names the test function takes from its context, without the framework's own keys. */
|
|
88
93
|
fixtures: string[];
|
|
94
|
+
/** The test function takes its fixtures whole, or keeps the rest of them, so it may use any. */
|
|
95
|
+
takesAllFixtures: boolean;
|
|
89
96
|
/** Uses Vitest 5's `bench` fixture. */
|
|
90
97
|
usesBench: boolean;
|
|
98
|
+
/** Vitest: the state options the test sets itself, to `false` too, which replace those of its tags. */
|
|
99
|
+
ownStateOptions?: StateOption[];
|
|
91
100
|
}
|
|
92
101
|
export interface SuiteDeclaration extends Declaration {
|
|
93
102
|
kind: 'describe';
|
|
@@ -128,6 +137,7 @@ export type ApiCall = TestDeclaration | SuiteDeclaration | {
|
|
|
128
137
|
fn: FunctionNode | null;
|
|
129
138
|
testContext: unknown;
|
|
130
139
|
fixtures: string[];
|
|
140
|
+
takesAllFixtures: boolean;
|
|
131
141
|
}
|
|
132
142
|
/** Playwright `test.step()` or `test.step.skip()`. */
|
|
133
143
|
| {
|
|
@@ -183,6 +193,8 @@ export interface Returns {
|
|
|
183
193
|
/** A module read to see whether it exports a custom `test` object. */
|
|
184
194
|
export interface ModuleSource {
|
|
185
195
|
code: string;
|
|
196
|
+
/** The file's bytes when it isn't UTF-8 text, such as an image. */
|
|
197
|
+
bytes?: Uint8Array;
|
|
186
198
|
relativeFilePath: string;
|
|
187
199
|
/** Reads the modules this module imports in turn. */
|
|
188
200
|
readModule: ModuleReader;
|
package/dist/parse.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export interface ParsedFile {
|
|
|
15
15
|
end: number;
|
|
16
16
|
} | null;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The offset of the first bracket nested more than `limit` deep, or null. Parentheses, brackets, braces and template
|
|
20
|
+
* substitutions count; strings, comments, regular expressions and template text are skipped. A closing bracket closes
|
|
21
|
+
* up to the one it matches, so a literal read wrongly unbalances only the brackets around it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function tooDeeplyNested(text: string, limit: number): number | null;
|
|
18
24
|
/** The first parser error, located at its first label, or at the start of the file when it has none. */
|
|
19
25
|
export declare function firstError(errors: readonly OxcError[]): ParsedFile['error'];
|
|
20
26
|
export declare function parseFile(fileName: string, code: string): ParsedFile;
|
package/dist/parse.js
CHANGED
|
@@ -7,6 +7,200 @@ function parserOptions(fileName) {
|
|
|
7
7
|
const sourceType = extension.startsWith('.c') ? 'commonjs' : extension.startsWith('.m') ? 'module' : 'unambiguous';
|
|
8
8
|
return { lang, sourceType, preserveParens: false };
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* How deep brackets may nest. oxc-parser recurses on the native stack, which no catch can stop: brackets about 2,400
|
|
12
|
+
* deep crash a worker thread's 4 MB stack and 4,800 the main thread's 8 MB. Of the 10,000 files in this package's
|
|
13
|
+
* development dependencies, minified bundles included, the deepest nests 34.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_NESTING = 500;
|
|
16
|
+
/** Words after which a `/` starts a regular expression rather than a division. */
|
|
17
|
+
const BEFORE_REGEX = new Set([
|
|
18
|
+
'return',
|
|
19
|
+
'typeof',
|
|
20
|
+
'instanceof',
|
|
21
|
+
'in',
|
|
22
|
+
'of',
|
|
23
|
+
'new',
|
|
24
|
+
'delete',
|
|
25
|
+
'void',
|
|
26
|
+
'throw',
|
|
27
|
+
'case',
|
|
28
|
+
'do',
|
|
29
|
+
'else',
|
|
30
|
+
'yield',
|
|
31
|
+
'await',
|
|
32
|
+
]);
|
|
33
|
+
const Char = {
|
|
34
|
+
Tab: 9,
|
|
35
|
+
LineFeed: 10,
|
|
36
|
+
CarriageReturn: 13,
|
|
37
|
+
Space: 32,
|
|
38
|
+
DoubleQuote: 34,
|
|
39
|
+
Dollar: 36,
|
|
40
|
+
SingleQuote: 39,
|
|
41
|
+
OpenParen: 40,
|
|
42
|
+
CloseParen: 41,
|
|
43
|
+
Star: 42,
|
|
44
|
+
Slash: 47,
|
|
45
|
+
LessThan: 60,
|
|
46
|
+
GreaterThan: 62,
|
|
47
|
+
OpenBracket: 91,
|
|
48
|
+
Backslash: 92,
|
|
49
|
+
CloseBracket: 93,
|
|
50
|
+
Backtick: 96,
|
|
51
|
+
OpenBrace: 123,
|
|
52
|
+
CloseBrace: 125,
|
|
53
|
+
};
|
|
54
|
+
/** Letters, digits, `_`, `$` and anything outside ASCII, as identifiers and numbers use. */
|
|
55
|
+
function isWordChar(code) {
|
|
56
|
+
return ((code >= 97 && code <= 122) ||
|
|
57
|
+
(code >= 65 && code <= 90) ||
|
|
58
|
+
(code >= 48 && code <= 57) ||
|
|
59
|
+
code === 95 ||
|
|
60
|
+
code === Char.Dollar ||
|
|
61
|
+
code > 127);
|
|
62
|
+
}
|
|
63
|
+
/** Whether the code right before `end` is a word after which a `/` starts a regular expression. */
|
|
64
|
+
function afterKeyword(text, end) {
|
|
65
|
+
let wordEnd = end;
|
|
66
|
+
while (wordEnd > 0 && /\s/.test(text[wordEnd - 1]))
|
|
67
|
+
wordEnd--;
|
|
68
|
+
let wordStart = wordEnd;
|
|
69
|
+
while (wordStart > 0 && isWordChar(text.charCodeAt(wordStart - 1)))
|
|
70
|
+
wordStart--;
|
|
71
|
+
return BEFORE_REGEX.has(text.slice(wordStart, wordEnd));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Whether the slash at `at` ends a JSX tag, as in `</ul>` or `<OrderLeg leg={leg} />`, where a regular expression
|
|
75
|
+
* could otherwise start and run on over the brackets after it.
|
|
76
|
+
*/
|
|
77
|
+
function endsJsxTag(text, at) {
|
|
78
|
+
if (text.charCodeAt(at - 1) === Char.LessThan)
|
|
79
|
+
return true;
|
|
80
|
+
if (text.charCodeAt(at + 1) !== Char.GreaterThan)
|
|
81
|
+
return false;
|
|
82
|
+
let before = at - 1;
|
|
83
|
+
while (before > 0 && /\s/.test(text[before]))
|
|
84
|
+
before--;
|
|
85
|
+
return text.charCodeAt(before) === Char.CloseBrace;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The offset of the first bracket nested more than `limit` deep, or null. Parentheses, brackets, braces and template
|
|
89
|
+
* substitutions count; strings, comments, regular expressions and template text are skipped. A closing bracket closes
|
|
90
|
+
* up to the one it matches, so a literal read wrongly unbalances only the brackets around it.
|
|
91
|
+
*/
|
|
92
|
+
export function tooDeeplyNested(text, limit) {
|
|
93
|
+
// Open brackets by their character; a substitution is kept as its backtick.
|
|
94
|
+
const open = [];
|
|
95
|
+
const counts = new Uint32Array(128);
|
|
96
|
+
let regexAllowed = true;
|
|
97
|
+
let index = 0;
|
|
98
|
+
const push = (code) => {
|
|
99
|
+
open.push(code);
|
|
100
|
+
counts[code]++;
|
|
101
|
+
return open.length > limit;
|
|
102
|
+
};
|
|
103
|
+
// Reads template text up to its end or a substitution; true when the substitution is one too deep.
|
|
104
|
+
const templateText = () => {
|
|
105
|
+
while (index < text.length) {
|
|
106
|
+
const code = text.charCodeAt(index);
|
|
107
|
+
if (code === Char.Backslash)
|
|
108
|
+
index += 2;
|
|
109
|
+
else if (code === Char.Backtick) {
|
|
110
|
+
index++;
|
|
111
|
+
regexAllowed = false;
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
else if (code === Char.Dollar && text.charCodeAt(index + 1) === Char.OpenBrace) {
|
|
115
|
+
if (push(Char.Backtick))
|
|
116
|
+
return true;
|
|
117
|
+
index += 2;
|
|
118
|
+
regexAllowed = true;
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
else
|
|
122
|
+
index++;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
};
|
|
126
|
+
// Skips a string or regular expression; neither goes past the end of the line.
|
|
127
|
+
const literal = (end) => {
|
|
128
|
+
let inClass = false;
|
|
129
|
+
while (index < text.length) {
|
|
130
|
+
const code = text.charCodeAt(index);
|
|
131
|
+
if (code === Char.LineFeed)
|
|
132
|
+
return;
|
|
133
|
+
index += code === Char.Backslash ? 2 : 1;
|
|
134
|
+
if (end === Char.Slash && code === Char.OpenBracket)
|
|
135
|
+
inClass = true;
|
|
136
|
+
else if (code === Char.CloseBracket)
|
|
137
|
+
inClass = false;
|
|
138
|
+
else if (code === end && !inClass)
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
while (index < text.length) {
|
|
143
|
+
const code = text.charCodeAt(index);
|
|
144
|
+
const next = text.charCodeAt(index + 1);
|
|
145
|
+
const start = index;
|
|
146
|
+
if (code === Char.Space || code === Char.LineFeed || code === Char.Tab || code === Char.CarriageReturn) {
|
|
147
|
+
index++;
|
|
148
|
+
}
|
|
149
|
+
else if (code === Char.Slash && next === Char.Slash) {
|
|
150
|
+
const end = text.indexOf('\n', index);
|
|
151
|
+
index = end === -1 ? text.length : end;
|
|
152
|
+
}
|
|
153
|
+
else if (code === Char.Slash && next === Char.Star) {
|
|
154
|
+
const end = text.indexOf('*/', index + 2);
|
|
155
|
+
index = end === -1 ? text.length : end + 2;
|
|
156
|
+
}
|
|
157
|
+
else if (code === Char.SingleQuote ||
|
|
158
|
+
code === Char.DoubleQuote ||
|
|
159
|
+
(code === Char.Slash && (regexAllowed || afterKeyword(text, start)) && !endsJsxTag(text, start))) {
|
|
160
|
+
index++;
|
|
161
|
+
literal(code);
|
|
162
|
+
regexAllowed = false;
|
|
163
|
+
}
|
|
164
|
+
else if (code === Char.Backtick) {
|
|
165
|
+
index++;
|
|
166
|
+
if (templateText())
|
|
167
|
+
return index;
|
|
168
|
+
}
|
|
169
|
+
else if (isWordChar(code)) {
|
|
170
|
+
while (index < text.length && isWordChar(text.charCodeAt(index)))
|
|
171
|
+
index++;
|
|
172
|
+
regexAllowed = false;
|
|
173
|
+
}
|
|
174
|
+
else if (code === Char.OpenParen || code === Char.OpenBracket || code === Char.OpenBrace) {
|
|
175
|
+
if (push(code))
|
|
176
|
+
return start;
|
|
177
|
+
index++;
|
|
178
|
+
regexAllowed = true;
|
|
179
|
+
}
|
|
180
|
+
else if (code === Char.CloseParen || code === Char.CloseBracket || code === Char.CloseBrace) {
|
|
181
|
+
index++;
|
|
182
|
+
// A block can end right before a regular expression; a value in parentheses or brackets is divided.
|
|
183
|
+
regexAllowed = code === Char.CloseBrace;
|
|
184
|
+
const opener = code === Char.CloseParen ? Char.OpenParen : code === Char.CloseBracket ? Char.OpenBracket : Char.OpenBrace;
|
|
185
|
+
const closesSubstitution = code === Char.CloseBrace && counts[Char.Backtick] !== 0;
|
|
186
|
+
if (counts[opener] === 0 && !closesSubstitution)
|
|
187
|
+
continue;
|
|
188
|
+
let closed;
|
|
189
|
+
do {
|
|
190
|
+
closed = open.pop();
|
|
191
|
+
counts[closed]--;
|
|
192
|
+
} while (closed !== opener && !(closed === Char.Backtick && code === Char.CloseBrace));
|
|
193
|
+
// A substitution after this one is as deep as the one it closed, so it is never the first too deep.
|
|
194
|
+
if (closed === Char.Backtick)
|
|
195
|
+
templateText();
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
index++;
|
|
199
|
+
regexAllowed = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
10
204
|
/** The first parser error, located at its first label, or at the start of the file when it has none. */
|
|
11
205
|
export function firstError(errors) {
|
|
12
206
|
const first = errors.at(0);
|
|
@@ -18,6 +212,16 @@ export function firstError(errors) {
|
|
|
18
212
|
export function parseFile(fileName, code) {
|
|
19
213
|
// A byte order mark is not part of the code, and editors don't count it as a column.
|
|
20
214
|
const text = code.charCodeAt(0) === 0xfeff ? code.slice(1) : code;
|
|
215
|
+
const tooDeep = tooDeeplyNested(text, MAX_NESTING);
|
|
216
|
+
if (tooDeep !== null) {
|
|
217
|
+
const message = `Brackets are nested more than ${MAX_NESTING} deep, too deep to parse safely`;
|
|
218
|
+
return {
|
|
219
|
+
source: new SourceText(text),
|
|
220
|
+
program: parseSync(fileName, '', parserOptions(fileName)).program,
|
|
221
|
+
comments: [],
|
|
222
|
+
error: { message, start: tooDeep, end: tooDeep + 1 },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
21
225
|
const result = parseSync(fileName, text, parserOptions(fileName));
|
|
22
226
|
return {
|
|
23
227
|
source: new SourceText(text),
|