@specific.dev/spectest 0.56.2 → 0.58.0

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/src/index.ts CHANGED
@@ -252,6 +252,27 @@ export interface ServiceConfig {
252
252
  tls?: readonly ServiceTls[];
253
253
  /** Bind-mounted volumes for state that survives snapshot/fork. */
254
254
  volumes?: readonly VolumeMount[];
255
+ /**
256
+ * Opt this service in to **code coverage collection** (experimental).
257
+ *
258
+ * The service gets a writable directory bind-mounted at
259
+ * `/spectest/coverage/`. The service's own coverage tool writes **lcov**
260
+ * reports there — cumulative from process start, never reset. After
261
+ * bring-up and after every test, spectest runs `command` (if given) inside
262
+ * the container, then reads every file in the directory and records the
263
+ * set of source files that ran. Only the `SF:` records are read.
264
+ *
265
+ * - `true`: the service writes its reports by itself (a dump on a timer,
266
+ * an exit-time dump for short-lived processes).
267
+ * - `{ command }`: run this (via `sh -c`) in the container to make the
268
+ * service write a report, before the directory is read.
269
+ *
270
+ * The directory must hold at least one lcov report each time it is read;
271
+ * an empty directory or a failed command fails the test with a clear
272
+ * error, so a hole in the coverage data is never silent. See
273
+ * `spectest docs /services/coverage`.
274
+ */
275
+ coverage?: ServiceCoverage;
255
276
  /**
256
277
  * Files seeded into the container's filesystem **before it starts**.
257
278
  * Each entry's `content` is written to a VM-host staging path and
@@ -1017,6 +1038,32 @@ export type ServiceImage =
1017
1038
  exclude?: readonly string[];
1018
1039
  };
1019
1040
 
1041
+ /**
1042
+ * A service's opt-in to coverage collection — see `ServiceConfig.coverage`.
1043
+ * `true` when the service writes its own reports; `{ command }` when
1044
+ * spectest must run a command in the container to produce one.
1045
+ */
1046
+ export type ServiceCoverage =
1047
+ | true
1048
+ | {
1049
+ /** Run this (via `sh -c`) in the container before the coverage
1050
+ * directory is read, so the service writes a fresh lcov report. */
1051
+ command?: string;
1052
+ /**
1053
+ * Collect **frontend** coverage for the app this service serves.
1054
+ * The code runs in the guest browser — spectest's own process — so
1055
+ * the user cannot write a report for it: spectest turns on V8
1056
+ * coverage for every script `ctx.browser()`/`ctx.mobile()` loads
1057
+ * from this service's origin, maps it back to original sources
1058
+ * through the served source maps, and adds one lcov report to this
1059
+ * service's capture. Scripts a source map can't resolve are
1060
+ * attributed to their served path (which the repo mapping then
1061
+ * treats as unknown — the safe answer). Needs the app to serve
1062
+ * source maps to the test environment.
1063
+ */
1064
+ browser?: boolean;
1065
+ };
1066
+
1020
1067
  export interface VolumeMount {
1021
1068
  /**
1022
1069
  * Named shared volume. Two services mounting the same `name` share one
@@ -1236,6 +1283,39 @@ export interface FakeContext {
1236
1283
  certificate(hostnames: readonly string[]): Promise<CertificateMaterial>;
1237
1284
  }
1238
1285
 
1286
+ /**
1287
+ * Check one service's `coverage` field. Exported so the daemon applies the
1288
+ * same rule to a runtime `startService` spec.
1289
+ */
1290
+ export function validateCoverage(service: string, cov: unknown): void {
1291
+ if (cov === undefined || cov === true) return;
1292
+ if (typeof cov === "object" && cov !== null) {
1293
+ const o = cov as { command?: unknown; browser?: unknown };
1294
+ const cmdOk =
1295
+ o.command === undefined ||
1296
+ (typeof o.command === "string" && o.command.trim().length > 0);
1297
+ const browserOk = o.browser === undefined || typeof o.browser === "boolean";
1298
+ if (cmdOk && browserOk && (o.command !== undefined || o.browser === true)) return;
1299
+ }
1300
+ throw new Error(
1301
+ `service "${service}" has an invalid \`coverage\` value — use \`true\`, \`{ command: "<shell command>" }\`, or \`{ browser: true }\``,
1302
+ );
1303
+ }
1304
+
1305
+ /** Whether a `coverage` value collects reports from the container's own
1306
+ * filesystem (`/spectest/coverage/`). True for `true`, `{ command }`, and
1307
+ * any object that isn't browser-only. */
1308
+ export function coverageUsesContainer(cov: ServiceCoverage | undefined): boolean {
1309
+ if (cov === undefined) return false;
1310
+ if (cov === true) return true;
1311
+ return cov.command !== undefined || cov.browser !== true;
1312
+ }
1313
+
1314
+ /** Whether a `coverage` value collects frontend (browser) coverage. */
1315
+ export function coverageUsesBrowser(cov: ServiceCoverage | undefined): boolean {
1316
+ return typeof cov === "object" && cov.browser === true;
1317
+ }
1318
+
1239
1319
  function validateEnvironmentConfig<S extends ServicesMap>(
1240
1320
  config: EnvironmentConfig<S>,
1241
1321
  ): void {
@@ -1281,6 +1361,7 @@ function validateEnvironmentConfig<S extends ServicesMap>(
1281
1361
  );
1282
1362
  }
1283
1363
  }
1364
+ validateCoverage(name, svc.coverage);
1284
1365
  for (const raw of svc.hostnames ?? []) {
1285
1366
  const h = raw.toLowerCase();
1286
1367
  if (!HOSTNAME_RE.test(hostPatternBody(h))) {