@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 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 count that went
96
- * *down* is a counter that restarted (the process was replaced): the
97
- * new value stands on its own. A file not in `prev` is new: everything
98
- * in it counts. Only records with at least one execution are kept — a
99
- * file with nothing new in it did not run.
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>): 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). */
@@ -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 count that went
219
- * *down* is a counter that restarted (the process was replaced): the
220
- * new value stands on its own. A file not in `prev` is new: everything
221
- * in it counts. Only records with at least one execution are kept — a
222
- * file with nothing new in it did not run.
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 b = before?.lines.get(l) ?? 0;
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 b = before?.functions.get(name) ?? 0;
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
- reports.push({ name: r.name, content: renderLcovCounts(subtractLcovCounts(now, prev)) });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/spectest",
3
- "version": "0.60.0",
3
+ "version": "0.60.2",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
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 restarted = subtractLcovCounts(parseLcovCounts("SF:/app/b.py\nDA:1,2\nend_of_record\n"), parseLcovCounts(boot));
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);
@@ -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 count that went
281
- * *down* is a counter that restarted (the process was replaced): the
282
- * new value stands on its own. A file not in `prev` is new: everything
283
- * in it counts. Only records with at least one execution are kept — a
284
- * file with nothing new in it did not run.
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 b = before?.lines.get(l) ?? 0;
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 b = before?.functions.get(name) ?? 0;
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
- reports.push({ name: r.name, content: renderLcovCounts(subtractLcovCounts(now, prev)) });
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
  }