test-inventory 0.2.1 → 0.3.0
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 +18 -0
- package/README.md +3 -1
- package/dist/ast.d.ts +11 -0
- package/dist/ast.js +30 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +59 -12
- package/dist/extract.js +26 -41
- package/dist/filter.d.ts +37 -0
- package/dist/filter.js +153 -0
- package/dist/frameworks/bindings.d.ts +37 -6
- package/dist/frameworks/bindings.js +167 -33
- package/dist/frameworks/common.d.ts +6 -1
- package/dist/frameworks/common.js +25 -3
- package/dist/frameworks/playwright.d.ts +2 -1
- package/dist/frameworks/playwright.js +58 -26
- package/dist/frameworks/vitest.d.ts +1 -1
- package/dist/frameworks/vitest.js +110 -67
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/json.d.ts +7 -1
- package/dist/json.js +30 -9
- package/dist/model.d.ts +23 -3
- package/dist/records.js +5 -3
- package/dist/types.d.ts +16 -0
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { childNodes, isFunctionNode, isImportMetaVitest, propertyKey, propertyName, unwrap, } from "../ast.js";
|
|
2
2
|
import { snippet } from "../source.js";
|
|
3
|
-
import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, requiredModule, } from "./bindings.js";
|
|
4
|
-
import { atLeast, findFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, text, } from "./common.js";
|
|
3
|
+
import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
|
|
4
|
+
import { atLeast, findFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, 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)
|
|
@@ -37,6 +37,8 @@ const HOOKS = new Map([
|
|
|
37
37
|
['afterAll', null],
|
|
38
38
|
['aroundAll', null],
|
|
39
39
|
]);
|
|
40
|
+
/** First members of chains that never declare a test, even when Vitest refuses the rest of the chain. */
|
|
41
|
+
const NOT_DECLARING = new Set([...HOOKS.keys(), ...DERIVATIONS]);
|
|
40
42
|
/** First members that show a name imported from a local file is used as Vitest's `test`. */
|
|
41
43
|
const TEST_MEMBERS = new Set([...MODIFIERS.test, ...MODIFIERS.describe, ...TABLES, ...CONDITIONS, ...KINDS.keys()]);
|
|
42
44
|
/** Calls Vitest moves to the top of the file before running it: `vi.mock`, `vi.unmock` and `vi.hoisted`. */
|
|
@@ -102,9 +104,12 @@ class VitestBindings {
|
|
|
102
104
|
/** Names imported from other modules, which may be test objects there. */
|
|
103
105
|
imported;
|
|
104
106
|
follow;
|
|
107
|
+
/** The module's functions and the variables they may build test objects in; see `ModuleFunctions`. */
|
|
108
|
+
functions;
|
|
105
109
|
constructor(program, version, follow) {
|
|
106
110
|
this.imported = moduleImports(program, (source) => source === 'vitest' || MISMATCHED_SOURCES.has(source));
|
|
107
111
|
this.follow = follow;
|
|
112
|
+
this.functions = new ModuleFunctions(program, this.imported, follow);
|
|
108
113
|
for (const statement of program.body) {
|
|
109
114
|
if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type')
|
|
110
115
|
continue;
|
|
@@ -139,30 +144,49 @@ class VitestBindings {
|
|
|
139
144
|
this.declarations.add(declarator);
|
|
140
145
|
return bound;
|
|
141
146
|
});
|
|
147
|
+
this.functions.keep(declarators.filter((declarator) => !this.declarations.has(declarator)));
|
|
142
148
|
}
|
|
143
149
|
isTestName(name) {
|
|
144
|
-
return this.kinds.has(name) || this.importsTestObject(name);
|
|
150
|
+
return this.kinds.has(name) || this.importsTestObject(name) || this.holdsTestObject(name);
|
|
145
151
|
}
|
|
146
|
-
|
|
152
|
+
/**
|
|
153
|
+
* `followsCalls` is cleared where a call is not followed to what it returns: while the names are bound, so that only
|
|
154
|
+
* a value asked about is followed, as following reads other modules, and in the scanned file, whose calls the walk
|
|
155
|
+
* follows in their scopes.
|
|
156
|
+
*/
|
|
157
|
+
isTestObject(expression, followsCalls = true) {
|
|
147
158
|
const node = unwrap(expression);
|
|
148
|
-
if (node.type === 'Identifier')
|
|
149
|
-
|
|
159
|
+
if (node.type === 'Identifier') {
|
|
160
|
+
const { name } = node;
|
|
161
|
+
return (this.kinds.get(name) === 'test' || this.importsTestObject(name) || (followsCalls && this.holdsTestObject(name)));
|
|
162
|
+
}
|
|
150
163
|
if (node.type === 'MemberExpression') {
|
|
151
164
|
return KINDS.get(propertyName(node) ?? '') === 'test' && this.isNamespace(node.object);
|
|
152
165
|
}
|
|
153
166
|
// `process.env.CI ? vitestTest : skippedTest` is a test object either way.
|
|
154
167
|
if (node.type === 'ConditionalExpression') {
|
|
155
|
-
return this.isTestObject(node.consequent) || this.isTestObject(node.alternate);
|
|
168
|
+
return this.isTestObject(node.consequent, followsCalls) || this.isTestObject(node.alternate, followsCalls);
|
|
156
169
|
}
|
|
157
170
|
if (node.type !== 'CallExpression')
|
|
158
171
|
return false;
|
|
159
172
|
const callee = unwrap(node.callee);
|
|
173
|
+
// `defineBrokerTest()`, a function that returns a test object.
|
|
174
|
+
if (callee.type === 'Identifier')
|
|
175
|
+
return followsCalls && this.returnsTestObject(callee.name);
|
|
160
176
|
return (callee.type === 'MemberExpression' &&
|
|
161
177
|
DERIVATIONS.has(propertyName(callee) ?? '') &&
|
|
162
|
-
this.isTestObject(callee.object));
|
|
178
|
+
this.isTestObject(callee.object, followsCalls));
|
|
179
|
+
}
|
|
180
|
+
returnsTestObject(name) {
|
|
181
|
+
return this.functions.returnsTestObject(name, (expression) => this.isTestObject(expression));
|
|
182
|
+
}
|
|
183
|
+
holdsTestObject(name) {
|
|
184
|
+
return this.functions.holdsTestObject(name, (expression) => this.isTestObject(expression));
|
|
163
185
|
}
|
|
164
|
-
reexports(source, imported) {
|
|
165
|
-
|
|
186
|
+
reexports(source, imported, exported) {
|
|
187
|
+
if (source !== 'vitest')
|
|
188
|
+
return this.follow(source, imported, exported);
|
|
189
|
+
return exported === 'test object' && KINDS.has(imported);
|
|
166
190
|
}
|
|
167
191
|
/** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
|
|
168
192
|
importsTestObject(name) {
|
|
@@ -189,7 +213,7 @@ class VitestBindings {
|
|
|
189
213
|
const { id, init } = declarator;
|
|
190
214
|
if (!init)
|
|
191
215
|
return false;
|
|
192
|
-
if (id.type === 'Identifier' && this.isTestObject(init)) {
|
|
216
|
+
if (id.type === 'Identifier' && this.isTestObject(init, false)) {
|
|
193
217
|
this.kinds.set(id.name, 'test');
|
|
194
218
|
return true;
|
|
195
219
|
}
|
|
@@ -231,10 +255,11 @@ export class VitestAdapter {
|
|
|
231
255
|
// A test object the file builds from an imported one, as in `const it = base.extend({...})`, is followed quietly:
|
|
232
256
|
// a call through the import itself is what reports an import that can't be followed.
|
|
233
257
|
const followed = new Map();
|
|
234
|
-
this.bindings = new VitestBindings(parsed.program, file.version, (source, imported) => {
|
|
235
|
-
const key = `${source}\0${imported}`;
|
|
236
|
-
if (!followed.has(key))
|
|
237
|
-
followed.set(key, followModule(source, imported, readModule, this.framework()).exported);
|
|
258
|
+
this.bindings = new VitestBindings(parsed.program, file.version, (source, imported, exported = 'test object') => {
|
|
259
|
+
const key = `${source}\0${exported}\0${imported}`;
|
|
260
|
+
if (!followed.has(key)) {
|
|
261
|
+
followed.set(key, followModule(source, imported, readModule, this.framework(), exported).exported);
|
|
262
|
+
}
|
|
238
263
|
return followed.get(key) === true;
|
|
239
264
|
});
|
|
240
265
|
this.problems = [...this.bindings.problems];
|
|
@@ -259,8 +284,8 @@ export class VitestAdapter {
|
|
|
259
284
|
this.contextSkip(call, context) ??
|
|
260
285
|
this.chainCall(call, context));
|
|
261
286
|
}
|
|
262
|
-
testFunction(expression, scopes) {
|
|
263
|
-
return this.aliasOf(expression, { scopes, inBody: false, returnsOf
|
|
287
|
+
testFunction(expression, scopes, returnsOf) {
|
|
288
|
+
return this.aliasOf(expression, { scopes, inBody: false, returnsOf });
|
|
264
289
|
}
|
|
265
290
|
/** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
|
|
266
291
|
benchCall(call, context) {
|
|
@@ -290,29 +315,24 @@ export class VitestAdapter {
|
|
|
290
315
|
if (last && (TABLES.has(last.name) || CONDITIONS.has(last.name)) && !last.args && !last.template) {
|
|
291
316
|
return { kind: 'other' };
|
|
292
317
|
}
|
|
293
|
-
|
|
318
|
+
const callee = snippet(text(this.file, call.callee));
|
|
319
|
+
const removed = (link) => link.name === 'sequential' && atLeast(this.file.version, 5, 0);
|
|
320
|
+
const valid = (link) => {
|
|
294
321
|
const called = link.args !== null || link.template !== null;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return {
|
|
310
|
-
kind: 'invalid',
|
|
311
|
-
problem: problem('removed-api', call, `sequential was removed in Vitest 5; ${callee}(...) fails.`, callee),
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return this.declaration(call, kind, links, context);
|
|
322
|
+
if (MODIFIERS[kind].has(link.name))
|
|
323
|
+
return !called && !removed(link);
|
|
324
|
+
return TABLES.has(link.name) ? called : CONDITIONS.has(link.name) && link.args !== null;
|
|
325
|
+
};
|
|
326
|
+
const invalid = links.find((link) => !valid(link));
|
|
327
|
+
if (!invalid)
|
|
328
|
+
return this.declaration(call, kind, links, context);
|
|
329
|
+
const refusal = removed(invalid)
|
|
330
|
+
? problem('removed-api', call, `sequential was removed in Vitest 5; ${callee}(...) fails.`, callee)
|
|
331
|
+
: problem('invalid-chain', call, `${callee} is not part of Vitest's API.`, callee);
|
|
332
|
+
if (NOT_DECLARING.has(links[0].name))
|
|
333
|
+
return { kind: 'invalid', problem: refusal };
|
|
334
|
+
// Which of its parts the chain means is not known, so it declares a plain test or describe.
|
|
335
|
+
return refused(call, refusal, context, () => this.declaration(call, kind, [], context));
|
|
316
336
|
}
|
|
317
337
|
/**
|
|
318
338
|
* The function a chain calls and the links after it, or null when it isn't one of Vitest's. An import that can't be
|
|
@@ -402,7 +422,7 @@ export class VitestAdapter {
|
|
|
402
422
|
*/
|
|
403
423
|
returned(call, links, args, lookup) {
|
|
404
424
|
const callee = unwrap(call.callee);
|
|
405
|
-
if (callee.type !== 'Identifier'
|
|
425
|
+
if (callee.type !== 'Identifier')
|
|
406
426
|
return null;
|
|
407
427
|
const returns = lookup.returnsOf(callee.name, lookup.scopes);
|
|
408
428
|
if (returns)
|
|
@@ -528,6 +548,7 @@ export class VitestAdapter {
|
|
|
528
548
|
// (title, fn, timeout), (title, options, fn) or the removed (title, fn, options)
|
|
529
549
|
let options;
|
|
530
550
|
let body;
|
|
551
|
+
let refusal = null;
|
|
531
552
|
const [second, third] = [args.at(1), args.at(2)];
|
|
532
553
|
if (args.some((arg) => arg.type === 'SpreadElement')) {
|
|
533
554
|
// With spread arguments, as in test(...args), the function and options are unknown.
|
|
@@ -540,29 +561,49 @@ export class VitestAdapter {
|
|
|
540
561
|
body = second;
|
|
541
562
|
if (third !== undefined && this.isOptions(third, undefined, context)) {
|
|
542
563
|
if (atLeast(this.file.version, 4, 0)) {
|
|
543
|
-
|
|
544
|
-
kind: 'invalid',
|
|
545
|
-
problem: problem('removed-api', third, `${callee}() takes options as its second argument since Vitest 4; with options third the call fails.`, snippet(text(this.file, third))),
|
|
546
|
-
};
|
|
564
|
+
refusal = problem('removed-api', third, `${callee}() takes options as its second argument since Vitest 4; with options third the call fails.`, snippet(text(this.file, third)));
|
|
547
565
|
}
|
|
548
566
|
options = third;
|
|
549
567
|
}
|
|
550
568
|
}
|
|
551
569
|
const fn = isFunctionNode(body) ? body : null;
|
|
552
570
|
const read = this.readDeclarationOptions(options, context, problems);
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
571
|
+
const chain = new Set(links.filter((link) => MODIFIERS[kind].has(link.name)).map((link) => link.name));
|
|
572
|
+
const flags = new Set(chain);
|
|
573
|
+
let conditions = links.filter((link) => CONDITIONS.has(link.name)).map((link) => this.linkCondition(link, context));
|
|
574
|
+
// A test's own options win over its chain, so `test.skip('title', { skip: false }, fn)` runs. The chain wins for a
|
|
575
|
+
// test's mode, and for everything on a describe.
|
|
576
|
+
for (const [flag, value] of read.flags) {
|
|
577
|
+
if (value)
|
|
578
|
+
flags.add(flag);
|
|
579
|
+
else if (kind === 'test' && flag !== 'sequential')
|
|
580
|
+
flags.delete(flag);
|
|
581
|
+
}
|
|
582
|
+
if (read.skip && kind === 'test') {
|
|
583
|
+
flags.delete('skip');
|
|
584
|
+
conditions = [read.skip];
|
|
585
|
+
}
|
|
586
|
+
else if (read.skip) {
|
|
587
|
+
conditions.push(read.skip);
|
|
588
|
+
}
|
|
589
|
+
// Vitest passes a describe's options down to the tests and describes inside, where their own options and `only`
|
|
590
|
+
// win. A `skip` passed down skips them, and turns todo into skip, as skip goes before todo.
|
|
591
|
+
const line = this.file.source.line(call.start);
|
|
592
|
+
const around = context.passedDown;
|
|
593
|
+
const passed = read.skip ? null : around;
|
|
594
|
+
const skips = passed?.condition.applies === 'always' && !flags.has('only');
|
|
561
595
|
const table = links.find((link) => TABLES.has(link.name)) ?? null;
|
|
562
596
|
const isTodo = flags.has('todo') || body === undefined;
|
|
563
|
-
const states = this.states(flags, conditions, isTodo,
|
|
564
|
-
|
|
565
|
-
|
|
597
|
+
const states = this.states(flags, conditions, isTodo && !skips, line);
|
|
598
|
+
const skipped = () => states.some((setting) => setting.state === 'skip' && !setting.condition);
|
|
599
|
+
// The skip is set where the describe that passes it down sets it. A skipped describe around already skips what is
|
|
600
|
+
// inside, as it does when its own `only` beats its skip; a todo describe gets the skip itself.
|
|
601
|
+
if (skips && !skipped() && (!passed.covered || (kind === 'describe' && isTodo))) {
|
|
602
|
+
states.push({ state: 'skip', condition: null, reason: null, hasDynamicReason: false, line: passed.line });
|
|
603
|
+
}
|
|
604
|
+
// The chain's mode wins over the options'. `{ concurrent: false }` is how Vitest 5 turns parallel off, in place of
|
|
605
|
+
// `sequential`.
|
|
606
|
+
const parallel = chain.has('concurrent') || (!chain.has('sequential') && read.concurrent === true);
|
|
566
607
|
const mode = parallel
|
|
567
608
|
? 'parallel'
|
|
568
609
|
: flags.has('sequential') || read.concurrent === false
|
|
@@ -580,18 +621,20 @@ export class VitestAdapter {
|
|
|
580
621
|
caseCount: table ? this.caseCount(call, table, context, problems) : null,
|
|
581
622
|
problems,
|
|
582
623
|
};
|
|
624
|
+
let declared;
|
|
583
625
|
if (kind === 'describe') {
|
|
584
626
|
// A table's cases are passed instead.
|
|
585
627
|
const first = table ? null : fn?.params.at(0);
|
|
586
628
|
if (fn && first?.type === 'Identifier')
|
|
587
629
|
this.suiteParameters.set(fn, first.name);
|
|
588
|
-
|
|
630
|
+
const own = read.skip ? { condition: read.skip, line } : around;
|
|
631
|
+
const passesDown = own && { ...own, covered: (around?.covered ?? false) || skipped() };
|
|
632
|
+
declared = { kind: 'describe', ...base, isShuffled: flags.has('shuffle'), passesDown };
|
|
589
633
|
}
|
|
590
|
-
|
|
591
|
-
kind: 'test',
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
};
|
|
634
|
+
else {
|
|
635
|
+
declared = { kind: 'test', ...base, testContext: skipNames(fn, table?.name === 'for' ? 1 : table ? null : 0) };
|
|
636
|
+
}
|
|
637
|
+
return refusal ? refused(call, refusal, context, () => declared) : declared;
|
|
595
638
|
}
|
|
596
639
|
/** Whether an argument holds options: an object, a name for one, or anything but a function before the body. */
|
|
597
640
|
isOptions(node, next, context) {
|
|
@@ -639,9 +682,9 @@ export class VitestAdapter {
|
|
|
639
682
|
readDeclarationOptions(node, context, problems) {
|
|
640
683
|
const details = { tags: [], annotations: [], locks: [], meta: null };
|
|
641
684
|
const result = {
|
|
642
|
-
flags:
|
|
685
|
+
flags: new Map(),
|
|
643
686
|
concurrent: null,
|
|
644
|
-
|
|
687
|
+
skip: null,
|
|
645
688
|
details,
|
|
646
689
|
};
|
|
647
690
|
if (!node)
|
|
@@ -657,15 +700,15 @@ export class VitestAdapter {
|
|
|
657
700
|
else if (option?.resolved.ok && flag === 'concurrent') {
|
|
658
701
|
result.concurrent = Boolean(option.resolved.value);
|
|
659
702
|
}
|
|
660
|
-
else if (option?.resolved.ok
|
|
661
|
-
result.flags.
|
|
703
|
+
else if (option?.resolved.ok) {
|
|
704
|
+
result.flags.set(flag, Boolean(option.resolved.value));
|
|
662
705
|
}
|
|
663
706
|
}
|
|
664
707
|
const skip = options.values.get('skip');
|
|
665
708
|
if (skip) {
|
|
666
|
-
result.
|
|
709
|
+
result.skip = skip.resolved.ok
|
|
667
710
|
? { applies: skip.resolved.value ? 'always' : 'never', text: null }
|
|
668
|
-
: { applies: 'sometimes', text: snippet(text(this.file, skip.node)) }
|
|
711
|
+
: { applies: 'sometimes', text: snippet(text(this.file, skip.node)) };
|
|
669
712
|
}
|
|
670
713
|
const tags = options.values.get('tags');
|
|
671
714
|
if (tags && atLeast(this.file.version, 4, 1)) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { scan } from './scan.ts';
|
|
2
2
|
export { scanSource } from './scan-source.ts';
|
|
3
|
+
export { filterInventory, pickTestFields } from './filter.ts';
|
|
4
|
+
export type { InventoryFilter, PickedInventory } from './filter.ts';
|
|
3
5
|
export type { CommentsMode, ScanOptions, ScanSourceOptions } from './options.ts';
|
|
4
|
-
export type { Annotation, DeclaredAt, Diagnostic, DiagnosticCode, DiagnosticLevel, Framework, Inventory, MetaValue, StateSource, SuiteRecord, Summary, TestRecord, TestState, Tool, } from './types.ts';
|
|
6
|
+
export type { Annotation, DeclaredAt, Diagnostic, DiagnosticCode, DiagnosticLevel, Framework, Inventory, InventoryLine, MetaValue, StateSource, SuiteRecord, Summary, TestRecord, TestState, Tool, } from './types.ts';
|
package/dist/index.js
CHANGED
package/dist/json.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { Inventory } from './types.ts';
|
|
2
|
+
/** An inventory as the command line writes it, with every key of the test records or only the picked ones. */
|
|
3
|
+
export type Output = Omit<Inventory, 'tests'> & {
|
|
4
|
+
tests: object[];
|
|
5
|
+
};
|
|
2
6
|
/**
|
|
3
7
|
* Writes an inventory as JSON one record at a time, so a large inventory never becomes one huge string.
|
|
4
8
|
* The text is exactly what `JSON.stringify(inventory, null, pretty ? 2 : undefined)` returns.
|
|
5
9
|
*/
|
|
6
|
-
export declare function writeInventory(inventory:
|
|
10
|
+
export declare function writeInventory(inventory: Output, pretty: boolean, write: (chunk: string) => Promise<void>): Promise<void>;
|
|
11
|
+
/** Writes an inventory as JSON Lines: one line per record, each marked by `type`, as `InventoryLine` describes. */
|
|
12
|
+
export declare function writeInventoryLines(inventory: Output, write: (chunk: string) => Promise<void>): Promise<void>;
|
package/dist/json.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/** Collects small pieces into larger chunks, so a stream gets few writes. */
|
|
2
2
|
const CHUNK_SIZE = 64 * 1024;
|
|
3
|
+
function chunked(write) {
|
|
4
|
+
let buffer = '';
|
|
5
|
+
return {
|
|
6
|
+
emit: async (text) => {
|
|
7
|
+
buffer += text;
|
|
8
|
+
if (buffer.length >= CHUNK_SIZE) {
|
|
9
|
+
await write(buffer);
|
|
10
|
+
buffer = '';
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
flush: () => write(buffer),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
3
16
|
/**
|
|
4
17
|
* Writes an inventory as JSON one record at a time, so a large inventory never becomes one huge string.
|
|
5
18
|
* The text is exactly what `JSON.stringify(inventory, null, pretty ? 2 : undefined)` returns.
|
|
6
19
|
*/
|
|
7
20
|
export async function writeInventory(inventory, pretty, write) {
|
|
8
|
-
|
|
9
|
-
const emit = async (text) => {
|
|
10
|
-
buffer += text;
|
|
11
|
-
if (buffer.length >= CHUNK_SIZE) {
|
|
12
|
-
await write(buffer);
|
|
13
|
-
buffer = '';
|
|
14
|
-
}
|
|
15
|
-
};
|
|
21
|
+
const { emit, flush } = chunked(write);
|
|
16
22
|
const stringify = (value, depth) => {
|
|
17
23
|
const json = JSON.stringify(value, null, pretty ? 2 : undefined);
|
|
18
24
|
return pretty ? json.replaceAll('\n', `\n${' '.repeat(depth)}`) : json;
|
|
@@ -34,5 +40,20 @@ export async function writeInventory(inventory, pretty, write) {
|
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
await emit(`${newline(0)}}`);
|
|
37
|
-
await
|
|
43
|
+
await flush();
|
|
44
|
+
}
|
|
45
|
+
/** Writes an inventory as JSON Lines: one line per record, each marked by `type`, as `InventoryLine` describes. */
|
|
46
|
+
export async function writeInventoryLines(inventory, write) {
|
|
47
|
+
const { emit, flush } = chunked(write);
|
|
48
|
+
const line = (type, record) => emit(`${JSON.stringify({ type, ...record })}\n`);
|
|
49
|
+
const { schemaVersion, tool, framework, frameworkVersion } = inventory;
|
|
50
|
+
await line('inventory', { schemaVersion, tool, framework, frameworkVersion });
|
|
51
|
+
for (const suite of inventory.suites)
|
|
52
|
+
await line('suite', suite);
|
|
53
|
+
for (const test of inventory.tests)
|
|
54
|
+
await line('test', test);
|
|
55
|
+
for (const diagnostic of inventory.diagnostics)
|
|
56
|
+
await line('diagnostic', diagnostic);
|
|
57
|
+
await line('summary', inventory.summary);
|
|
58
|
+
await flush();
|
|
38
59
|
}
|
package/dist/model.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface StateSetting {
|
|
|
12
12
|
/** The reason could not be worked out to a string, so `reason` holds its source text. */
|
|
13
13
|
hasDynamicReason: boolean;
|
|
14
14
|
line: number;
|
|
15
|
+
/**
|
|
16
|
+
* Set by code in a test or hook body, which runs only when the test does: a state set while the file loads, on the
|
|
17
|
+
* test or a describe around it, keeps the test from getting there.
|
|
18
|
+
*/
|
|
19
|
+
afterLoad?: boolean;
|
|
15
20
|
}
|
|
16
21
|
export interface Span {
|
|
17
22
|
start: number;
|
|
@@ -64,6 +69,8 @@ export interface TestDeclaration extends Declaration {
|
|
|
64
69
|
export interface SuiteDeclaration extends Declaration {
|
|
65
70
|
kind: 'describe';
|
|
66
71
|
isShuffled: boolean;
|
|
72
|
+
/** What the describe passes down to the calls inside it, such as Vitest's `skip` option; null for nothing. */
|
|
73
|
+
passesDown: unknown;
|
|
67
74
|
}
|
|
68
75
|
export interface Condition {
|
|
69
76
|
/** `always` and `never` are conditions that could be worked out from the file. */
|
|
@@ -92,9 +99,15 @@ export type ApiCall = TestDeclaration | SuiteDeclaration | {
|
|
|
92
99
|
kind: 'annotations';
|
|
93
100
|
items: Annotation[];
|
|
94
101
|
problems: Problem[];
|
|
95
|
-
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A call the runner refuses. When it is shaped like a declaration, as `describe.sequential('title', fn)` is in Vitest
|
|
105
|
+
* 5, `declaration` is what it would declare, so that its tests are listed as not loaded rather than missing.
|
|
106
|
+
*/
|
|
107
|
+
| {
|
|
96
108
|
kind: 'invalid';
|
|
97
109
|
problem: Problem;
|
|
110
|
+
declaration?: TestDeclaration | SuiteDeclaration;
|
|
98
111
|
} | {
|
|
99
112
|
kind: 'other';
|
|
100
113
|
};
|
|
@@ -105,6 +118,11 @@ export interface CallContext {
|
|
|
105
118
|
inTest: boolean;
|
|
106
119
|
/** Inside a test or hook body, where calls don't declare tests. */
|
|
107
120
|
inBody: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* What the describe the call is registered in passes down, as the framework returned it: the describe around a
|
|
123
|
+
* helper's call, not where the helper is written. Null outside any describe.
|
|
124
|
+
*/
|
|
125
|
+
passedDown: unknown;
|
|
108
126
|
/**
|
|
109
127
|
* What a function declared in the file, or imported from a local module, returns, for a call on its result such as
|
|
110
128
|
* `conditionalTest({ min: 22 })('title', fn)`; null when the name is not a function the scan can find.
|
|
@@ -119,6 +137,8 @@ export interface Returns {
|
|
|
119
137
|
expression: Expression;
|
|
120
138
|
scopes: readonly Node[];
|
|
121
139
|
}[] | null;
|
|
140
|
+
/** What the functions the values call return, looked up in the file that writes the function. */
|
|
141
|
+
returnsOf: CallContext['returnsOf'];
|
|
122
142
|
}
|
|
123
143
|
/** A module read to see whether it exports a custom `test` object. */
|
|
124
144
|
export interface ModuleSource {
|
|
@@ -140,8 +160,8 @@ export interface Adapter {
|
|
|
140
160
|
classify(call: CallExpression, context: CallContext): ApiCall | null;
|
|
141
161
|
/**
|
|
142
162
|
* The test or describe function a value holds, in the adapter's own terms, as `it.skip` in `return it.skip`; null
|
|
143
|
-
* when it holds none. A function the value calls is
|
|
163
|
+
* when it holds none. A function the value calls is followed to what it returns through `returnsOf`.
|
|
144
164
|
*/
|
|
145
|
-
testFunction(expression: Expression, scopes: readonly Node[]): unknown;
|
|
165
|
+
testFunction(expression: Expression, scopes: readonly Node[], returnsOf: CallContext['returnsOf']): unknown;
|
|
146
166
|
}
|
|
147
167
|
export {};
|
package/dist/records.js
CHANGED
|
@@ -2,12 +2,14 @@ import { REJECTS_FILE, toDiagnostic } from "./diagnostics.js";
|
|
|
2
2
|
import { problem } from "./frameworks/common.js";
|
|
3
3
|
import { hashNodes, shortHash } from "./hash.js";
|
|
4
4
|
/**
|
|
5
|
-
* The state the runner applies, from the candidates nearest the test first. An unconditional skip, fixme or todo
|
|
6
|
-
*
|
|
5
|
+
* The state the runner applies, from the candidates nearest the test first. An unconditional skip, fixme or todo wins
|
|
6
|
+
* over an unconditional expected failure, since the test doesn't run, and one set while the file loads wins over one
|
|
7
|
+
* in a test or hook body, which then never runs; then the nearest conditional state.
|
|
7
8
|
*/
|
|
8
9
|
function chooseState(candidates) {
|
|
9
10
|
const always = candidates.filter((candidate) => candidate.setting.condition === null);
|
|
10
|
-
|
|
11
|
+
const stops = always.filter((candidate) => candidate.setting.state !== 'expectedToFail');
|
|
12
|
+
return (stops.find((candidate) => !candidate.setting.afterLoad) ?? stops.at(0) ?? always.at(0) ?? candidates.at(0) ?? null);
|
|
11
13
|
}
|
|
12
14
|
/** The scope and the scopes around it, innermost first. */
|
|
13
15
|
function outward(scope) {
|
package/dist/types.d.ts
CHANGED
|
@@ -49,6 +49,22 @@ export interface Inventory {
|
|
|
49
49
|
tests: TestRecord[];
|
|
50
50
|
diagnostics: Diagnostic[];
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* One line of the JSON Lines output (`--format jsonl`): the inventory with each record on its own line, marked by
|
|
54
|
+
* `type`. The `inventory` line comes first, then the suites, tests and diagnostics in the order of the JSON output,
|
|
55
|
+
* and the `summary` line last.
|
|
56
|
+
*/
|
|
57
|
+
export type InventoryLine = ({
|
|
58
|
+
type: 'inventory';
|
|
59
|
+
} & Pick<Inventory, 'schemaVersion' | 'tool' | 'framework' | 'frameworkVersion'>) | ({
|
|
60
|
+
type: 'suite';
|
|
61
|
+
} & SuiteRecord) | ({
|
|
62
|
+
type: 'test';
|
|
63
|
+
} & TestRecord) | ({
|
|
64
|
+
type: 'diagnostic';
|
|
65
|
+
} & Diagnostic) | ({
|
|
66
|
+
type: 'summary';
|
|
67
|
+
} & Summary);
|
|
52
68
|
/** @inline */
|
|
53
69
|
export interface Tool {
|
|
54
70
|
name: 'test-inventory';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-inventory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Test metadata from source code, not from test results.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"testing",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"lint": "eslint .",
|
|
72
72
|
"lint:fix": "eslint . --fix",
|
|
73
73
|
"format": "prettier --write . --ignore-path .gitignore --ignore-path .prettierignore --ignore-path .git/info/exclude",
|
|
74
|
-
"format:check": "
|
|
74
|
+
"format:check": "node scripts/format-check.ts",
|
|
75
75
|
"docs:api": "typedoc",
|
|
76
76
|
"docs:dev": "npm run docs:api && vitepress dev docs",
|
|
77
77
|
"docs:build": "npm run docs:api && vitepress build docs",
|