ucn 5.5.3 → 5.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.
Files changed (77) hide show
  1. package/.claude/skills/ucn/SKILL.md +37 -12
  2. package/.claude/skills/ucn/references/commands.md +18 -14
  3. package/.claude/skills/ucn/references/resolution-rules.md +4 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +4 -4
  5. package/CHANGELOG.md +485 -0
  6. package/README.md +66 -29
  7. package/cli/flags.js +184 -0
  8. package/cli/index.js +137 -324
  9. package/core/account.js +44 -87
  10. package/core/analysis.js +274 -108
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +14 -0
  13. package/core/cache.js +103 -16
  14. package/core/call-fit.js +130 -0
  15. package/core/callers.js +353 -71
  16. package/core/capture.js +363 -0
  17. package/core/change-kinds.js +1107 -0
  18. package/core/check.js +1150 -139
  19. package/core/command-contracts.js +8 -7
  20. package/core/constructed-class.js +1195 -0
  21. package/core/contract-membership.js +142 -0
  22. package/core/deadcode.js +455 -106
  23. package/core/deletion.js +421 -0
  24. package/core/discovery.js +104 -9
  25. package/core/entrypoint-toml.js +110 -0
  26. package/core/entrypoints.js +12 -6
  27. package/core/evidence-bands.js +68 -0
  28. package/core/evidence-bands.json +44 -0
  29. package/core/execute.js +172 -161
  30. package/core/hot-summary.js +241 -0
  31. package/core/index-ir.js +5 -3
  32. package/core/ir.js +6 -2
  33. package/core/member-signatures.js +789 -0
  34. package/core/outgoing-check.js +116 -0
  35. package/core/output/analysis-ext.js +20 -10
  36. package/core/output/analysis.js +98 -60
  37. package/core/output/check.js +101 -37
  38. package/core/output/doctor.js +34 -1
  39. package/core/output/find.js +2 -1
  40. package/core/output/lines.js +63 -3
  41. package/core/output/public.js +29 -11
  42. package/core/output/refactoring.js +32 -0
  43. package/core/output/reporting.js +19 -2
  44. package/core/output/search.js +14 -2
  45. package/core/output/tracing.js +36 -8
  46. package/core/output-budget.js +146 -61
  47. package/core/parallel-build.js +3 -3
  48. package/core/project.js +106 -23
  49. package/core/provenance-type.js +12 -5
  50. package/core/public-command.js +35 -9
  51. package/core/python-binding-identity.js +707 -0
  52. package/core/python-modules.js +6 -0
  53. package/core/reflection.js +248 -31
  54. package/core/registry.js +623 -72
  55. package/core/reporting.js +69 -34
  56. package/core/review-items.js +75 -0
  57. package/core/runtime-entrypoints.js +159 -0
  58. package/core/search.js +148 -164
  59. package/core/shared.js +0 -45
  60. package/core/snapshot.js +350 -0
  61. package/core/source-universe.js +707 -0
  62. package/core/test-units.js +3698 -0
  63. package/core/tracing.js +270 -658
  64. package/core/verify.js +63 -22
  65. package/languages/adapter.js +8 -3
  66. package/languages/c-family.js +12 -0
  67. package/languages/csharp.js +634 -0
  68. package/languages/go.js +120 -0
  69. package/languages/index.js +466 -4
  70. package/languages/java.js +712 -0
  71. package/languages/javascript.js +1498 -6
  72. package/languages/lexical-scope.js +68 -0
  73. package/languages/python.js +1708 -3
  74. package/languages/rust.js +209 -104
  75. package/languages/static-js.js +88 -0
  76. package/mcp/server.js +44 -110
  77. package/package.json +11 -5
