ucn 5.6.0 → 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.
- package/.claude/skills/ucn/SKILL.md +4 -2
- package/.claude/skills/ucn/references/commands.md +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +5 -8
- package/core/analysis.js +3 -3
- package/core/cache.js +3 -1
- package/core/call-fit.js +130 -0
- package/core/callers.js +1 -1
- package/core/capture.js +40 -8
- package/core/change-kinds.js +221 -2
- package/core/check.js +48 -15
- package/core/command-contracts.js +2 -2
- package/core/deadcode.js +94 -23
- package/core/deletion.js +29 -9
- package/core/entrypoint-toml.js +110 -0
- package/core/index-ir.js +2 -2
- package/core/ir.js +1 -1
- package/core/outgoing-check.js +116 -0
- package/core/output/check.js +5 -2
- package/core/output/reporting.js +4 -0
- package/core/output/search.js +3 -0
- package/core/output/tracing.js +1 -1
- package/core/output-budget.js +29 -11
- package/core/python-binding-identity.js +707 -0
- package/core/reflection.js +99 -10
- package/core/registry.js +2 -2
- package/core/runtime-entrypoints.js +159 -0
- package/core/search.js +61 -38
- package/core/test-units.js +116 -42
- package/core/tracing.js +193 -635
- package/core/verify.js +37 -12
- package/languages/csharp.js +4 -4
- package/languages/go.js +1 -1
- package/languages/index.js +45 -1
- package/languages/java.js +4 -4
- package/languages/javascript.js +40 -6
- package/languages/python.js +49 -15
- package/languages/rust.js +126 -112
- package/languages/static-js.js +88 -0
- package/mcp/server.js +1 -1
- package/package.json +4 -3
package/core/test-units.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* core/test-units.js — what a test is, where it is, and how to run it.
|
|
3
3
|
*
|
|
4
4
|
* One identity for a test, shared by `tests`, the affected-test selection
|
|
5
|
-
* and `check`: a UNIT is (file, suite chain, name,
|
|
5
|
+
* and `check`: a UNIT is (file, suite chain, name, source extent), read from the
|
|
6
6
|
* declaration the language's runner collects (the language module's
|
|
7
|
-
* `testUnits`). A
|
|
8
|
-
* source range holds it;
|
|
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
9
|
*
|
|
10
10
|
* A RUNNER is named only on evidence: the test file imports the runner's
|
|
11
11
|
* own module, or the nearest build manifest names it. A SELECTOR (the
|
|
@@ -1556,6 +1556,44 @@ function topLevelTestClasses(model, filePath) {
|
|
|
1556
1556
|
* @param {object} index - ProjectIndex
|
|
1557
1557
|
* @returns {object} per-operation test model
|
|
1558
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
|
+
|
|
1559
1597
|
function createTestModel(index, options = {}) {
|
|
1560
1598
|
const model = {
|
|
1561
1599
|
manifests: createManifests(index),
|
|
@@ -1663,6 +1701,11 @@ function createTestModel(index, options = {}) {
|
|
|
1663
1701
|
const out = [];
|
|
1664
1702
|
for (const other of symbols) {
|
|
1665
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
|
+
}
|
|
1666
1709
|
const end = other.endLine ?? other.startLine;
|
|
1667
1710
|
const ownEnd = symbol.endLine ?? symbol.startLine;
|
|
1668
1711
|
if (other.startLine > symbol.startLine || end < ownEnd) continue;
|
|
@@ -2697,6 +2740,7 @@ function createTestModel(index, options = {}) {
|
|
|
2697
2740
|
(byFixture && states.some(each => each.regime === 'unittest'));
|
|
2698
2741
|
const unit = {
|
|
2699
2742
|
...base,
|
|
2743
|
+
...(!base.inherited && rowFile === filePath && row.sourceSpan && { sourceSpan: row.sourceSpan }),
|
|
2700
2744
|
...(skipped && { skipped: true }),
|
|
2701
2745
|
...(sometimes && { conditional: true }),
|
|
2702
2746
|
...(dynamic && { dynamic }),
|
|
@@ -2777,14 +2821,14 @@ function createTestModel(index, options = {}) {
|
|
|
2777
2821
|
inherited: {
|
|
2778
2822
|
file: relativeOf(base.filePath), filePath: base.filePath, owner: base.cls.name,
|
|
2779
2823
|
ownerLine: base.cls.startLine, line: test.alias ? test.of : test.line,
|
|
2780
|
-
startLine: test.startLine, endLine: test.endLine,
|
|
2824
|
+
startLine: test.startLine, endLine: test.endLine, sourceSpan: test.sourceSpan,
|
|
2781
2825
|
},
|
|
2782
2826
|
}, test, around, reopened(test) || (standing.regime === 'unittest' ? null : byForce(test)), base.filePath);
|
|
2783
2827
|
}
|
|
2784
2828
|
if (units.length > before) {
|
|
2785
2829
|
units.push({
|
|
2786
2830
|
kind: 'suite', name: cls.name, suite: cls.suite,
|
|
2787
|
-
line: cls.line, startLine: cls.startLine, endLine: cls.endLine,
|
|
2831
|
+
line: cls.line, startLine: cls.startLine, endLine: cls.endLine, sourceSpan: cls.sourceSpan,
|
|
2788
2832
|
});
|
|
2789
2833
|
}
|
|
2790
2834
|
}
|
|
@@ -2808,7 +2852,7 @@ function createTestModel(index, options = {}) {
|
|
|
2808
2852
|
kind: 'test', name: entry.name, suite: [], line: entry.line, startLine: entry.line, endLine: entry.line,
|
|
2809
2853
|
inherited: {
|
|
2810
2854
|
file: relativeOf(target), filePath: target, owner: null, ownerLine: 0,
|
|
2811
|
-
line: fn.line, startLine: fn.startLine, endLine: fn.endLine,
|
|
2855
|
+
line: fn.line, startLine: fn.startLine, endLine: fn.endLine, sourceSpan: fn.sourceSpan,
|
|
2812
2856
|
},
|
|
2813
2857
|
}, row, [], (on === 'unknown' ? 'collection-switch' : null) || byForce(row), target);
|
|
2814
2858
|
}
|
|
@@ -2875,7 +2919,7 @@ function createTestModel(index, options = {}) {
|
|
|
2875
2919
|
const relativePath = index.files.get(filePath).relativePath;
|
|
2876
2920
|
const runner = model.runnerOf(filePath) || {};
|
|
2877
2921
|
const classesHere = [...(found.classes || [])];
|
|
2878
|
-
const within = (unit, cls) =>
|
|
2922
|
+
const within = (unit, cls) => containsTestUnit(cls, unit);
|
|
2879
2923
|
// A setup that can stop a test, in a class the test's class
|
|
2880
2924
|
// derives from: the test runs only sometimes.
|
|
2881
2925
|
const guardedAbove = cls => cls.bases.length > 0 &&
|
|
@@ -2943,7 +2987,7 @@ function createTestModel(index, options = {}) {
|
|
|
2943
2987
|
if (inherited.tests.some(({ test }) => !test.needsClassMark || cls.marked) && !own) {
|
|
2944
2988
|
units.push({
|
|
2945
2989
|
kind: 'suite', name: cls.name, suite: cls.suite,
|
|
2946
|
-
line: cls.line, startLine: cls.startLine, endLine: cls.endLine,
|
|
2990
|
+
line: cls.line, startLine: cls.startLine, endLine: cls.endLine, sourceSpan: cls.sourceSpan,
|
|
2947
2991
|
});
|
|
2948
2992
|
}
|
|
2949
2993
|
for (const { test, base, body, uncertain, maybe, again } of inherited.tests) {
|
|
@@ -2969,7 +3013,7 @@ function createTestModel(index, options = {}) {
|
|
|
2969
3013
|
alternatives: maybe.map(other => ({
|
|
2970
3014
|
file: index.files.get(other.at.filePath).relativePath,
|
|
2971
3015
|
filePath: other.at.filePath, ownerLine: other.at.cls.startLine,
|
|
2972
|
-
startLine: other.row.startLine, endLine: other.row.endLine,
|
|
3016
|
+
startLine: other.row.startLine, endLine: other.row.endLine, sourceSpan: other.row.sourceSpan,
|
|
2973
3017
|
})),
|
|
2974
3018
|
}),
|
|
2975
3019
|
};
|
|
@@ -2979,7 +3023,7 @@ function createTestModel(index, options = {}) {
|
|
|
2979
3023
|
if (body && body.at.cls === cls && !uncertain && !again) {
|
|
2980
3024
|
units.push({
|
|
2981
3025
|
kind: 'test', name: test.name, suite, ...(cls.namespace && { namespace: cls.namespace }),
|
|
2982
|
-
line: body.row.line, startLine: body.row.startLine, endLine: body.row.endLine, ...state,
|
|
3026
|
+
line: body.row.line, startLine: body.row.startLine, endLine: body.row.endLine, sourceSpan: body.row.sourceSpan, ...state,
|
|
2983
3027
|
});
|
|
2984
3028
|
continue;
|
|
2985
3029
|
}
|
|
@@ -2994,7 +3038,7 @@ function createTestModel(index, options = {}) {
|
|
|
2994
3038
|
file: index.files.get(at.filePath).relativePath,
|
|
2995
3039
|
filePath: at.filePath, owner: at.cls.name,
|
|
2996
3040
|
ownerLine: at.cls.startLine, line: where.line,
|
|
2997
|
-
startLine: where.startLine, endLine: where.endLine,
|
|
3041
|
+
startLine: where.startLine, endLine: where.endLine, sourceSpan: where.sourceSpan,
|
|
2998
3042
|
},
|
|
2999
3043
|
});
|
|
3000
3044
|
}
|
|
@@ -3010,7 +3054,7 @@ function createTestModel(index, options = {}) {
|
|
|
3010
3054
|
units.push({
|
|
3011
3055
|
kind: 'test', name: row.name, suite: [...cls.suite, cls.name],
|
|
3012
3056
|
...(cls.namespace && { namespace: cls.namespace }),
|
|
3013
|
-
line: row.line, startLine: row.startLine, endLine: row.endLine,
|
|
3057
|
+
line: row.line, startLine: row.startLine, endLine: row.endLine, sourceSpan: row.sourceSpan,
|
|
3014
3058
|
...(cls.skipped && { skipped: true }),
|
|
3015
3059
|
...((cls.sometimes || cls.guard || guardedAbove(cls)) && { conditional: true }),
|
|
3016
3060
|
dynamic: 'declared-in-a-base-class',
|
|
@@ -3019,7 +3063,7 @@ function createTestModel(index, options = {}) {
|
|
|
3019
3063
|
if (added && !own && inherited.tests.length === 0) {
|
|
3020
3064
|
units.push({
|
|
3021
3065
|
kind: 'suite', name: cls.name, suite: cls.suite,
|
|
3022
|
-
line: cls.line, startLine: cls.startLine, endLine: cls.endLine,
|
|
3066
|
+
line: cls.line, startLine: cls.startLine, endLine: cls.endLine, sourceSpan: cls.sourceSpan,
|
|
3023
3067
|
});
|
|
3024
3068
|
}
|
|
3025
3069
|
}
|
|
@@ -3089,8 +3133,9 @@ function createTestModel(index, options = {}) {
|
|
|
3089
3133
|
* empty answer: the line belongs to no unit (file-level code).
|
|
3090
3134
|
* @returns {Array<{filePath: string, unit: object}>}
|
|
3091
3135
|
*/
|
|
3092
|
-
model.runsAt = (filePath,
|
|
3136
|
+
model.runsAt = (filePath, site) => {
|
|
3093
3137
|
const out = [];
|
|
3138
|
+
const point = testSite(site);
|
|
3094
3139
|
// The units of a file that may run this code. A module its runner
|
|
3095
3140
|
// does not collect by its name declares none: it is not read for
|
|
3096
3141
|
// the answer.
|
|
@@ -3101,19 +3146,22 @@ function createTestModel(index, options = {}) {
|
|
|
3101
3146
|
}
|
|
3102
3147
|
return model.unitsOf(file).units;
|
|
3103
3148
|
};
|
|
3104
|
-
const unit = model.unitAt(filePath,
|
|
3149
|
+
const unit = model.unitAt(filePath, point);
|
|
3105
3150
|
if (unit && unit.kind === 'test') out.push({ filePath, unit });
|
|
3106
|
-
const
|
|
3107
|
-
|
|
3108
|
-
.sort((a, b) => b.startLine - a.startLine)[0];
|
|
3151
|
+
const holders = innermostTestRows(model.declared(filePath)?.classes || [], point);
|
|
3152
|
+
const holder = holders.length === 1 ? holders[0] : null;
|
|
3109
3153
|
// The method that holds the line: a test's declaration, or a method
|
|
3110
3154
|
// a deriving class's test takes its body from.
|
|
3111
|
-
const
|
|
3112
|
-
|
|
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;
|
|
3113
3160
|
const importers = [...(index.exportGraph?.get(filePath) || [])];
|
|
3114
3161
|
if (template) {
|
|
3115
3162
|
const held = from => !!from && from.filePath === filePath && from.ownerLine === holder.startLine &&
|
|
3116
|
-
from.startLine === template.startLine && from.endLine === template.endLine
|
|
3163
|
+
from.startLine === template.startLine && from.endLine === template.endLine &&
|
|
3164
|
+
(!from.sourceSpan || !template.sourceSpan || from.sourceSpan.start === template.sourceSpan.start);
|
|
3117
3165
|
for (const file of [...new Set([...descendantFiles(holder.name), ...importers, filePath])].sort(codeUnitCompare)) {
|
|
3118
3166
|
for (const candidate of unitsIn(file)) {
|
|
3119
3167
|
// Every test whose body this is: the one a deriving
|
|
@@ -3127,13 +3175,14 @@ function createTestModel(index, options = {}) {
|
|
|
3127
3175
|
}
|
|
3128
3176
|
}
|
|
3129
3177
|
// A function another module imports under a test's name runs there.
|
|
3130
|
-
const
|
|
3131
|
-
|
|
3178
|
+
const functions = holder ? [] : innermostTestRows(model.declared(filePath)?.functions || [], point);
|
|
3179
|
+
const fn = functions.length === 1 ? functions[0] : null;
|
|
3132
3180
|
if (fn) {
|
|
3133
3181
|
for (const file of [...new Set([...importers, filePath])].sort(codeUnitCompare)) {
|
|
3134
3182
|
for (const candidate of unitsIn(file)) {
|
|
3135
3183
|
const from = candidate.inherited;
|
|
3136
|
-
if (from && from.filePath === filePath && from.owner == null && from.startLine === fn.startLine
|
|
3184
|
+
if (from && from.filePath === filePath && from.owner == null && from.startLine === fn.startLine &&
|
|
3185
|
+
(!from.sourceSpan || !fn.sourceSpan || from.sourceSpan.start === fn.sourceSpan.start)) {
|
|
3137
3186
|
out.push({ filePath: file, unit: candidate });
|
|
3138
3187
|
}
|
|
3139
3188
|
}
|
|
@@ -3228,23 +3277,27 @@ function createTestModel(index, options = {}) {
|
|
|
3228
3277
|
rows.sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
|
|
3229
3278
|
for (const row of rows) {
|
|
3230
3279
|
if (row.startLine == null || row.endLine == null || covered(row)) continue;
|
|
3231
|
-
if (model.runsAt(filePath,
|
|
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
|
+
}
|
|
3232
3284
|
}
|
|
3233
3285
|
}
|
|
3234
3286
|
model.ranges.set(filePath, ranges);
|
|
3235
3287
|
return ranges;
|
|
3236
3288
|
};
|
|
3237
3289
|
|
|
3238
|
-
/**
|
|
3239
|
-
model.
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
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;
|
|
3248
3301
|
};
|
|
3249
3302
|
|
|
3250
3303
|
/** Tests a unit stands for: itself, or every test a suite holds. */
|
|
@@ -3253,7 +3306,7 @@ function createTestModel(index, options = {}) {
|
|
|
3253
3306
|
if (!unit) return units.filter(candidate => candidate.kind === 'test');
|
|
3254
3307
|
if (unit.kind === 'test') return [unit];
|
|
3255
3308
|
return units.filter(candidate => candidate.kind === 'test' &&
|
|
3256
|
-
|
|
3309
|
+
containsTestUnit(unit, candidate));
|
|
3257
3310
|
};
|
|
3258
3311
|
|
|
3259
3312
|
/**
|
|
@@ -3372,6 +3425,7 @@ function addTestCaseRows(model, index, filePath, term, matches, options) {
|
|
|
3372
3425
|
taken.add(unit.startLine);
|
|
3373
3426
|
added.push({
|
|
3374
3427
|
line: unit.startLine,
|
|
3428
|
+
...(unit.sourceSpan && { column: unit.sourceSpan.startColumn, callStart: unit.sourceSpan.start }),
|
|
3375
3429
|
content: (index.getLineContent(filePath, unit.startLine) || '').trim(),
|
|
3376
3430
|
matchType: 'test-case',
|
|
3377
3431
|
...(options.extra || {}),
|
|
@@ -3399,7 +3453,7 @@ function addTestCaseRows(model, index, filePath, term, matches, options) {
|
|
|
3399
3453
|
return;
|
|
3400
3454
|
}
|
|
3401
3455
|
for (const match of counted) {
|
|
3402
|
-
const unit = model.unitAt(filePath, match
|
|
3456
|
+
const unit = model.unitAt(filePath, match);
|
|
3403
3457
|
if (unit && unit.kind === 'test') add(unit);
|
|
3404
3458
|
}
|
|
3405
3459
|
if (options.titles !== false && term) {
|
|
@@ -3408,7 +3462,7 @@ function addTestCaseRows(model, index, filePath, term, matches, options) {
|
|
|
3408
3462
|
for (const unit of model.unitsOf(filePath).units) {
|
|
3409
3463
|
if (!unit.titled || unit.name == null || !named.test(unit.name)) continue;
|
|
3410
3464
|
if (options.titleNeedsMatch &&
|
|
3411
|
-
!counted.some(match =>
|
|
3465
|
+
!counted.some(match => containsTestSite(unit, match))) continue;
|
|
3412
3466
|
add(unit);
|
|
3413
3467
|
}
|
|
3414
3468
|
}
|
|
@@ -3422,7 +3476,8 @@ function addTestCaseRows(model, index, filePath, term, matches, options) {
|
|
|
3422
3476
|
*/
|
|
3423
3477
|
function testKey(relativePath, unit) {
|
|
3424
3478
|
return [relativePath, ...unit.suite, unit.name == null ? '' : unit.name, `@${unit.startLine}`,
|
|
3425
|
-
...(unit.
|
|
3479
|
+
...(unit.sourceSpan ? [`#${unit.sourceSpan.start}`] : []),
|
|
3480
|
+
...(unit.inherited ? [`<${unit.inherited.file}:${unit.inherited.startLine}:${unit.inherited.sourceSpan?.start ?? ''}`] : [])].join('\0');
|
|
3426
3481
|
}
|
|
3427
3482
|
|
|
3428
3483
|
/**
|
|
@@ -3462,9 +3517,28 @@ function linkFile(model, index, filePath, lines, extra = []) {
|
|
|
3462
3517
|
for (const test of model.testsWithin(filePath, unit)) count(test);
|
|
3463
3518
|
};
|
|
3464
3519
|
let fileLevel = false;
|
|
3465
|
-
|
|
3466
|
-
|
|
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);
|
|
3467
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
|
+
}
|
|
3468
3542
|
fileLevel = true;
|
|
3469
3543
|
continue;
|
|
3470
3544
|
}
|
|
@@ -3616,7 +3690,7 @@ function unitLabel(unit) {
|
|
|
3616
3690
|
|
|
3617
3691
|
module.exports = {
|
|
3618
3692
|
createTestModel, selectorText, unitLabel, shellWord, globMatches, testCountText, standsForSeveral,
|
|
3619
|
-
addTestCaseRows, linkFile, linkFiles, commandsFor,
|
|
3693
|
+
addTestCaseRows, linkFile, linkFiles, commandsFor, testSite, testSiteKey,
|
|
3620
3694
|
// Exposed for tests.
|
|
3621
3695
|
scriptRunners, nodeTestOptions, configWords, tableText, namesRequirement, focusedOut,
|
|
3622
3696
|
shellSplit, configArguments, pytestOptions, markSelects, pathGlob, nodeTestPaths, nodeRunsFile,
|