@specific.dev/spectest 0.60.0 → 0.60.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.
@@ -92,13 +92,22 @@ export interface LcovRecordCounts {
92
92
  * path are merged (a tool may split a file across records). */
93
93
  export declare function parseLcovCounts(text: string): Map<string, LcovRecordCounts>;
94
94
  /**
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.
95
+ * `now` minus `prev`, per line and per function. A file not in `prev`
96
+ * is new: everything in it counts. A **record in which any count went
97
+ * down** is a process that restarted (a restart resets every counter of
98
+ * that process at once): the whole record stands as-is, never
99
+ * subtracted — per line would report a line that ran exactly as often
100
+ * again as "not executed", and the restarted boot set (1 − 1) would
101
+ * vanish. Per record, not per report: a combined coverage.py file can
102
+ * hold several processes. A restart in which no counter dropped is not
103
+ * detectable from counts and is accepted as the residual (a service
104
+ * restarting mid-test is an anomaly already). Lines with a zero delta
105
+ * stay in the record: the server distinguishes "ran 0 times" from "not
106
+ * a counted line". Only records with at least one execution are kept —
107
+ * a file with nothing new in it did not run. `restarted` receives the
108
+ * paths of the records taken as-is.
100
109
  */
101
- export declare function subtractLcovCounts(now: Map<string, LcovRecordCounts>, prev: Map<string, LcovRecordCounts>): Map<string, LcovRecordCounts>;
110
+ export declare function subtractLcovCounts(now: Map<string, LcovRecordCounts>, prev: Map<string, LcovRecordCounts>, restarted?: string[]): Map<string, LcovRecordCounts>;
102
111
  /** Counts rendered back to lcov, records in path order. */
103
112
  export declare function renderLcovCounts(counts: Map<string, LcovRecordCounts>): string;
104
113
  /** 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.1",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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);
@@ -277,31 +277,44 @@ export function parseLcovCounts(text: string): Map<string, LcovRecordCounts> {
277
277
  }
278
278
 
279
279
  /**
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.
280
+ * `now` minus `prev`, per line and per function. A file not in `prev`
281
+ * is new: everything in it counts. A **record in which any count went
282
+ * down** is a process that restarted (a restart resets every counter of
283
+ * that process at once): the whole record stands as-is, never
284
+ * subtracted — per line would report a line that ran exactly as often
285
+ * again as "not executed", and the restarted boot set (1 − 1) would
286
+ * vanish. Per record, not per report: a combined coverage.py file can
287
+ * hold several processes. A restart in which no counter dropped is not
288
+ * detectable from counts and is accepted as the residual (a service
289
+ * restarting mid-test is an anomaly already). Lines with a zero delta
290
+ * stay in the record: the server distinguishes "ran 0 times" from "not
291
+ * a counted line". Only records with at least one execution are kept —
292
+ * a file with nothing new in it did not run. `restarted` receives the
293
+ * paths of the records taken as-is.
285
294
  */
286
295
  export function subtractLcovCounts(
287
296
  now: Map<string, LcovRecordCounts>,
288
297
  prev: Map<string, LcovRecordCounts>,
298
+ restarted?: string[],
289
299
  ): Map<string, LcovRecordCounts> {
290
300
  const out = new Map<string, LcovRecordCounts>();
291
301
  for (const [file, rec] of now) {
292
302
  const before = prev.get(file);
303
+ const dropped =
304
+ !!before &&
305
+ ([...rec.lines].some(([l, h]) => h < (before.lines.get(l) ?? 0)) ||
306
+ [...rec.functions].some(([n, h]) => h < (before.functions.get(n) ?? 0)));
307
+ if (dropped) restarted?.push(file);
293
308
  const lines = new Map<number, number>();
294
309
  const functions = new Map<string, number>();
295
310
  let executed = false;
296
311
  for (const [l, h] of rec.lines) {
297
- const b = before?.lines.get(l) ?? 0;
298
- const d = h < b ? h : h - b;
312
+ const d = dropped ? h : h - (before?.lines.get(l) ?? 0);
299
313
  lines.set(l, d);
300
314
  if (d > 0) executed = true;
301
315
  }
302
316
  for (const [name, h] of rec.functions) {
303
- const b = before?.functions.get(name) ?? 0;
304
- const d = h < b ? h : h - b;
317
+ const d = dropped ? h : h - (before?.functions.get(name) ?? 0);
305
318
  functions.set(name, d);
306
319
  if (d > 0) executed = true;
307
320
  }
@@ -374,7 +387,13 @@ export function applyCoverageDelta(
374
387
  const now = parseLcovCounts(r.content);
375
388
  const prev = prevByName.get(r.name) ?? new Map<string, LcovRecordCounts>();
376
389
  prevByName.set(r.name, now);
377
- reports.push({ name: r.name, content: renderLcovCounts(subtractLcovCounts(now, prev)) });
390
+ const restarted: string[] = [];
391
+ reports.push({ name: r.name, content: renderLcovCounts(subtractLcovCounts(now, prev, restarted)) });
392
+ if (restarted.length > 0) {
393
+ console.warn(
394
+ `[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`,
395
+ );
396
+ }
378
397
  }
379
398
  return { ...capture, reports };
380
399
  }