test-inventory 0.2.1 → 0.4.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.
@@ -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, destructuredNames, 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)
@@ -37,6 +37,11 @@ const HOOKS = new Map([
37
37
  ['afterAll', null],
38
38
  ['aroundAll', null],
39
39
  ]);
40
+ function isHook(name) {
41
+ return HOOKS.has(name);
42
+ }
43
+ /** First members of chains that never declare a test, even when Vitest refuses the rest of the chain. */
44
+ const NOT_DECLARING = new Set([...HOOKS.keys(), ...DERIVATIONS]);
40
45
  /** First members that show a name imported from a local file is used as Vitest's `test`. */
41
46
  const TEST_MEMBERS = new Set([...MODIFIERS.test, ...MODIFIERS.describe, ...TABLES, ...CONDITIONS, ...KINDS.keys()]);
42
47
  /** Calls Vitest moves to the top of the file before running it: `vi.mock`, `vi.unmock` and `vi.hoisted`. */
@@ -89,6 +94,23 @@ function hoistedValue(call, parents) {
89
94
  const parent = parents.at(-1);
90
95
  return parent?.type === 'AwaitExpression' ? parent : call;
91
96
  }
97
+ /** Keys of the test context that Vitest provides itself, with the version that added each; they are not fixtures. */
98
+ const CONTEXT_KEYS = new Map([
99
+ ['task', [3, 0]],
100
+ ['expect', [3, 0]],
101
+ ['skip', [3, 0]],
102
+ ['onTestFailed', [3, 0]],
103
+ ['onTestFinished', [3, 0]],
104
+ ['annotate', [3, 2]],
105
+ ['signal', [3, 2]],
106
+ ['bench', [5, 0]],
107
+ ]);
108
+ function fixturesOf(fn, index, version) {
109
+ return destructuredNames(fn, index).filter((name) => {
110
+ const since = CONTEXT_KEYS.get(name);
111
+ return !since || !atLeast(version, ...since);
112
+ });
113
+ }
92
114
  /** The names a file binds to Vitest's `test`, `describe`, hooks and namespace. */