@@ -0,0 +1,3698 @@
1
+ /**
2
+ * core/test-units.js — what a test is, where it is, and how to run it.
3
+ *
4
+ * One identity for a test, shared by `tests`, the affected-test selection
5
+ * and `check`: a UNIT is (file, suite chain, name, source extent), read from the
6
+ * declaration the language's runner collects (the language module's
7
+ * `testUnits`). A site of test code belongs to the innermost unit whose
8
+ * source range holds it; an ambiguous line alone does not select a sibling.
9
+ *
10
+ * A RUNNER is named only on evidence: the test file imports the runner's
11
+ * own module, or the nearest build manifest names it. A SELECTOR (the
12
+ * command that runs one unit, or one file) is built only for a runner whose
13
+ * selection syntax is modeled here and only when everything the command
14
+ * depends on is known; otherwise the unit is reported without a command and
15
+ * with the reason. The runner's own configuration is part of that: what
16
+ * is data is read (pytest's options and naming rules, jest's roots and
17
+ * path patterns, vitest's directory, the options a mocha or node script
18
+ * passes), a configuration written as code only where it is one object
19
+ * written out that nothing else in the file can change, and a
20
+ * configuration that can change what a command selects and is not read
21
+ * withholds the command. Nothing here executes anything.
22
+ *
23
+ * A COUNT of tests is of the units listed and not skipped. It is exact
24
+ * only where every test the runner would report is listed: a file that
25
+ * holds tests the reader does not list, or a declaration that stands for
26
+ * several of the runner's tests, makes it a lower bound (`moreTests`),
27
+ * said wherever the count is printed.
28
+ *
29
+ * A test-path file is SUPPORT when it declares no unit, declares no class
30
+ * that could inherit one, and is not named the way the runner names test
31
+ * files: helpers, fixtures, shared setup. It is listed, never counted as a
32
+ * test and never offered as something to run.
33
+ *
34
+ * State lives in the model object, created per operation: manifests can
35
+ * change between two questions asked of one long-lived index.
36
+ */
37
+
38
+ 'use strict';
39
+
40
+ const fs = require('fs');
41
+ const path = require('path');
42
+ const { langTraits, getParser, safeParse, getLanguageAdapter } = require('../languages');
43
+ const { codeUnitCompare, escapeRegExp } = require('./shared');
44
+ const { createSignatures } = require('./member-signatures');
45
+ const { csharpUsings } = require('./type-denotation');
46
+
47
+ const CONTAINER_KINDS = new Set(['class', 'struct', 'module', 'namespace', 'interface', 'impl',
48
+ 'record', 'enum', 'trait', 'object']);
49
+ const CALLABLE_KINDS = new Set(['function', 'method', 'static', 'constructor', 'classmethod',
50
+ 'getter', 'setter', 'arrow', 'generator']);
51
+
52
+ /** A shell word: bare when it needs no quoting, single-quoted otherwise. */
53
+ function shellWord(value) {
54
+ const text = String(value);
55
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(text)) return text;
56
+ return `'${text.replace(/'/g, `'\\''`)}'`;
57
+ }
58
+
59
+ function regexLiteral(text) {
60
+ return String(text).replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
61
+ }
62
+
63
+ function posixDir(relativePath) {
64
+ const dir = path.posix.dirname(String(relativePath).replace(/\\/g, '/'));
65
+ return dir === '.' ? '' : dir;
66
+ }
67
+
68
+ function relativeTo(dir, relativePath) {
69
+ const rel = String(relativePath).replace(/\\/g, '/');
70
+ if (!dir) return rel;
71
+ return rel.startsWith(dir + '/') ? rel.slice(dir.length + 1) : null;
72
+ }
73
+
74
+ /** Directories from the file's own up to the project root, nearest first. */
75
+ function ancestorDirs(relativePath) {
76
+ const out = [];
77
+ let dir = posixDir(relativePath);
78
+ for (;;) {
79
+ out.push(dir);
80
+ if (!dir) break;
81
+ dir = posixDir(dir);
82
+ }
83
+ return out;
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Manifests
88
+ // ---------------------------------------------------------------------------
89
+
90
+ function createManifests(index) {
91
+ const texts = new Map();
92
+ const listings = new Map();
93
+ const read = (dir, name) => {
94
+ const key = `${dir}\0${name}`;
95
+ if (texts.has(key)) return texts.get(key);
96
+ let text;
97
+ try {
98
+ text = fs.readFileSync(path.join(index.root, dir, name), 'utf8');
99
+ } catch (e) {
100
+ text = null;
101
+ }
102
+ texts.set(key, text);
103
+ return text;
104
+ };
105
+ const list = dir => {
106
+ if (listings.has(dir)) return listings.get(dir);
107
+ let names;
108
+ try {
109
+ names = fs.readdirSync(path.join(index.root, dir)).sort(codeUnitCompare);
110
+ } catch (e) {
111
+ names = [];
112
+ }
113
+ listings.set(dir, names);
114
+ return names;
115
+ };
116
+ const json = (dir, name) => {
117
+ const text = read(dir, name);
118
+ if (text == null) return null;
119
+ try {
120
+ const value = JSON.parse(text);
121
+ return value && typeof value === 'object' ? value : null;
122
+ } catch (e) {
123
+ return null;
124
+ }
125
+ };
126
+ return { read, list, json };
127
+ }
128
+
129
+ /** The table `[name]` of a TOML or INI text, up to the next table header. */
130
+ function tableText(text, header) {
131
+ const lines = String(text).split('\n');
132
+ const out = [];
133
+ let inside = false;
134
+ for (const line of lines) {
135
+ const head = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(?:[#;].*)?$/.exec(line);
136
+ if (head) {
137
+ inside = head[1] === header;
138
+ continue;
139
+ }
140
+ if (inside) out.push(line);
141
+ }
142
+ return inside || out.length > 0 ? out.join('\n') : null;
143
+ }
144
+
145
+ function hasTable(text, header) {
146
+ if (text == null) return false;
147
+ return String(text).split('\n').some(line => {
148
+ const head = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(?:[#;].*)?$/.exec(line);
149
+ return !!head && head[1] === header;
150
+ });
151
+ }
152
+
153
+ /**
154
+ * The values of `key` in a pytest configuration table: a TOML array or
155
+ * string, or INI words continued on indented lines. Null when not set.
156
+ */
157
+ function configWords(table, key) {
158
+ if (table == null) return null;
159
+ const lines = table.split('\n');
160
+ for (let i = 0; i < lines.length; i++) {
161
+ const match = new RegExp(`^${key}\\s*[=:]\\s*(.*)$`).exec(lines[i]);
162
+ if (!match) continue;
163
+ let value = match[1];
164
+ if (value.trimStart().startsWith('[')) {
165
+ // A TOML array, possibly over several lines.
166
+ let j = i;
167
+ while (!value.includes(']') && j + 1 < lines.length) value += '\n' + lines[++j];
168
+ } else {
169
+ for (let j = i + 1; j < lines.length && /^\s+\S/.test(lines[j]); j++) value += '\n' + lines[j];
170
+ }
171
+ const quoted = [...value.matchAll(/"([^"]*)"|'([^']*)'/g)].map(m => m[1] != null ? m[1] : m[2]);
172
+ const words = quoted.length > 0
173
+ ? quoted.flatMap(word => word.split(/\s+/))
174
+ : value.replace(/[#;].*$/gm, '').split(/[\s,[\]]+/);
175
+ return words.filter(Boolean);
176
+ }
177
+ return null;
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Runners
182
+ // ---------------------------------------------------------------------------
183
+
184
+ // Package and command names of JavaScript runners; `true` marks the ones
185
+ // whose selection syntax is modeled.
186
+ const JS_RUNNER_PACKAGES = new Map([
187
+ ['jest', 'jest'], ['vitest', 'vitest'], ['mocha', 'mocha'],
188
+ ['ava', 'ava'], ['tap', 'tap'], ['jasmine', 'jasmine'], ['karma', 'karma'], ['uvu', 'uvu'],
189
+ ['@playwright/test', 'playwright'], ['playwright', 'playwright'], ['cypress', 'cypress'],
190
+ ['@japa/runner', 'japa'], ['tape', 'tape'], ['qunit', 'qunit'], ['lab', 'lab'], ['@hapi/lab', 'lab'],
191
+ ['bun', 'bun'], ['deno', 'deno'], ['wdio', 'wdio'], ['@web/test-runner', 'wtr'], ['wtr', 'wtr'],
192
+ ['react-scripts', 'react-scripts'], ['ng', 'angular'], ['nx', 'nx'], ['turbo', 'turbo'], ['lerna', 'lerna'],
193
+ ]);
194
+ const MODELED_JS = new Set(['node-test', 'jest', 'vitest', 'mocha']);
195
+ /**
196
+ * The tests of a file that a focus (`.only`) keeps from running, by each
197
+ * runner's own rule (read from the runners and measured). `units` are the
198
+ * reader's, each with `parent` (the index of the suite it stands in, -1
199
+ * for the file) and `only` as written.
200
+ * - vitest: a test runs when it or a suite around it is marked.
201
+ * - jest: a suite passes its mark down to suites inside it, and to its own
202
+ * tests unless one of those is marked itself.
203
+ * - mocha: a suite with marked tests of its own runs those and nothing
204
+ * else of it; otherwise it runs its marked suites (narrowed further when
205
+ * they hold marks) and the unmarked ones that hold a mark.
206
+ * node:test honours the mark only under --test-only: nothing is left out.
207
+ * @returns {Set<object>} the units that do not run
208
+ */
209
+ function focusedOut(units, runner) {
210
+ if (!units.some(unit => unit.only) || !['jest', 'vitest', 'mocha'].includes(runner)) return new Set();
211
+ const children = new Map();
212
+ units.forEach((unit, i) => {
213
+ const parent = unit.parent == null ? -1 : unit.parent;
214
+ if (!children.has(parent)) children.set(parent, []);
215
+ children.get(parent).push(i);
216
+ });
217
+ const kids = i => children.get(i) || [];
218
+ const isSuite = i => units[i].kind === 'suite';
219
+ const runs = new Set();
220
+ if (runner === 'vitest') {
221
+ const visit = (parent, included) => {
222
+ for (const i of kids(parent)) {
223
+ const include = included || !!units[i].only;
224
+ if (isSuite(i)) visit(i, include);
225
+ else if (include) runs.add(i);
226
+ }
227
+ };
228
+ visit(-1, false);
229
+ } else if (runner === 'jest') {
230
+ const modes = new Map();
231
+ const modeOf = i => {
232
+ if (i === -1) return null;
233
+ if (!modes.has(i)) modes.set(i, units[i].only ? 'only' : modeOf(units[i].parent == null ? -1 : units[i].parent));
234
+ return modes.get(i);
235
+ };
236
+ units.forEach((unit, i) => {
237
+ if (unit.kind !== 'test') return;
238
+ const parent = unit.parent == null ? -1 : unit.parent;
239
+ const narrowed = modeOf(parent) === 'only' && kids(parent).some(k => !isSuite(k) && units[k].only);
240
+ if (unit.only || (!narrowed && modeOf(parent) === 'only')) runs.add(i);
241
+ });
242
+ // No test ends up focused (an empty marked suite): nothing is left out.
243
+ if (runs.size === 0) return new Set();
244
+ } else {
245
+ const holdsMark = i => kids(i).some(k => units[k].only || (isSuite(k) && holdsMark(k)));
246
+ const all = i => {
247
+ for (const k of kids(i)) {
248
+ if (isSuite(k)) all(k);
249
+ else runs.add(k);
250
+ }
251
+ };
252
+ const narrow = i => {
253
+ const own = kids(i).filter(k => !isSuite(k) && units[k].only);
254
+ if (own.length > 0) {
255
+ for (const k of own) runs.add(k);
256
+ return;
257
+ }
258
+ for (const k of kids(i)) {
259
+ if (!isSuite(k)) continue;
260
+ if (units[k].only && !holdsMark(k)) all(k);
261
+ else if (units[k].only || holdsMark(k)) narrow(k);
262
+ }
263
+ };
264
+ narrow(-1);
265
+ }
266
+ return new Set(units.filter((unit, i) => unit.kind === 'test' && !runs.has(i)));
267
+ }
268
+ // Node options that take the next word as their value.
269
+ const NODE_VALUE_OPTIONS = new Set(['--import', '--require', '-r', '--loader', '--experimental-loader',
270
+ '--conditions', '-C', '--env-file', '--test-reporter', '--test-reporter-destination',
271
+ '--test-concurrency', '--test-timeout', '--test-shard', '--test-name-pattern', '--test-skip-pattern',
272
+ '--test-coverage-include', '--test-coverage-exclude', '--watch-path', '--input-type', '--title']);
273
+ // Node options that decide how a test file loads; everything else the
274
+ // script passes (reporters, coverage, concurrency) is not needed to run one.
275
+ const NODE_LOADING_OPTIONS = /^(--import|--require|-r|--loader|--experimental-loader|--conditions|-C|--env-file|--experimental-[\w-]+|--no-experimental-[\w-]+|--enable-source-maps|--preserve-symlinks|--no-warnings)(=|$)/;
276
+
277
+ /**
278
+ * Runner commands named by a package.json script: the first word of each
279
+ * command of the script, and `--test` after `node`.
280
+ */
281
+ function scriptRunners(script) {
282
+ const runners = [];
283
+ if (typeof script !== 'string') return runners;
284
+ for (const command of script.split(/&&|\|\||;|\|/)) {
285
+ const words = command.trim().split(/\s+/).filter(Boolean);
286
+ // Leading environment assignments and wrappers name no runner.
287
+ let i = 0;
288
+ while (i < words.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i]) ||
289
+ ['npx', 'cross-env', 'env', 'c8', 'nyc', 'dotenv', '--', 'exec', 'pnpm', 'yarn', 'npm', 'run', 'bunx', 'time'].includes(words[i]))) i++;
290
+ const head = words[i];
291
+ if (!head) continue;
292
+ const base = head.split('/').pop();
293
+ if ((base === 'node' || base === 'tsx' || base === 'ts-node') &&
294
+ words.slice(i + 1).some(word => word === '--test' || word.startsWith('--test='))) {
295
+ runners.push('node-test');
296
+ continue;
297
+ }
298
+ if (base === 'bun' && words[i + 1] === 'test') { runners.push('bun'); continue; }
299
+ if (base === 'deno' && words[i + 1] === 'test') { runners.push('deno'); continue; }
300
+ if (JS_RUNNER_PACKAGES.has(base)) runners.push(JS_RUNNER_PACKAGES.get(base));
301
+ else runners.push(`?${base}`);
302
+ }
303
+ return runners;
304
+ }
305
+
306
+ /**
307
+ * The `node` options of a script that is one `node ... --test ...` command,
308
+ * kept when they decide how a file loads. Null when the script is anything
309
+ * else: then nothing says how the project's tests are loaded.
310
+ */
311
+ function nodeTestOptions(script) {
312
+ if (typeof script !== 'string' || /[&|;$`<>(){}*]/.test(script.replace(/\*/g, '')) && /[&|;$`<>(){}]/.test(script)) return null;
313
+ const words = script.trim().split(/\s+/).filter(Boolean);
314
+ if (words[0] !== 'node' || !words.includes('--test')) return null;
315
+ const kept = [];
316
+ for (let i = 1; i < words.length; i++) {
317
+ const word = words[i];
318
+ if (!word.startsWith('-')) continue;
319
+ const takesValue = NODE_VALUE_OPTIONS.has(word);
320
+ if (NODE_LOADING_OPTIONS.test(word)) {
321
+ kept.push(word);
322
+ if (takesValue && i + 1 < words.length) kept.push(words[i + 1]);
323
+ }
324
+ if (takesValue) i++;
325
+ }
326
+ return kept;
327
+ }
328
+
329
+ function jsRunner(model, fileEntry) {
330
+ // 1. The file's own imports.
331
+ const modules = langTraits(fileEntry.language)?.testRunnerModules?.() || new Map();
332
+ const imported = new Set();
333
+ for (const mod of fileEntry.imports || []) {
334
+ const runner = modules.get(mod);
335
+ if (runner) imported.add(runner);
336
+ }
337
+ if (imported.size > 1) return { id: null, reason: 'ambiguous-runner' };
338
+ let fromFile = imported.size === 1 ? [...imported][0] : null;
339
+ if (fromFile && !MODELED_JS.has(fromFile)) return { id: null, reason: 'runner-not-modeled', runner: fromFile };
340
+
341
+ // 2. The nearest package manifest.
342
+ let dir = null;
343
+ let manifest = null;
344
+ for (const candidate of ancestorDirs(fileEntry.relativePath)) {
345
+ manifest = model.manifests.json(candidate, 'package.json');
346
+ if (manifest) { dir = candidate; break; }
347
+ }
348
+ const script = manifest && manifest.scripts && typeof manifest.scripts.test === 'string'
349
+ ? manifest.scripts.test : null;
350
+ if (fromFile) {
351
+ return { id: fromFile, dir: dir || '', evidence: 'test-file-import', script, scripts: manifest ? manifest.scripts : null };
352
+ }
353
+ if (!manifest) return { id: null, reason: 'no-runner-evidence' };
354
+ const named = [...new Set(scriptRunners(script).filter(name => !name.startsWith('?')))];
355
+ if (named.length === 1) {
356
+ if (!MODELED_JS.has(named[0])) return { id: null, reason: 'runner-not-modeled', runner: named[0] };
357
+ return { id: named[0], dir, evidence: 'package-script', script, scripts: manifest.scripts };
358
+ }
359
+ if (named.length > 1) return { id: null, reason: 'ambiguous-runner' };
360
+ const declared = new Set();
361
+ for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
362
+ for (const name of Object.keys(manifest[field] || {})) {
363
+ if (JS_RUNNER_PACKAGES.has(name)) declared.add(JS_RUNNER_PACKAGES.get(name));
364
+ }
365
+ }
366
+ if (declared.size === 1) {
367
+ const only = [...declared][0];
368
+ if (!MODELED_JS.has(only)) return { id: null, reason: 'runner-not-modeled', runner: only };
369
+ return { id: only, dir, evidence: 'package-dependency', script, scripts: manifest.scripts };
370
+ }
371
+ return { id: null, reason: declared.size > 1 ? 'ambiguous-runner' : 'no-runner-evidence' };
372
+ }
373
+
374
+ /** pytest's configuration file for a directory, by pytest's own order. */
375
+ function pytestConfigIn(model, dir) {
376
+ const ini = model.manifests.read(dir, 'pytest.ini');
377
+ if (ini != null) return { file: 'pytest.ini', table: tableText(ini, 'pytest') || '' };
378
+ const project = model.manifests.read(dir, 'pyproject.toml');
379
+ if (project != null) {
380
+ for (const header of ['tool.pytest.ini_options', 'tool.pytest']) {
381
+ if (hasTable(project, header)) return { file: 'pyproject.toml', table: tableText(project, header) || '' };
382
+ }
383
+ }
384
+ const tox = model.manifests.read(dir, 'tox.ini');
385
+ if (tox != null && hasTable(tox, 'pytest')) return { file: 'tox.ini', table: tableText(tox, 'pytest') || '' };
386
+ const setup = model.manifests.read(dir, 'setup.cfg');
387
+ if (setup != null && hasTable(setup, 'tool:pytest')) {
388
+ return { file: 'setup.cfg', table: tableText(setup, 'tool:pytest') || '' };
389
+ }
390
+ return null;
391
+ }
392
+
393
+ /**
394
+ * Does a requirements file or a manifest name a package as a requirement:
395
+ * the name where a requirement starts (a line, a list item, a quoted
396
+ * string, a key), up to a version or the end. A word in a comment or in
397
+ * another package's name is not one.
398
+ */
399
+ function namesRequirement(text, name) {
400
+ const shape = new RegExp(`(^|["'\\s,\\[])${name}(\\[[^\\]]*\\])?\\s*([<>=!~;,"'\\]]|$)`);
401
+ return String(text).split('\n').some(raw => shape.test(raw.replace(/(^|\s)[#;].*$/, '').trim()));
402
+ }
403
+
404
+ function pythonRunner(model, fileEntry) {
405
+ const dirs = ancestorDirs(fileEntry.relativePath);
406
+ for (const dir of dirs) {
407
+ const config = pytestConfigIn(model, dir);
408
+ if (config) {
409
+ return {
410
+ id: 'pytest', dir, evidence: config.file,
411
+ collection: {
412
+ files: configWords(config.table, 'python_files'),
413
+ classes: configWords(config.table, 'python_classes'),
414
+ functions: configWords(config.table, 'python_functions'),
415
+ options: pytestOptions(configArguments(config, 'addopts')),
416
+ // Fixtures every test is given, and whether a test a
417
+ // module imports is collected there too.
418
+ usefixtures: configWords(config.table, 'usefixtures') || [],
419
+ importedTests: !/^(false|no|off|0)$/i.test((configWords(config.table, 'collect_imported_tests') || [])[0] || ''),
420
+ },
421
+ };
422
+ }
423
+ }
424
+ if ((fileEntry.imports || []).some(mod => mod === 'pytest' || String(mod).startsWith('pytest.'))) {
425
+ return { id: 'pytest', dir: '', evidence: 'test-file-import', collection: {} };
426
+ }
427
+ for (const dir of dirs) {
428
+ const conftest = dir ? `${dir}/conftest.py` : 'conftest.py';
429
+ if (model.hasFile(conftest)) return { id: 'pytest', dir: '', evidence: 'conftest.py', collection: {} };
430
+ }
431
+ for (const dir of dirs) {
432
+ for (const name of ['pyproject.toml', 'requirements.txt', 'setup.cfg', 'tox.ini']) {
433
+ const text = model.manifests.read(dir, name);
434
+ if (text != null && namesRequirement(text, 'pytest')) {
435
+ return { id: 'pytest', dir, evidence: name, collection: {} };
436
+ }
437
+ }
438
+ }
439
+ return { id: null, reason: 'no-runner-evidence' };
440
+ }
441
+
442
+ function goRunner(model, fileEntry) {
443
+ for (const dir of ancestorDirs(fileEntry.relativePath)) {
444
+ if (model.manifests.read(dir, 'go.mod') != null) return { id: 'go-test', dir, evidence: 'go.mod' };
445
+ }
446
+ return { id: null, reason: 'no-runner-evidence' };
447
+ }
448
+
449
+ function rustRunner(model, fileEntry) {
450
+ for (const dir of ancestorDirs(fileEntry.relativePath)) {
451
+ const text = model.manifests.read(dir, 'Cargo.toml');
452
+ if (text == null) continue;
453
+ if (!hasTable(text, 'package')) return { id: null, reason: 'no-package-manifest' };
454
+ const name = /^\s*name\s*=\s*"([^"]+)"/m.exec(tableText(text, 'package') || '');
455
+ // Targets the manifest lays out itself are not where cargo's
456
+ // conventions put them.
457
+ const customTargets = ['lib', 'bin', 'test'].some(header => {
458
+ const table = tableText(text, header);
459
+ return table != null && /^\s*(path|name|harness)\s*=/m.test(table);
460
+ });
461
+ return {
462
+ id: 'cargo-test', dir, evidence: 'Cargo.toml',
463
+ packageName: name ? name[1] : null,
464
+ customTargets,
465
+ };
466
+ }
467
+ return { id: null, reason: 'no-runner-evidence' };
468
+ }
469
+
470
+ function javaRunner(model, fileEntry) {
471
+ for (const dir of ancestorDirs(fileEntry.relativePath)) {
472
+ if (model.manifests.read(dir, 'pom.xml') != null) return { id: 'maven', dir, evidence: 'pom.xml' };
473
+ for (const name of ['build.gradle', 'build.gradle.kts']) {
474
+ if (model.manifests.read(dir, name) != null) return { id: null, reason: 'runner-not-modeled', runner: 'gradle' };
475
+ }
476
+ }
477
+ return { id: null, reason: 'no-runner-evidence' };
478
+ }
479
+
480
+ function dotnetRunner(model, fileEntry) {
481
+ for (const dir of ancestorDirs(fileEntry.relativePath)) {
482
+ const projects = model.manifests.list(dir).filter(name => name.endsWith('.csproj'));
483
+ if (projects.length === 0) continue;
484
+ if (projects.length > 1) return { id: null, reason: 'ambiguous-runner' };
485
+ const text = model.manifests.read(dir, projects[0]) || '';
486
+ if (!/Microsoft\.NET\.Test\.Sdk|xunit|nunit|MSTest/i.test(text)) return { id: null, reason: 'no-runner-evidence' };
487
+ return { id: 'dotnet-test', dir, evidence: projects[0] };
488
+ }
489
+ return { id: null, reason: 'no-runner-evidence' };
490
+ }
491
+
492
+ // ---------------------------------------------------------------------------
493
+ // What the runner's own configuration changes
494
+ // ---------------------------------------------------------------------------
495
+
496
+ /** Words of a shell-like option string: quotes group, a backslash escapes. */
497
+ function shellSplit(text) {
498
+ const words = [];
499
+ let current = null;
500
+ let quote = null;
501
+ for (let i = 0; i < text.length; i++) {
502
+ const ch = text[i];
503
+ if (quote) {
504
+ if (ch === quote) quote = null;
505
+ else if (ch === '\\' && quote === '"' && i + 1 < text.length) current += text[++i];
506
+ else current += ch;
507
+ } else if (ch === '"' || ch === "'") {
508
+ quote = ch;
509
+ if (current == null) current = '';
510
+ } else if (/\s/.test(ch)) {
511
+ if (current != null) words.push(current);
512
+ current = null;
513
+ } else if (ch === '\\' && i + 1 < text.length) {
514
+ current = (current == null ? '' : current) + text[++i];
515
+ } else {
516
+ current = (current == null ? '' : current) + ch;
517
+ }
518
+ }
519
+ if (current != null) words.push(current);
520
+ return words;
521
+ }
522
+
523
+ /** A TOML line without its comment. */
524
+ function withoutTomlComment(line) {
525
+ let quote = null;
526
+ for (let i = 0; i < line.length; i++) {
527
+ const ch = line[i];
528
+ if (quote) {
529
+ if (ch === '\\' && quote === '"') i++;
530
+ else if (ch === quote) quote = null;
531
+ } else if (ch === '"' || ch === "'") {
532
+ quote = ch;
533
+ } else if (ch === '#') {
534
+ return line.slice(0, i);
535
+ }
536
+ }
537
+ return line;
538
+ }
539
+
540
+ /**
541
+ * The arguments a pytest configuration gives for `key` (addopts): a TOML
542
+ * array holds one argument per string; a string, and an INI value with its
543
+ * continuation lines, is split the way a shell would. [] when the key is
544
+ * not set, null when its value is not read.
545
+ */
546
+ function configArguments(config, key) {
547
+ if (!config || config.table == null) return [];
548
+ const lines = config.table.split('\n');
549
+ const toml = config.file === 'pyproject.toml';
550
+ for (let i = 0; i < lines.length; i++) {
551
+ const match = new RegExp(`^${key}\\s*[=:]\\s*(.*)$`).exec(lines[i]);
552
+ if (!match) continue;
553
+ if (!toml) {
554
+ let value = match[1];
555
+ for (let j = i + 1; j < lines.length && /^\s+\S/.test(lines[j]); j++) value += ' ' + lines[j].trim();
556
+ return shellSplit(value);
557
+ }
558
+ let value = withoutTomlComment(match[1]);
559
+ if (/"""|'''/.test(value)) return null;
560
+ const strings = text => [...text.matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'/g)]
561
+ .map(found => found[1] != null ? found[1].replace(/\\(.)/g, '$1') : found[2]);
562
+ if (value.trimStart().startsWith('[')) {
563
+ for (let j = i + 1; !/\]\s*$/.test(value) && j < lines.length; j++) value += '\n' + withoutTomlComment(lines[j]);
564
+ return /\]\s*$/.test(value) ? strings(value) : null;
565
+ }
566
+ const whole = strings(value);
567
+ return whole.length === 1 ? shellSplit(whole[0]) : null;
568
+ }
569
+ return [];
570
+ }
571
+
572
+ // pytest options that change neither what is collected nor what a command
573
+ // that names a test runs. --ignore leaves a path out of a whole run only:
574
+ // a path given to the command is still collected (measured).
575
+ const PYTEST_INERT_FLAGS = new Set(['-v', '-vv', '-vvv', '--verbose', '-q', '-qq', '--quiet', '-s', '-l',
576
+ '--showlocals', '-x', '--exitfirst', '--strict-markers', '--strict-config', '--strict', '--no-header',
577
+ '--no-summary', '--disable-warnings', '--disable-pytest-warnings', '--ff', '--failed-first', '--nf',
578
+ '--new-first', '--cache-clear', '--full-trace', '--runxfail', '--no-cov-on-fail', '--cov-branch',
579
+ '--cov-append', '--no-cov', '--cov']);
580
+ const PYTEST_INERT_VALUED = new Set(['--tb', '--color', '--durations', '--durations-min', '--maxfail',
581
+ '--capture', '-W', '--pythonwarnings', '--cov', '--cov-report', '--cov-config', '--cov-fail-under',
582
+ '--cov-context', '--junitxml', '--junit-xml', '--junit-prefix', '--log-level', '--log-cli-level',
583
+ '--log-format', '--log-date-format', '--log-file', '--log-file-level', '--basetemp', '--import-mode',
584
+ '--timeout', '--timeout-method', '-n', '--numprocesses', '--dist', '--maxprocesses', '--reruns',
585
+ '--reruns-delay', '--show-capture', '--code-highlight', '--verbosity', '--asyncio-mode', '--ignore',
586
+ '--ignore-glob', '-r']);
587
+ // pytest's own plugins that decide what is collected, selected or skipped:
588
+ // switching one of these off changes which tests run. Any other plugin
589
+ // switched off (-p no:name) changes none of that; one switched ON is code
590
+ // that is not read.
591
+ const PYTEST_SELECTING_PLUGINS = new Set(['python', 'unittest', 'mark', 'skipping', 'main', 'runner', 'fixtures',
592
+ 'stepwise', 'setuponly', 'setupplan', 'nose']);
593
+
594
+ /**
595
+ * What pytest's configured options do to selection: `markExpr` (-m) and
596
+ * `deselect` (--deselect) are applied to the units; `unread` says an
597
+ * option is there that may select, add or leave out tests and is not
598
+ * modeled (-k, a path, a plugin, an override): no command is exact then.
599
+ */
600
+ function pytestOptions(words) {
601
+ const out = { markExpr: null, deselect: [], unread: false };
602
+ if (words == null) return { ...out, unread: true };
603
+ for (let i = 0; i < words.length; i++) {
604
+ const word = words[i];
605
+ const cut = word.startsWith('--') ? word.indexOf('=') : -1;
606
+ const name = cut > 0 ? word.slice(0, cut) : word;
607
+ const inline = cut > 0 ? word.slice(cut + 1) : null;
608
+ const value = () => (inline != null ? inline : words[++i]);
609
+ if (name === '-m') {
610
+ const expression = value();
611
+ if (expression == null) out.unread = true;
612
+ else out.markExpr = expression;
613
+ } else if (name === '--deselect') {
614
+ const id = value();
615
+ if (id) out.deselect.push(id);
616
+ else out.unread = true;
617
+ } else if (name === '-p') {
618
+ const plugin = String(value() || '');
619
+ if (!plugin.startsWith('no:') || PYTEST_SELECTING_PLUGINS.has(plugin.slice(3))) out.unread = true;
620
+ } else if (/^-r[A-Za-z]+$/.test(word) || /^-W\S+$/.test(word) || /^-n\S+$/.test(word)) {
621
+ // -ra, -Werror, -n4: the value is in the word.
622
+ } else if (inline != null ? PYTEST_INERT_VALUED.has(name) : PYTEST_INERT_FLAGS.has(name) && !PYTEST_INERT_VALUED.has(name)) {
623
+ // nothing
624
+ } else if (inline == null && name === '--cov') {
625
+ // --cov takes an optional value: a word after it that is no
626
+ // option would be read as a path otherwise.
627
+ if (i + 1 < words.length && !words[i + 1].startsWith('-')) i++;
628
+ } else if (inline == null && PYTEST_INERT_VALUED.has(name)) {
629
+ i++;
630
+ } else {
631
+ out.unread = true;
632
+ }
633
+ }
634
+ return out;
635
+ }
636
+
637
+ /**
638
+ * Does pytest's mark expression select a test with these marks: true,
639
+ * false, or null when it cannot be said (`open`: the test may carry marks
640
+ * that are not read; an expression in a form not modeled).
641
+ */
642
+ function markSelects(expression, marks, open) {
643
+ const tokens = String(expression).match(/\(|\)|[^\s()]+/g) || [];
644
+ let at = 0;
645
+ const not = value => (value == null ? null : !value);
646
+ const primary = () => {
647
+ const token = tokens[at++];
648
+ if (token === 'not') return not(primary());
649
+ if (token === '(') {
650
+ const inner = either();
651
+ if (tokens[at++] !== ')') throw new Error('unbalanced');
652
+ return inner;
653
+ }
654
+ if (token == null || token === ')' || token === 'and' || token === 'or') throw new Error('unexpected');
655
+ // name(arg=value): a mark with arguments, not modeled.
656
+ if (tokens[at] === '(') throw new Error('arguments');
657
+ return marks.has(token) ? true : open ? null : false;
658
+ };
659
+ const both = () => {
660
+ let value = primary();
661
+ while (tokens[at] === 'and') {
662
+ at++;
663
+ const next = primary();
664
+ value = value === false || next === false ? false : value == null || next == null ? null : true;
665
+ }
666
+ return value;
667
+ };
668
+ const either = () => {
669
+ let value = both();
670
+ while (tokens[at] === 'or') {
671
+ at++;
672
+ const next = both();
673
+ value = value === true || next === true ? true : value == null || next == null ? null : false;
674
+ }
675
+ return value;
676
+ };
677
+ try {
678
+ const value = either();
679
+ return at === tokens.length ? value : null;
680
+ } catch (e) {
681
+ return null;
682
+ }
683
+ }
684
+
685
+ /** A path glob as a regular expression: `**`, `*`, `?`, `{a,b}`, `[...]`. */
686
+ function pathGlob(glob) {
687
+ const text = String(glob).replace(/^\.\//, '');
688
+ let source = '';
689
+ for (let i = 0; i < text.length; i++) {
690
+ const ch = text[i];
691
+ if (ch === '*' && text[i + 1] === '*') {
692
+ i++;
693
+ if (text[i + 1] === '/') {
694
+ i++;
695
+ source += '(?:.*/)?';
696
+ } else {
697
+ source += '.*';
698
+ }
699
+ } else if (ch === '*') {
700
+ source += '[^/]*';
701
+ } else if (ch === '?') {
702
+ source += '[^/]';
703
+ } else if (ch === '{' || ch === '[') {
704
+ const end = text.indexOf(ch === '{' ? '}' : ']', i);
705
+ if (end < 0) return null;
706
+ source += ch === '[' ? text.slice(i, end + 1)
707
+ : `(?:${text.slice(i + 1, end).split(',').map(part => part.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')).join('|')})`;
708
+ i = end;
709
+ } else {
710
+ source += ch.replace(/[.+^$()|\\]/, '\\$&');
711
+ }
712
+ }
713
+ try {
714
+ return new RegExp(`^${source}$`);
715
+ } catch (e) {
716
+ return null;
717
+ }
718
+ }
719
+
720
+ // The files `node --test` runs when it is given none (node's own list).
721
+ const NODE_DEFAULT_TEST_FILES = [/(^|\/)[^/]*[._-]test\.[cm]?js$/, /(^|\/)test-[^/]*\.[cm]?js$/,
722
+ /(^|\/)test\.[cm]?js$/, /(^|\/)test\/.*\.[cm]?js$/];
723
+
724
+ /**
725
+ * The paths a script that is one `node ... --test ...` command names, as
726
+ * written (a glob stays a glob). Null when the script is anything else.
727
+ */
728
+ function nodeTestPaths(script) {
729
+ if (nodeTestOptions(script) == null) return null;
730
+ const words = script.trim().split(/\s+/).filter(Boolean);
731
+ const paths = [];
732
+ for (let i = 1; i < words.length; i++) {
733
+ const word = words[i];
734
+ if (word.startsWith('-')) {
735
+ if (NODE_VALUE_OPTIONS.has(word)) i++;
736
+ continue;
737
+ }
738
+ paths.push(word.replace(/^(['"])(.*)\1$/, '$2'));
739
+ }
740
+ return paths;
741
+ }
742
+
743
+ /**
744
+ * Does node run a file as a test under the project's own script: one of
745
+ * the paths the script names, or, when it names none, one node looks for
746
+ * by itself (every file under a `test` directory among them). node runs a
747
+ * file that declares no test as one test. Null: the script does not say.
748
+ */
749
+ function nodeRunsFile(runner, file) {
750
+ const paths = nodeTestPaths(runner.script);
751
+ if (paths == null || file == null) return null;
752
+ if (paths.length === 0) return !/(^|\/)node_modules\//.test(file) && NODE_DEFAULT_TEST_FILES.some(pattern => pattern.test(file));
753
+ return paths.some(written => {
754
+ const target = written.replace(/^\.\//, '');
755
+ if (target === file) return true;
756
+ if (!/[*?{[]/.test(target)) return false;
757
+ const pattern = pathGlob(target);
758
+ return !!pattern && pattern.test(file);
759
+ });
760
+ }
761
+
762
+ /**
763
+ * The words a project's test script passes to a runner: those of the
764
+ * `test` script and of the scripts it names, after the runner's own name.
765
+ * [] when the script does not call the runner by name.
766
+ */
767
+ function runnerScriptWords(runner, name) {
768
+ const scripts = runner.scripts && typeof runner.scripts === 'object' ? runner.scripts : {};
769
+ const texts = [];
770
+ if (typeof runner.script === 'string') {
771
+ texts.push(runner.script);
772
+ for (const word of runner.script.split(/[\s"'&|;]+/)) {
773
+ if (word !== 'test' && typeof scripts[word] === 'string') texts.push(scripts[word]);
774
+ }
775
+ }
776
+ const out = [];
777
+ for (const text of texts) {
778
+ for (const segment of text.split(/&&|\|\||;|\|/)) {
779
+ const words = segment.trim().split(/\s+/).filter(Boolean);
780
+ const at = words.findIndex(word => word.split('/').pop() === name || word.split('/').pop() === `_${name}`);
781
+ if (at >= 0) out.push(...words.slice(at + 1));
782
+ }
783
+ }
784
+ return out;
785
+ }
786
+
787
+ // Options of a runner's command line that point it at another
788
+ // configuration or select files: a script that passes one runs something
789
+ // a command built here does not.
790
+ const SCRIPT_CONFIG_FLAGS = /^(--config|-c|--rootDir|--roots|--projects|--selectProjects|--project|--workspace|--root|--dir|--testMatch|--testRegex|--testPathPattern|--testPathPatterns|--package|--opts)(=|$)/;
791
+ // Options of a mocha command line that decide how a test file loads.
792
+ const MOCHA_LOADING_OPTIONS = new Set(['--require', '-r', '--loader', '--import', '--ui', '-u', '--file', '--node-option', '-n']);
793
+ const MOCHA_VALUE_OPTIONS = new Set([...MOCHA_LOADING_OPTIONS, '--reporter', '-R', '--reporter-option', '--reporter-options',
794
+ '-O', '--timeout', '-t', '--timeouts', '--slow', '-s', '--retries', '--grep', '-g', '--fgrep', '-f', '--ignore',
795
+ '--exclude', '--spec', '--jobs', '-j', '--watch-files', '--watch-ignore', '--extension', '--config', '--package']);
796
+
797
+ /**
798
+ * The options of the project's mocha script that a command for one file
799
+ * needs to load it the same way (--require, --ui, --loader, --file).
800
+ */
801
+ function mochaLoadingOptions(runner) {
802
+ const words = runnerScriptWords(runner, 'mocha');
803
+ const kept = [];
804
+ for (let i = 0; i < words.length; i++) {
805
+ const word = words[i].replace(/^(['"])(.*)\1$/, '$2');
806
+ const cut = word.startsWith('--') ? word.indexOf('=') : -1;
807
+ const name = cut > 0 ? word.slice(0, cut) : word;
808
+ if (MOCHA_LOADING_OPTIONS.has(name)) {
809
+ kept.push(word);
810
+ if (cut < 0 && i + 1 < words.length) kept.push(words[++i].replace(/^(['"])(.*)\1$/, '$2'));
811
+ } else if (cut < 0 && MOCHA_VALUE_OPTIONS.has(name)) {
812
+ i++;
813
+ }
814
+ }
815
+ return kept;
816
+ }
817
+
818
+ // Configuration files of the JavaScript runners.
819
+ const JS_CONFIG_NAMES = {
820
+ jest: new Set(['jest.config.js', 'jest.config.cjs', 'jest.config.mjs', 'jest.config.ts', 'jest.config.cts',
821
+ 'jest.config.mts', 'jest.config.json']),
822
+ vitest: new Set(['vitest.config.js', 'vitest.config.cjs', 'vitest.config.mjs', 'vitest.config.ts',
823
+ 'vitest.config.cts', 'vitest.config.mts', 'vite.config.js', 'vite.config.cjs', 'vite.config.mjs',
824
+ 'vite.config.ts', 'vite.config.cts', 'vite.config.mts', 'vitest.workspace.js', 'vitest.workspace.mjs',
825
+ 'vitest.workspace.ts', 'vitest.workspace.mts', 'vitest.workspace.json', 'vitest.projects.js',
826
+ 'vitest.projects.ts', 'vitest.projects.json']),
827
+ mocha: new Set(['.mocharc', '.mocharc.js', '.mocharc.cjs', '.mocharc.mjs', '.mocharc.json', '.mocharc.jsonc',
828
+ '.mocharc.yaml', '.mocharc.yml']),
829
+ };
830
+ const JS_CONFIG_PACKAGE_KEY = { jest: 'jest', mocha: 'mocha' };
831
+ // The keys of a configuration that are known to leave alone which tests a
832
+ // command for one file runs. Every other key may select: it is evaluated
833
+ // where it is modeled (JS_MODELED_KEYS), and the configuration is not read
834
+ // otherwise. vitest's are the keys of `test`; mocha's are compared with
835
+ // dashes and case folded (it takes both spellings).
836
+ const JS_INERT_KEYS = {
837
+ jest: new Set(['automock', 'bail', 'cache', 'cacheDirectory', 'ci', 'clearMocks', 'collectCoverage',
838
+ 'collectCoverageFrom', 'coverageDirectory', 'coveragePathIgnorePatterns', 'coverageProvider',
839
+ 'coverageReporters', 'coverageThreshold', 'detectLeaks', 'detectOpenHandles', 'displayName',
840
+ 'errorOnDeprecated', 'expand', 'extensionsToTreatAsEsm', 'fakeTimers', 'forceCoverageMatch', 'forceExit',
841
+ 'globalSetup', 'globalTeardown', 'globals', 'json', 'logHeapUsage', 'maxConcurrency', 'maxWorkers',
842
+ 'moduleDirectories', 'moduleNameMapper', 'modulePaths', 'noStackTrace', 'notify', 'notifyMode',
843
+ 'openHandlesTimeout', 'outputFile', 'passWithNoTests', 'prettierPath', 'randomize', 'reporters',
844
+ 'resetMocks', 'resetModules', 'resolver', 'restoreMocks', 'sandboxInjectedGlobals', 'setupFiles',
845
+ 'setupFilesAfterEnv', 'showSeed', 'silent', 'slowTestThreshold', 'snapshotFormat', 'snapshotResolver',
846
+ 'snapshotSerializers', 'testEnvironment', 'testEnvironmentOptions', 'testFailureExitCode',
847
+ 'testLocationInResults', 'testResultsProcessor', 'testTimeout', 'transform', 'transformIgnorePatterns',
848
+ 'unmockedModulePathPatterns', 'useStderr', 'verbose', 'watchPathIgnorePatterns', 'watchPlugins',
849
+ 'watchman', 'workerIdleMemoryLimit', 'workerThreads', 'dependencyExtractor', 'waitForUnhandledRejections',
850
+ '//',
851
+ // Names earlier versions of jest used for keys of this list.
852
+ 'testURL', 'timers', 'setupTestFrameworkScriptFile', 'mapCoverage', 'name', 'extraGlobals', 'browser']),
853
+ vitest: new Set(['alias', 'api', 'attachmentsDir', 'bail', 'benchmark', 'cache', 'chaiConfig', 'clearMocks',
854
+ 'coverage', 'css', 'dangerouslyIgnoreUnhandledErrors', 'deps', 'diff', 'disableConsoleIntercept', 'env',
855
+ 'environment', 'environmentMatchGlobs', 'environmentOptions', 'expandSnapshotDiff', 'expect', 'fakeTimers',
856
+ 'fileParallelism', 'globals', 'globalSetup', 'hookTimeout', 'includeTaskLocation', 'isolate',
857
+ 'logHeapUsage', 'maxConcurrency', 'maxWorkers', 'minWorkers', 'mockReset', 'name', 'onConsoleLog',
858
+ 'onStackTrace', 'open', 'outputFile', 'passWithNoTests', 'pool', 'poolMatchGlobs', 'poolOptions',
859
+ 'printConsoleTrace', 'provide', 'reporters', 'resolveSnapshotPath', 'restoreMocks', 'retry', 'sequence',
860
+ 'server', 'setupFiles', 'silent', 'slowTestThreshold', 'snapshotFormat', 'snapshotSerializers',
861
+ 'teardownTimeout', 'testTimeout', 'ui', 'unstubEnvs', 'unstubGlobals', 'update', 'watch']),
862
+ mocha: new Set(['allowuncaught', 'asynconly', 'bail', 'checkleaks', 'color', 'colors', 'diff',
863
+ 'enablesourcemaps', 'exit', 'failzero', 'fulltrace', 'global', 'globals', 'import', 'inlinediffs', 'jobs',
864
+ 'loader', 'nodeoption', 'nodeoptions', 'parallel', 'recursive', 'reporter', 'reporteroption',
865
+ 'reporteroptions', 'require', 'retries', 'slow', 'sort', 'timeout', 'timeouts', 'watch', 'watchfiles',
866
+ 'watchignore']),
867
+ };
868
+ const JS_MODELED_KEYS = {
869
+ jest: new Set(['roots', 'testRegex', 'testPathIgnorePatterns']),
870
+ vitest: new Set(['dir']),
871
+ mocha: new Set(),
872
+ };
873
+ // The keys of a vite configuration, beside `test` and `plugins`, that
874
+ // leave vitest's file selection alone.
875
+ const VITE_INERT_KEYS = new Set(['appType', 'assetsInclude', 'base', 'build', 'cacheDir', 'clearScreen', 'css',
876
+ 'define', 'dev', 'envDir', 'envPrefix', 'environments', 'esbuild', 'experimental', 'future', 'html', 'json',
877
+ 'legacy', 'logLevel', 'mode', 'optimizeDeps', 'oxc', 'preview', 'publicDir', 'resolve', 'server', 'ssr',
878
+ 'worker']);
879
+ // The functions a runner's own package gives a configuration to pass
880
+ // through unchanged: the name alone proves nothing, the import does.
881
+ const CONFIG_IDENTITY = {
882
+ jest: { modules: new Set(['jest']), names: new Set(['defineConfig']) },
883
+ vitest: { modules: new Set(['vitest/config', 'vite', 'vitest']), names: new Set(['defineConfig', 'defineProject']) },
884
+ };
885
+ const CONFIG_WRAPPERS = new Set(['parenthesized_expression', 'as_expression', 'satisfies_expression',
886
+ 'type_assertion', 'non_null_expression']);
887
+ // Modules whose functions act on the process or the disk, and the
888
+ // functions of them that only read.
889
+ const CONFIG_ACTING_MODULES = new Set(['fs', 'fs/promises', 'child_process', 'module', 'vm', 'worker_threads',
890
+ 'cluster', 'process', 'inspector', 'repl']);
891
+ const CONFIG_READING_CALLS = new Set(['readFileSync', 'existsSync', 'readdirSync', 'statSync', 'lstatSync',
892
+ 'realpathSync', 'createRequire']);
893
+ const CONFIG_GLOBAL_NAMESPACES = new Set(['JSON', 'Math', 'Object', 'Array', 'String', 'Number', 'Boolean']);
894
+ const CONFIG_CONSTRUCTORS = new Set(['URL', 'RegExp', 'Set', 'Map', 'Date']);
895
+ const CONFIG_NAME_NODES = new Set(['identifier', 'shorthand_property_identifier', 'shorthand_property_identifier_pattern']);
896
+
897
+ /** The value of a string literal node (no substitutions), or null. */
898
+ function stringLiteralValue(node) {
899
+ if (!node || (node.type !== 'string' && node.type !== 'template_string')) return null;
900
+ if (node.namedChildren.some(child => child.type === 'template_substitution')) return null;
901
+ const escapes = { n: '\n', t: '\t', r: '\r', b: '\b', f: '\f', v: '\v', 0: '\0' };
902
+ return node.text.slice(1, -1).replace(/\\(u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[\s\S])/g, (whole, escape) => {
903
+ if (escape[0] === 'u' || escape[0] === 'x') {
904
+ const code = parseInt(escape.replace(/^[ux]\{?|\}$/g, ''), 16);
905
+ return Number.isNaN(code) ? whole : String.fromCodePoint(code);
906
+ }
907
+ return Object.prototype.hasOwnProperty.call(escapes, escape) ? escapes[escape] : escape;
908
+ });
909
+ }
910
+
911
+ /** JSON text with its comments taken out; what a string holds is left as it is. */
912
+ function withoutJsonComments(text) {
913
+ let out = '';
914
+ let i = 0;
915
+ while (i < text.length) {
916
+ const ch = text[i];
917
+ if (ch === '"') {
918
+ let end = i + 1;
919
+ while (end < text.length && text[end] !== '"') end += text[end] === '\\' ? 2 : 1;
920
+ out += text.slice(i, end + 1);
921
+ i = end + 1;
922
+ } else if (ch === '/' && text[i + 1] === '/') {
923
+ while (i < text.length && text[i] !== '\n') i++;
924
+ } else if (ch === '/' && text[i + 1] === '*') {
925
+ const end = text.indexOf('*/', i + 2);
926
+ i = end === -1 ? text.length : end + 2;
927
+ out += ' ';
928
+ } else {
929
+ out += ch;
930
+ i++;
931
+ }
932
+ }
933
+ return out;
934
+ }
935
+
936
+ /**
937
+ * The keys a configuration sets, each with readers of its value: as
938
+ * strings (null when the value is not a string or a list of strings
939
+ * written out), as an object of keys, and whether it is a list of calls
940
+ * of functions that packages provide. Null when the value is no object.
941
+ */
942
+ function configKeysOfJson(value) {
943
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
944
+ const keys = new Map();
945
+ for (const [key, held] of Object.entries(value)) {
946
+ keys.set(key, {
947
+ strings: () => typeof held === 'string' ? [held]
948
+ : Array.isArray(held) && held.every(item => typeof item === 'string') ? held : null,
949
+ object: () => configKeysOfJson(held),
950
+ packageCalls: () => Array.isArray(held) && held.length === 0,
951
+ });
952
+ }
953
+ return keys;
954
+ }
955
+
956
+ /**
957
+ * The top-level keys of a YAML mapping written as a block, one `key:` to
958
+ * a line at the left margin; their values are not read. Null for every
959
+ * other way of writing one (a flow mapping, indented keys, a merge key, a
960
+ * key that is not a plain or quoted word): what such a file sets is not
961
+ * read.
962
+ */
963
+ function configKeysOfYaml(text) {
964
+ const keys = new Map();
965
+ let started = false;
966
+ for (const line of String(text).split(/\r?\n/)) {
967
+ if (/^\s*(#.*)?$/.test(line)) continue;
968
+ if (!started && keys.size === 0 && /^---\s*(#.*)?$/.test(line)) {
969
+ started = true;
970
+ continue;
971
+ }
972
+ // The value of the key above: indented lines, or a list at the margin.
973
+ if (/^[ \t]/.test(line) || /^-(\s|$)/.test(line)) {
974
+ if (keys.size === 0) return null;
975
+ continue;
976
+ }
977
+ const found = /^([A-Za-z][\w-]*|"[A-Za-z][\w-]*"|'[A-Za-z][\w-]*')\s*:(\s|$)/.exec(line);
978
+ if (!found) return null;
979
+ keys.set(found[1].replace(/^['"]|['"]$/g, ''), { strings: () => null, object: () => null, packageCalls: () => false });
980
+ }
981
+ return keys;
982
+ }
983
+
984
+ /**
985
+ * The keys a configuration written as code sets, when the file is one
986
+ * object written out and nothing else in it can change that object or act
987
+ * on the run. The file may hold only: imports that bind names, variable
988
+ * declarations whose values are built without running code of the file
989
+ * (literals, names, operators, calls of functions that packages and the
990
+ * language provide), type declarations, and ONE export (`module.exports =`
991
+ * or `export default`) of an object literal: written in place, held by a
992
+ * variable that is named nowhere else, or passed through the function the
993
+ * runner's own package provides for that (proved by the import). A second
994
+ * statement that writes the export, a name for the object that is used
995
+ * again, a function of the file that is called, a write anywhere: the file
996
+ * is code whose effect is not read (null).
997
+ */
998
+ function configKeysOfCode(text, typed, runnerId) {
999
+ const tree = safeParse(getParser(typed ? 'typescript' : 'javascript'), text);
1000
+ if (!tree || tree.rootNode.hasError) return null;
1001
+ const root = tree.rootNode;
1002
+ const bindings = new Map(); // local name -> { module, imported } (imported null: the module itself)
1003
+ const declared = new Map(); // local name -> value nodes of its declarators
1004
+ const packaged = name => {
1005
+ const binding = bindings.get(name);
1006
+ return !!binding && !/^\.{0,2}\//.test(binding.module) && !/^[A-Za-z]:[\\/]/.test(binding.module);
1007
+ };
1008
+ const moduleName = binding => binding.module.replace(/^node:/, '');
1009
+ const requiredModule = node => {
1010
+ if (!node || node.type !== 'call_expression') return null;
1011
+ const callee = node.childForFieldName('function');
1012
+ const args = node.childForFieldName('arguments');
1013
+ if (!callee || callee.type !== 'identifier' || callee.text !== 'require' || !args || args.namedChildren.length !== 1) return null;
1014
+ return stringLiteralValue(args.namedChildren[0]);
1015
+ };
1016
+ /** Is a callee a function that a package or the language provides? */
1017
+ const calleeProvided = callee => {
1018
+ if (!callee) return false;
1019
+ if (CONFIG_WRAPPERS.has(callee.type)) return calleeProvided(callee.namedChildren[0]);
1020
+ if (callee.type === 'identifier') {
1021
+ const binding = bindings.get(callee.text);
1022
+ return packaged(callee.text) && !CONFIG_ACTING_MODULES.has(moduleName(binding)) ||
1023
+ (!!binding && packaged(callee.text) && CONFIG_READING_CALLS.has(binding.imported));
1024
+ }
1025
+ if (callee.type === 'call_expression') {
1026
+ // require('path').join, a function a package's function returns
1027
+ return requiredModule(callee) != null ? !CONFIG_ACTING_MODULES.has(requiredModule(callee).replace(/^node:/, ''))
1028
+ : calleeProvided(callee.childForFieldName('function')) && inert(callee.childForFieldName('arguments'));
1029
+ }
1030
+ if (callee.type !== 'member_expression') return false;
1031
+ const property = callee.childForFieldName('property');
1032
+ let base = callee;
1033
+ while (base && (base.type === 'member_expression' || CONFIG_WRAPPERS.has(base.type))) {
1034
+ base = base.type === 'member_expression' ? base.childForFieldName('object') : base.namedChildren[0];
1035
+ }
1036
+ if (!base || !property) return false;
1037
+ if (base.type === 'call_expression') {
1038
+ const required = requiredModule(base);
1039
+ if (required != null) {
1040
+ return !CONFIG_ACTING_MODULES.has(required.replace(/^node:/, '')) || CONFIG_READING_CALLS.has(property.text);
1041
+ }
1042
+ return calleeProvided(base.childForFieldName('function')) && inert(base.childForFieldName('arguments'));
1043
+ }
1044
+ if (base.type === 'meta_property') return true;
1045
+ if (base.type !== 'identifier') return false;
1046
+ if (base.text === 'process') {
1047
+ // process.cwd() and nothing else of the process is called.
1048
+ return sameNodeText(callee.childForFieldName('object'), 'process') && property.text === 'cwd';
1049
+ }
1050
+ if (CONFIG_GLOBAL_NAMESPACES.has(base.text) && !bindings.has(base.text) && !declared.has(base.text)) return true;
1051
+ const binding = bindings.get(base.text);
1052
+ if (!binding || !packaged(base.text)) return false;
1053
+ return !CONFIG_ACTING_MODULES.has(moduleName(binding)) || CONFIG_READING_CALLS.has(property.text);
1054
+ };
1055
+ const sameNodeText = (node, name) => !!node && node.type === 'identifier' && node.text === name;
1056
+ /** Does evaluating an expression run nothing of this file and write nothing? */
1057
+ function inert(node) {
1058
+ if (!node) return true;
1059
+ switch (node.type) {
1060
+ case 'string': case 'number': case 'true': case 'false': case 'null': case 'undefined': case 'regex':
1061
+ case 'identifier': case 'property_identifier': case 'shorthand_property_identifier':
1062
+ case 'private_property_identifier': case 'comment': case 'meta_property': case 'this':
1063
+ case 'string_fragment': case 'escape_sequence': case 'optional_chain':
1064
+ return true;
1065
+ // A function written here is not called here.
1066
+ case 'arrow_function': case 'function_expression': case 'function': case 'generator_function':
1067
+ return true;
1068
+ case 'template_string': case 'template_substitution': case 'parenthesized_expression': case 'array':
1069
+ case 'object': case 'pair': case 'spread_element': case 'binary_expression': case 'ternary_expression':
1070
+ case 'subscript_expression': case 'member_expression': case 'computed_property_name': case 'arguments':
1071
+ case 'as_expression': case 'satisfies_expression': case 'non_null_expression': case 'type_assertion':
1072
+ return node.namedChildren.every(child => /(^|_)type(s|_arguments|_annotation)?$|^type_|_type$/.test(child.type) || inert(child));
1073
+ case 'unary_expression':
1074
+ return node.children[0].type !== 'delete' && node.namedChildren.every(inert);
1075
+ case 'new_expression': {
1076
+ const made = node.childForFieldName('constructor');
1077
+ return !!made && made.type === 'identifier' && CONFIG_CONSTRUCTORS.has(made.text) &&
1078
+ !bindings.has(made.text) && !declared.has(made.text) && inert(node.childForFieldName('arguments'));
1079
+ }
1080
+ case 'call_expression': {
1081
+ if (requiredModule(node) != null) return true;
1082
+ return calleeProvided(node.childForFieldName('function')) && inert(node.childForFieldName('arguments'));
1083
+ }
1084
+ default:
1085
+ return false;
1086
+ }
1087
+ }
1088
+
1089
+ let exported = null;
1090
+ let exports = 0;
1091
+ let commonJs = false;
1092
+ const values = []; // every value a declaration evaluates
1093
+ for (const statement of root.namedChildren) {
1094
+ switch (statement.type) {
1095
+ case 'comment': case 'hash_bang_line': case 'empty_statement':
1096
+ case 'type_alias_declaration': case 'interface_declaration':
1097
+ break;
1098
+ case 'import_statement': {
1099
+ const clause = statement.namedChildren.find(child => child.type === 'import_clause');
1100
+ const source = stringLiteralValue(statement.childForFieldName('source'));
1101
+ // An import for its effect runs code that is not read.
1102
+ if (!clause || source == null) return null;
1103
+ for (const part of clause.namedChildren) {
1104
+ if (part.type === 'identifier') bindings.set(part.text, { module: source, imported: 'default' });
1105
+ else if (part.type === 'namespace_import') {
1106
+ const name = part.namedChildren.find(child => child.type === 'identifier');
1107
+ if (name) bindings.set(name.text, { module: source, imported: null });
1108
+ } else if (part.type === 'named_imports') {
1109
+ for (const specifier of part.namedChildren) {
1110
+ if (specifier.type !== 'import_specifier') continue;
1111
+ const name = specifier.childForFieldName('name');
1112
+ const alias = specifier.childForFieldName('alias');
1113
+ if (name) bindings.set((alias || name).text, { module: source, imported: name.text });
1114
+ }
1115
+ }
1116
+ }
1117
+ break;
1118
+ }
1119
+ case 'lexical_declaration': case 'variable_declaration': {
1120
+ for (const declarator of statement.namedChildren) {
1121
+ if (declarator.type !== 'variable_declarator') return null;
1122
+ const target = declarator.childForFieldName('name');
1123
+ const value = declarator.childForFieldName('value');
1124
+ const required = requiredModule(value);
1125
+ if (target && target.type === 'identifier') {
1126
+ if (required != null) bindings.set(target.text, { module: required, imported: null });
1127
+ else {
1128
+ if (!declared.has(target.text)) declared.set(target.text, []);
1129
+ declared.get(target.text).push(value || null);
1130
+ if (value) values.push(value);
1131
+ }
1132
+ } else if (target && target.type === 'object_pattern' && required != null) {
1133
+ for (const part of target.namedChildren) {
1134
+ if (part.type === 'shorthand_property_identifier_pattern') {
1135
+ bindings.set(part.text, { module: required, imported: part.text });
1136
+ } else if (part.type === 'pair_pattern') {
1137
+ const key = part.childForFieldName('key');
1138
+ const local = part.childForFieldName('value');
1139
+ if (!key || !local || local.type !== 'identifier' || key.type !== 'property_identifier') return null;
1140
+ bindings.set(local.text, { module: required, imported: key.text });
1141
+ } else {
1142
+ return null;
1143
+ }
1144
+ }
1145
+ } else {
1146
+ return null;
1147
+ }
1148
+ }
1149
+ break;
1150
+ }
1151
+ case 'expression_statement': {
1152
+ const expression = statement.namedChildren[0];
1153
+ // A directive ("use strict").
1154
+ if (expression && expression.type === 'string') break;
1155
+ if (!expression || expression.type !== 'assignment_expression') return null;
1156
+ const left = expression.childForFieldName('left');
1157
+ if (!left || left.type !== 'member_expression' || !sameNodeText(left.childForFieldName('object'), 'module') ||
1158
+ left.childForFieldName('property')?.text !== 'exports') return null;
1159
+ exported = expression.childForFieldName('right');
1160
+ commonJs = true;
1161
+ exports++;
1162
+ break;
1163
+ }
1164
+ case 'export_statement': {
1165
+ if (!statement.children.some(child => child.type === 'default')) return null;
1166
+ exported = statement.childForFieldName('value');
1167
+ exports++;
1168
+ break;
1169
+ }
1170
+ default:
1171
+ return null;
1172
+ }
1173
+ }
1174
+ if (exports !== 1 || !exported) return null;
1175
+ if (!values.every(inert)) return null;
1176
+ // The names that reach the exported object: each is used exactly as
1177
+ // often as reading it once needs.
1178
+ const uses = new Map();
1179
+ const count = node => {
1180
+ if (CONFIG_NAME_NODES.has(node.type)) uses.set(node.text, (uses.get(node.text) || 0) + 1);
1181
+ for (const child of node.namedChildren) count(child);
1182
+ };
1183
+ count(root);
1184
+ if ((uses.get('module') || 0) !== (commonJs ? 1 : 0) || uses.has('exports') || uses.has('eval') ||
1185
+ uses.has('globalThis') || uses.has('global')) return null;
1186
+ // `require` only as the call of one written-out module name.
1187
+ let requires = 0;
1188
+ const countRequires = node => {
1189
+ if (requiredModule(node) != null) requires++;
1190
+ for (const child of node.namedChildren) countRequires(child);
1191
+ };
1192
+ countRequires(root);
1193
+ if ((uses.get('require') || 0) !== requires) return null;
1194
+ const identity = CONFIG_IDENTITY[runnerId];
1195
+ const objectOf = (node, depth = 0) => {
1196
+ if (!node || depth > 6) return null;
1197
+ if (CONFIG_WRAPPERS.has(node.type)) return objectOf(node.namedChildren[0], depth + 1);
1198
+ if (node.type === 'object') return node;
1199
+ if (node.type === 'identifier') {
1200
+ // One declaration, and this the only other place it is named.
1201
+ const held = declared.get(node.text);
1202
+ if (!held || held.length !== 1 || bindings.has(node.text) || uses.get(node.text) !== 2) return null;
1203
+ return objectOf(held[0], depth + 1);
1204
+ }
1205
+ if (node.type === 'call_expression' && identity) {
1206
+ const callee = node.childForFieldName('function');
1207
+ const args = node.childForFieldName('arguments');
1208
+ const binding = callee && callee.type === 'identifier' && !declared.has(callee.text) ? bindings.get(callee.text) : null;
1209
+ if (binding && identity.modules.has(binding.module) && identity.names.has(binding.imported) &&
1210
+ args && args.namedChildren.length === 1) return objectOf(args.namedChildren[0], depth + 1);
1211
+ }
1212
+ return null;
1213
+ };
1214
+ const fromObject = node => {
1215
+ const object = objectOf(node);
1216
+ if (!object) return null;
1217
+ const keys = new Map();
1218
+ for (const member of object.namedChildren) {
1219
+ if (member.type === 'comment') continue;
1220
+ if (member.type !== 'pair') return null;
1221
+ const key = member.childForFieldName('key');
1222
+ const value = member.childForFieldName('value');
1223
+ const written = !key ? null : key.type === 'property_identifier' ? key.text : stringLiteralValue(key);
1224
+ if (written == null || written === '__proto__' || !inert(value)) return null;
1225
+ keys.set(written, {
1226
+ strings: () => {
1227
+ const one = stringLiteralValue(value);
1228
+ if (one != null) return [one];
1229
+ if (!value || value.type !== 'array') return null;
1230
+ const all = value.namedChildren.filter(item => item.type !== 'comment').map(stringLiteralValue);
1231
+ return all.every(item => item != null) ? all : null;
1232
+ },
1233
+ object: () => fromObject(value),
1234
+ // A list of what functions of packages return (plugins).
1235
+ packageCalls: () => !!value && value.type === 'array' &&
1236
+ value.namedChildren.every(item => item.type === 'comment' ||
1237
+ (item.type === 'call_expression' && requiredModule(item) == null && inert(item))),
1238
+ });
1239
+ }
1240
+ return keys;
1241
+ };
1242
+ return fromObject(exported);
1243
+ }
1244
+
1245
+ function configKeysOf(model, dir, name, runnerId) {
1246
+ const text = model.manifests.read(dir, name);
1247
+ if (text == null) return null;
1248
+ if (/\.(json|jsonc)$/.test(name)) {
1249
+ try {
1250
+ return configKeysOfJson(JSON.parse(withoutJsonComments(text)));
1251
+ } catch (e) {
1252
+ return null;
1253
+ }
1254
+ }
1255
+ if (/\.(yaml|yml)$/.test(name)) return configKeysOfYaml(text);
1256
+ if (!/\.[cm]?[jt]s$/.test(name)) return null;
1257
+ return configKeysOfCode(text, /\.[cm]?ts$/.test(name), runnerId);
1258
+ }
1259
+
1260
+ /**
1261
+ * What a JavaScript runner's configuration does to a command that names a
1262
+ * test file: `inert` (every key it sets is known to leave the selection
1263
+ * alone), `modeled` (`accepts(file)` says whether the configuration
1264
+ * leaves the file in; `ownPattern`: it replaces the runner's default file
1265
+ * pattern), or `unread` (a key that may select is set and not modeled, the
1266
+ * configuration is code that is not one written-out object, or one stands
1267
+ * above the runner's directory).
1268
+ */
1269
+ function jsRunnerConfig(model, runner) {
1270
+ const memoKey = `${runner.id}\0${runner.dir || ''}`;
1271
+ if (model.jsConfigs.has(memoKey)) return model.jsConfigs.get(memoKey);
1272
+ const answer = (() => {
1273
+ // The project's own script points the runner somewhere else.
1274
+ if (runnerScriptWords(runner, runner.id).some(word => SCRIPT_CONFIG_FLAGS.test(word))) return { state: 'unread' };
1275
+ const names = JS_CONFIG_NAMES[runner.id];
1276
+ const packageKey = JS_CONFIG_PACKAGE_KEY[runner.id];
1277
+ const here = runner.dir || '';
1278
+ for (const dir of ancestorDirs(here ? `${here}/x` : 'x').slice(1)) {
1279
+ if (model.manifests.list(dir).some(name => names.has(name))) return { state: 'unread' };
1280
+ const above = packageKey ? model.manifests.json(dir, 'package.json') : null;
1281
+ if (above && above[packageKey] != null) return { state: 'unread' };
1282
+ }
1283
+ let files = model.manifests.list(here).filter(name => names.has(name));
1284
+ if (runner.id === 'vitest') {
1285
+ if (files.some(name => /^vitest\.(workspace|projects)\./.test(name))) return { state: 'unread' };
1286
+ // vitest's own file is the one read when both are there.
1287
+ if (files.some(name => /^vitest\.config\./.test(name))) files = files.filter(name => /^vitest\.config\./.test(name));
1288
+ }
1289
+ const sources = files.map(name => configKeysOf(model, here, name, runner.id));
1290
+ const manifest = packageKey ? model.manifests.json(here, 'package.json') : null;
1291
+ if (manifest && manifest[packageKey] != null) {
1292
+ const held = manifest[packageKey];
1293
+ sources.push(configKeysOfJson(held));
1294
+ }
1295
+ if (sources.length === 0) return { state: 'inert' };
1296
+ if (sources.some(source => source == null) || (runner.id !== 'mocha' && sources.length > 1)) return { state: 'unread' };
1297
+ let keys = new Map();
1298
+ for (const source of sources) for (const [key, reader] of source) keys.set(key, reader);
1299
+ if (runner.id === 'vitest') {
1300
+ // The runner's part of a vite configuration is its `test` key;
1301
+ // a plugin can rewrite the configuration, so each one has to
1302
+ // be what a package's function returns.
1303
+ for (const [key, reader] of keys) {
1304
+ if (key === 'test' || VITE_INERT_KEYS.has(key)) continue;
1305
+ if (key === 'plugins' && reader.packageCalls()) continue;
1306
+ return { state: 'unread' };
1307
+ }
1308
+ const test = keys.get('test');
1309
+ if (!test) return { state: 'inert' };
1310
+ keys = test.object();
1311
+ if (!keys) return { state: 'unread' };
1312
+ }
1313
+ const fold = key => runner.id === 'mocha' ? key.replace(/-/g, '').toLowerCase() : key;
1314
+ const selecting = [...keys.keys()].filter(key => !JS_INERT_KEYS[runner.id].has(fold(key)));
1315
+ if (selecting.length === 0) return { state: 'inert', keys: new Set(keys.keys()) };
1316
+ if (!selecting.every(key => JS_MODELED_KEYS[runner.id].has(key))) return { state: 'unread' };
1317
+ const absolute = file => path.posix.join(String(model.root).replace(/\\/g, '/'), here, file);
1318
+ if (runner.id === 'jest') {
1319
+ const read = key => keys.has(key) ? keys.get(key).strings() : undefined;
1320
+ const roots = read('roots');
1321
+ const regex = read('testRegex');
1322
+ const ignored = read('testPathIgnorePatterns');
1323
+ if (roots === null || regex === null || ignored === null) return { state: 'unread' };
1324
+ let patterns;
1325
+ let ignores;
1326
+ const base = absolute('').replace(/\/$/, '');
1327
+ // jest puts the root directory where a pattern says <rootDir>.
1328
+ const pattern = source => new RegExp(source.replace(/<rootDir>/g, base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
1329
+ try {
1330
+ patterns = regex ? regex.map(pattern) : null;
1331
+ ignores = (ignored || ['/node_modules/']).map(pattern);
1332
+ } catch (e) {
1333
+ return { state: 'unread' };
1334
+ }
1335
+ const rootDirs = roots ? roots.map(root => path.posix.join(base, root.replace(/^<rootDir>\/?/, ''))) : null;
1336
+ if (roots && roots.some(root => path.posix.isAbsolute(root.replace(/^<rootDir>/, '')) && !root.startsWith('<rootDir>'))) {
1337
+ return { state: 'unread' };
1338
+ }
1339
+ return {
1340
+ state: 'modeled', ownPattern: !!patterns,
1341
+ accepts: file => {
1342
+ const full = absolute(file);
1343
+ if (rootDirs && !rootDirs.some(dir => full.startsWith(dir.replace(/\/$/, '') + '/'))) return false;
1344
+ if (ignores.some(pattern => pattern.test(full))) return false;
1345
+ return !patterns || patterns.some(pattern => pattern.test(full));
1346
+ },
1347
+ };
1348
+ }
1349
+ if (runner.id === 'vitest') {
1350
+ const dir = keys.get('dir').strings();
1351
+ if (!dir || dir.length !== 1 || path.posix.isAbsolute(dir[0]) || dir[0].includes('..')) return { state: 'unread' };
1352
+ const under = dir[0].replace(/^\.\//, '').replace(/\/$/, '');
1353
+ return { state: 'modeled', ownPattern: false, accepts: file => !under || file.startsWith(`${under}/`) };
1354
+ }
1355
+ return { state: 'unread' };
1356
+ })();
1357
+ model.jsConfigs.set(memoKey, answer);
1358
+ return answer;
1359
+ }
1360
+
1361
+ const RUNNER_OF_FAMILY = {
1362
+ javascript: jsRunner,
1363
+ python: pythonRunner,
1364
+ go: goRunner,
1365
+ rust: rustRunner,
1366
+ java: javaRunner,
1367
+ dotnet: dotnetRunner,
1368
+ };
1369
+
1370
+ // ---------------------------------------------------------------------------
1371
+ // Selectors
1372
+ // ---------------------------------------------------------------------------
1373
+
1374
+ function withheld(reason) {
1375
+ return { selector: null, reason };
1376
+ }
1377
+
1378
+ function command(runner, words) {
1379
+ return {
1380
+ selector: {
1381
+ command: words.join(' '),
1382
+ ...(runner.dir && { cwd: runner.dir }),
1383
+ },
1384
+ };
1385
+ }
1386
+
1387
+ // The files jest and vitest look for when the project configures nothing.
1388
+ // Neither runs a file outside its pattern, even when given the path
1389
+ // (measured): for such a file the command depends on a configuration that
1390
+ // is not read here.
1391
+ const DEFAULT_TEST_FILES = {
1392
+ jest: [/(^|\/)__tests__\/.*\.[jt]sx?$/, /(^|\/)([^/]*\.)?(spec|test)+\.[jt]sx?$/],
1393
+ vitest: [/\.(test|spec)\.[cm]?[jt]sx?$/],
1394
+ };
1395
+
1396
+ function lookedFor(runnerId, relativePath) {
1397
+ return DEFAULT_TEST_FILES[runnerId].some(pattern => pattern.test(relativePath));
1398
+ }
1399
+
1400
+ function titlePattern(unit) {
1401
+ return `^${regexLiteral([...unit.suite, unit.name].join(' '))}$`;
1402
+ }
1403
+
1404
+ const SELECTORS = {
1405
+ 'pytest'(model, runner, fileEntry, unit) {
1406
+ const file = relativeTo(runner.dir, fileEntry.relativePath);
1407
+ if (file == null) return withheld('outside-runner-root');
1408
+ // An option of the project's that selects, adds or leaves out
1409
+ // tests and is not modeled: no command is known to be exact.
1410
+ const options = runner.collection && runner.collection.options;
1411
+ if (options && options.unread) return withheld('runner-options-not-read');
1412
+ if (unit && unit.deselected) return withheld('deselected-by-options');
1413
+ if (!unit) return command(runner, ['pytest', shellWord(file)]);
1414
+ return command(runner, ['pytest', shellWord([file, ...unit.suite, unit.name].join('::'))]);
1415
+ },
1416
+ 'node-test'(model, runner, fileEntry, unit, filePath) {
1417
+ const file = relativeTo(runner.dir, fileEntry.relativePath);
1418
+ if (file == null) return withheld('outside-runner-root');
1419
+ const options = runner.evidence === 'package-script' || runner.script
1420
+ ? nodeTestOptions(runner.script) : [];
1421
+ const typed = langTraits(fileEntry.language)?.typeSystem === 'structural' &&
1422
+ /\.[cm]?tsx?$/.test(fileEntry.relativePath);
1423
+ // A typed file runs only with what the project's own command loads.
1424
+ if (typed && (!options || options.length === 0)) return withheld('loader-unknown');
1425
+ const words = ['node', '--test', ...(options || []).map(shellWord)];
1426
+ if (unit) {
1427
+ // node tries a pattern on a test's own name and on its full
1428
+ // path (ancestors joined by spaces), and a matching suite runs
1429
+ // all it holds: the full path selects one test unless another
1430
+ // name in the file reads the same.
1431
+ const full = [...unit.suite, unit.name].join(' ');
1432
+ const key = [...unit.suite, unit.name].join('\0');
1433
+ const clash = model.unitsOf(filePath).units.some(other => other !== unit && other.name != null &&
1434
+ (other.name === full || [...other.suite, other.name].join(' ') === full) &&
1435
+ [...other.suite, other.name].join('\0') !== key);
1436
+ if (clash) return withheld('name-not-unique');
1437
+ words.push(`--test-name-pattern=${shellWord(`^${regexLiteral(full)}$`)}`);
1438
+ }
1439
+ words.push(shellWord(file));
1440
+ return command(runner, words);
1441
+ },
1442
+ 'jest'(model, runner, fileEntry, unit) {
1443
+ const file = relativeTo(runner.dir, fileEntry.relativePath);
1444
+ if (file == null) return withheld('outside-runner-root');
1445
+ // jest holds a path it is given to its configuration: the file
1446
+ // pattern, the roots, the ignored paths (measured).
1447
+ const config = jsRunnerConfig(model, runner);
1448
+ if (config.state === 'unread') return withheld('runner-config-not-read');
1449
+ if (config.state === 'modeled' && !config.accepts(file)) return withheld('outside-runner-config');
1450
+ if (!(config.state === 'modeled' && config.ownPattern) && !lookedFor('jest', file)) {
1451
+ return withheld('runner-config-not-read');
1452
+ }
1453
+ // A bare path is a pattern jest tries on every test file's path.
1454
+ const words = ['npx', 'jest', '--runTestsByPath', shellWord(file)];
1455
+ if (unit) words.push('-t', shellWord(titlePattern(unit)));
1456
+ return command(runner, words);
1457
+ },
1458
+ 'vitest'(model, runner, fileEntry, unit, filePath) {
1459
+ const file = relativeTo(runner.dir, fileEntry.relativePath);
1460
+ if (file == null) return withheld('outside-runner-root');
1461
+ const config = jsRunnerConfig(model, runner);
1462
+ if (config.state === 'unread') return withheld('runner-config-not-read');
1463
+ if (config.state === 'modeled' && !config.accepts(file)) return withheld('outside-runner-config');
1464
+ if (!lookedFor('vitest', file)) return withheld('runner-config-not-read');
1465
+ // vitest runs every file whose path contains the one given.
1466
+ if (model.pathsContaining(runner.dir, file) > 1) return withheld('path-not-unique');
1467
+ // vitest refuses a focused test where CI is set unless told to
1468
+ // allow it (measured).
1469
+ const focus = model.declared(filePath)?.focus ? ['--allowOnly'] : [];
1470
+ const words = ['npx', 'vitest', 'run', ...focus, shellWord(file)];
1471
+ if (unit) words.push('-t', shellWord(titlePattern(unit)));
1472
+ return command(runner, words);
1473
+ },
1474
+ 'mocha'(model, runner, fileEntry, unit) {
1475
+ const file = relativeTo(runner.dir, fileEntry.relativePath);
1476
+ if (file == null) return withheld('outside-runner-root');
1477
+ // mocha adds the files its configuration names to the one given,
1478
+ // and applies its grep and ignore to it (measured).
1479
+ const config = jsRunnerConfig(model, runner);
1480
+ if (config.state !== 'inert') return withheld('runner-config-not-read');
1481
+ const loading = mochaLoadingOptions(runner);
1482
+ // A typed file loads only with what the project's own command or
1483
+ // configuration registers.
1484
+ const registers = loading.length > 0 || ['require', 'loader', 'import', 'node-option'].some(key => config.keys && config.keys.has(key));
1485
+ if (/\.[cm]?tsx?$/.test(file) && !registers) return withheld('loader-unknown');
1486
+ const words = ['npx', 'mocha', ...loading.map(shellWord), shellWord(file)];
1487
+ if (unit) words.push('--grep', shellWord(titlePattern(unit)));
1488
+ return command(runner, words);
1489
+ },
1490
+ 'go-test'(model, runner, fileEntry, unit) {
1491
+ const file = relativeTo(runner.dir, fileEntry.relativePath);
1492
+ if (file == null) return withheld('outside-runner-root');
1493
+ if (fileEntry.buildConstraint || (unit && unit.notInEveryBuild)) return withheld('build-constraint');
1494
+ const dir = posixDir(file);
1495
+ const pkg = dir ? `./${dir}` : '.';
1496
+ if (!unit) return command(runner, ['go', 'test', shellWord(pkg)]);
1497
+ return command(runner, ['go', 'test', shellWord(pkg), '-run', shellWord(`^${regexLiteral(unit.name)}$`)]);
1498
+ },
1499
+ 'cargo-test'(model, runner, fileEntry, unit) {
1500
+ if (runner.customTargets) return withheld('custom-targets');
1501
+ const located = model.rustTarget(runner, fileEntry, unit ? unit.line : 1);
1502
+ if (!located.target) return withheld(located.reason);
1503
+ const words = ['cargo', 'test', ...located.target];
1504
+ if (!unit) {
1505
+ if (located.segs.length > 0) words.push('--', shellWord(`${located.segs.join('::')}::`));
1506
+ return command(runner, words);
1507
+ }
1508
+ if (unit.notInEveryBuild) return withheld('conditional-compilation');
1509
+ words.push('--', shellWord([...located.segs, unit.name].join('::')), '--exact');
1510
+ if (unit.ignored) words.push('--include-ignored');
1511
+ return command(runner, words);
1512
+ },
1513
+ 'maven'(model, runner, fileEntry, unit, filePath) {
1514
+ if (!unit) {
1515
+ const classes = topLevelTestClasses(model, filePath);
1516
+ if (classes.length === 0) return withheld('no-test-class');
1517
+ return command(runner, ['mvn', 'test', shellWord(`-Dtest=${classes.join(',')}`)]);
1518
+ }
1519
+ const owner = `${unit.namespace ? unit.namespace + '.' : ''}${unit.suite.join('$')}`;
1520
+ return command(runner, ['mvn', 'test', shellWord(`-Dtest=${owner}#${unit.name}`)]);
1521
+ },
1522
+ 'dotnet-test'(model, runner, fileEntry, unit, filePath) {
1523
+ // The filter's own operators are escaped; '+' (a nested type) is not one.
1524
+ const quoted = text => String(text).replace(/[()&|=!~\\]/g, '\\$&');
1525
+ if (!unit) {
1526
+ const classes = topLevelTestClasses(model, filePath);
1527
+ if (classes.length === 0) return withheld('no-test-class');
1528
+ // A fixture built once for each set of arguments runs under
1529
+ // names that carry them: no filter by the class's name holds them.
1530
+ if ((model.declared(filePath)?.classes || []).some(cls => cls.dynamic === 'generated-cases')) {
1531
+ return withheld('generated-cases');
1532
+ }
1533
+ const terms = classes.flatMap(name => [`FullyQualifiedName~${quoted(name)}.`, `FullyQualifiedName~${quoted(name)}+`]);
1534
+ return command(runner, ['dotnet', 'test', '--filter', shellWord(terms.join('|'))]);
1535
+ }
1536
+ const owner = `${unit.namespace ? unit.namespace + '.' : ''}${unit.suite.join('+')}`;
1537
+ return command(runner, ['dotnet', 'test', '--filter', shellWord(`FullyQualifiedName=${quoted(`${owner}.${unit.name}`)}`)]);
1538
+ },
1539
+ };
1540
+
1541
+ /** Qualified names of the outermost classes of a file that hold tests. */
1542
+ function topLevelTestClasses(model, filePath) {
1543
+ const names = new Set();
1544
+ for (const unit of model.unitsOf(filePath).units) {
1545
+ if (unit.kind !== 'test' || unit.suite.length === 0) continue;
1546
+ names.add(`${unit.namespace ? unit.namespace + '.' : ''}${unit.suite[0]}`);
1547
+ }
1548
+ return [...names].sort(codeUnitCompare);
1549
+ }
1550
+
1551
+ // ---------------------------------------------------------------------------
1552
+ // The model
1553
+ // ---------------------------------------------------------------------------
1554
+
1555
+ /**
1556
+ * @param {object} index - ProjectIndex
1557
+ * @returns {object} per-operation test model
1558
+ */
1559
+ // Positions are query facts, never persisted test-reader state. Caller
1560
+ // provenance identifies the physical token even when its displayed line
1561
+ // holds several calls or tests.
1562
+ function testSite(site) {
1563
+ if (typeof site === 'number') return { line: site };
1564
+ const point = site.provenance?.facts?.site || {};
1565
+ return {
1566
+ line: site.line,
1567
+ ...(Number.isInteger(site.column ?? point.column) && { column: site.column ?? point.column }),
1568
+ ...(Number.isInteger(site.callStart ?? point.start) && { callStart: site.callStart ?? point.start }),
1569
+ ...(Number.isInteger(site.callEnd ?? point.end) && { callEnd: site.callEnd ?? point.end }),
1570
+ };
1571
+ }
1572
+
1573
+ function testSiteKey(site) {
1574
+ const point = testSite(site);
1575
+ return `${point.line}:${point.column ?? ''}:${point.callStart ?? ''}`;
1576
+ }
1577
+
1578
+ function containsTestSite(row, site) {
1579
+ const point = testSite(site);
1580
+ if (row.startLine > point.line || row.endLine < point.line) return false;
1581
+ return require('./callable-scope').containsCall(row, point) !== false;
1582
+ }
1583
+
1584
+ function containsTestUnit(outer, inner) {
1585
+ if (outer.sourceSpan && inner.sourceSpan) {
1586
+ return outer.sourceSpan.start <= inner.sourceSpan.start && outer.sourceSpan.end >= inner.sourceSpan.end;
1587
+ }
1588
+ return outer.startLine <= inner.startLine && outer.endLine >= inner.endLine;
1589
+ }
1590
+
1591
+ function innermostTestRows(rows, site) {
1592
+ const candidates = rows.filter(row => containsTestSite(row, site));
1593
+ return candidates.filter(row => !candidates.some(other => other !== row &&
1594
+ containsTestUnit(row, other) && !containsTestUnit(other, row)));
1595
+ }
1596
+
1597
+ function createTestModel(index, options = {}) {
1598
+ const model = {
1599
+ manifests: createManifests(index),
1600
+ byFile: new Map(),
1601
+ rawByFile: new Map(),
1602
+ exports: new Map(),
1603
+ runners: new Map(),
1604
+ jsConfigs: new Map(),
1605
+ gates: new Map(),
1606
+ plugins: new Map(),
1607
+ guards: new Map(),
1608
+ foreign: new Map(),
1609
+ ranges: new Map(),
1610
+ readerCaches: new Map(),
1611
+ root: index.root,
1612
+ relPaths: null,
1613
+ };
1614
+
1615
+ model.hasFile = relativePath => {
1616
+ if (!model.relPaths) {
1617
+ model.relPaths = new Set();
1618
+ for (const [, entry] of index.files) model.relPaths.add(entry.relativePath);
1619
+ }
1620
+ return model.relPaths.has(relativePath);
1621
+ };
1622
+
1623
+ /** How many indexed files under `dir` have a path that contains `part`. */
1624
+ model.pathsContaining = (dir, part) => {
1625
+ model.hasFile('');
1626
+ let count = 0;
1627
+ for (const relativePath of model.relPaths) {
1628
+ const inside = relativeTo(dir, relativePath);
1629
+ if (inside != null && inside.includes(part)) count++;
1630
+ }
1631
+ return count;
1632
+ };
1633
+
1634
+ model.runnerOf = filePath => {
1635
+ if (model.runners.has(filePath)) return model.runners.get(filePath);
1636
+ const fileEntry = index.files.get(filePath);
1637
+ const family = fileEntry && langTraits(fileEntry.language)?.testRunnerFamily;
1638
+ const find = family && RUNNER_OF_FAMILY[family];
1639
+ const runner = find ? find(model, fileEntry) : { id: null, reason: 'runner-not-modeled' };
1640
+ model.runners.set(filePath, runner);
1641
+ return runner;
1642
+ };
1643
+
1644
+ /**
1645
+ * The cargo target that compiles a file and the module path of a line
1646
+ * in it, from the module tree: `--lib`, `--bin NAME` or `--test NAME`.
1647
+ */
1648
+ model.rustTarget = (runner, fileEntry, line) => {
1649
+ const { rustModulePathAt } = require('./rust-modules');
1650
+ const filePath = path.join(index.root, fileEntry.relativePath);
1651
+ const located = rustModulePathAt(index, index.files.has(filePath) ? filePath : fileEntry.path || filePath, line);
1652
+ if (!located) return { target: null, reason: 'target-unknown' };
1653
+ const rootEntry = index.files.get(located.root);
1654
+ const rootRel = rootEntry && relativeTo(runner.dir, rootEntry.relativePath);
1655
+ if (rootRel == null) return { target: null, reason: 'target-unknown' };
1656
+ const segs = located.segs;
1657
+ if (rootRel === 'src/lib.rs') return { target: ['--lib'], segs };
1658
+ if (rootRel === 'src/main.rs') {
1659
+ return runner.packageName
1660
+ ? { target: ['--bin', shellWord(runner.packageName)], segs }
1661
+ : { target: null, reason: 'target-unknown' };
1662
+ }
1663
+ let match = /^src\/bin\/([^/]+)\.rs$/.exec(rootRel) || /^src\/bin\/([^/]+)\/main\.rs$/.exec(rootRel);
1664
+ if (match) return { target: ['--bin', shellWord(match[1])], segs };
1665
+ match = /^tests\/([^/]+)\.rs$/.exec(rootRel) || /^tests\/([^/]+)\/main\.rs$/.exec(rootRel);
1666
+ if (match) return { target: ['--test', shellWord(match[1])], segs };
1667
+ return { target: null, reason: 'target-unknown' };
1668
+ };
1669
+
1670
+ const helpersFor = (filePath, fileEntry, depth = 0) => {
1671
+ const symbols = fileEntry.symbols || [];
1672
+ let lines = null;
1673
+ return {
1674
+ /**
1675
+ * What a module this file imports exports under a name, when
1676
+ * the module is a file of the project whose language reads
1677
+ * exports: followed a few modules deep, never around a cycle.
1678
+ */
1679
+ exported(moduleName, name) {
1680
+ if (depth >= 4) return null;
1681
+ const relativePath = (fileEntry.moduleResolved || {})[moduleName];
1682
+ if (!relativePath) return null;
1683
+ const target = path.join(index.root, relativePath);
1684
+ const targetEntry = index.files.get(target);
1685
+ const read = targetEntry && langTraits(targetEntry.language)?.testExport;
1686
+ if (typeof read !== 'function') return null;
1687
+ const key = `${target}\0${name}`;
1688
+ if (model.exports.has(key)) return model.exports.get(key);
1689
+ model.exports.set(key, null);
1690
+ let answer;
1691
+ try {
1692
+ answer = read(targetEntry, helpersFor(target, targetEntry, depth + 1), name);
1693
+ } catch (e) {
1694
+ answer = null;
1695
+ }
1696
+ model.exports.set(key, answer);
1697
+ return answer;
1698
+ },
1699
+ /** Symbols whose range holds this one, outermost first. */
1700
+ enclosing(symbol) {
1701
+ const out = [];
1702
+ for (const other of symbols) {
1703
+ if (other === symbol) continue;
1704
+ if (other.sourceSpan && symbol.sourceSpan) {
1705
+ if (!containsTestUnit(other, symbol) || containsTestUnit(symbol, other)) continue;
1706
+ if (CONTAINER_KINDS.has(other.type) || CALLABLE_KINDS.has(other.type)) out.push(other);
1707
+ continue;
1708
+ }
1709
+ const end = other.endLine ?? other.startLine;
1710
+ const ownEnd = symbol.endLine ?? symbol.startLine;
1711
+ if (other.startLine > symbol.startLine || end < ownEnd) continue;
1712
+ if (other.startLine === symbol.startLine && end === ownEnd) {
1713
+ // Same lines: the holder is the one named as the owner.
1714
+ if (!(CONTAINER_KINDS.has(other.type) && (symbol.className === other.name ||
1715
+ symbol.enclosingType === other.name))) continue;
1716
+ }
1717
+ if (CONTAINER_KINDS.has(other.type) || CALLABLE_KINDS.has(other.type)) out.push(other);
1718
+ }
1719
+ return out.sort((a, b) => a.startLine - b.startLine ||
1720
+ (b.endLine ?? b.startLine) - (a.endLine ?? a.startLine));
1721
+ },
1722
+ /**
1723
+ * The annotations a type of the project carries itself, when
1724
+ * one type of that name is declared: what an annotation of
1725
+ * the project stands for. Null otherwise.
1726
+ */
1727
+ metaAnnotations(name) {
1728
+ const defs = (index.symbols.get(String(name).split('.').pop()) || [])
1729
+ .filter(def => CONTAINER_KINDS.has(def.type));
1730
+ if (defs.length !== 1) return null;
1731
+ return (defs[0].annotationsWithArgs || []).map(annotation => String(annotation.name).split('.').pop());
1732
+ },
1733
+ /** The types of this name the project declares: the namespace (package) and file of each. */
1734
+ typesNamed: name => (index.symbols.get(String(name).replace(/[<[(].*$/, '').split('.').pop().trim()) || [])
1735
+ .filter(def => CONTAINER_KINDS.has(def.type))
1736
+ .map(def => ({ namespace: def.namespace || '', file: index.files.get(def.file)?.relativePath || null })),
1737
+ /** Does the project declare a type of this name? */
1738
+ declaresType: name => (index.symbols.get(String(name).replace(/[<[(].*$/, '').split('.').pop().trim()) || [])
1739
+ .some(def => CONTAINER_KINDS.has(def.type)),
1740
+ isContainer: symbol => CONTAINER_KINDS.has(symbol.type),
1741
+ isCallable: symbol => CALLABLE_KINDS.has(symbol.type),
1742
+ /** Names of the types a class derives from, through the project. */
1743
+ ancestors(className) {
1744
+ const seen = new Set();
1745
+ const queue = [className];
1746
+ while (queue.length > 0 && seen.size < 64) {
1747
+ const parents = index.extendsGraph?.get(queue.shift()) || [];
1748
+ // One entry per declaration of the name, each with
1749
+ // the bases it writes.
1750
+ const names = parents.flatMap(parent => typeof parent === 'string' ? [parent]
1751
+ : Array.isArray(parent?.parents) ? parent.parents : parent?.name ? [parent.name] : []);
1752
+ for (const written of names) {
1753
+ const name = String(written).replace(/[<[(].*$/, '').trim();
1754
+ if (!name || seen.has(name)) continue;
1755
+ seen.add(name);
1756
+ queue.push(name);
1757
+ // A written `pkg.Base` is also known by its last name.
1758
+ const last = name.split('.').pop();
1759
+ if (last !== name && !seen.has(last)) { seen.add(last); queue.push(last); }
1760
+ }
1761
+ }
1762
+ return seen;
1763
+ },
1764
+ tree() {
1765
+ try {
1766
+ const content = index._readFile(filePath);
1767
+ return index._getParsedTree(filePath, content, fileEntry.language) ||
1768
+ safeParse(getParser(fileEntry.language), content) || null;
1769
+ } catch (e) {
1770
+ return null;
1771
+ }
1772
+ },
1773
+ lines() {
1774
+ if (!lines) {
1775
+ try {
1776
+ lines = index._readFile(filePath).split('\n');
1777
+ } catch (e) {
1778
+ lines = [];
1779
+ }
1780
+ }
1781
+ return lines;
1782
+ },
1783
+ collection: collectionFor(filePath, fileEntry),
1784
+ glob: globMatches,
1785
+ // What a reader keeps between two questions about one file.
1786
+ cache: (() => {
1787
+ if (!model.readerCaches.has(filePath)) model.readerCaches.set(filePath, new Map());
1788
+ return model.readerCaches.get(filePath);
1789
+ })(),
1790
+ };
1791
+ };
1792
+
1793
+ /**
1794
+ * What the runner's configuration says about collecting a file: its
1795
+ * naming rules, its options, and whether the runner collects a module
1796
+ * of this name at all (pytest: python_files, by the file's own name).
1797
+ */
1798
+ const collectionFor = (filePath, fileEntry) => {
1799
+ const runner = model.runnerOf(filePath) || {};
1800
+ const collection = runner.collection || {};
1801
+ if (runner.id !== 'pytest') return collection;
1802
+ const base = path.posix.basename(fileEntry.relativePath);
1803
+ const patterns = collection.files && collection.files.length > 0 ? collection.files : ['test_*.py', '*_test.py'];
1804
+ return { ...collection, collected: patterns.some(glob => globMatches(glob, base)) };
1805
+ };
1806
+
1807
+ /** What the language's reader finds in a file; null when not modeled. */
1808
+ model.declared = filePath => {
1809
+ if (model.rawByFile.has(filePath)) return model.rawByFile.get(filePath);
1810
+ const fileEntry = index.files.get(filePath);
1811
+ const read = fileEntry && langTraits(fileEntry.language)?.testUnits;
1812
+ let found = null;
1813
+ if (typeof read === 'function') {
1814
+ try {
1815
+ found = read(fileEntry, helpersFor(filePath, fileEntry));
1816
+ } catch (e) {
1817
+ found = null;
1818
+ }
1819
+ }
1820
+ model.rawByFile.set(filePath, found);
1821
+ return found;
1822
+ };
1823
+
1824
+ /**
1825
+ * The class a base name written in `filePath` denotes: one declared in
1826
+ * the same file, else the only one of that name in files of the same
1827
+ * runner family. Null when none or several.
1828
+ */
1829
+ const baseClass = (filePath, written) => {
1830
+ const name = String(written).replace(/<.*$/, '').split('.').pop().trim();
1831
+ if (!name) return null;
1832
+ const own = (model.declared(filePath)?.classes || []).filter(cls => cls.name === name);
1833
+ if (own.length === 1) return { filePath, cls: own[0] };
1834
+ if (own.length > 1) return null;
1835
+ const family = langTraits(index.files.get(filePath)?.language)?.testRunnerFamily;
1836
+ const hits = [];
1837
+ const seenFiles = new Set();
1838
+ for (const def of index.symbols.get(name) || []) {
1839
+ if (!CONTAINER_KINDS.has(def.type) || seenFiles.has(def.file)) continue;
1840
+ if (langTraits(index.files.get(def.file)?.language)?.testRunnerFamily !== family) continue;
1841
+ seenFiles.add(def.file);
1842
+ for (const cls of model.declared(def.file)?.classes || []) {
1843
+ if (cls.name === name) hits.push({ filePath: def.file, cls });
1844
+ }
1845
+ }
1846
+ return hits.length === 1 ? hits[0] : null;
1847
+ };
1848
+
1849
+ // What a written type name denotes and whether two methods are one
1850
+ // member, by the language's lookup where it is modeled.
1851
+ const signatures = createSignatures(index, {
1852
+ classes: filePath => model.declared(filePath)?.classes || [],
1853
+ parents: (filePath, cls) => parentsOf(filePath, cls),
1854
+ aliasTerm: (filePath, line) => ((model.declared(filePath)?.aliases || []).find(alias => alias.line === line) || {}).term || null,
1855
+ // Files one runner's manifest builds are compiled together.
1856
+ unitOf: filePath => {
1857
+ const runner = model.runnerOf(filePath);
1858
+ return runner && runner.id && runner.dir != null ? `${runner.id}\0${runner.dir}` : null;
1859
+ },
1860
+ });
1861
+ const parentMemo = new Map();
1862
+ const resolving = new Set();
1863
+ /**
1864
+ * The class each base written on a class denotes, in the order
1865
+ * written. A language whose name lookup is modeled says which class
1866
+ * a name is at the place it is written (a class of the project that
1867
+ * shares the spelling of a type from outside is not it); otherwise the
1868
+ * one class of that name. A base that is no class the project declares
1869
+ * once stands as an opaque entry, `safe` when the language's reader
1870
+ * knows it brings neither a test nor a constructor.
1871
+ */
1872
+ const parentsOf = (filePath, cls) => {
1873
+ const key = `${filePath}\0${cls.startLine}\0${cls.name}`;
1874
+ if (parentMemo.has(key)) return parentMemo.get(key);
1875
+ const opaque = written => ({
1876
+ opaque: true, name: String(written).replace(/[<[(].*$/, '').split('.').pop().trim(),
1877
+ safe: (cls.plainBases || []).includes(written),
1878
+ });
1879
+ // A class whose bases are being resolved inside its own
1880
+ // resolution: the language rejects the cycle.
1881
+ if (resolving.has(key)) return cls.bases.map(written => ({ written, node: opaque(written) }));
1882
+ resolving.add(key);
1883
+ const out = cls.bases.map((written, i) => {
1884
+ const looked = signatures.baseOf(filePath, cls, (cls.baseTerms || [])[i]);
1885
+ const base = looked === undefined ? baseClass(filePath, written) : looked;
1886
+ return { written, node: base || opaque(written) };
1887
+ });
1888
+ resolving.delete(key);
1889
+ parentMemo.set(key, out);
1890
+ return out;
1891
+ };
1892
+
1893
+ const nodeKey = node => node.opaque ? `?${node.name}` : `${node.filePath}\0${node.cls.startLine}\0${node.cls.name}`;
1894
+ const linearMemo = new Map();
1895
+
1896
+ /**
1897
+ * The order in which a class's members are looked up: the class, then
1898
+ * the classes it derives from. `c3` is Python's method resolution
1899
+ * order; otherwise depth first in the order written (a superclass
1900
+ * before the interfaces). A base that is no class of the project
1901
+ * stands in the order as an opaque entry (`parentsOf`).
1902
+ */
1903
+ const linearize = (filePath, cls, mode, trail = new Set()) => {
1904
+ const key = `${filePath}\0${cls.startLine}\0${cls.name}`;
1905
+ if (linearMemo.has(key)) return linearMemo.get(key);
1906
+ const self = { filePath, cls };
1907
+ if (trail.has(key)) return [self];
1908
+ if (cls.imported) {
1909
+ // A class bound by an import is the class it names.
1910
+ const order = [self, ...linearize(cls.imported.filePath, cls.imported.cls, mode, trail)];
1911
+ linearMemo.set(key, order);
1912
+ return order;
1913
+ }
1914
+ trail.add(key);
1915
+ const parents = parentsOf(filePath, cls).map(parent => parent.node);
1916
+ const below = parent => parent.opaque ? [parent] : linearize(parent.filePath, parent.cls, mode, trail);
1917
+ let order = [self];
1918
+ if (mode === 'c3') {
1919
+ const sequences = [...parents.map(parent => below(parent).slice()), parents.slice()];
1920
+ while (sequences.some(sequence => sequence.length > 0)) {
1921
+ const head = sequences.filter(sequence => sequence.length > 0).map(sequence => sequence[0])
1922
+ .find(candidate => !sequences.some(other =>
1923
+ other.slice(1).some(node => nodeKey(node) === nodeKey(candidate))));
1924
+ if (!head) {
1925
+ // No consistent order: the language rejects the class.
1926
+ order = [self, { opaque: true, name: '', safe: false }];
1927
+ break;
1928
+ }
1929
+ order.push(head);
1930
+ for (const sequence of sequences) {
1931
+ if (sequence.length > 0 && nodeKey(sequence[0]) === nodeKey(head)) sequence.shift();
1932
+ }
1933
+ }
1934
+ } else {
1935
+ const seen = new Set([key]);
1936
+ for (const parent of parents) {
1937
+ for (const node of below(parent)) {
1938
+ if (seen.has(nodeKey(node))) continue;
1939
+ seen.add(nodeKey(node));
1940
+ order.push(node);
1941
+ }
1942
+ }
1943
+ }
1944
+ trail.delete(key);
1945
+ linearMemo.set(key, order);
1946
+ return order;
1947
+ };
1948
+ const orderOf = (filePath, cls) => linearize(filePath, cls, model.declared(filePath)?.linearization || null);
1949
+
1950
+ /**
1951
+ * Tests a runnable class runs without declaring them: for each
1952
+ * test-shaped name, the first class in lookup order that declares a
1953
+ * member of that name. A test found behind a base that is not read is
1954
+ * not attributed (`withheld`): that base may declare the name itself.
1955
+ *
1956
+ * Which member of a nearer class is "the same member" is the
1957
+ * language's and the runner's to say (`hiddenBy` on a test): by name
1958
+ * where a name is all a member has (Python; and MSTest, which takes
1959
+ * one method of each name), by signature where a language overloads
1960
+ * (a method of the same name with other parameters is another method
1961
+ * and hides nothing), or only a declared override of that signature
1962
+ * (NUnit: a method that hides by `new` leaves the inherited test
1963
+ * running).
1964
+ *
1965
+ * Two signatures are the same when their parameters have the same
1966
+ * types, and a type is what its name denotes where it is written
1967
+ * (`signatures.relate`): the answer can be that this is not known.
1968
+ * Then the test may be the class's or may not (`uncertain`): it is
1969
+ * listed as one that runs only sometimes, under no command, with
1970
+ * every body it may run (`maybe`).
1971
+ *
1972
+ * Where the runner takes a test from the nearest declaration that
1973
+ * carries its mark (`byDeclaration`: JUnit 4, TestNG, xUnit), a class
1974
+ * nearer that overrides the method without the mark does not hide it:
1975
+ * the test runs with that class's body (`body`).
1976
+ */
1977
+ const inheritedBy = (filePath, cls, select = null) => {
1978
+ const out = [];
1979
+ // What the classes nearer than the one looked at declare, nearest
1980
+ // first: every member by name, the methods with what they are
1981
+ // compared by, and the tests.
1982
+ const names = new Set();
1983
+ const testedNames = new Set();
1984
+ const members = [];
1985
+ const tested = [];
1986
+ const declare = (node, each) => {
1987
+ for (const member of each.members || []) names.add(member);
1988
+ // A method that is a test is compared as one: its row among
1989
+ // the members is the one that shares its parameter list (two
1990
+ // methods can start on one line).
1991
+ const lists = new Set((each.tests || []).map(test => test.params).filter(Boolean));
1992
+ const lines = new Set((each.tests || []).filter(test => !test.params).map(test => test.startLine));
1993
+ for (const row of each.memberRows || []) {
1994
+ if (!(row.params ? lists.has(row.params) : lines.has(row.startLine))) members.push({ row, at: node });
1995
+ }
1996
+ };
1997
+ const sideOf = ({ row, at }) => ({ filePath: at.filePath, cls: at.cls, row });
1998
+ /**
1999
+ * A declared override overrides one method: where the classes
2000
+ * above it are all read and declare one method of that name with
2001
+ * as many parameters, it is that one, however its types are
2002
+ * written.
2003
+ */
2004
+ const onlySlot = (member, test, node) => {
2005
+ const order = orderOf(member.at.filePath, member.at.cls).slice(1);
2006
+ if (order.some(above => above.opaque)) return false;
2007
+ const arity = (member.row.params || []).length;
2008
+ const universal = langTraits(index.files.get(member.at.filePath)?.language)?.universalSupertypeMembers || {};
2009
+ if (universal[member.row.name] === arity) return false;
2010
+ const slots = order.flatMap(above => (above.cls.memberRows || [])
2011
+ .filter(row => row.name === member.row.name && (row.params || []).length === arity)
2012
+ .map(row => ({ row, at: above })));
2013
+ return slots.length > 0 && slots.every(slot =>
2014
+ (slot.at.cls === node.cls && slot.row.startLine === test.startLine) ||
2015
+ signatures.relate(sideOf(slot), { filePath: node.filePath, cls: node.cls, row: test }) === 'same');
2016
+ };
2017
+ const start = { filePath, cls };
2018
+ declare(start, cls);
2019
+ for (const test of cls.tests || []) {
2020
+ names.add(test.name);
2021
+ testedNames.add(test.name);
2022
+ tested.push({ row: test, at: start });
2023
+ }
2024
+ let behindUnknown = false;
2025
+ let withheldAny = false;
2026
+ for (const node of orderOf(filePath, cls).slice(1)) {
2027
+ if (node.opaque) {
2028
+ if (!node.safe) behindUnknown = true;
2029
+ continue;
2030
+ }
2031
+ for (const test of node.cls.tests) {
2032
+ // A test its runner runs in the declaring class only.
2033
+ if (test.ownOnly) continue;
2034
+ const rule = test.hiddenBy || 'name';
2035
+ const mine = { filePath: node.filePath, cls: node.cls, row: test };
2036
+ let body = null;
2037
+ let uncertain = false;
2038
+ const maybe = [];
2039
+ let again = false;
2040
+ if (rule === 'name') {
2041
+ // A nearer class declares the same test.
2042
+ if (testedNames.has(test.name)) {
2043
+ // A runner that takes one method of each name
2044
+ // reports the inherited test too under a test
2045
+ // that hides it without overriding it, and runs
2046
+ // the nearer body for it (`againUnderHiding`:
2047
+ // measured with one version of MSTest, so the
2048
+ // test is listed as one that runs only sometimes).
2049
+ const nearer = tested.find(other => other.row.name === test.name);
2050
+ if (!test.againUnderHiding || !nearer || nearer.row.overrides || nearer.again) continue;
2051
+ body = nearer;
2052
+ again = true;
2053
+ tested.push({ row: test, at: node, again: true });
2054
+ } else {
2055
+ testedNames.add(test.name);
2056
+ tested.push({ row: test, at: node });
2057
+ // A nearer class declares the same member without the mark.
2058
+ if (names.has(test.name) && !test.byDeclaration) continue;
2059
+ }
2060
+ } else {
2061
+ // A runner that takes a method as replaced by one whose
2062
+ // parameters could be passed to it: another signature
2063
+ // is no proof the test is still the class's.
2064
+ const relate = other => {
2065
+ const relation = signatures.relate(mine, sideOf(other));
2066
+ return relation === 'different' && test.replacedByAssignable &&
2067
+ (other.row.params || []).length === (test.params || []).length ? 'unknown' : relation;
2068
+ };
2069
+ // A method the language does not let a class override
2070
+ // (`sealed`: always; `sealedOutside`: from another
2071
+ // package) is not replaced by an unmarked one of its
2072
+ // signature: the two are two methods, and the test
2073
+ // keeps its body. (A marked one is the class's test of
2074
+ // that signature: measured.)
2075
+ const overrides = other => !test.sealed &&
2076
+ (test.sealedOutside == null || (other.at.cls.namespace || '') === test.sealedOutside);
2077
+ // A nearer class declares the same test. (Where only a
2078
+ // declared override takes a test away, a test that
2079
+ // hides it by another declaration runs beside it.)
2080
+ let hidden = false;
2081
+ for (const other of tested) {
2082
+ if (other.row.name !== test.name || (rule === 'override' && !other.row.overrides)) continue;
2083
+ const relation = relate(other);
2084
+ if (relation === 'same') {
2085
+ hidden = true;
2086
+ break;
2087
+ }
2088
+ if (relation === 'unknown') uncertain = true;
2089
+ }
2090
+ testedNames.add(test.name);
2091
+ tested.push({ row: test, at: node });
2092
+ if (hidden) continue;
2093
+ // A nearer class declares the same member without the
2094
+ // mark: the nearest that is it for certain, and the
2095
+ // nearer ones that may be.
2096
+ for (const other of members) {
2097
+ if (other.row.name !== test.name || (rule === 'override' && !other.row.overrides) || !overrides(other)) continue;
2098
+ let relation = relate(other);
2099
+ if (relation === 'unknown' && other.row.overrides && onlySlot(other, test, node)) relation = 'same';
2100
+ if (relation === 'same') {
2101
+ body = other;
2102
+ break;
2103
+ }
2104
+ if (relation === 'unknown') maybe.push(other);
2105
+ }
2106
+ if (body && !test.byDeclaration) continue;
2107
+ // A class declares one method of a signature: beside
2108
+ // the one that is it, another of that class is not.
2109
+ const open = maybe.filter(other => !body || other.at.cls !== body.at.cls);
2110
+ maybe.length = 0;
2111
+ maybe.push(...open);
2112
+ if (maybe.length > 0) uncertain = true;
2113
+ }
2114
+ // A member the runner does not take as a test still hides
2115
+ // the one of that name further up.
2116
+ if (select && !select(test)) continue;
2117
+ if (behindUnknown) withheldAny = true;
2118
+ else {
2119
+ out.push({
2120
+ test, base: node,
2121
+ ...((test.byDeclaration || again) && body && { body }),
2122
+ ...(uncertain && { uncertain: true, maybe }),
2123
+ ...(again && { again: true }),
2124
+ });
2125
+ }
2126
+ }
2127
+ declare(node, node.cls);
2128
+ for (const test of node.cls.tests) names.add(test.name);
2129
+ }
2130
+ return { tests: out, open: behindUnknown, withheld: withheldAny };
2131
+ };
2132
+
2133
+ /**
2134
+ * Is there code beside a test file that the runner lets decide which
2135
+ * tests run (the language's `testGate`): a TestMain in its Go package
2136
+ * that may not run the tests. (pytest's conftest.py files are read
2137
+ * with the modules they stand above: see guardsOf.)
2138
+ */
2139
+ const gatedAtRunTime = (filePath, runner) => {
2140
+ const relativePath = index.files.get(filePath).relativePath;
2141
+ const decides = candidate => {
2142
+ if (!model.gates.has(candidate)) {
2143
+ const entry = index.files.get(candidate);
2144
+ const gate = entry && langTraits(entry.language)?.testGate;
2145
+ let answer = false;
2146
+ if (typeof gate === 'function') {
2147
+ try {
2148
+ answer = !!gate(entry, helpersFor(candidate, entry));
2149
+ } catch (e) {
2150
+ answer = true;
2151
+ }
2152
+ }
2153
+ model.gates.set(candidate, answer);
2154
+ }
2155
+ return model.gates.get(candidate);
2156
+ };
2157
+ if (runner.id === 'go-test') {
2158
+ if (!model.goTestFiles) {
2159
+ model.goTestFiles = new Map();
2160
+ for (const [candidate, entry] of index.files) {
2161
+ if (!/_test\.go$/.test(entry.relativePath)) continue;
2162
+ const dir = posixDir(entry.relativePath);
2163
+ if (!model.goTestFiles.has(dir)) model.goTestFiles.set(dir, []);
2164
+ model.goTestFiles.get(dir).push(candidate);
2165
+ }
2166
+ }
2167
+ return (model.goTestFiles.get(posixDir(relativePath)) || []).some(decides);
2168
+ }
2169
+ return false;
2170
+ };
2171
+
2172
+ // ---- a reader that gives facts per file (`derived`): pytest ----------
2173
+
2174
+ /** The project file a module name written in a file resolves to, or null. */
2175
+ const moduleFile = (filePath, moduleName) => {
2176
+ const relative = (index.files.get(filePath)?.moduleResolved || {})[moduleName];
2177
+ const target = relative ? path.join(index.root, relative) : null;
2178
+ return target && index.files.has(target) ? target : null;
2179
+ };
2180
+
2181
+ /** The project file a dotted module name stands for, from the runner's directory or the project's. */
2182
+ const moduleNamed = (filePath, dotted) => {
2183
+ const runner = model.runnerOf(filePath) || {};
2184
+ const relative = String(dotted).replace(/\./g, '/');
2185
+ for (const base of new Set([runner.dir || '', ''])) {
2186
+ for (const tail of [`${relative}.py`, `${relative}/__init__.py`]) {
2187
+ const candidate = base ? `${base}/${tail}` : tail;
2188
+ if (model.hasFile(candidate)) return path.join(index.root, candidate);
2189
+ }
2190
+ }
2191
+ return null;
2192
+ };
2193
+
2194
+ /**
2195
+ * Does a module declare tests where one of its functions is called
2196
+ * (a declaration inside a function that is no suite's callback), itself
2197
+ * or through the modules whose functions it calls?
2198
+ */
2199
+ const declaresWhenCalled = (filePath, seen = new Set()) => {
2200
+ if (seen.has(filePath)) return false;
2201
+ seen.add(filePath);
2202
+ const found = model.declared(filePath);
2203
+ if (!found) return false;
2204
+ if ((found.units || []).some(unit => unit.kind === 'test' &&
2205
+ (unit.indirect || unit.dynamic === 'declared-indirectly'))) return true;
2206
+ return (found.calledWhenCalled || []).some(name => {
2207
+ const target = moduleFile(filePath, name);
2208
+ return !!target && declaresWhenCalled(target, seen);
2209
+ });
2210
+ };
2211
+
2212
+ /**
2213
+ * What a name of a module is, as far as tests go: a class or a function
2214
+ * of its top, or what another name of the module or another module
2215
+ * gives it (an alias, an import, a star import), a few steps deep.
2216
+ */
2217
+ const resolveName = (filePath, name, depth = 0) => {
2218
+ if (!filePath || depth > 6) return null;
2219
+ const found = model.declared(filePath);
2220
+ if (!found) return null;
2221
+ const cls = (found.classes || []).find(other => other.name === name && other.suite.length === 0);
2222
+ if (cls) return { kind: 'class', filePath, cls };
2223
+ const fn = (found.functions || []).find(other => other.name === name);
2224
+ if (fn) return { kind: 'function', filePath, fn };
2225
+ for (const entry of found.imports || []) {
2226
+ if (entry.name !== name) continue;
2227
+ const from = entry.module == null ? filePath : moduleFile(filePath, entry.module);
2228
+ if (from === filePath && entry.original === name) continue;
2229
+ const hit = resolveName(from, entry.original, depth + 1);
2230
+ if (hit) return hit;
2231
+ }
2232
+ for (const star of found.stars || []) {
2233
+ const from = moduleFile(filePath, star.module);
2234
+ const there = from ? model.declared(from) : null;
2235
+ if (!there || there.exportsAll === 'unknown') continue;
2236
+ if (there.exportsAll ? !there.exportsAll.includes(name) : name.startsWith('_')) continue;
2237
+ const hit = resolveName(from, name, depth + 1);
2238
+ if (hit) return hit;
2239
+ }
2240
+ return null;
2241
+ };
2242
+
2243
+ /**
2244
+ * Switches other modules write on the classes and functions of a
2245
+ * module, each on a name it took from it. The object is one: a module
2246
+ * the runner loads after the writer sees the write, so what such a
2247
+ * class or function is depends on the order of the run. Looked for
2248
+ * among the modules that import this one.
2249
+ */
2250
+ const switchedElsewhere = filePath => {
2251
+ if (model.foreign.has(filePath)) return model.foreign.get(filePath);
2252
+ const answer = { classes: new Map(), functions: new Map() };
2253
+ model.foreign.set(filePath, answer);
2254
+ const add = (map, key, write) => {
2255
+ if (!map.has(key)) map.set(key, []);
2256
+ map.get(key).push(write);
2257
+ };
2258
+ for (const importer of [...(index.exportGraph?.get(filePath) || [])].sort(codeUnitCompare)) {
2259
+ if (importer === filePath || !index.files.has(importer)) continue;
2260
+ let text;
2261
+ try {
2262
+ text = index._readFile(importer);
2263
+ } catch (e) {
2264
+ continue;
2265
+ }
2266
+ if (!text.includes('__test__')) continue;
2267
+ const there = model.declared(importer);
2268
+ const switches = there && there.derived && there.module ? there.module.switches || {} : {};
2269
+ for (const name of Object.keys(switches).sort(codeUnitCompare)) {
2270
+ const hit = resolveName(importer, name);
2271
+ if (!hit || hit.filePath !== filePath) continue;
2272
+ const write = { on: switches[name].on, forced: switches[name].forced, by: importer };
2273
+ if (hit.kind === 'class') add(answer.classes, hit.cls, write);
2274
+ else add(answer.functions, hit.fn.name, write);
2275
+ }
2276
+ }
2277
+ return answer;
2278
+ };
2279
+
2280
+ /**
2281
+ * Does one of the functions a file calls in other modules of the
2282
+ * project stop a test when called (it skips, raises, or calls a
2283
+ * function of its own module that does)? One module deep.
2284
+ */
2285
+ const callStops = (filePath, calls) => (calls || []).some(call => {
2286
+ const target = moduleFile(filePath, call.module) || moduleNamed(filePath, call.module);
2287
+ const there = target ? model.declared(target) : null;
2288
+ return !!there && (there.stopping || []).includes(call.name);
2289
+ });
2290
+ /**
2291
+ * What a name of a module is as a row of a table of parameters
2292
+ * ('run', 'skip', 'sometimes'), read in the module that binds it and,
2293
+ * where that module took it from another, in that one: a few modules
2294
+ * deep. Null: the module does not bind it.
2295
+ */
2296
+ const readRow = (file, name, member, call, depth) => {
2297
+ const there = file ? model.declared(file) : null;
2298
+ if (!there || typeof there.rowNamed !== 'function') return null;
2299
+ const state = there.rowNamed(name, member, call);
2300
+ if (!state || typeof state !== 'object') return state;
2301
+ if (depth >= 4) return 'sometimes';
2302
+ if (state.via) return viaLookup(file, state.via, depth + 1) ?? 'sometimes';
2303
+ // A name a star import brings: read in the module that binds it.
2304
+ for (const module of state.stars || []) {
2305
+ const found = viaLookup(file, { module, name, ...(member && { member }), call }, depth + 1);
2306
+ if (found != null) return found;
2307
+ }
2308
+ return null;
2309
+ };
2310
+ const viaLookup = (filePath, via, depth) => {
2311
+ const direct = moduleFile(filePath, via.module) || moduleNamed(filePath, via.module);
2312
+ const state = readRow(direct, via.name, via.member || null, !!via.call, depth);
2313
+ if (state != null) return state;
2314
+ // Not a name of that module: a module of that package.
2315
+ let inner = null;
2316
+ if (direct && path.basename(direct) === '__init__.py') {
2317
+ for (const tail of [`${via.name}.py`, path.join(via.name, '__init__.py')]) {
2318
+ const candidate = path.join(path.dirname(direct), tail);
2319
+ if (!inner && index.files.has(candidate)) inner = candidate;
2320
+ }
2321
+ } else if (!direct && !String(via.module).startsWith('.')) {
2322
+ inner = moduleNamed(filePath, `${via.module}.${via.name}`);
2323
+ }
2324
+ if (!inner || !model.declared(inner)) return null;
2325
+ // The module itself is a plain row; calling it is not read.
2326
+ if (!via.member) return via.call ? 'sometimes' : 'run';
2327
+ return readRow(inner, via.member, null, !!via.call, depth);
2328
+ };
2329
+ /** The same for a name written in `filePath`: what is not found may be anything. */
2330
+ const viaState = (filePath, via) => viaLookup(filePath, via, 0) ?? 'sometimes';
2331
+ /**
2332
+ * What tables of parameters whose rows other modules name leave of a
2333
+ * test written in `filePath`: 'skip' (one of them has no row that runs
2334
+ * the body), 'sometimes' (one may have none), or null.
2335
+ */
2336
+ const tablesState = (filePath, tables) => {
2337
+ let answer = null;
2338
+ for (const table of tables || []) {
2339
+ const states = table.via.map(via => viaState(filePath, via));
2340
+ if (states.includes('run')) continue;
2341
+ if (table.rest === 'skip' && states.every(state => state === 'skip')) return 'skip';
2342
+ answer = 'sometimes';
2343
+ }
2344
+ return answer;
2345
+ };
2346
+ /** A file's fixtures, with what the functions they call and the rows they name elsewhere add. */
2347
+ const fixturesOf = (filePath, fixtures) => (fixtures || []).map(fixture =>
2348
+ !fixture.decides && (callStops(filePath, fixture.calls) || tablesState(filePath, fixture.tables))
2349
+ ? { ...fixture, decides: true } : fixture);
2350
+
2351
+ /**
2352
+ * What a file does when the runner loads it as a plugin (a conftest.py,
2353
+ * a module another names as one), with the modules it loads in turn:
2354
+ * whether a hook can change what is collected or run, whether it has
2355
+ * the hook a test module may have too, and the fixtures it brings.
2356
+ */
2357
+ const pluginFacts = (filePath, seen = new Set()) => {
2358
+ const top = seen.size === 0;
2359
+ if (top && model.plugins.has(filePath)) return model.plugins.get(filePath);
2360
+ if (seen.has(filePath)) return { decides: false, everywhere: false, generates: false, fixtures: [] };
2361
+ seen.add(filePath);
2362
+ const found = model.declared(filePath);
2363
+ const runtime = found && found.runtime;
2364
+ // A plugin that is not read may do anything.
2365
+ const answer = {
2366
+ decides: !runtime || runtime.decides,
2367
+ // A hook pytest calls once for the whole run decides for
2368
+ // every test of it, wherever the file stands.
2369
+ everywhere: !runtime || !!runtime.everywhere,
2370
+ generates: !runtime || !!runtime.generates,
2371
+ fixtures: runtime ? fixturesOf(filePath, runtime.fixtures) : [],
2372
+ };
2373
+ // A hook that calls a function of another module of the project
2374
+ // that can stop a test decides as if it stopped it itself.
2375
+ for (const call of (runtime && runtime.hookCalls) || []) {
2376
+ if (!callStops(filePath, [call])) continue;
2377
+ answer.decides = true;
2378
+ if (call.everywhere) answer.everywhere = true;
2379
+ if (call.generates) answer.generates = true;
2380
+ }
2381
+ for (const plugin of runtime ? runtime.plugins : []) {
2382
+ const target = plugin.by === 'import' ? moduleFile(filePath, plugin.module) : moduleNamed(filePath, plugin.module);
2383
+ // A module outside the project is a plugin like any installed one.
2384
+ if (!target) continue;
2385
+ const inner = pluginFacts(target, seen);
2386
+ answer.decides = answer.decides || inner.decides;
2387
+ answer.generates = answer.generates || inner.generates;
2388
+ // A module named as a plugin is one for the whole run.
2389
+ if (inner.everywhere || (plugin.by === 'name' && inner.decides)) answer.everywhere = true;
2390
+ answer.fixtures.push(...inner.fixtures);
2391
+ }
2392
+ // Fixtures taken by name from other modules of the project.
2393
+ for (const entry of (found && found.imports) || []) {
2394
+ const target = entry.module == null ? null : moduleFile(filePath, entry.module);
2395
+ const there = target ? model.declared(target) : null;
2396
+ const fixture = there && there.runtime
2397
+ ? fixturesOf(target, there.runtime.fixtures).find(other => other.function === entry.original) : null;
2398
+ if (fixture) answer.fixtures.push(fixture.name === fixture.function ? { ...fixture, name: entry.name } : fixture);
2399
+ }
2400
+ if (top) model.plugins.set(filePath, answer);
2401
+ return answer;
2402
+ };
2403
+
2404
+ /**
2405
+ * The fixture names that can stop a test: those that decide, and those
2406
+ * that ask for one that does. With `is`: the names that reach a
2407
+ * fixture of that kind instead.
2408
+ */
2409
+ const stoppingNames = (fixtures, is = fixture => fixture.decides) => {
2410
+ const names = new Set();
2411
+ for (const fixture of fixtures) if (is(fixture) && fixture.name != null) names.add(fixture.name);
2412
+ let grew = true;
2413
+ while (grew) {
2414
+ grew = false;
2415
+ for (const fixture of fixtures) {
2416
+ if (fixture.name == null || names.has(fixture.name) || !fixture.requests.some(name => names.has(name))) continue;
2417
+ names.add(fixture.name);
2418
+ grew = true;
2419
+ }
2420
+ }
2421
+ return names;
2422
+ };
2423
+
2424
+ /** Does a fixture every test gets have a table of parameters, its own or one it asks for? */
2425
+ const everyTabled = (fixtures, tabled) => fixtures.some(fixture => (fixture.autouse || fixture.name == null) &&
2426
+ (!!fixture.params || fixture.requests.some(name => tabled.has(name))));
2427
+
2428
+ /**
2429
+ * What the code beside a test module can do to its tests before their
2430
+ * bodies run: the conftest.py files above it, the plugins those and the
2431
+ * module name, the module's own fixtures. `gated`: every test of the
2432
+ * module runs only sometimes (a hook can change what is collected or
2433
+ * run, a fixture every test gets can stop it). `fixtures`: the ones a
2434
+ * test of the module can ask for.
2435
+ */
2436
+ const guardsOf = filePath => {
2437
+ if (model.guards.has(filePath)) return model.guards.get(filePath);
2438
+ const relativePath = index.files.get(filePath).relativePath;
2439
+ const fixtures = [];
2440
+ let gated = false;
2441
+ let generates = false;
2442
+ // The conftest.py files a run of this file alone loads whose hooks
2443
+ // decide for the whole run.
2444
+ const wholeRun = [];
2445
+ for (const dir of ancestorDirs(relativePath)) {
2446
+ const conftest = path.join(index.root, dir, 'conftest.py');
2447
+ if (conftest === filePath || !index.files.has(conftest)) continue;
2448
+ const facts = pluginFacts(conftest);
2449
+ if (facts.decides) gated = true;
2450
+ if (facts.generates) generates = true;
2451
+ if (facts.everywhere) wholeRun.push(index.files.get(conftest).relativePath);
2452
+ fixtures.push(...facts.fixtures);
2453
+ }
2454
+ // The module itself is no plugin: of its own hooks pytest calls
2455
+ // the one that generates tests; the modules it names are plugins.
2456
+ const own = pluginFacts(filePath);
2457
+ const runtime = model.declared(filePath)?.runtime;
2458
+ if (own.generates || (own.everywhere && !(runtime && runtime.everywhere))) gated = true;
2459
+ if (own.generates) generates = true;
2460
+ fixtures.push(...own.fixtures);
2461
+ // A conftest.py anywhere under the runner's directory whose hook
2462
+ // is called for the whole run: it is loaded with any run that
2463
+ // collects a file below it, and decides for every test of that run.
2464
+ const runner = model.runnerOf(filePath) || {};
2465
+ if (!gated) {
2466
+ if (!model.conftests) {
2467
+ model.hasFile('');
2468
+ model.conftests = [...model.relPaths].filter(other => path.posix.basename(other) === 'conftest.py')
2469
+ .sort(codeUnitCompare);
2470
+ }
2471
+ for (const other of model.conftests) {
2472
+ if (relativeTo(runner.dir, other) == null) continue;
2473
+ const conftest = path.join(index.root, other);
2474
+ if (conftest !== filePath && pluginFacts(conftest).everywhere) {
2475
+ gated = true;
2476
+ break;
2477
+ }
2478
+ }
2479
+ }
2480
+ const stopping = stoppingNames(fixtures);
2481
+ if (fixtures.some(fixture => (fixture.autouse || fixture.name == null) &&
2482
+ (fixture.decides || fixture.requests.some(name => stopping.has(name))))) gated = true;
2483
+ const tabled = stoppingNames(fixtures, fixture => fixture.params);
2484
+ const answer = {
2485
+ gated, fixtures, stopping, tabled,
2486
+ // A fixture every test gets that has a table of parameters, or
2487
+ // asks for one that has: every test runs once for each row.
2488
+ everyTabled: everyTabled(fixtures, tabled),
2489
+ // A hook that makes tests from a test (pytest_generate_tests).
2490
+ generates,
2491
+ wholeRun: wholeRun.sort(codeUnitCompare),
2492
+ };
2493
+ model.guards.set(filePath, answer);
2494
+ return answer;
2495
+ };
2496
+
2497
+ /**
2498
+ * The units of a file whose reader gives facts (pytest): the module's
2499
+ * own tests, the tests of every class the runner collects (its own and
2500
+ * the ones it inherits, in the language's lookup order), and the tests
2501
+ * the module holds under names it took from elsewhere or bound again.
2502
+ *
2503
+ * Whether a class is collected is settled over its lookup order: the
2504
+ * nearest `__test__` (false: no; not a literal: sometimes; the literal
2505
+ * True: yes whatever the class is called), a constructor anywhere in
2506
+ * it (pytest's classes only), abstract members nothing nearer
2507
+ * implements, a name the runner takes or a TestCase of unittest among
2508
+ * its bases, and the classes around it. Marks, setups and fixtures of
2509
+ * every class in that order reach its tests, as do the module's.
2510
+ */
2511
+ const derivedFacts = (filePath, found) => {
2512
+ // A module switched off holds no test.
2513
+ if (found.off) return { modeled: true, units: [], tests: 0, inheriting: false };
2514
+ const relativePath = index.files.get(filePath).relativePath;
2515
+ const relativeOf = other => index.files.get(other).relativePath;
2516
+ const runner = model.runnerOf(filePath) || {};
2517
+ const naming = runner.collection || {};
2518
+ const named = (patterns, fallback, name) => (patterns && patterns.length > 0 ? patterns : fallback)
2519
+ .some(pattern => /[*?[]/.test(pattern) ? globMatches(pattern, name) : name.startsWith(pattern));
2520
+ const declaredModule = found.module || {};
2521
+ // What the module does when it is imported and before its tests,
2522
+ // with what the functions it calls in other modules add.
2523
+ const moduleTable = tablesState(filePath, declaredModule.tables);
2524
+ const mod = {
2525
+ ...declaredModule,
2526
+ ...(!declaredModule.skip && callStops(filePath, declaredModule.skipCalls) && { skip: 'sometimes' }),
2527
+ ...(moduleTable && declaredModule.skip !== 'skip' && { skip: moduleTable }),
2528
+ ...(!declaredModule.guard && callStops(filePath, declaredModule.guardCalls) && { guard: true }),
2529
+ };
2530
+ const guards = guardsOf(filePath);
2531
+ let more = !!found.inheriting;
2532
+
2533
+ // Names the module takes from other modules, and other names for
2534
+ // what it holds: a test class or function among them runs here too.
2535
+ const entries = [...(found.imports || [])];
2536
+ const own = new Set([...(found.functions || []).map(fn => fn.name),
2537
+ ...(found.classes || []).filter(cls => cls.suite.length === 0).map(cls => cls.name),
2538
+ ...entries.map(entry => entry.name)]);
2539
+ for (const star of naming.importedTests === false ? [] : found.stars || []) {
2540
+ const from = moduleFile(filePath, star.module);
2541
+ const there = from ? model.declared(from) : null;
2542
+ // What a module that is not read brings is not seen.
2543
+ if (!there || there.exportsAll === 'unknown') {
2544
+ more = true;
2545
+ continue;
2546
+ }
2547
+ if ((there.stars || []).length > 0) more = true;
2548
+ const names = there.exportsAll || [
2549
+ ...(there.functions || []).map(fn => fn.name),
2550
+ ...(there.classes || []).filter(cls => cls.suite.length === 0).map(cls => cls.name),
2551
+ ...(there.imports || []).map(entry => entry.name),
2552
+ ].filter(name => !name.startsWith('_'));
2553
+ for (const name of new Set(names)) {
2554
+ if (!own.has(name)) entries.push({ name, original: name, module: star.module, line: star.line });
2555
+ }
2556
+ }
2557
+ const classesHere = [...(found.classes || [])];
2558
+ const taken = [];
2559
+ const bound = new Set();
2560
+ // A switch this module writes on a class it took from another
2561
+ // module: the class is that object, so the switch holds for every
2562
+ // class here that derives from it.
2563
+ const switchedHere = new Map();
2564
+ // Classes taken from other modules whose constructor is written here.
2565
+ const constructorsOpen = new Set();
2566
+ for (const entry of entries) {
2567
+ if (bound.has(entry.name)) continue;
2568
+ const from = entry.module == null ? filePath : moduleFile(filePath, entry.module);
2569
+ if (!from || (from === filePath && entry.original === entry.name)) continue;
2570
+ const hit = resolveName(from, entry.original);
2571
+ if (!hit) continue;
2572
+ // collect_imported_tests = false: only what the module
2573
+ // defines itself is collected in it.
2574
+ if (naming.importedTests === false && hit.filePath !== filePath) continue;
2575
+ bound.add(entry.name);
2576
+ // A switch written here on the name.
2577
+ const written = (mod.switches || {})[entry.name];
2578
+ if (hit.kind === 'class' && written) switchedHere.set(hit.cls, written);
2579
+ if (hit.kind === 'class' && (mod.constructorWrites || []).includes(entry.name)) constructorsOpen.add(hit.cls);
2580
+ if (hit.kind === 'class') {
2581
+ classesHere.push({
2582
+ name: entry.name, suite: [], bases: [], imported: { filePath: hit.filePath, cls: hit.cls },
2583
+ named: named(naming.classes, ['Test'], entry.name),
2584
+ ...(written && { enabled: written.on }),
2585
+ ...(written && written.forced && { forced: written.forced }),
2586
+ line: entry.line, startLine: entry.line, endLine: entry.line,
2587
+ members: [], tests: [], marks: [], marksOpen: false,
2588
+ });
2589
+ } else {
2590
+ taken.push({ entry, fn: hit.fn, target: hit.filePath, written });
2591
+ }
2592
+ }
2593
+
2594
+ // The classes around a class, outermost first.
2595
+ const outerOf = cls => cls.imported ? [] : classesHere.filter(outer => !outer.imported && outer !== cls &&
2596
+ outer.startLine <= cls.startLine && outer.endLine >= cls.endLine).sort((a, b) => a.startLine - b.startLine);
2597
+ const NO = Object.freeze({ collected: 'no' });
2598
+ const standings = new Map();
2599
+ const standingOf = cls => {
2600
+ if (standings.has(cls)) return standings.get(cls);
2601
+ standings.set(cls, NO);
2602
+ const answer = (() => {
2603
+ const order = orderOf(filePath, cls);
2604
+ const nodes = order.filter(node => !node.opaque);
2605
+ const real = nodes.map(node => node.cls);
2606
+ const unread = order.some(node => node.opaque && !node.safe);
2607
+ const unittest = real.some(each => each.unittestBase);
2608
+ let collected = 'yes';
2609
+ let why = null;
2610
+ const weaken = reason => {
2611
+ if (collected === 'yes') {
2612
+ collected = 'maybe';
2613
+ why = reason;
2614
+ }
2615
+ };
2616
+ // The nearest switch: one written here on a class taken
2617
+ // from another module comes before that class's own. One
2618
+ // another module writes on it holds where that module was
2619
+ // loaded first: which, is the order of the run.
2620
+ const switchOf = node => {
2621
+ const each = node.cls;
2622
+ if (switchedHere.has(each)) return { enabled: switchedHere.get(each).on, forced: switchedHere.get(each).forced };
2623
+ const others = (switchedElsewhere(node.filePath).classes.get(each) || []).filter(write => write.by !== filePath);
2624
+ if (others.length > 0) {
2625
+ // Whether it is on is not settled; it is taken
2626
+ // whatever it is called only where a write says so.
2627
+ return { enabled: 'unknown', forced: others.some(write => write.forced) ? 'unknown' : each.forced };
2628
+ }
2629
+ return each.enabled !== undefined ? { enabled: each.enabled, forced: each.forced } : null;
2630
+ };
2631
+ const switched = nodes.map(switchOf).find(Boolean) || null;
2632
+ if (switched && switched.enabled === false) return NO;
2633
+ if (!unittest) {
2634
+ // The runner refuses a class whose constructor is not
2635
+ // the language's own. Which one a class has is what its
2636
+ // lookup order binds first: a class that writes the
2637
+ // builtin's back has it again, whatever stands behind.
2638
+ let custom = false;
2639
+ for (const name of found.constructors || []) {
2640
+ for (const node of order) {
2641
+ if (node.opaque) {
2642
+ if (node.safe) continue;
2643
+ // A base that is not read may bind one.
2644
+ weaken('base-not-resolved');
2645
+ break;
2646
+ }
2647
+ const kind = constructorsOpen.has(node.cls) ? 'unknown' : (node.cls.constructs || {})[name];
2648
+ if (!kind) continue;
2649
+ if (kind === 'custom') custom = true;
2650
+ else if (kind === 'unknown') weaken('constructor-not-read');
2651
+ break;
2652
+ }
2653
+ }
2654
+ if (custom) return NO;
2655
+ }
2656
+ // Abstract members that nothing nearer implements: abc
2657
+ // makes the class abstract and pytest leaves it out.
2658
+ const implemented = new Set();
2659
+ let abstractLeft = false;
2660
+ for (const each of real) {
2661
+ const abstract = each.abstract || [];
2662
+ if (abstract.some(name => !implemented.has(name))) abstractLeft = true;
2663
+ for (const member of each.members || []) if (!abstract.includes(member)) implemented.add(member);
2664
+ }
2665
+ if (abstractLeft) {
2666
+ if (real.some(each => each.abc === true)) return NO;
2667
+ if (real.some(each => each.abc === 'unknown') || unread) weaken('base-not-resolved');
2668
+ }
2669
+ if (!unittest && !cls.named) {
2670
+ // Not the runner's by its name, nor one of unittest's
2671
+ // (a base from outside the project is taken to be a
2672
+ // TestCase where it is named like one, and not otherwise).
2673
+ const forced = switched ? switched.forced : undefined;
2674
+ if (forced === 'unknown') weaken('collection-switch');
2675
+ else if (forced !== true) return NO;
2676
+ }
2677
+ if (switched && switched.enabled === 'unknown') weaken('collection-switch');
2678
+ if (real.some(each => each.unsettled)) weaken('defined-conditionally');
2679
+ for (const outer of outerOf(cls)) {
2680
+ const around = standingOf(outer);
2681
+ // pytest collects a class inside a class it collects;
2682
+ // a class of unittest holds methods only.
2683
+ if (around.collected === 'no' || around.regime === 'unittest') return NO;
2684
+ if (around.collected === 'maybe') weaken(around.why);
2685
+ }
2686
+ // Each class's fixtures and setups, read in its own file.
2687
+ const fixtures = nodes.flatMap(node => fixturesOf(node.filePath, node.cls.fixtures));
2688
+ // Tables of parameters on the classes, their rows read where each is written.
2689
+ const tables = nodes.map(node => tablesState(node.filePath, node.cls.tables));
2690
+ return {
2691
+ collected, why, regime: unittest ? 'unittest' : 'pytest',
2692
+ // What reaches a class through its lookup order.
2693
+ skip: real.some(each => each.skip === 'skip') || tables.includes('skip') ? 'skip'
2694
+ : real.some(each => each.skip === 'sometimes') || tables.includes('sometimes') ? 'sometimes' : null,
2695
+ guard: nodes.some(node => node.cls.guard || callStops(node.filePath, node.cls.guardCalls)),
2696
+ // A class's own hook that makes tests from a test.
2697
+ generates: real.some(each => each.generates),
2698
+ fixtures,
2699
+ usefixtures: real.flatMap(each => each.usefixtures || []),
2700
+ usefixturesOpen: real.some(each => each.usefixturesOpen),
2701
+ open: unread,
2702
+ };
2703
+ })();
2704
+ standings.set(cls, answer);
2705
+ return answer;
2706
+ };
2707
+
2708
+ const classesOf = new Map(); // unit -> the classes around it, for the runner's mark expression
2709
+ let units = [];
2710
+ /** A unit with what reaches it: its own state, its classes', the module's, the code beside it. */
2711
+ const finish = (base, row, around, alsoOpen = null, rowFile = filePath) => {
2712
+ const states = around.map(standingOf);
2713
+ const dynamic = row.dynamic || alsoOpen || states.map(each => each.collected === 'maybe' ? each.why : null).find(Boolean) ||
2714
+ (mod.unsettled ? 'collection-switch' : null);
2715
+ const classFixtures = states.flatMap(each => each.fixtures);
2716
+ const scoped = classFixtures.length > 0 ? [...guards.fixtures, ...classFixtures] : guards.fixtures;
2717
+ const stopping = classFixtures.length > 0 ? stoppingNames(scoped) : guards.stopping;
2718
+ const requests = [...(row.requests || []), ...(mod.usefixtures || []), ...(naming.usefixtures || []),
2719
+ ...states.flatMap(each => each.usefixtures)];
2720
+ // The rows of the test's own table that other modules name,
2721
+ // read from the file the test is written in.
2722
+ const rowTable = tablesState(rowFile, row.tables);
2723
+ // One declaration the runner reports as several tests: a
2724
+ // table of parameters, its own, its classes', its module's, a
2725
+ // fixture's (one it asks for, or one every test gets), or a
2726
+ // hook that makes tests from it.
2727
+ const tabled = classFixtures.length > 0 ? stoppingNames(scoped, fixture => fixture.params) : guards.tabled;
2728
+ const byFixture = requests.some(name => tabled.has(name)) ||
2729
+ (classFixtures.length > 0 ? everyTabled(scoped, tabled) : guards.everyTabled);
2730
+ const several = (row.marks || []).includes('parametrize') || (found.marks || []).includes('parametrize') ||
2731
+ around.some(cls => orderOf(filePath, cls).some(node => !node.opaque && (node.cls.marks || []).includes('parametrize'))) ||
2732
+ byFixture || guards.generates || states.some(each => each.generates);
2733
+ const skipped = !!row.skipped || rowTable === 'skip' || mod.skip === 'skip' || states.some(each => each.skip === 'skip');
2734
+ const sometimes = !!row.conditional || !!dynamic || rowTable === 'sometimes' || mod.skip === 'sometimes' ||
2735
+ !!mod.guard || guards.gated ||
2736
+ states.some(each => each.skip === 'sometimes' || each.guard || each.usefixturesOpen || each.generates) ||
2737
+ !!row.requestsOpen || !!mod.usefixturesOpen || requests.some(name => stopping.has(name)) ||
2738
+ // A test of unittest takes no parameters: under a
2739
+ // fixture's table the runner reports an error for it.
2740
+ (byFixture && states.some(each => each.regime === 'unittest'));
2741
+ const unit = {
2742
+ ...base,
2743
+ ...(!base.inherited && rowFile === filePath && row.sourceSpan && { sourceSpan: row.sourceSpan }),
2744
+ ...(skipped && { skipped: true }),
2745
+ ...(sometimes && { conditional: true }),
2746
+ ...(dynamic && { dynamic }),
2747
+ ...(several && { several: true }),
2748
+ ...(row.marks && { marks: row.marks, marksOpen: !!row.marksOpen }),
2749
+ };
2750
+ classesOf.set(unit, around);
2751
+ units.push(unit);
2752
+ };
2753
+ // A function the runner takes only by a switch that is not settled.
2754
+ const byForce = row => !row.named && row.forced === 'unknown' ? 'collection-switch' : null;
2755
+
2756
+ // Functions of this module whose switch another module writes.
2757
+ const switchedBy = switchedElsewhere(filePath).functions;
2758
+ for (const row of found.units) {
2759
+ if (row.kind !== 'test') continue;
2760
+ finish({ kind: 'test', name: row.name, suite: [], line: row.line, startLine: row.startLine, endLine: row.endLine },
2761
+ row, [], byForce(row) || (switchedBy.has(row.name) ? 'collection-switch' : null));
2762
+ }
2763
+ // One the module leaves switched off, or under a name the runner
2764
+ // does not take, that another module switches on.
2765
+ for (const fn of switchedBy.size > 0 ? found.functions || [] : []) {
2766
+ if (found.units.some(row => row.kind === 'test' && row.name === fn.name && row.line === fn.line)) continue;
2767
+ if (!switchedBy.get(fn.name)?.some(write => write.on !== false && (fn.named || write.forced))) continue;
2768
+ finish({ kind: 'test', name: fn.name, suite: [], line: fn.line, startLine: fn.startLine, endLine: fn.endLine },
2769
+ fn, [], 'collection-switch');
2770
+ }
2771
+ for (const cls of classesHere) {
2772
+ const standing = standingOf(cls);
2773
+ if (standing.collected === 'no') continue;
2774
+ // unittest takes the methods its loader names; pytest the
2775
+ // names its patterns take and the ones switched on.
2776
+ // A member its class switches off is none, unless a switch
2777
+ // this module writes in a way that is not read may have
2778
+ // turned it on again.
2779
+ const live = row => !row.off || !!found.switchesOpen;
2780
+ const select = standing.regime === 'unittest' ? row => live(row) && row.prefixed
2781
+ : row => live(row) && (row.named || !!row.forced);
2782
+ const reopened = row => row.off ? 'collection-switch' : null;
2783
+ const inherited = cls.bases.length > 0 || cls.imported
2784
+ ? inheritedBy(filePath, cls, select) : { tests: [], open: false, withheld: false };
2785
+ const rows = (cls.tests || []).filter(select);
2786
+ // unittest runs runTest when a class has no other test.
2787
+ if (standing.regime === 'unittest' && rows.length === 0 && inherited.tests.length === 0) {
2788
+ const fallback = (cls.tests || []).find(row => row.name === 'runTest');
2789
+ if (fallback) rows.push(fallback);
2790
+ // The nearest one in the lookup order, where the class has none.
2791
+ else if (cls.bases.length > 0 || cls.imported) {
2792
+ const above = inheritedBy(filePath, cls, row => row.name === 'runTest');
2793
+ inherited.tests.push(...above.tests);
2794
+ if (above.withheld) inherited.withheld = true;
2795
+ }
2796
+ }
2797
+ // A base that is not read may bring tests of its own.
2798
+ if (inherited.withheld || standing.open) more = true;
2799
+ const around = [...outerOf(cls), cls];
2800
+ const suite = [...cls.suite, cls.name];
2801
+ const before = units.length;
2802
+ for (const row of rows) {
2803
+ const open = reopened(row) || (standing.regime === 'unittest' ? null : byForce(row));
2804
+ if (row.alias) {
2805
+ // Another name for a test of the class: its body stands where that test is.
2806
+ finish({
2807
+ kind: 'test', name: row.name, suite, line: row.line, startLine: row.line, endLine: row.line,
2808
+ inherited: {
2809
+ file: relativePath, filePath, owner: cls.name, ownerLine: cls.startLine,
2810
+ line: row.of, startLine: row.startLine, endLine: row.endLine,
2811
+ },
2812
+ }, row, around, open);
2813
+ } else {
2814
+ finish({ kind: 'test', name: row.name, suite, line: row.line, startLine: row.startLine, endLine: row.endLine },
2815
+ row, around, open);
2816
+ }
2817
+ }
2818
+ for (const { test, base } of inherited.tests) {
2819
+ finish({
2820
+ kind: 'test', name: test.name, suite, line: cls.line, startLine: cls.line, endLine: cls.line,
2821
+ inherited: {
2822
+ file: relativeOf(base.filePath), filePath: base.filePath, owner: base.cls.name,
2823
+ ownerLine: base.cls.startLine, line: test.alias ? test.of : test.line,
2824
+ startLine: test.startLine, endLine: test.endLine, sourceSpan: test.sourceSpan,
2825
+ },
2826
+ }, test, around, reopened(test) || (standing.regime === 'unittest' ? null : byForce(test)), base.filePath);
2827
+ }
2828
+ if (units.length > before) {
2829
+ units.push({
2830
+ kind: 'suite', name: cls.name, suite: cls.suite,
2831
+ line: cls.line, startLine: cls.startLine, endLine: cls.endLine, sourceSpan: cls.sourceSpan,
2832
+ });
2833
+ }
2834
+ }
2835
+ for (const { entry, fn, target, written } of taken) {
2836
+ // The switch is the function's own, whoever holds it: a write
2837
+ // made here comes after the ones of the module it is taken from.
2838
+ // Without one, a third module's write holds where that module
2839
+ // was loaded first.
2840
+ const others = written ? [] : (switchedElsewhere(target).functions.get(fn.name) || [])
2841
+ .filter(write => write.by !== filePath);
2842
+ // And where a switch this module writes is not read, one its
2843
+ // own module switched off may be on again, under any name.
2844
+ const reopened = !written && !!fn.off && !!found.switchesOpen;
2845
+ const on = written ? written.on : others.length > 0 || reopened ? 'unknown' : fn.off ? false : true;
2846
+ const row = {
2847
+ ...fn, named: named(naming.functions, ['test'], entry.name),
2848
+ forced: written ? written.forced : others.some(write => write.forced) || reopened ? 'unknown' : fn.forced,
2849
+ };
2850
+ if ((!row.named && !row.forced) || on === false) continue;
2851
+ finish({
2852
+ kind: 'test', name: entry.name, suite: [], line: entry.line, startLine: entry.line, endLine: entry.line,
2853
+ inherited: {
2854
+ file: relativeOf(target), filePath: target, owner: null, ownerLine: 0,
2855
+ line: fn.line, startLine: fn.startLine, endLine: fn.endLine, sourceSpan: fn.sourceSpan,
2856
+ },
2857
+ }, row, [], (on === 'unknown' ? 'collection-switch' : null) || byForce(row), target);
2858
+ }
2859
+
2860
+ // The runner's configured options: a mark expression and tests
2861
+ // left out by name apply to a test however it is named on the
2862
+ // command line (measured).
2863
+ const options = naming.options;
2864
+ if (options && (options.markExpr || options.deselect.length > 0)) {
2865
+ const file = relativeTo(runner.dir, relativePath);
2866
+ units = units.map(unit => {
2867
+ if (unit.kind !== 'test' || unit.skipped) return unit;
2868
+ const id = [file, ...unit.suite, unit.name].join('::');
2869
+ let selected = !(file != null && options.deselect.some(prefix => id.startsWith(prefix)));
2870
+ if (selected && options.markExpr) {
2871
+ const marks = new Set([...(unit.marks || []), ...(found.marks || [])]);
2872
+ let open = !!unit.marksOpen || !!found.marksOpen;
2873
+ // Marks of the classes around the test, and of every
2874
+ // class those derive from.
2875
+ for (const cls of classesOf.get(unit) || []) {
2876
+ for (const node of orderOf(filePath, cls)) {
2877
+ if (node.opaque) {
2878
+ if (!node.safe) open = true;
2879
+ continue;
2880
+ }
2881
+ for (const mark of node.cls.marks || []) marks.add(mark);
2882
+ if (node.cls.marksOpen) open = true;
2883
+ }
2884
+ }
2885
+ selected = markSelects(options.markExpr, marks, open);
2886
+ }
2887
+ if (selected === false) return { ...unit, skipped: true, deselected: true };
2888
+ return selected == null ? { ...unit, conditional: true } : unit;
2889
+ });
2890
+ }
2891
+ units.sort((a, b) => a.startLine - b.startLine || (b.endLine - a.endLine) ||
2892
+ codeUnitCompare(a.name || '', b.name || ''));
2893
+ return {
2894
+ modeled: true,
2895
+ units,
2896
+ // A test that is declared and not executed is not counted.
2897
+ tests: units.filter(unit => unit.kind === 'test' && !unit.skipped).length,
2898
+ inheriting: more,
2899
+ };
2900
+ };
2901
+
2902
+ /**
2903
+ * A file's units: the tests and suites it declares, by line, and the
2904
+ * tests its runnable classes inherit (placed at the class). `modeled`
2905
+ * is false when the language's units are not read.
2906
+ */
2907
+ model.unitsOf = filePath => {
2908
+ if (model.byFile.has(filePath)) return model.byFile.get(filePath);
2909
+ const found = model.declared(filePath);
2910
+ let facts;
2911
+ if (!found) {
2912
+ facts = { modeled: false, units: [], tests: 0, inheriting: false };
2913
+ } else if (found.notCollected) {
2914
+ // A module its runner does not collect declares no test.
2915
+ facts = { modeled: true, units: [], tests: 0, inheriting: false };
2916
+ } else if (found.derived) {
2917
+ facts = derivedFacts(filePath, found);
2918
+ } else {
2919
+ const relativePath = index.files.get(filePath).relativePath;
2920
+ const runner = model.runnerOf(filePath) || {};
2921
+ const classesHere = [...(found.classes || [])];
2922
+ const within = (unit, cls) => containsTestUnit(cls, unit);
2923
+ // A setup that can stop a test, in a class the test's class
2924
+ // derives from: the test runs only sometimes.
2925
+ const guardedAbove = cls => cls.bases.length > 0 &&
2926
+ orderOf(filePath, cls).slice(1).some(node => !node.opaque && node.cls.guard);
2927
+ const guarded = classesHere.filter(cls => cls.runnable && guardedAbove(cls));
2928
+ // A class its runner refuses (one of its marked methods is one
2929
+ // the runner does not accept), or a class deriving from one:
2930
+ // none of its tests runs, its own or inherited.
2931
+ const refusalMemo = new Map();
2932
+ const refusalOf = cls => {
2933
+ if (!refusalMemo.has(cls)) {
2934
+ const order = orderOf(filePath, cls);
2935
+ const refused = order.some(node => !node.opaque && (node.cls.refused || node.cls.mayRefuse));
2936
+ // @RunWith is inherited, including through an
2937
+ // intermediate class that declares no tests. A runner
2938
+ // anywhere in the ancestry, or an unread base that may
2939
+ // name one, prevents a definite standard-runner refusal.
2940
+ const custom = order.some(node => node.opaque ? !node.safe : node.cls.runner || node.cls.mayRefuse);
2941
+ refusalMemo.set(cls, refused ? (custom ? 'conditional' : 'refused') : null);
2942
+ }
2943
+ return refusalMemo.get(cls);
2944
+ };
2945
+ const refusedHere = cls => refusalOf(cls) === 'refused';
2946
+ const maybeRefused = cls => refusalOf(cls) === 'conditional';
2947
+ const runsIn = unit => classesHere.filter(cls => within(unit, cls)).sort((a, b) => b.startLine - a.startLine)[0];
2948
+ // A focus leaves other tests of the file declared and not
2949
+ // executed, by the rule of the runner that runs the file.
2950
+ const left = found.focus ? focusedOut(found.units, model.runnerOf(filePath).id) : new Set();
2951
+ let units = found.units.map(unit => {
2952
+ let out = unit;
2953
+ if (unit.kind === 'test' && !unit.conditional && guarded.some(cls => within(unit, cls))) {
2954
+ out = { ...out, conditional: true };
2955
+ }
2956
+ if (left.has(unit)) out = { ...out, skipped: true };
2957
+ if (unit.kind === 'test' && !out.skipped) {
2958
+ const holder = runsIn(unit);
2959
+ if (holder && refusedHere(holder)) out = { ...out, skipped: true };
2960
+ else if (holder && maybeRefused(holder) && !out.conditional) out = { ...out, conditional: true };
2961
+ }
2962
+ return out;
2963
+ });
2964
+ // jest refuses a file that gives a table of cases no row
2965
+ // (measured): none of its tests runs.
2966
+ if (found.emptyTable && runner.id === 'jest') {
2967
+ units = units.map(unit => unit.kind === 'test' && !unit.skipped ? { ...unit, conditional: true } : unit);
2968
+ }
2969
+ // `open`: the file calls through a name that may be its
2970
+ // runner's declaring function, or hands that function on.
2971
+ let inheriting = !!found.inheriting || !!found.open;
2972
+ // A function of another module that declares tests when it is
2973
+ // called: the tests are this file's, and are not listed here.
2974
+ if ((found.calledModules || []).some(name => {
2975
+ const target = moduleFile(filePath, name);
2976
+ return !!target && target !== filePath && declaresWhenCalled(target);
2977
+ })) inheriting = true;
2978
+ // node runs a file that declares no test as one test.
2979
+ if (found.units.length === 0 && runner.id === 'node-test' &&
2980
+ nodeRunsFile(runner, relativeTo(runner.dir, relativePath))) inheriting = true;
2981
+ for (const cls of classesHere) {
2982
+ if (!cls.runnable || cls.bases.length === 0) continue;
2983
+ const inherited = inheritedBy(filePath, cls);
2984
+ const own = units.some(unit => unit.kind === 'test' && within(unit, cls));
2985
+ // A base that is not read may bring tests of its own.
2986
+ if (inherited.withheld || inherited.open) inheriting = true;
2987
+ if (inherited.tests.some(({ test }) => !test.needsClassMark || cls.marked) && !own) {
2988
+ units.push({
2989
+ kind: 'suite', name: cls.name, suite: cls.suite,
2990
+ line: cls.line, startLine: cls.startLine, endLine: cls.endLine, sourceSpan: cls.sourceSpan,
2991
+ });
2992
+ }
2993
+ for (const { test, base, body, uncertain, maybe, again } of inherited.tests) {
2994
+ // A runner that wants the class that runs a test
2995
+ // marked takes none through one that is not.
2996
+ if (test.needsClassMark && !cls.marked) continue;
2997
+ // No command names a test whose runner cannot be told
2998
+ // which one it is under the class that inherits it, or
2999
+ // one that a method of the class may have replaced
3000
+ // (their parameter types are not resolved).
3001
+ const unnamed = test.dynamic || cls.dynamic || test.inheritedDynamic ||
3002
+ (uncertain && 'signature-not-resolved') || (again && 'reported-again');
3003
+ const state = {
3004
+ ...((test.skipped || cls.skipped || refusedHere(cls)) && { skipped: true }),
3005
+ ...((cls.sometimes || test.conditional || cls.guard || guardedAbove(cls) || uncertain || again || maybeRefused(cls)) && { conditional: true }),
3006
+ ...(unnamed && { dynamic: unnamed }),
3007
+ ...((test.several || cls.several) && { several: true }),
3008
+ // The bodies of the methods that may be the test's
3009
+ // instead, where the runner gives a test the body
3010
+ // of the method that replaces it: a change to one
3011
+ // of them may reach it.
3012
+ ...(uncertain && test.byDeclaration && maybe.length > 0 && {
3013
+ alternatives: maybe.map(other => ({
3014
+ file: index.files.get(other.at.filePath).relativePath,
3015
+ filePath: other.at.filePath, ownerLine: other.at.cls.startLine,
3016
+ startLine: other.row.startLine, endLine: other.row.endLine, sourceSpan: other.row.sourceSpan,
3017
+ })),
3018
+ }),
3019
+ };
3020
+ const suite = [...cls.suite, cls.name];
3021
+ // The class redeclares the member without the mark:
3022
+ // the test is the inherited one, its body the class's own.
3023
+ if (body && body.at.cls === cls && !uncertain && !again) {
3024
+ units.push({
3025
+ kind: 'test', name: test.name, suite, ...(cls.namespace && { namespace: cls.namespace }),
3026
+ line: body.row.line, startLine: body.row.startLine, endLine: body.row.endLine, sourceSpan: body.row.sourceSpan, ...state,
3027
+ });
3028
+ continue;
3029
+ }
3030
+ // Where the body stands: the declaration that makes the
3031
+ // test, or the nearest class that redeclares the member.
3032
+ const at = body ? body.at : base;
3033
+ const where = body ? body.row : test;
3034
+ units.push({
3035
+ kind: 'test', name: test.name, suite, ...(cls.namespace && { namespace: cls.namespace }),
3036
+ line: cls.line, startLine: cls.line, endLine: cls.line, ...state,
3037
+ inherited: {
3038
+ file: index.files.get(at.filePath).relativePath,
3039
+ filePath: at.filePath, owner: at.cls.name,
3040
+ ownerLine: at.cls.startLine, line: where.line,
3041
+ startLine: where.startLine, endLine: where.endLine, sourceSpan: where.sourceSpan,
3042
+ },
3043
+ });
3044
+ }
3045
+ // A mark on a class that makes its methods tests holds for
3046
+ // the classes deriving from it (`classMark`): the methods
3047
+ // this class declares that such a mark takes (`candidate`).
3048
+ if (!cls.classMark && orderOf(filePath, cls).slice(1).some(node => !node.opaque && node.cls.classMark)) {
3049
+ let added = false;
3050
+ for (const row of cls.memberRows || []) {
3051
+ if (!row.candidate || units.some(unit => unit.kind === 'test' && unit.name === row.name &&
3052
+ unit.startLine === row.startLine)) continue;
3053
+ added = true;
3054
+ units.push({
3055
+ kind: 'test', name: row.name, suite: [...cls.suite, cls.name],
3056
+ ...(cls.namespace && { namespace: cls.namespace }),
3057
+ line: row.line, startLine: row.startLine, endLine: row.endLine, sourceSpan: row.sourceSpan,
3058
+ ...(cls.skipped && { skipped: true }),
3059
+ ...((cls.sometimes || cls.guard || guardedAbove(cls)) && { conditional: true }),
3060
+ dynamic: 'declared-in-a-base-class',
3061
+ });
3062
+ }
3063
+ if (added && !own && inherited.tests.length === 0) {
3064
+ units.push({
3065
+ kind: 'suite', name: cls.name, suite: cls.suite,
3066
+ line: cls.line, startLine: cls.startLine, endLine: cls.endLine, sourceSpan: cls.sourceSpan,
3067
+ });
3068
+ }
3069
+ }
3070
+ }
3071
+ // Code the runner hands the decision to: what it does is not
3072
+ // read, so a test under it runs only sometimes.
3073
+ if (gatedAtRunTime(filePath, runner)) {
3074
+ units = units.map(unit => unit.kind === 'test' && !unit.skipped ? { ...unit, conditional: true } : unit);
3075
+ }
3076
+ units.sort((a, b) => a.startLine - b.startLine || (b.endLine - a.endLine) ||
3077
+ codeUnitCompare(a.name || '', b.name || ''));
3078
+ facts = {
3079
+ modeled: true,
3080
+ units,
3081
+ // A test that is declared and not executed is not counted.
3082
+ tests: units.filter(unit => unit.kind === 'test' && !unit.skipped).length,
3083
+ inheriting,
3084
+ };
3085
+ }
3086
+ model.byFile.set(filePath, facts);
3087
+ return facts;
3088
+ };
3089
+
3090
+ /**
3091
+ * What has to be the same for two files to share one command: a hook
3092
+ * that decides for a whole run is loaded with every file below its
3093
+ * conftest.py, so a file from elsewhere joined to one of those would
3094
+ * run under a hook its own command never loads; a module that
3095
+ * switches a class of another module on or off does it for every
3096
+ * module loaded after it.
3097
+ */
3098
+ model.joinKey = filePath => {
3099
+ const found = model.declared(filePath);
3100
+ if (!found || !found.derived || found.notCollected) return '';
3101
+ // A module that writes a switch on a name it took from another
3102
+ // module changes that object for every module loaded after it:
3103
+ // it is run by itself.
3104
+ const alone = Object.keys((found.module && found.module.switches) || {}).length > 0 ||
3105
+ ((found.module && found.module.constructorWrites) || []).length > 0
3106
+ ? `\0switches:${index.files.get(filePath).relativePath}` : '';
3107
+ return guardsOf(filePath).wholeRun.join('\0') + alone;
3108
+ };
3109
+
3110
+ /** Classes that derive from the one named, through the project. */
3111
+ const descendantFiles = className => {
3112
+ const files = new Set();
3113
+ const seen = new Set([className]);
3114
+ const queue = [className];
3115
+ while (queue.length > 0 && seen.size < 512) {
3116
+ for (const child of index.extendedByGraph?.get(queue.shift()) || []) {
3117
+ const name = typeof child === 'string' ? child : child?.name;
3118
+ if (!name || seen.has(name)) continue;
3119
+ seen.add(name);
3120
+ queue.push(name);
3121
+ for (const def of index.symbols.get(name) || []) {
3122
+ if (CONTAINER_KINDS.has(def.type)) files.add(def.file);
3123
+ }
3124
+ }
3125
+ }
3126
+ return files;
3127
+ };
3128
+
3129
+ /**
3130
+ * The tests that execute a line of test code: the unit that holds it,
3131
+ * and every test a deriving class inherits from the method that holds
3132
+ * it. A line in a suite outside its tests answers with the suite. An
3133
+ * empty answer: the line belongs to no unit (file-level code).
3134
+ * @returns {Array<{filePath: string, unit: object}>}
3135
+ */
3136
+ model.runsAt = (filePath, site) => {
3137
+ const out = [];
3138
+ const point = testSite(site);
3139
+ // The units of a file that may run this code. A module its runner
3140
+ // does not collect by its name declares none: it is not read for
3141
+ // the answer.
3142
+ const unitsIn = file => {
3143
+ if (!model.byFile.has(file)) {
3144
+ const entry = index.files.get(file);
3145
+ if (entry && collectionFor(file, entry).collected === false) return [];
3146
+ }
3147
+ return model.unitsOf(file).units;
3148
+ };
3149
+ const unit = model.unitAt(filePath, point);
3150
+ if (unit && unit.kind === 'test') out.push({ filePath, unit });
3151
+ const holders = innermostTestRows(model.declared(filePath)?.classes || [], point);
3152
+ const holder = holders.length === 1 ? holders[0] : null;
3153
+ // The method that holds the line: a test's declaration, or a method
3154
+ // a deriving class's test takes its body from.
3155
+ const templates = holder ? innermostTestRows([...holder.tests, ...(holder.memberRows || [])], point) : [];
3156
+ // Test/member rows can describe the same body; distinct spans must
3157
+ // not be collapsed merely because their declaration lines match.
3158
+ const template = templates.length > 0 && templates.every(row => row.name === templates[0].name &&
3159
+ row.sourceSpan?.start === templates[0].sourceSpan?.start) ? templates[0] : null;
3160
+ const importers = [...(index.exportGraph?.get(filePath) || [])];
3161
+ if (template) {
3162
+ const held = from => !!from && from.filePath === filePath && from.ownerLine === holder.startLine &&
3163
+ from.startLine === template.startLine && from.endLine === template.endLine &&
3164
+ (!from.sourceSpan || !template.sourceSpan || from.sourceSpan.start === template.sourceSpan.start);
3165
+ for (const file of [...new Set([...descendantFiles(holder.name), ...importers, filePath])].sort(codeUnitCompare)) {
3166
+ for (const candidate of unitsIn(file)) {
3167
+ // Every test whose body this is: the one a deriving
3168
+ // class inherits, another name for it in its class;
3169
+ // and every test whose body it may be.
3170
+ if ((held(candidate.inherited) || (candidate.alternatives || []).some(held)) &&
3171
+ !out.some(seen => seen.filePath === file && seen.unit === candidate)) {
3172
+ out.push({ filePath: file, unit: candidate });
3173
+ }
3174
+ }
3175
+ }
3176
+ }
3177
+ // A function another module imports under a test's name runs there.
3178
+ const functions = holder ? [] : innermostTestRows(model.declared(filePath)?.functions || [], point);
3179
+ const fn = functions.length === 1 ? functions[0] : null;
3180
+ if (fn) {
3181
+ for (const file of [...new Set([...importers, filePath])].sort(codeUnitCompare)) {
3182
+ for (const candidate of unitsIn(file)) {
3183
+ const from = candidate.inherited;
3184
+ if (from && from.filePath === filePath && from.owner == null && from.startLine === fn.startLine &&
3185
+ (!from.sourceSpan || !fn.sourceSpan || from.sourceSpan.start === fn.sourceSpan.start)) {
3186
+ out.push({ filePath: file, unit: candidate });
3187
+ }
3188
+ }
3189
+ }
3190
+ }
3191
+ if (out.length === 0 && unit) out.push({ filePath, unit });
3192
+ return out;
3193
+ };
3194
+
3195
+ /**
3196
+ * Can a file hold test code at all? Asked before a file is read for
3197
+ * the tests in it. A language says so from what the index keeps
3198
+ * (`testCodeGate`): whether a declaration of the file carries a mark
3199
+ * that can make a test, and which types its classes derive from. No
3200
+ * mark here and none in the file of a class of the project one of its
3201
+ * classes derives from: no test, and no body of one. A language
3202
+ * without the gate is read. (`options.ungated` reads every file: the
3203
+ * gate must never change an answer.)
3204
+ */
3205
+ const gates = new Map();
3206
+ const projectDeclaresType = name => (index.symbols.get(name) || []).some(def => CONTAINER_KINDS.has(def.type));
3207
+ model.mayHoldTests = (filePath, trail = new Set()) => {
3208
+ if (options.ungated) return true;
3209
+ if (gates.has(filePath)) return gates.get(filePath);
3210
+ const entry = index.files.get(filePath);
3211
+ const gate = entry && langTraits(entry.language)?.testCodeGate;
3212
+ if (typeof gate !== 'function') return true;
3213
+ if (trail.has(filePath)) return false;
3214
+ let answer;
3215
+ try {
3216
+ const facts = gate(entry, projectDeclaresType);
3217
+ answer = !!facts.marked;
3218
+ // A base written under a using alias is the type the alias
3219
+ // names (the file's own, a global one, a project file's).
3220
+ const aliases = !answer && langTraits(entry.language)?.typeNameScoping === 'namespace-usings'
3221
+ ? csharpUsings(index, filePath) : null;
3222
+ if (aliases && aliases.unreadable) answer = true;
3223
+ const next = new Set(trail).add(filePath);
3224
+ for (const base of answer ? [] : facts.bases || []) {
3225
+ const names = [base];
3226
+ if (aliases && aliases.aliases.has(base)) names.push(String(aliases.aliases.get(base)).split('.').pop());
3227
+ for (const def of names.flatMap(name => index.symbols.get(name) || [])) {
3228
+ if (!CONTAINER_KINDS.has(def.type) || !def.file || def.file === filePath) continue;
3229
+ if (model.mayHoldTests(def.file, next)) {
3230
+ answer = true;
3231
+ break;
3232
+ }
3233
+ }
3234
+ if (answer) break;
3235
+ }
3236
+ } catch (e) {
3237
+ answer = true;
3238
+ }
3239
+ // An answer reached inside a cycle of classes is not kept: it may
3240
+ // be 'no' only because the walk came back to its start.
3241
+ if (answer || trail.size === 0) gates.set(filePath, answer);
3242
+ return answer;
3243
+ };
3244
+
3245
+ /**
3246
+ * The lines of a file a test executes, as [start, end] ranges: its own
3247
+ * tests and suites, and the methods and functions a test another file
3248
+ * lists runs as its body (a base class's test a deriving class
3249
+ * inherits, a method that gives an inherited test its body, a function
3250
+ * another module imports under a test's name). This is what makes code
3251
+ * test code, whatever its file is called: a base class in a file whose
3252
+ * name is no test file's holds the bodies of the tests that inherit
3253
+ * from it. Empty for a file the model knows no test in.
3254
+ */
3255
+ model.testRangesOf = filePath => {
3256
+ if (model.ranges.has(filePath)) return model.ranges.get(filePath);
3257
+ model.ranges.set(filePath, []);
3258
+ const ranges = [];
3259
+ const found = model.mayHoldTests(filePath) ? model.declared(filePath) : null;
3260
+ if (found) {
3261
+ for (const unit of model.unitsOf(filePath).units) {
3262
+ if (!unit.inherited) ranges.push([unit.startLine, unit.endLine]);
3263
+ }
3264
+ const covered = row => ranges.some(([start, end]) => row.startLine >= start && row.endLine <= end);
3265
+ const rows = [...(found.functions || [])];
3266
+ for (const cls of found.classes || []) {
3267
+ rows.push(...(cls.tests || []));
3268
+ // A method that is no test gives one its body only in a
3269
+ // class that inherits a test of its name.
3270
+ if (cls.bases.length === 0 || (cls.memberRows || []).length === 0) continue;
3271
+ const inherited = new Set();
3272
+ for (const node of orderOf(filePath, cls).slice(1)) {
3273
+ if (!node.opaque) for (const test of node.cls.tests || []) inherited.add(test.name);
3274
+ }
3275
+ if (inherited.size > 0) rows.push(...cls.memberRows.filter(row => inherited.has(row.name)));
3276
+ }
3277
+ rows.sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
3278
+ for (const row of rows) {
3279
+ if (row.startLine == null || row.endLine == null || covered(row)) continue;
3280
+ if (model.runsAt(filePath, { line: row.startLine,
3281
+ column: row.sourceSpan?.startColumn, callStart: row.sourceSpan?.start }).length > 0) {
3282
+ ranges.push([row.startLine, row.endLine]);
3283
+ }
3284
+ }
3285
+ }
3286
+ model.ranges.set(filePath, ranges);
3287
+ return ranges;
3288
+ };
3289
+
3290
+ /** Innermost units at a physical site. A line alone may be ambiguous. */
3291
+ model.unitsAt = (filePath, site) => {
3292
+ const candidates = model.unitsOf(filePath).units.filter(unit =>
3293
+ !unit.inherited && containsTestSite(unit, site));
3294
+ return candidates.filter(unit => !candidates.some(other => other !== unit &&
3295
+ containsTestUnit(unit, other) &&
3296
+ (!containsTestUnit(other, unit) || (other.kind === 'test' && unit.kind !== 'test'))));
3297
+ };
3298
+ model.unitAt = (filePath, site) => {
3299
+ const units = model.unitsAt(filePath, site);
3300
+ return units.length === 1 ? units[0] : null;
3301
+ };
3302
+
3303
+ /** Tests a unit stands for: itself, or every test a suite holds. */
3304
+ model.testsWithin = (filePath, unit) => {
3305
+ const { units } = model.unitsOf(filePath);
3306
+ if (!unit) return units.filter(candidate => candidate.kind === 'test');
3307
+ if (unit.kind === 'test') return [unit];
3308
+ return units.filter(candidate => candidate.kind === 'test' &&
3309
+ containsTestUnit(unit, candidate));
3310
+ };
3311
+
3312
+ /**
3313
+ * `tests`: declares a unit, or can hold tests the declarations do not
3314
+ * show. `support`: test-path code that is not a test. `unmodeled`: the
3315
+ * language's units are not read.
3316
+ */
3317
+ model.roleOf = filePath => {
3318
+ const facts = model.unitsOf(filePath);
3319
+ if (!facts.modeled) return 'unmodeled';
3320
+ if (facts.tests > 0 || facts.units.length > 0 || facts.inheriting) return 'tests';
3321
+ // A language whose tests are all declarations: none read, none there.
3322
+ if (model.declared(filePath)?.complete || model.declared(filePath)?.notCollected) return 'support';
3323
+ const fileEntry = index.files.get(filePath);
3324
+ // node runs every file it is told to, or looks for, as a test: a
3325
+ // script that declares none is one.
3326
+ const runner = model.runnerOf(filePath) || {};
3327
+ if (runner.id === 'node-test' && nodeRunsFile(runner, relativeTo(runner.dir, fileEntry.relativePath))) return 'tests';
3328
+ const named = langTraits(fileEntry.language)?.testFileNames || [];
3329
+ const base = path.posix.basename(fileEntry.relativePath);
3330
+ const collection = (model.runnerOf(filePath) || {}).collection || {};
3331
+ if (collection.files && collection.files.some(glob => globMatches(glob, base))) return 'tests';
3332
+ if (named.some(pattern => pattern.test(fileEntry.relativePath) || pattern.test(base))) return 'tests';
3333
+ return 'support';
3334
+ };
3335
+
3336
+ /** The command that runs a unit (or the file when `unit` is null). */
3337
+ model.selectorOf = (filePath, unit) => {
3338
+ const fileEntry = index.files.get(filePath);
3339
+ const runner = model.runnerOf(filePath);
3340
+ if (!runner.id) return withheld(runner.reason || 'no-runner-evidence');
3341
+ const build = SELECTORS[runner.id];
3342
+ if (!build) return withheld('runner-not-modeled');
3343
+ if (unit && unit.kind !== 'test') return withheld('not-a-test');
3344
+ if (unit && unit.name == null) return withheld('name-not-literal');
3345
+ if (unit && unit.dynamic) return withheld(unit.dynamic);
3346
+ // Runners select by name: two declarations of one name in one
3347
+ // suite are one selection.
3348
+ if (unit && model.unitsOf(filePath).units.some(other => other !== unit && other.kind === 'test' &&
3349
+ other.name === unit.name && other.suite.length === unit.suite.length &&
3350
+ other.suite.every((name, i) => name === unit.suite[i]) &&
3351
+ !(other.inherited && unit.inherited && other.startLine !== unit.startLine))) {
3352
+ return withheld('name-not-unique');
3353
+ }
3354
+ try {
3355
+ return build(model, runner, fileEntry, unit || null, filePath);
3356
+ } catch (e) {
3357
+ return withheld('selector-failed');
3358
+ }
3359
+ };
3360
+
3361
+ /** A unit as an answer row. */
3362
+ model.describe = (filePath, unit) => {
3363
+ const { selector, reason } = unit.kind === 'test' ? model.selectorOf(filePath, unit) : withheld('not-a-test');
3364
+ return {
3365
+ kind: unit.kind,
3366
+ name: unit.name,
3367
+ suite: unit.suite,
3368
+ line: unit.line,
3369
+ ...(unit.inherited && { inheritedFrom: `${unit.inherited.file}:${unit.inherited.line}` }),
3370
+ ...(unit.conditional && { conditional: true }),
3371
+ ...(unit.skipped && { skipped: true }),
3372
+ ...(unit.kind === 'test' && selector && { selector }),
3373
+ ...(unit.kind === 'test' && !selector && { selectorWithheld: reason }),
3374
+ };
3375
+ };
3376
+
3377
+ /** What a file is to a runner: role, runner, the command for the file. */
3378
+ model.fileFacts = filePath => {
3379
+ const role = model.roleOf(filePath);
3380
+ const runner = model.runnerOf(filePath);
3381
+ const facts = model.unitsOf(filePath);
3382
+ const out = { role, tests: facts.tests };
3383
+ // The runner reports more tests than are counted: the file holds
3384
+ // some that are not listed, or a listed one stands for several.
3385
+ if (role === 'tests' && (facts.inheriting ||
3386
+ facts.units.some(unit => unit.kind === 'test' && !unit.skipped && standsForSeveral(unit)))) out.moreTests = true;
3387
+ if (runner.id) out.runner = runner.id;
3388
+ if (role === 'tests') {
3389
+ const tests = facts.units.filter(unit => unit.kind === 'test');
3390
+ // A module its runner is told not to collect has no command.
3391
+ // A file whose tests are all declared inside functions has
3392
+ // them only where something calls those functions; one whose
3393
+ // every test is left out of some builds has no command that
3394
+ // is known to run it.
3395
+ const { selector, reason } = model.declared(filePath)?.off
3396
+ ? withheld('collection-switched-off')
3397
+ : tests.length > 0 && tests.every(unit => unit.dynamic === 'declared-indirectly')
3398
+ ? withheld('declared-indirectly')
3399
+ : tests.length > 0 && tests.every(unit => unit.notInEveryBuild)
3400
+ ? withheld('conditional-tests')
3401
+ : tests.length > 0 && tests.every(unit => unit.deselected)
3402
+ ? withheld('deselected-by-options') : model.selectorOf(filePath, null);
3403
+ if (selector) out.run = selector;
3404
+ else out.runWithheld = reason;
3405
+ }
3406
+ return out;
3407
+ };
3408
+
3409
+ return model;
3410
+ }
3411
+
3412
+ /**
3413
+ * Add a `test-case` row for every test that holds one of the matches that
3414
+ * count, and for every titled unit whose title names the term. The row
3415
+ * sits on the unit's first line; a line that already has a row keeps it.
3416
+ * @param {object[]} matches - rows of one file, extended in place
3417
+ * @param {{counts: function, titleNeedsMatch?: boolean, titles?: boolean, extra?: object}} options
3418
+ */
3419
+ function addTestCaseRows(model, index, filePath, term, matches, options) {
3420
+ const taken = new Set(matches.map(match => match.line));
3421
+ const counted = matches.filter(options.counts);
3422
+ const added = [];
3423
+ const add = unit => {
3424
+ if (taken.has(unit.startLine)) return;
3425
+ taken.add(unit.startLine);
3426
+ added.push({
3427
+ line: unit.startLine,
3428
+ ...(unit.sourceSpan && { column: unit.sourceSpan.startColumn, callStart: unit.sourceSpan.start }),
3429
+ content: (index.getLineContent(filePath, unit.startLine) || '').trim(),
3430
+ matchType: 'test-case',
3431
+ ...(options.extra || {}),
3432
+ });
3433
+ };
3434
+ if (!model.unitsOf(filePath).modeled) {
3435
+ // A language whose tests are not read as units: the function the
3436
+ // harness invokes around a match stands for the test. No name,
3437
+ // suite or command is claimed for it.
3438
+ const fileEntry = index.files.get(filePath);
3439
+ let kindOf;
3440
+ try {
3441
+ kindOf = getLanguageAdapter(fileEntry.language)?.getEntryPointKind || null;
3442
+ } catch (e) {
3443
+ kindOf = null;
3444
+ }
3445
+ for (const symbol of (kindOf && fileEntry.symbols) || []) {
3446
+ if (kindOf(symbol) !== 'test') continue;
3447
+ const end = symbol.endLine ?? symbol.startLine;
3448
+ if (counted.some(match => match.line >= symbol.startLine && match.line <= end)) {
3449
+ add({ startLine: symbol.startLine });
3450
+ }
3451
+ }
3452
+ matches.push(...added);
3453
+ return;
3454
+ }
3455
+ for (const match of counted) {
3456
+ const unit = model.unitAt(filePath, match);
3457
+ if (unit && unit.kind === 'test') add(unit);
3458
+ }
3459
+ if (options.titles !== false && term) {
3460
+ // A title is prose: the term counts where it stands as a word.
3461
+ const named = new RegExp('(^|[^\\w$])' + escapeRegExp(term) + '([^\\w$]|$)');
3462
+ for (const unit of model.unitsOf(filePath).units) {
3463
+ if (!unit.titled || unit.name == null || !named.test(unit.name)) continue;
3464
+ if (options.titleNeedsMatch &&
3465
+ !counted.some(match => containsTestSite(unit, match))) continue;
3466
+ add(unit);
3467
+ }
3468
+ }
3469
+ matches.push(...added);
3470
+ }
3471
+
3472
+ /**
3473
+ * One declaration: two tests of one name on two lines are two tests, and
3474
+ * so are two a class inherits under one name (two methods of that name
3475
+ * with other parameters), which stand on the class's line both.
3476
+ */
3477
+ function testKey(relativePath, unit) {
3478
+ return [relativePath, ...unit.suite, unit.name == null ? '' : unit.name, `@${unit.startLine}`,
3479
+ ...(unit.sourceSpan ? [`#${unit.sourceSpan.start}`] : []),
3480
+ ...(unit.inherited ? [`<${unit.inherited.file}:${unit.inherited.startLine}:${unit.inherited.sourceSpan?.start ?? ''}`] : [])].join('\0');
3481
+ }
3482
+
3483
+ /**
3484
+ * What the lines of a test-path file that reach a symbol amount to: the
3485
+ * file's role and runner, the units of THIS file that execute those lines
3486
+ * (plus `extra`: units of this file that execute lines of another, handed
3487
+ * over by that file's answer), how many tests that is, and the command for
3488
+ * the file. `moreTests`: `tests` is a lower bound (the file holds tests
3489
+ * the reader does not list, or a counted declaration stands for several
3490
+ * of the runner's tests). `conditionalTests`: how many of the counted
3491
+ * tests run only sometimes. `carried` (not enumerable) holds the units of
3492
+ * OTHER files that execute these lines: a test a class inherits runs where
3493
+ * the class is.
3494
+ * @returns {{role: string, runner?: string, run?: object, runWithheld?: string,
3495
+ * units: object[], tests: number, moreTests?: boolean, conditionalTests?: number,
3496
+ * testKeys: string[], carried: Array<{filePath: string, unit: object}>}}
3497
+ */
3498
+ function linkFile(model, index, filePath, lines, extra = []) {
3499
+ const facts = model.fileFacts(filePath);
3500
+ const relativePath = index.files.get(filePath).relativePath;
3501
+ const rows = new Map();
3502
+ const tests = new Set();
3503
+ const sometimes = new Set();
3504
+ const carried = [];
3505
+ // Tests the file holds and does not list: which of them these lines
3506
+ // reach is not known.
3507
+ let more = facts.role === 'tests' && model.unitsOf(filePath).inheriting;
3508
+ const count = test => {
3509
+ if (test.skipped) return;
3510
+ tests.add(testKey(relativePath, test));
3511
+ if (test.conditional) sometimes.add(testKey(relativePath, test));
3512
+ if (standsForSeveral(test)) more = true;
3513
+ };
3514
+ const take = unit => {
3515
+ const key = `${unit.kind}\0${testKey(relativePath, unit)}`;
3516
+ if (!rows.has(key)) rows.set(key, model.describe(filePath, unit));
3517
+ for (const test of model.testsWithin(filePath, unit)) count(test);
3518
+ };
3519
+ let fileLevel = false;
3520
+ const sites = new Map(lines.map(site => [testSiteKey(site), {
3521
+ ...testSite(site), wholeLine: typeof site === 'number',
3522
+ }]));
3523
+ for (const site of [...sites.values()].sort((a, b) => a.line - b.line || (a.column || 0) - (b.column || 0))) {
3524
+ if (site.wholeLine) {
3525
+ // Legacy numeric inputs deliberately select the whole source
3526
+ // line (the matrix uses every line of a file). Precise callers
3527
+ // pass objects, so a missing position never implies this scope.
3528
+ const units = model.unitsAt(filePath, site);
3529
+ if (units.length > 1) {
3530
+ for (const unit of units) take(unit);
3531
+ continue;
3532
+ }
3533
+ }
3534
+ const runs = model.runsAt(filePath, site);
3535
+ if (runs.length === 0) {
3536
+ if (model.unitsAt(filePath, site).length > 1) {
3537
+ // A line without a physical position cannot select one of
3538
+ // several test bodies. Keep the file, withhold exact units.
3539
+ more = true;
3540
+ continue;
3541
+ }
3542
+ fileLevel = true;
3543
+ continue;
3544
+ }
3545
+ for (const run of runs) {
3546
+ if (run.filePath === filePath) take(run.unit);
3547
+ else carried.push(run);
3548
+ }
3549
+ }
3550
+ for (const unit of extra) take(unit);
3551
+ // Code outside every unit runs with the file: all its tests count.
3552
+ if (fileLevel && facts.role === 'tests') {
3553
+ for (const test of model.testsWithin(filePath, null)) count(test);
3554
+ }
3555
+ const units = [...rows.values()].sort((a, b) => a.line - b.line || codeUnitCompare(a.name || '', b.name || ''));
3556
+ const out = {
3557
+ role: facts.role,
3558
+ ...(facts.runner && { runner: facts.runner }),
3559
+ ...(facts.run && { run: facts.run }),
3560
+ ...(facts.runWithheld && { runWithheld: facts.runWithheld }),
3561
+ units,
3562
+ tests: tests.size,
3563
+ // The runner reports more tests than are counted here: the
3564
+ // count is a lower bound.
3565
+ ...(more && { moreTests: true }),
3566
+ // Tests counted that run only sometimes.
3567
+ ...(sometimes.size > 0 && { conditionalTests: sometimes.size }),
3568
+ testKeys: [...tests].sort(codeUnitCompare),
3569
+ };
3570
+ Object.defineProperty(out, 'carried', { value: carried, enumerable: false });
3571
+ Object.defineProperty(out, 'conditionalKeys', { value: [...sometimes].sort(codeUnitCompare), enumerable: false });
3572
+ return out;
3573
+ }
3574
+
3575
+ /**
3576
+ * Link several files at once: each file's own lines, then the units that
3577
+ * other files' lines carried into it. A file that only receives carried
3578
+ * units (its class inherits a test whose body is elsewhere) gets a row of
3579
+ * its own.
3580
+ * @param {Map<string, number[]>} linesByFile - absolute path -> lines
3581
+ * @returns {Map<string, object>} absolute path -> linkFile answer, plus
3582
+ * `from`: the relative paths whose lines its carried units execute
3583
+ */
3584
+ function linkFiles(model, index, linesByFile) {
3585
+ const carriedInto = new Map(); // absolute path -> { units: Map, from: Set }
3586
+ for (const [filePath, lines] of linesByFile) {
3587
+ for (const run of linkFile(model, index, filePath, lines).carried) {
3588
+ let held = carriedInto.get(run.filePath);
3589
+ if (!held) {
3590
+ held = { units: new Map(), from: new Set() };
3591
+ carriedInto.set(run.filePath, held);
3592
+ }
3593
+ held.units.set(testKey('', run.unit), run.unit);
3594
+ held.from.add(index.files.get(filePath).relativePath);
3595
+ }
3596
+ }
3597
+ const out = new Map();
3598
+ const all = new Set([...linesByFile.keys(), ...carriedInto.keys()]);
3599
+ for (const filePath of [...all].sort(codeUnitCompare)) {
3600
+ const held = carriedInto.get(filePath);
3601
+ const linked = linkFile(model, index, filePath, linesByFile.get(filePath) || [],
3602
+ held ? [...held.units.values()] : []);
3603
+ if (held) linked.from = [...held.from].sort(codeUnitCompare);
3604
+ out.set(filePath, linked);
3605
+ }
3606
+ return out;
3607
+ }
3608
+
3609
+ // Runners whose file command ends in the path and takes several of them.
3610
+ const TAKES_MANY = new Set(['pytest', 'node-test', 'jest', 'vitest', 'mocha', 'go-test']);
3611
+ // Runners in which a focus in one file holds for every file of the run:
3612
+ // such a file is run by itself, or the others' tests would not run.
3613
+ const FOCUS_SPANS_THE_RUN = new Set(['mocha']);
3614
+
3615
+ /**
3616
+ * The commands that run a set of test files, files of one runner and one
3617
+ * working directory joined where the runner takes several paths.
3618
+ * @returns {string[]}
3619
+ */
3620
+ function commandsFor(model, filePaths) {
3621
+ const groups = new Map();
3622
+ const single = [];
3623
+ for (const filePath of filePaths) {
3624
+ // The file's own answer decides: a command it withholds is not
3625
+ // offered again here.
3626
+ const selector = model.fileFacts(filePath).run;
3627
+ if (!selector) continue;
3628
+ const runner = model.runnerOf(filePath);
3629
+ const cut = selector.command.lastIndexOf(' ');
3630
+ const last = selector.command.slice(cut + 1);
3631
+ if (!TAKES_MANY.has(runner.id) || cut < 0 || /['"\\]/.test(last) ||
3632
+ (FOCUS_SPANS_THE_RUN.has(runner.id) && model.declared(filePath)?.focus)) {
3633
+ if (!single.includes(selectorText(selector))) single.push(selectorText(selector));
3634
+ continue;
3635
+ }
3636
+ const head = selector.command.slice(0, cut);
3637
+ const key = `${selector.cwd || ''}\0${head}\0${model.joinKey(filePath)}`;
3638
+ if (!groups.has(key)) groups.set(key, { cwd: selector.cwd, head, paths: [] });
3639
+ const group = groups.get(key);
3640
+ if (!group.paths.includes(last)) group.paths.push(last);
3641
+ }
3642
+ const joined = [...groups.values()].map(group =>
3643
+ selectorText({ command: `${group.head} ${group.paths.join(' ')}`, ...(group.cwd && { cwd: group.cwd }) }));
3644
+ return [...joined, ...single];
3645
+ }
3646
+
3647
+ /** fnmatch as pytest applies it to a name: `*`, `?`, and `[...]`. */
3648
+ function globMatches(glob, name) {
3649
+ const source = String(glob).replace(/[.+^${}()|\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.');
3650
+ try {
3651
+ return new RegExp(`^${source}$`).test(name);
3652
+ } catch (e) {
3653
+ return false;
3654
+ }
3655
+ }
3656
+
3657
+ /** A selector as one shell line. */
3658
+ function selectorText(selector) {
3659
+ if (!selector) return null;
3660
+ return selector.cwd ? `(cd ${shellWord(selector.cwd)} && ${selector.command})` : selector.command;
3661
+ }
3662
+
3663
+ // Declarations whose names at run time are not the written one, or that
3664
+ // exist once for every call of the function around them.
3665
+ const SEVERAL_KINDS = new Set(['generated-cases', 'name-not-literal', 'suite-name-not-literal', 'declared-indirectly']);
3666
+
3667
+ /**
3668
+ * Does one declaration stand for several of the runner's tests: a table
3669
+ * of cases, a parametrized test, a title computed at run time, a
3670
+ * declaration inside a function that may be called more than once.
3671
+ */
3672
+ function standsForSeveral(unit) {
3673
+ return !!unit.several || SEVERAL_KINDS.has(unit.dynamic);
3674
+ }
3675
+
3676
+ /**
3677
+ * A count of tests as text: "3 tests", "at least 3 tests" when tests
3678
+ * that are not listed may add to it, "(1 conditional)" for the ones that
3679
+ * run only sometimes.
3680
+ */
3681
+ function testCountText(count, more, conditional) {
3682
+ if (!count) return more ? 'tests not listed' : '';
3683
+ return `${more ? 'at least ' : ''}${count} test${count === 1 ? '' : 's'}${conditional ? ` (${conditional} conditional)` : ''}`;
3684
+ }
3685
+
3686
+ /** A unit's name as people write it: `suite > suite > name`. */
3687
+ function unitLabel(unit) {
3688
+ return [...(unit.suite || []), unit.name == null ? '(computed name)' : unit.name].join(' > ');
3689
+ }
3690
+
3691
+ module.exports = {
3692
+ createTestModel, selectorText, unitLabel, shellWord, globMatches, testCountText, standsForSeveral,
3693
+ addTestCaseRows, linkFile, linkFiles, commandsFor, testSite, testSiteKey,
3694
+ // Exposed for tests.
3695
+ scriptRunners, nodeTestOptions, configWords, tableText, namesRequirement, focusedOut,
3696
+ shellSplit, configArguments, pytestOptions, markSelects, pathGlob, nodeTestPaths, nodeRunsFile,
3697
+ mochaLoadingOptions, runnerScriptWords,
3698
+ };