@specific.dev/spectest 0.60.0 → 0.60.2
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/dist/daemon.js +23 -0
- package/dist/harness/coverage.d.ts +21 -6
- package/dist/harness/coverage.js +27 -11
- package/package.json +1 -1
- package/src/daemon.ts +21 -0
- package/src/harness/coverage.test.ts +14 -1
- package/src/harness/coverage.ts +35 -10
package/dist/daemon.js
CHANGED
|
@@ -3115,6 +3115,26 @@ async function runCoverageAdapters(svc) {
|
|
|
3115
3115
|
clearTimeout(timer);
|
|
3116
3116
|
}
|
|
3117
3117
|
}
|
|
3118
|
+
/** Container name → its working directory, read once per container
|
|
3119
|
+
* (`docker inspect`); forks with the env like the rest of module memory.
|
|
3120
|
+
* Empty when the image sets none (paths are then reported from `/`). */
|
|
3121
|
+
const COVERAGE_ROOTS = new Map();
|
|
3122
|
+
async function coverageRoot(service) {
|
|
3123
|
+
const cached = COVERAGE_ROOTS.get(service);
|
|
3124
|
+
if (cached !== undefined)
|
|
3125
|
+
return cached;
|
|
3126
|
+
let root = "";
|
|
3127
|
+
try {
|
|
3128
|
+
const r = await docker(["inspect", "-f", "{{.Config.WorkingDir}}", service], 10_000);
|
|
3129
|
+
if (r.code === 0)
|
|
3130
|
+
root = r.stdout.trim();
|
|
3131
|
+
}
|
|
3132
|
+
catch {
|
|
3133
|
+
// Best-effort: the tree then falls back to a common-prefix guess.
|
|
3134
|
+
}
|
|
3135
|
+
COVERAGE_ROOTS.set(service, root);
|
|
3136
|
+
return root;
|
|
3137
|
+
}
|
|
3118
3138
|
async function captureServiceCoverage() {
|
|
3119
3139
|
const l = loaded;
|
|
3120
3140
|
if (!l)
|
|
@@ -3130,6 +3150,9 @@ async function captureServiceCoverage() {
|
|
|
3130
3150
|
if (error)
|
|
3131
3151
|
return { service: svc.name, reports: [], error };
|
|
3132
3152
|
const read = await readCoverageDir(svc.name, coverageHostDir(WORKSPACE, svc.name));
|
|
3153
|
+
const root = await coverageRoot(svc.name);
|
|
3154
|
+
if (root)
|
|
3155
|
+
read.root = root;
|
|
3133
3156
|
// What ships is what ran during this capture, whatever the tool
|
|
3134
3157
|
// counts: cumulative lcov is subtracted against the previous
|
|
3135
3158
|
// capture (module memory, forks with the env); a delta adapter's
|
|
@@ -17,6 +17,12 @@ export interface CoverageReport {
|
|
|
17
17
|
export interface ServiceCoverageCapture {
|
|
18
18
|
service: string;
|
|
19
19
|
reports: CoverageReport[];
|
|
20
|
+
/** The container's working directory (`docker inspect`), the root
|
|
21
|
+
* the reported container paths are relative to for a `COPY . <dir>`
|
|
22
|
+
* image. The server strips it to draw a project-relative tree; it
|
|
23
|
+
* never has to fetch code to do so. Absent for a container it could
|
|
24
|
+
* not be read from. */
|
|
25
|
+
root?: string;
|
|
20
26
|
/** Set when the capture failed: the command failed, the directory was
|
|
21
27
|
* empty, no file held an `SF:` record, or the reports were too large.
|
|
22
28
|
* A case with an error here fails — a hole in the coverage data must
|
|
@@ -92,13 +98,22 @@ export interface LcovRecordCounts {
|
|
|
92
98
|
* path are merged (a tool may split a file across records). */
|
|
93
99
|
export declare function parseLcovCounts(text: string): Map<string, LcovRecordCounts>;
|
|
94
100
|
/**
|
|
95
|
-
* `now` minus `prev`, per line and per function. A
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
101
|
+
* `now` minus `prev`, per line and per function. A file not in `prev`
|
|
102
|
+
* is new: everything in it counts. A **record in which any count went
|
|
103
|
+
* down** is a process that restarted (a restart resets every counter of
|
|
104
|
+
* that process at once): the whole record stands as-is, never
|
|
105
|
+
* subtracted — per line would report a line that ran exactly as often
|
|
106
|
+
* again as "not executed", and the restarted boot set (1 − 1) would
|
|
107
|
+
* vanish. Per record, not per report: a combined coverage.py file can
|
|
108
|
+
* hold several processes. A restart in which no counter dropped is not
|
|
109
|
+
* detectable from counts and is accepted as the residual (a service
|
|
110
|
+
* restarting mid-test is an anomaly already). Lines with a zero delta
|
|
111
|
+
* stay in the record: the server distinguishes "ran 0 times" from "not
|
|
112
|
+
* a counted line". Only records with at least one execution are kept —
|
|
113
|
+
* a file with nothing new in it did not run. `restarted` receives the
|
|
114
|
+
* paths of the records taken as-is.
|
|
100
115
|
*/
|
|
101
|
-
export declare function subtractLcovCounts(now: Map<string, LcovRecordCounts>, prev: Map<string, LcovRecordCounts
|
|
116
|
+
export declare function subtractLcovCounts(now: Map<string, LcovRecordCounts>, prev: Map<string, LcovRecordCounts>, restarted?: string[]): Map<string, LcovRecordCounts>;
|
|
102
117
|
/** Counts rendered back to lcov, records in path order. */
|
|
103
118
|
export declare function renderLcovCounts(counts: Map<string, LcovRecordCounts>): string;
|
|
104
119
|
/** Forget every previous capture (tests). */
|
package/dist/harness/coverage.js
CHANGED
|
@@ -215,29 +215,41 @@ export function parseLcovCounts(text) {
|
|
|
215
215
|
return out;
|
|
216
216
|
}
|
|
217
217
|
/**
|
|
218
|
-
* `now` minus `prev`, per line and per function. A
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
218
|
+
* `now` minus `prev`, per line and per function. A file not in `prev`
|
|
219
|
+
* is new: everything in it counts. A **record in which any count went
|
|
220
|
+
* down** is a process that restarted (a restart resets every counter of
|
|
221
|
+
* that process at once): the whole record stands as-is, never
|
|
222
|
+
* subtracted — per line would report a line that ran exactly as often
|
|
223
|
+
* again as "not executed", and the restarted boot set (1 − 1) would
|
|
224
|
+
* vanish. Per record, not per report: a combined coverage.py file can
|
|
225
|
+
* hold several processes. A restart in which no counter dropped is not
|
|
226
|
+
* detectable from counts and is accepted as the residual (a service
|
|
227
|
+
* restarting mid-test is an anomaly already). Lines with a zero delta
|
|
228
|
+
* stay in the record: the server distinguishes "ran 0 times" from "not
|
|
229
|
+
* a counted line". Only records with at least one execution are kept —
|
|
230
|
+
* a file with nothing new in it did not run. `restarted` receives the
|
|
231
|
+
* paths of the records taken as-is.
|
|
223
232
|
*/
|
|
224
|
-
export function subtractLcovCounts(now, prev) {
|
|
233
|
+
export function subtractLcovCounts(now, prev, restarted) {
|
|
225
234
|
const out = new Map();
|
|
226
235
|
for (const [file, rec] of now) {
|
|
227
236
|
const before = prev.get(file);
|
|
237
|
+
const dropped = !!before &&
|
|
238
|
+
([...rec.lines].some(([l, h]) => h < (before.lines.get(l) ?? 0)) ||
|
|
239
|
+
[...rec.functions].some(([n, h]) => h < (before.functions.get(n) ?? 0)));
|
|
240
|
+
if (dropped)
|
|
241
|
+
restarted?.push(file);
|
|
228
242
|
const lines = new Map();
|
|
229
243
|
const functions = new Map();
|
|
230
244
|
let executed = false;
|
|
231
245
|
for (const [l, h] of rec.lines) {
|
|
232
|
-
const
|
|
233
|
-
const d = h < b ? h : h - b;
|
|
246
|
+
const d = dropped ? h : h - (before?.lines.get(l) ?? 0);
|
|
234
247
|
lines.set(l, d);
|
|
235
248
|
if (d > 0)
|
|
236
249
|
executed = true;
|
|
237
250
|
}
|
|
238
251
|
for (const [name, h] of rec.functions) {
|
|
239
|
-
const
|
|
240
|
-
const d = h < b ? h : h - b;
|
|
252
|
+
const d = dropped ? h : h - (before?.functions.get(name) ?? 0);
|
|
241
253
|
functions.set(name, d);
|
|
242
254
|
if (d > 0)
|
|
243
255
|
executed = true;
|
|
@@ -309,7 +321,11 @@ export function applyCoverageDelta(capture, mode, deltaReports) {
|
|
|
309
321
|
const now = parseLcovCounts(r.content);
|
|
310
322
|
const prev = prevByName.get(r.name) ?? new Map();
|
|
311
323
|
prevByName.set(r.name, now);
|
|
312
|
-
|
|
324
|
+
const restarted = [];
|
|
325
|
+
reports.push({ name: r.name, content: renderLcovCounts(subtractLcovCounts(now, prev, restarted)) });
|
|
326
|
+
if (restarted.length > 0) {
|
|
327
|
+
console.warn(`[coverage] service "${capture.service}" report ${r.name}: counters dropped in ${restarted.length} file(s) (${restarted.slice(0, 3).join(", ")}) — a process restarted; those records count whole`);
|
|
328
|
+
}
|
|
313
329
|
}
|
|
314
330
|
return { ...capture, reports };
|
|
315
331
|
}
|
package/package.json
CHANGED
package/src/daemon.ts
CHANGED
|
@@ -3759,6 +3759,25 @@ async function runCoverageAdapters(
|
|
|
3759
3759
|
}
|
|
3760
3760
|
}
|
|
3761
3761
|
|
|
3762
|
+
/** Container name → its working directory, read once per container
|
|
3763
|
+
* (`docker inspect`); forks with the env like the rest of module memory.
|
|
3764
|
+
* Empty when the image sets none (paths are then reported from `/`). */
|
|
3765
|
+
const COVERAGE_ROOTS = new Map<string, string>();
|
|
3766
|
+
|
|
3767
|
+
async function coverageRoot(service: string): Promise<string> {
|
|
3768
|
+
const cached = COVERAGE_ROOTS.get(service);
|
|
3769
|
+
if (cached !== undefined) return cached;
|
|
3770
|
+
let root = "";
|
|
3771
|
+
try {
|
|
3772
|
+
const r = await docker(["inspect", "-f", "{{.Config.WorkingDir}}", service], 10_000);
|
|
3773
|
+
if (r.code === 0) root = r.stdout.trim();
|
|
3774
|
+
} catch {
|
|
3775
|
+
// Best-effort: the tree then falls back to a common-prefix guess.
|
|
3776
|
+
}
|
|
3777
|
+
COVERAGE_ROOTS.set(service, root);
|
|
3778
|
+
return root;
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3762
3781
|
async function captureServiceCoverage(): Promise<ServiceCoverageCapture[]> {
|
|
3763
3782
|
const l = loaded;
|
|
3764
3783
|
if (!l) return [];
|
|
@@ -3771,6 +3790,8 @@ async function captureServiceCoverage(): Promise<ServiceCoverageCapture[]> {
|
|
|
3771
3790
|
const { error, deltaReports } = await runCoverageAdapters(svc);
|
|
3772
3791
|
if (error) return { service: svc.name, reports: [], error };
|
|
3773
3792
|
const read = await readCoverageDir(svc.name, coverageHostDir(WORKSPACE, svc.name));
|
|
3793
|
+
const root = await coverageRoot(svc.name);
|
|
3794
|
+
if (root) read.root = root;
|
|
3774
3795
|
// What ships is what ran during this capture, whatever the tool
|
|
3775
3796
|
// counts: cumulative lcov is subtracted against the previous
|
|
3776
3797
|
// capture (module memory, forks with the env); a delta adapter's
|
|
@@ -159,8 +159,21 @@ describe("coverage delta", () => {
|
|
|
159
159
|
expect(d.get("/app/a.py")!.lines.get(1)).toBe(0);
|
|
160
160
|
expect(d.get("/app/a.py")!.functions.get("main")).toBe(0);
|
|
161
161
|
expect(d.get("/app/c.py")!.lines.get(1)).toBe(2);
|
|
162
|
-
const
|
|
162
|
+
const names: string[] = [];
|
|
163
|
+
const restarted = subtractLcovCounts(parseLcovCounts("SF:/app/b.py\nDA:1,2\nend_of_record\n"), parseLcovCounts(boot), names);
|
|
163
164
|
expect(restarted.get("/app/b.py")!.lines.get(1)).toBe(2);
|
|
165
|
+
expect(names).toEqual(["/app/b.py"]);
|
|
166
|
+
// A restart is per record: one dropped counter means every count in
|
|
167
|
+
// that record stands, including a line that ran exactly as often
|
|
168
|
+
// again (the boot line: 1 before, 1 now — not 0). Other records in
|
|
169
|
+
// the same report still subtract.
|
|
170
|
+
const mixed = subtractLcovCounts(
|
|
171
|
+
parseLcovCounts("SF:/app/a.py\nFNDA:1,main\nDA:1,1\nDA:2,1\nDA:3,0\nend_of_record\nSF:/app/b.py\nDA:1,3\nend_of_record\n"),
|
|
172
|
+
parseLcovCounts("SF:/app/a.py\nFNDA:1,main\nDA:1,1\nDA:2,1\nDA:3,7\nend_of_record\nSF:/app/b.py\nDA:1,1\nend_of_record\n"),
|
|
173
|
+
);
|
|
174
|
+
expect(mixed.get("/app/a.py")!.lines.get(1)).toBe(1);
|
|
175
|
+
expect(mixed.get("/app/a.py")!.functions.get("main")).toBe(1);
|
|
176
|
+
expect(mixed.get("/app/b.py")!.lines.get(1)).toBe(2);
|
|
164
177
|
// A record with no counts at all: presence is its signal, new only once.
|
|
165
178
|
const bare = "SF:/app/bare.py\nend_of_record\n";
|
|
166
179
|
expect(subtractLcovCounts(parseLcovCounts(bare), new Map()).has("/app/bare.py")).toBe(true);
|
package/src/harness/coverage.ts
CHANGED
|
@@ -57,6 +57,12 @@ export interface CoverageReport {
|
|
|
57
57
|
export interface ServiceCoverageCapture {
|
|
58
58
|
service: string;
|
|
59
59
|
reports: CoverageReport[];
|
|
60
|
+
/** The container's working directory (`docker inspect`), the root
|
|
61
|
+
* the reported container paths are relative to for a `COPY . <dir>`
|
|
62
|
+
* image. The server strips it to draw a project-relative tree; it
|
|
63
|
+
* never has to fetch code to do so. Absent for a container it could
|
|
64
|
+
* not be read from. */
|
|
65
|
+
root?: string;
|
|
60
66
|
/** Set when the capture failed: the command failed, the directory was
|
|
61
67
|
* empty, no file held an `SF:` record, or the reports were too large.
|
|
62
68
|
* A case with an error here fails — a hole in the coverage data must
|
|
@@ -277,31 +283,44 @@ export function parseLcovCounts(text: string): Map<string, LcovRecordCounts> {
|
|
|
277
283
|
}
|
|
278
284
|
|
|
279
285
|
/**
|
|
280
|
-
* `now` minus `prev`, per line and per function. A
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
286
|
+
* `now` minus `prev`, per line and per function. A file not in `prev`
|
|
287
|
+
* is new: everything in it counts. A **record in which any count went
|
|
288
|
+
* down** is a process that restarted (a restart resets every counter of
|
|
289
|
+
* that process at once): the whole record stands as-is, never
|
|
290
|
+
* subtracted — per line would report a line that ran exactly as often
|
|
291
|
+
* again as "not executed", and the restarted boot set (1 − 1) would
|
|
292
|
+
* vanish. Per record, not per report: a combined coverage.py file can
|
|
293
|
+
* hold several processes. A restart in which no counter dropped is not
|
|
294
|
+
* detectable from counts and is accepted as the residual (a service
|
|
295
|
+
* restarting mid-test is an anomaly already). Lines with a zero delta
|
|
296
|
+
* stay in the record: the server distinguishes "ran 0 times" from "not
|
|
297
|
+
* a counted line". Only records with at least one execution are kept —
|
|
298
|
+
* a file with nothing new in it did not run. `restarted` receives the
|
|
299
|
+
* paths of the records taken as-is.
|
|
285
300
|
*/
|
|
286
301
|
export function subtractLcovCounts(
|
|
287
302
|
now: Map<string, LcovRecordCounts>,
|
|
288
303
|
prev: Map<string, LcovRecordCounts>,
|
|
304
|
+
restarted?: string[],
|
|
289
305
|
): Map<string, LcovRecordCounts> {
|
|
290
306
|
const out = new Map<string, LcovRecordCounts>();
|
|
291
307
|
for (const [file, rec] of now) {
|
|
292
308
|
const before = prev.get(file);
|
|
309
|
+
const dropped =
|
|
310
|
+
!!before &&
|
|
311
|
+
([...rec.lines].some(([l, h]) => h < (before.lines.get(l) ?? 0)) ||
|
|
312
|
+
[...rec.functions].some(([n, h]) => h < (before.functions.get(n) ?? 0)));
|
|
313
|
+
if (dropped) restarted?.push(file);
|
|
293
314
|
const lines = new Map<number, number>();
|
|
294
315
|
const functions = new Map<string, number>();
|
|
295
316
|
let executed = false;
|
|
296
317
|
for (const [l, h] of rec.lines) {
|
|
297
|
-
const
|
|
298
|
-
const d = h < b ? h : h - b;
|
|
318
|
+
const d = dropped ? h : h - (before?.lines.get(l) ?? 0);
|
|
299
319
|
lines.set(l, d);
|
|
300
320
|
if (d > 0) executed = true;
|
|
301
321
|
}
|
|
302
322
|
for (const [name, h] of rec.functions) {
|
|
303
|
-
const
|
|
304
|
-
const d = h < b ? h : h - b;
|
|
323
|
+
const d = dropped ? h : h - (before?.functions.get(name) ?? 0);
|
|
305
324
|
functions.set(name, d);
|
|
306
325
|
if (d > 0) executed = true;
|
|
307
326
|
}
|
|
@@ -374,7 +393,13 @@ export function applyCoverageDelta(
|
|
|
374
393
|
const now = parseLcovCounts(r.content);
|
|
375
394
|
const prev = prevByName.get(r.name) ?? new Map<string, LcovRecordCounts>();
|
|
376
395
|
prevByName.set(r.name, now);
|
|
377
|
-
|
|
396
|
+
const restarted: string[] = [];
|
|
397
|
+
reports.push({ name: r.name, content: renderLcovCounts(subtractLcovCounts(now, prev, restarted)) });
|
|
398
|
+
if (restarted.length > 0) {
|
|
399
|
+
console.warn(
|
|
400
|
+
`[coverage] service "${capture.service}" report ${r.name}: counters dropped in ${restarted.length} file(s) (${restarted.slice(0, 3).join(", ")}) — a process restarted; those records count whole`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
378
403
|
}
|
|
379
404
|
return { ...capture, reports };
|
|
380
405
|
}
|