93
115
  class VitestBindings {
94
116
  kinds = new Map();
@@ -102,9 +124,12 @@ class VitestBindings {
102
124
  /** Names imported from other modules, which may be test objects there. */
103
125
  imported;
104
126
  follow;
127
+ /** The module's functions and the variables they may build test objects in; see `ModuleFunctions`. */
128
+ functions;
105
129
  constructor(program, version, follow) {
106
130
  this.imported = moduleImports(program, (source) => source === 'vitest' || MISMATCHED_SOURCES.has(source));
107
131
  this.follow = follow;
132
+ this.functions = new ModuleFunctions(program, this.imported, follow);
108
133
  for (const statement of program.body) {
109
134
  if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type')
110
135
  continue;
@@ -139,30 +164,49 @@ class VitestBindings {
139
164
  this.declarations.add(declarator);
140
165
  return bound;
141
166
  });
167
+ this.functions.keep(declarators.filter((declarator) => !this.declarations.has(declarator)));
142
168
  }
143
169
  isTestName(name) {
144
- return this.kinds.has(name) || this.importsTestObject(name);
170
+ return this.kinds.has(name) || this.importsTestObject(name) || this.holdsTestObject(name);
145
171
  }
146
- isTestObject(expression) {
172
+ /**
173
+ * `followsCalls` is cleared where a call is not followed to what it returns: while the names are bound, so that only
174
+ * a value asked about is followed, as following reads other modules, and in the scanned file, whose calls the walk
175
+ * follows in their scopes.
176
+ */
177
+ isTestObject(expression, followsCalls = true) {
147
178
  const node = unwrap(expression);
148
- if (node.type === 'Identifier')
149
- return this.kinds.get(node.name) === 'test' || this.importsTestObject(node.name);
179
+ if (node.type === 'Identifier') {
180
+ const { name } = node;
181
+ return (this.kinds.get(name) === 'test' || this.importsTestObject(name) || (followsCalls && this.holdsTestObject(name)));
182
+ }
150
183
  if (node.type === 'MemberExpression') {
151
184
  return KINDS.get(propertyName(node) ?? '') === 'test' && this.isNamespace(node.object);
152
185
  }
153
186
  // `process.env.CI ? vitestTest : skippedTest` is a test object either way.
154
187
  if (node.type === 'ConditionalExpression') {
155
- return this.isTestObject(node.consequent) || this.isTestObject(node.alternate);
188
+ return this.isTestObject(node.consequent, followsCalls) || this.isTestObject(node.alternate, followsCalls);
156
189
  }
157
190
  if (node.type !== 'CallExpression')
158
191
  return false;
159
192
  const callee = unwrap(node.callee);
193
+ // `defineBrokerTest()`, a function that returns a test object.
194
+ if (callee.type === 'Identifier')
195
+ return followsCalls && this.returnsTestObject(callee.name);
160
196
  return (callee.type === 'MemberExpression' &&
161
197
  DERIVATIONS.has(propertyName(callee) ?? '') &&
162
- this.isTestObject(callee.object));
198
+ this.isTestObject(callee.object, followsCalls));
199
+ }
200
+ returnsTestObject(name) {
201
+ return this.functions.returnsTestObject(name, (expression) => this.isTestObject(expression));
202
+ }
203
+ holdsTestObject(name) {
204
+ return this.functions.holdsTestObject(name, (expression) => this.isTestObject(expression));
163
205
  }
164
- reexports(source, imported) {
165
- return source === 'vitest' ? KINDS.has(imported) : this.follow(source, imported);
206
+ reexports(source, imported, exported) {
207
+ if (source !== 'vitest')
208
+ return this.follow(source, imported, exported);
209
+ return exported === 'test object' && KINDS.has(imported);
166
210
  }
167
211
  /** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
168
212
  importsTestObject(name) {
@@ -178,7 +222,7 @@ class VitestBindings {
178
222
  if (kind) {
179
223
  this.kinds.set(local, kind);
180
224
  }
181
- else if (HOOKS.has(imported)) {
225
+ else if (isHook(imported)) {
182
226
  this.hooks.set(local, imported);
183
227
  }
184
228
  else if (imported === 'bench' && atLeast(version, 5, 0)) {
@@ -189,7 +233,7 @@ class VitestBindings {
189
233
  const { id, init } = declarator;
190
234
  if (!init)
191
235
  return false;
192
- if (id.type === 'Identifier' && this.isTestObject(init)) {
236
+ if (id.type === 'Identifier' && this.isTestObject(init, false)) {
193
237
  this.kinds.set(id.name, 'test');
194
238
  return true;
195
239
  }
@@ -203,7 +247,7 @@ class VitestBindings {
203
247
  const kind = KINDS.get(key ?? '');
204
248
  if (kind)
205
249
  this.kinds.set(property.value.name, kind);
206
- if (key !== null && HOOKS.has(key))
250
+ if (key !== null && isHook(key))
207
251
  this.hooks.set(property.value.name, key);
208
252
  }
209
253
  return true;
@@ -231,10 +275,11 @@ export class VitestAdapter {
231
275
  // A test object the file builds from an imported one, as in `const it = base.extend({...})`, is followed quietly:
232
276
  // a call through the import itself is what reports an import that can't be followed.
233
277
  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);
278
+ this.bindings = new VitestBindings(parsed.program, file.version, (source, imported, exported = 'test object') => {
279
+ const key = `${source}\0${exported}\0${imported}`;
280
+ if (!followed.has(key)) {
281
+ followed.set(key, followModule(source, imported, readModule, this.framework(), exported).exported);
282
+ }
238
283
  return followed.get(key) === true;
239
284
  });
240
285
  this.problems = [...this.bindings.problems];
@@ -259,8 +304,8 @@ export class VitestAdapter {
259
304
  this.contextSkip(call, context) ??
260
305
  this.chainCall(call, context));
261
306
  }
262
- testFunction(expression, scopes) {
263
- return this.aliasOf(expression, { scopes, inBody: false, returnsOf: null });
307
+ testFunction(expression, scopes, returnsOf) {
308
+ return this.aliasOf(expression, { scopes, inBody: false, returnsOf });
264
309
  }
265
310
  /** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
266
311
  benchCall(call, context) {
@@ -282,7 +327,7 @@ export class VitestAdapter {
282
327
  if (!resolved)
283
328
  return null;
284
329
  const { kind, links } = resolved;
285
- if (kind === 'test' && links.length === 1 && HOOKS.has(links[0].name))
330
+ if (kind === 'test' && links.length === 1 && isHook(links[0].name))
286
331
  return this.hook(call, links[0].name);
287
332
  if (links.length === 1 && DERIVATIONS.has(links[0].name))
288
333
  return { kind: 'other' };
@@ -290,29 +335,24 @@ export class VitestAdapter {
290
335
  if (last && (TABLES.has(last.name) || CONDITIONS.has(last.name)) && !last.args && !last.template) {
291
336
  return { kind: 'other' };
292
337
  }
293
- for (const link of links) {
338
+ const callee = snippet(text(this.file, call.callee));
339
+ const removed = (link) => link.name === 'sequential' && atLeast(this.file.version, 5, 0);
340
+ const valid = (link) => {
294
341
  const called = link.args !== null || link.template !== null;
295
- const valid = MODIFIERS[kind].has(link.name)
296
- ? !called
297
- : TABLES.has(link.name)
298
- ? called
299
- : CONDITIONS.has(link.name) && link.args !== null;
300
- if (!valid) {
301
- const callee = snippet(text(this.file, call.callee));
302
- return {
303
- kind: 'invalid',
304
- problem: problem('invalid-chain', call, `${callee} is not part of Vitest's API.`, callee),
305
- };
306
- }
307
- if (link.name === 'sequential' && atLeast(this.file.version, 5, 0)) {
308
- const callee = snippet(text(this.file, call.callee));
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);
342
+ if (MODIFIERS[kind].has(link.name))
343
+ return !called && !removed(link);
344
+ return TABLES.has(link.name) ? called : CONDITIONS.has(link.name) && link.args !== null;
345
+ };
346
+ const invalid = links.find((link) => !valid(link));
347
+ if (!invalid)
348
+ return this.declaration(call, kind, links, context);
349
+ const refusal = removed(invalid)
350
+ ? problem('removed-api', call, `sequential was removed in Vitest 5; ${callee}(...) fails.`, callee)
351
+ : problem('invalid-chain', call, `${callee} is not part of Vitest's API.`, callee);
352
+ if (NOT_DECLARING.has(links[0].name))
353
+ return { kind: 'invalid', problem: refusal };
354
+ // Which of its parts the chain means is not known, so it declares a plain test or describe.
355
+ return refused(call, refusal, context, () => this.declaration(call, kind, [], context));
316
356
  }
317
357
  /**
318
358
  * 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 +442,7 @@ export class VitestAdapter {
402
442
  */
403
443
  returned(call, links, args, lookup) {
404
444
  const callee = unwrap(call.callee);
405
- if (callee.type !== 'Identifier' || !lookup.returnsOf)
445
+ if (callee.type !== 'Identifier')
406
446
  return null;
407
447
  const returns = lookup.returnsOf(callee.name, lookup.scopes);
408
448
  if (returns)
@@ -472,27 +512,28 @@ export class VitestAdapter {
472
512
  return null;
473
513
  const declaration = this.file.values.declarationOf(callee.name, context.scopes);
474
514
  const hook = declaration === null
475
- ? HOOKS.has(callee.name) && callee.name
515
+ ? isHook(callee.name) && callee.name
476
516
  : this.bindings.declarations.has(declaration) && this.bindings.hooks.get(callee.name);
477
517
  return hook ? this.hook(call, hook) : null;
478
518
  }
479
519
  hook(call, name) {
480
520
  const fn = findFunction(call.arguments);
481
521
  const index = HOOKS.get(name);
482
- return { kind: 'hook', fn, testContext: index === null ? null : skipNames(fn, index) };
522
+ return {
523
+ kind: 'hook',
524
+ name,
525
+ fn,
526
+ testContext: index === null ? null : contextNames(fn, index),
527
+ fixtures: fixturesOf(fn, index, this.file.version),
528
+ };
483
529
  }
484
530
  contextSkip(call, context) {
485
531
  const names = context.testContext;
486
532
  if (!names)
487
533
  return null;
488
- const callee = unwrap(call.callee);
489
- const isSkip = callee.type === 'Identifier'
490
- ? names.functions.has(callee.name)
491
- : callee.type === 'MemberExpression' &&
492
- propertyName(callee) === 'skip' &&
493
- callee.object.type === 'Identifier' &&
494
- names.objects.has(callee.object.name);
495
- if (!isSkip)
534
+ if (atLeast(this.file.version, 3, 2) && callsContext(call, names, 'annotate'))
535
+ return this.annotate(call, context);
536
+ if (!callsContext(call, names, 'skip'))
496
537
  return null;
497
538
  const [first, second] = call.arguments;
498
539
  // skip(), skip(note) or skip(condition, note); before Vitest 3.1 the first argument is always the note.
@@ -507,6 +548,29 @@ export class VitestAdapter {
507
548
  problems: reason.problem ? [reason.problem] : [],
508
549
  };
509
550
  }
551
+ /**
552
+ * `annotate(message, type?)` from the test context (3.2), as an annotation of type `notice` when no type is given; the
553
+ * second argument may also be an attachment.
554
+ */
555
+ annotate(call, context) {
556
+ const args = call.arguments.map((arg) => arg.type === 'SpreadElement' ? null : this.file.values.resolve(arg, context.scopes));
557
+ const [message, second] = args;
558
+ // A second argument that isn't a string is an attachment, given in place of the type.
559
+ let type = 'notice';
560
+ if (args.length > 1 && !second?.ok)
561
+ type = null;
562
+ else if (second?.ok && typeof second.value === 'string')
563
+ type = second.value;
564
+ if (message?.ok && typeof message.value === 'string' && type !== null) {
565
+ const annotation = { type, description: message.value, lineStart: this.file.source.line(call.start) };
566
+ return { kind: 'annotations', items: [annotation], problems: [] };
567
+ }
568
+ const source = snippet(text(this.file, call));
569
+ const unresolved = [message, second].find((item) => item && !item.ok);
570
+ const from = unresolved && !unresolved.ok ? unresolved.importedFrom : null;
571
+ const missing = problem('unresolved-annotation', call, `Annotation ${source}${origin(from)} could not be worked out; it is missing.`, source);
572
+ return { kind: 'annotations', items: [], problems: [missing] };
573
+ }
510
574
  declaration(call, kind, links, context) {
511
575
  const args = call.arguments;
512
576
  const callee = snippet(text(this.file, call.callee));
@@ -528,6 +592,8 @@ export class VitestAdapter {
528
592
  // (title, fn, timeout), (title, options, fn) or the removed (title, fn, options)
529
593
  let options;
530
594
  let body;
595
+ let timeout;
596
+ let refusal = null;
531
597
  const [second, third] = [args.at(1), args.at(2)];
532
598
  if (args.some((arg) => arg.type === 'SpreadElement')) {
533
599
  // With spread arguments, as in test(...args), the function and options are unknown.
@@ -540,29 +606,63 @@ export class VitestAdapter {
540
606
  body = second;
541
607
  if (third !== undefined && this.isOptions(third, undefined, context)) {
542
608
  if (atLeast(this.file.version, 4, 0)) {
543
- return {
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
- };
609
+ 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
610
  }
548
611
  options = third;
549
612
  }
613
+ else if (third === undefined ||
614
+ isFunctionNode(second) ||
615
+ namedFunction(this.file, context.scopes, second) ||
616
+ this.file.values.resolve(third, context.scopes).ok) {
617
+ // With imported options and a body passed by name, either could be the body, so the third is no timeout.
618
+ timeout = third;
619
+ }
550
620
  }
551
621
  const fn = isFunctionNode(body) ? body : null;
552
622
  const read = this.readDeclarationOptions(options, context, problems);
553
- const flags = new Set([
554
- ...links.filter((link) => MODIFIERS[kind].has(link.name)).map((link) => link.name),
555
- ...read.flags,
556
- ]);
557
- const conditions = [
558
- ...links.filter((link) => CONDITIONS.has(link.name)).map((link) => this.linkCondition(link, context)),
559
- ...read.conditions,
560
- ];
623
+ if (timeout) {
624
+ const node = timeout;
625
+ const option = { node, resolved: this.file.values.resolve(node, context.scopes) };
626
+ const value = readCount(this.file, 'timeout', option, problems);
627
+ if (value !== null)
628
+ read.details.run.timeout = value;
629
+ }
630
+ const chain = new Set(links.filter((link) => MODIFIERS[kind].has(link.name)).map((link) => link.name));
631
+ const flags = new Set(chain);
632
+ let conditions = links.filter((link) => CONDITIONS.has(link.name)).map((link) => this.linkCondition(link, context));
633
+ // A test's own options win over its chain, so `test.skip('title', { skip: false }, fn)` runs. The chain wins for a
634
+ // test's mode, and for everything on a describe.
635
+ for (const [flag, value] of read.flags) {
636
+ if (value)
637
+ flags.add(flag);
638
+ else if (kind === 'test' && flag !== 'sequential')
639
+ flags.delete(flag);
640
+ }
641
+ if (read.skip && kind === 'test') {
642
+ flags.delete('skip');
643
+ conditions = [read.skip];
644
+ }
645
+ else if (read.skip) {
646
+ conditions.push(read.skip);
647
+ }
648
+ // Vitest passes a describe's options down to the tests and describes inside, where their own options and `only`
649
+ // win. A `skip` passed down skips them, and turns todo into skip, as skip goes before todo.
650
+ const line = this.file.source.line(call.start);
651
+ const around = context.passedDown;
652
+ const passed = read.skip ? null : around;
653
+ const skips = passed?.condition.applies === 'always' && !flags.has('only');
561
654
  const table = links.find((link) => TABLES.has(link.name)) ?? null;
562
655
  const isTodo = flags.has('todo') || body === undefined;
563
- const states = this.states(flags, conditions, isTodo, this.file.source.line(call.start));
564
- // `{ concurrent: false }` is how Vitest 5 turns parallel off, in place of `sequential`.
565
- const parallel = read.concurrent ?? flags.has('concurrent');
656
+ const states = this.states(flags, conditions, isTodo && !skips, line);
657
+ const skipped = () => states.some((setting) => setting.state === 'skip' && !setting.condition);
658
+ // The skip is set where the describe that passes it down sets it. A skipped describe around already skips what is
659
+ // inside, as it does when its own `only` beats its skip; a todo describe gets the skip itself.
660
+ if (skips && !skipped() && (!passed.covered || (kind === 'describe' && isTodo))) {
661
+ states.push({ state: 'skip', condition: null, reason: null, hasDynamicReason: false, line: passed.line });
662
+ }
663
+ // The chain's mode wins over the options'. `{ concurrent: false }` is how Vitest 5 turns parallel off, in place of
664
+ // `sequential`.
665
+ const parallel = chain.has('concurrent') || (!chain.has('sequential') && read.concurrent === true);
566
666
  const mode = parallel
567
667
  ? 'parallel'
568
668
  : flags.has('sequential') || read.concurrent === false
@@ -580,18 +680,27 @@ export class VitestAdapter {
580
680
  caseCount: table ? this.caseCount(call, table, context, problems) : null,
581
681
  problems,
582
682
  };
683
+ let declared;
583
684
  if (kind === 'describe') {
584
685
  // A table's cases are passed instead.
585
686
  const first = table ? null : fn?.params.at(0);
586
687
  if (fn && first?.type === 'Identifier')
587
688
  this.suiteParameters.set(fn, first.name);
588
- return { kind: 'describe', ...base, isShuffled: flags.has('shuffle') };
689
+ const own = read.skip ? { condition: read.skip, line } : around;
690
+ const passesDown = own && { ...own, covered: (around?.covered ?? false) || skipped() };
691
+ declared = { kind: 'describe', ...base, isShuffled: flags.has('shuffle'), passesDown };
589
692
  }
590
- return {
591
- kind: 'test',
592
- ...base,
593
- testContext: skipNames(fn, table?.name === 'for' ? 1 : table ? null : 0),
594
- };
693
+ else {
694
+ const index = table?.name === 'for' ? 1 : table ? null : 0;
695
+ declared = {
696
+ kind: 'test',
697
+ ...base,
698
+ testContext: contextNames(base.hashed, index),
699
+ fixtures: fixturesOf(base.hashed, index, this.file.version),
700
+ usesBench: atLeast(this.file.version, 5, 0) && destructuredNames(base.hashed, index).includes('bench'),
701
+ };
702
+ }
703
+ return refusal ? refused(call, refusal, context, () => declared) : declared;
595
704
  }
596
705
  /** Whether an argument holds options: an object, a name for one, or anything but a function before the body. */
597
706
  isOptions(node, next, context) {
@@ -637,11 +746,11 @@ export class VitestAdapter {
637
746
  return fails;
638
747
  }
639
748
  readDeclarationOptions(node, context, problems) {
640
- const details = { tags: [], annotations: [], locks: [], meta: null };
749
+ const details = { tags: [], annotations: [], locks: [], meta: null, run: {} };
641
750
  const result = {
642
- flags: [],
751
+ flags: new Map(),
643
752
  concurrent: null,
644
- conditions: [],
753
+ skip: null,
645
754
  details,
646
755
  };
647
756
  if (!node)
@@ -657,15 +766,15 @@ export class VitestAdapter {
657
766
  else if (option?.resolved.ok && flag === 'concurrent') {
658
767
  result.concurrent = Boolean(option.resolved.value);
659
768
  }
660
- else if (option?.resolved.ok && option.resolved.value) {
661
- result.flags.push(flag);
769
+ else if (option?.resolved.ok) {
770
+ result.flags.set(flag, Boolean(option.resolved.value));
662
771
  }
663
772
  }
664
773
  const skip = options.values.get('skip');
665
774
  if (skip) {
666
- result.conditions.push(skip.resolved.ok
775
+ result.skip = skip.resolved.ok
667
776
  ? { applies: skip.resolved.value ? 'always' : 'never', text: null }
668
- : { applies: 'sometimes', text: snippet(text(this.file, skip.node)) });
777
+ : { applies: 'sometimes', text: snippet(text(this.file, skip.node)) };
669
778
  }
670
779
  const tags = options.values.get('tags');
671
780
  if (tags && atLeast(this.file.version, 4, 1)) {
@@ -680,8 +789,46 @@ export class VitestAdapter {
680
789
  const meta = options.values.get('meta');
681
790
  if (meta)
682
791
  details.meta = this.meta(meta, context, problems);
792
+ for (const key of ['timeout', 'repeats']) {
793
+ const option = options.values.get(key);
794
+ const value = option ? readCount(this.file, key, option, problems) : null;
795
+ if (value !== null)
796
+ details.run[key] = value;
797
+ }
798
+ const retry = options.values.get('retry');
799
+ if (retry)
800
+ this.retry(retry, details, context, problems);
683
801
  return result;
684
802
  }
803
+ /** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`; the runner ignores an object before. */
804
+ retry(option, details, context, problems) {
805
+ const node = unwrap(option.node);
806
+ if (node.type !== 'ObjectExpression') {
807
+ const count = readCount(this.file, 'retry', option, problems);
808
+ if (count !== null)
809
+ details.run.retry = { count, delay: null, condition: null };
810
+ return;
811
+ }
812
+ if (!atLeast(this.file.version, 4, 1))
813
+ return;
814
+ const values = readOptions(this.file, context.scopes, node);
815
+ problems.push(...unresolvedOptions(this.file, values, 'the retry is not applied'));
816
+ const read = (key) => {
817
+ const value = values.values.get(key);
818
+ return value ? readCount(this.file, `retry.${key}`, value, problems) : undefined;
819
+ };
820
+ const count = read('count');
821
+ const delay = read('delay');
822
+ if (count === null)
823
+ return;
824
+ const condition = values.values.get('condition');
825
+ details.run.retry = {
826
+ // Vitest retries 0 times when the object has no count.
827
+ count: count ?? 0,
828
+ delay: delay ?? null,
829
+ condition: condition ? snippet(text(this.file, condition.node)) : null,
830
+ };
831
+ }
685
832
  meta(option, context, problems) {
686
833
  const entries = readOptions(this.file, context.scopes, option.node);
687
834
  problems.push(...unresolvedOptions(this.file, entries, 'its meta values are missing'));
@@ -738,19 +885,32 @@ function templateRows(template) {
738
885
  function isMetaValue(value) {
739
886
  return value !== undefined;
740
887
  }
741
- function skipNames(fn, index) {
888
+ function contextNames(fn, index) {
742
889
  const parameter = index === null ? undefined : fn?.params[index];
743
890
  if (!parameter)
744
891
  return null;
745
- const names = { objects: new Set(), functions: new Set() };
892
+ const names = { objects: new Set(), skip: new Set(), annotate: new Set() };
746
893
  if (parameter.type === 'Identifier')
747
894
  names.objects.add(parameter.name);
748
895
  if (parameter.type === 'ObjectPattern') {
749
896
  for (const property of parameter.properties) {
750
- if (property.type === 'Property' && propertyKey(property) === 'skip' && property.value.type === 'Identifier') {
751
- names.functions.add(property.value.name);
897
+ const key = property.type === 'Property' ? propertyKey(property) : null;
898
+ if ((key === 'skip' || key === 'annotate') &&
899
+ property.type === 'Property' &&
900
+ property.value.type === 'Identifier') {
901
+ names[key].add(property.value.name);
752
902
  }
753
903
  }
754
904
  }
755
905
  return names;
756
906
  }
907
+ /** Whether a call is `name(...)` for a context function taken out of the context, or `context.name(...)`. */
908
+ function callsContext(call, names, member) {
909
+ const callee = unwrap(call.callee);
910
+ return callee.type === 'Identifier'
911
+ ? names[member].has(callee.name)
912
+ : callee.type === 'MemberExpression' &&
913
+ propertyName(callee) === member &&
914
+ callee.object.type === 'Identifier' &&
915
+ names.objects.has(callee.object.name);
916
+ }
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, FileRecord, Framework, HookCounts, Inventory, InventoryLine, MetaValue, StateSource, SuiteRecord, Summary, TestRecord, TestState, Tool, } from './types.ts';
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { scan } from "./scan.js";
2
2
  export { scanSource } from "./scan-source.js";
3
+ export { filterInventory, pickTestFields } from "./filter.js";
package/dist/inventory.js CHANGED
@@ -10,6 +10,7 @@ export function createInventory(framework, frameworkVersion, fileCount, files, p
10
10
  framework,
11
11
  frameworkVersion,
12
12
  summary: summarize(fileCount, suites, tests, diagnostics),
13
+ files: files.map((file) => file.file),
13
14
  suites,
14
15
  tests,
15
16
  diagnostics,
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: Inventory, pretty: boolean, write: (chunk: string) => Promise<void>): Promise<void>;
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
- let buffer = '';
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,22 @@ export async function writeInventory(inventory, pretty, write) {
34
40
  }
35
41
  }
36
42
  await emit(`${newline(0)}}`);
37
- await write(buffer);
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 file of inventory.files)
52
+ await line('file', file);
53
+ for (const suite of inventory.suites)
54
+ await line('suite', suite);
55
+ for (const test of inventory.tests)
56
+ await line('test', test);
57
+ for (const diagnostic of inventory.diagnostics)
58
+ await line('diagnostic', diagnostic);
59
+ await line('summary', inventory.summary);
60
+ await flush();
38
61
  }