@empiricalrun/test-run 0.12.0 → 0.13.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @empiricalrun/test-run
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d270c6d: feat: add cancellation watcher to self destruct
8
+ - 2d9919d: feat: consolidate zip utils and move to streaming
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies [2d9919d]
13
+ - @empiricalrun/r2-uploader@0.7.0
14
+
3
15
  ## 0.12.0
4
16
 
5
17
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ import { Platform, TestCase } from "./types";
6
6
  import { getProjectsFromPlaywrightConfig } from "./utils/config";
7
7
  export { getProjectsFromPlaywrightConfig, parseTestListOutput, Platform, runSpecificTestsCmd, spawnCmd, };
8
8
  export * from "./glob-matcher";
9
+ export { type CancellationWatcher, startCancellationWatcher, } from "./lib/cancellation-watcher";
9
10
  export { filterArrayByGlobMatchersSet, generateProjectFilters } from "./utils";
10
11
  export declare function runSingleTest({ testName, suites, filePath, projects, envOverrides, repoDir, stdout, stderr, }: {
11
12
  testName: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,IAAI,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAI/E,OAAO,EAAkB,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE7C,OAAO,EAAE,+BAA+B,EAAE,MAAM,gBAAgB,CAAC;AAMjE,OAAO,EACL,+BAA+B,EAC/B,mBAAmB,EACnB,QAAQ,EACR,mBAAmB,EACnB,QAAQ,GACT,CAAC;AACF,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,4BAA4B,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAc/E,wBAAsB,aAAa,CAAC,EAClC,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,OAAO,EACP,MAAM,EACN,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAChC,GAAG,OAAO,CAAC;IACV,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,oBAAoB,CAAC;CACnC,CAAC,CAoBD;AAED,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IACnE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;CACjD,CAAC,CAeD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,IAAI,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAI/E,OAAO,EAAkB,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE7C,OAAO,EAAE,+BAA+B,EAAE,MAAM,gBAAgB,CAAC;AAMjE,OAAO,EACL,+BAA+B,EAC/B,mBAAmB,EACnB,QAAQ,EACR,mBAAmB,EACnB,QAAQ,GACT,CAAC;AACF,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EACL,KAAK,mBAAmB,EACxB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,4BAA4B,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAc/E,wBAAsB,aAAa,CAAC,EAClC,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,OAAO,EACP,MAAM,EACN,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAChC,GAAG,OAAO,CAAC;IACV,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,oBAAoB,CAAC;CACnC,CAAC,CAoBD;AAED,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IACnE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;CACjD,CAAC,CAeD"}
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.generateProjectFilters = exports.filterArrayByGlobMatchersSet = exports.spawnCmd = exports.runSpecificTestsCmd = exports.Platform = exports.parseTestListOutput = exports.getProjectsFromPlaywrightConfig = void 0;
20
+ exports.generateProjectFilters = exports.filterArrayByGlobMatchersSet = exports.startCancellationWatcher = exports.spawnCmd = exports.runSpecificTestsCmd = exports.Platform = exports.parseTestListOutput = exports.getProjectsFromPlaywrightConfig = void 0;
21
21
  exports.runSingleTest = runSingleTest;
22
22
  exports.listProjectsAndTests = listProjectsAndTests;
23
23
  const fs_1 = __importDefault(require("fs"));
@@ -37,6 +37,8 @@ Object.defineProperty(exports, "getProjectsFromPlaywrightConfig", { enumerable:
37
37
  // The bin entrypoint has support for mobile also
38
38
  const supportedPlatform = types_1.Platform.WEB;
39
39
  __exportStar(require("./glob-matcher"), exports);
40
+ var cancellation_watcher_1 = require("./lib/cancellation-watcher");
41
+ Object.defineProperty(exports, "startCancellationWatcher", { enumerable: true, get: function () { return cancellation_watcher_1.startCancellationWatcher; } });
40
42
  var utils_2 = require("./utils");
41
43
  Object.defineProperty(exports, "filterArrayByGlobMatchersSet", { enumerable: true, get: function () { return utils_2.filterArrayByGlobMatchersSet; } });
42
44
  Object.defineProperty(exports, "generateProjectFilters", { enumerable: true, get: function () { return utils_2.generateProjectFilters; } });
@@ -0,0 +1,5 @@
1
+ export type CancellationWatcher = {
2
+ stop: () => void;
3
+ };
4
+ export declare function startCancellationWatcher(testRunId: string, apiKey: string, onCancel: () => void, pollIntervalMs?: number): CancellationWatcher;
5
+ //# sourceMappingURL=cancellation-watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cancellation-watcher.d.ts","sourceRoot":"","sources":["../../src/lib/cancellation-watcher.ts"],"names":[],"mappings":"AAqCA,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,IAAI,EACpB,cAAc,SAA2B,GACxC,mBAAmB,CAgCrB"}
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startCancellationWatcher = startCancellationWatcher;
4
+ const DOMAIN = process.env.DASHBOARD_DOMAIN || "https://dash.empirical.run";
5
+ const DEFAULT_POLL_INTERVAL_MS = 5000;
6
+ async function checkTestRunStatus(testRunId, apiKey) {
7
+ const url = `${DOMAIN}/api/test-runs/${testRunId}/status`;
8
+ try {
9
+ const response = await fetch(url, {
10
+ headers: { Authorization: `Bearer ${apiKey}` },
11
+ });
12
+ if (!response.ok) {
13
+ console.log(`[CancellationWatcher] Failed to check status: HTTP ${response.status} from ${url}`);
14
+ return { isTerminal: false, status: null };
15
+ }
16
+ const result = (await response.json());
17
+ const status = result.data || { isTerminal: false, status: null };
18
+ return status;
19
+ }
20
+ catch (error) {
21
+ const message = error instanceof Error ? error.message : String(error);
22
+ console.log(`[CancellationWatcher] Failed to check status: ${message}`);
23
+ return { isTerminal: false, status: null };
24
+ }
25
+ }
26
+ function startCancellationWatcher(testRunId, apiKey, onCancel, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS) {
27
+ let stopped = false;
28
+ console.log(`[CancellationWatcher] Starting watcher for test run ${testRunId} (polling every ${pollIntervalMs}ms)`);
29
+ console.log(`[CancellationWatcher] Dashboard domain: ${DOMAIN}`);
30
+ const poll = async () => {
31
+ while (!stopped) {
32
+ const { status } = await checkTestRunStatus(testRunId, apiKey);
33
+ if (status === "cancelling" || status === "cancelled") {
34
+ console.log(`[CancellationWatcher] Test run ${testRunId} is ${status}, triggering cancellation`);
35
+ onCancel();
36
+ stopped = true;
37
+ return;
38
+ }
39
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
40
+ }
41
+ };
42
+ poll();
43
+ return {
44
+ stop: () => {
45
+ console.log(`[CancellationWatcher] Stopping watcher for ${testRunId}`);
46
+ stopped = true;
47
+ },
48
+ };
49
+ }
package/dist/lib/cmd.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type ChildProcess } from "child_process";
1
2
  import { CommandToRun } from "../types";
2
3
  export declare function getCommandFromString(command: string): {
3
4
  command: string;
@@ -8,6 +9,7 @@ export declare function runTestsForCmd({ command, args, env }: CommandToRun, cwd
8
9
  stderr?: NodeJS.WritableStream;
9
10
  }): Promise<{
10
11
  hasTestPassed: boolean;
12
+ wasCancelled: boolean;
11
13
  }>;
12
14
  export declare function spawnCmd(command: string, args: string[], options: {
13
15
  cwd: string;
@@ -16,6 +18,7 @@ export declare function spawnCmd(command: string, args: string[], options: {
16
18
  throwOnError: boolean;
17
19
  stdout?: NodeJS.WritableStream;
18
20
  stderr?: NodeJS.WritableStream;
21
+ onSpawn?: (proc: ChildProcess) => void;
19
22
  }): Promise<{
20
23
  code: number;
21
24
  output?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"cmd.d.ts","sourceRoot":"","sources":["../../src/lib/cmd.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAeA;AAED,wBAAsB,cAAc,CAClC,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,YAAY,EACpC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IACR,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAChC;;GAiBF;AAED,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IACP,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAChC,GACA,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA+C5C"}
1
+ {"version":3,"file":"cmd.d.ts","sourceRoot":"","sources":["../../src/lib/cmd.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAS,MAAM,eAAe,CAAC;AAGzD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAMxC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAeA;AAED,wBAAsB,cAAc,CAClC,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,YAAY,EACpC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IACR,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAChC;;;GAqEF;AAED,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IACP,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;CACxC,GACA,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiD5C"}
package/dist/lib/cmd.js CHANGED
@@ -5,6 +5,7 @@ exports.runTestsForCmd = runTestsForCmd;
5
5
  exports.spawnCmd = spawnCmd;
6
6
  const child_process_1 = require("child_process");
7
7
  const logger_1 = require("../logger");
8
+ const cancellation_watcher_1 = require("./cancellation-watcher");
8
9
  function getCommandFromString(command) {
9
10
  const regex = /[^\s"']+|"([^"]*)"|'([^']*)'/g;
10
11
  const matches = command.match(regex) || [];
@@ -21,7 +22,37 @@ function getCommandFromString(command) {
21
22
  }
22
23
  async function runTestsForCmd({ command, args, env }, cwd, options) {
23
24
  logger_1.logger.debug(`Running cmd: ${command} with args: ${args}`);
25
+ const testRunId = process.env.TEST_RUN_GITHUB_ACTION_ID;
26
+ const apiKey = process.env.EMPIRICALRUN_API_KEY;
27
+ console.log(`[CancellationWatcher] Environment: TEST_RUN_GITHUB_ACTION_ID=${testRunId}, EMPIRICALRUN_API_KEY=${apiKey ? "***" : "undefined"}`);
24
28
  let hasTestPassed = true;
29
+ let wasCancelled = false;
30
+ let cancellationWatcher;
31
+ let childProcess;
32
+ const cancelHandler = () => {
33
+ wasCancelled = true;
34
+ console.log(`[CancellationWatcher] Cancel handler invoked, killing child process (pid: ${childProcess?.pid})`);
35
+ if (childProcess && !childProcess.killed) {
36
+ // Use SIGTERM for more forceful termination that propagates to child processes
37
+ childProcess.kill("SIGTERM");
38
+ // Also try to kill the process group to ensure Playwright workers are stopped
39
+ if (childProcess.pid) {
40
+ try {
41
+ process.kill(-childProcess.pid, "SIGTERM");
42
+ console.log(`[CancellationWatcher] Sent SIGTERM to process group ${childProcess.pid}`);
43
+ }
44
+ catch {
45
+ // Process group kill may fail if not a group leader
46
+ }
47
+ }
48
+ }
49
+ };
50
+ if (testRunId && apiKey) {
51
+ cancellationWatcher = (0, cancellation_watcher_1.startCancellationWatcher)(testRunId, apiKey, cancelHandler);
52
+ }
53
+ else {
54
+ console.log("[CancellationWatcher] Not starting watcher - missing testRunId or apiKey");
55
+ }
25
56
  try {
26
57
  await spawnCmd(command, args, {
27
58
  cwd,
@@ -30,12 +61,18 @@ async function runTestsForCmd({ command, args, env }, cwd, options) {
30
61
  throwOnError: true,
31
62
  stdout: options?.stdout,
32
63
  stderr: options?.stderr,
64
+ onSpawn: (proc) => {
65
+ childProcess = proc;
66
+ },
33
67
  });
34
68
  }
35
69
  catch {
36
70
  hasTestPassed = false;
37
71
  }
38
- return { hasTestPassed };
72
+ finally {
73
+ cancellationWatcher?.stop();
74
+ }
75
+ return { hasTestPassed, wasCancelled };
39
76
  }
40
77
  async function spawnCmd(command, args, options) {
41
78
  let output = options.captureOutput ? "" : undefined;
@@ -44,9 +81,10 @@ async function spawnCmd(command, args, options) {
44
81
  const p = (0, child_process_1.spawn)(command, args, {
45
82
  env: { ...process.env, ...options.envOverrides },
46
83
  cwd: options.cwd,
47
- // Ensure child process receives signals
48
- detached: false,
84
+ // Create new process group so we can kill all child processes together
85
+ detached: true,
49
86
  });
87
+ options.onSpawn?.(p);
50
88
  // Setup signal handlers and get cleanup function
51
89
  const cleanupSignalHandlers = setupProcessSignalHandlers(p);
52
90
  p.stdout.on("data", (x) => {
@@ -12,7 +12,7 @@ interface UploadOptions {
12
12
  export declare function runPlaywrightMergeReports(options: MergeReportsOptions): Promise<{
13
13
  success: boolean;
14
14
  }>;
15
- export declare function extractUrlMappingsFromBlobs(blobDir: string): Record<string, string>;
15
+ export declare function extractUrlMappingsFromBlobs(blobDir: string): Promise<Record<string, string>>;
16
16
  export declare function patchMergedHtmlReport(htmlFilePath: string, urlMappings: Record<string, string>): Promise<void>;
17
17
  export declare function patchSummaryJson(jsonFilePath: string, urlMappings: Record<string, string>): Promise<void>;
18
18
  export declare function uploadMergedReports(cwd: string, outputDir: string, uploadOptions: UploadOptions): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"merge-reports.d.ts","sourceRoot":"","sources":["../../src/lib/merge-reports.ts"],"names":[],"mappings":"AAYA,UAAU,mBAAmB;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,UAAU,aAAa;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA2B/B;AAED,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA4BxB;AAED,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CAqFf;AAED,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CAuBf;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkDf;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAuDhC"}
1
+ {"version":3,"file":"merge-reports.d.ts","sourceRoot":"","sources":["../../src/lib/merge-reports.ts"],"names":[],"mappings":"AAgBA,UAAU,mBAAmB;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,UAAU,aAAa;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AA+BD,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA2B/B;AAED,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA2BjC;AAED,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CA8Ff;AAED,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkDf;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAuDhC"}
@@ -10,11 +10,28 @@ exports.patchSummaryJson = patchSummaryJson;
10
10
  exports.uploadMergedReports = uploadMergedReports;
11
11
  exports.mergeReports = mergeReports;
12
12
  const r2_uploader_1 = require("@empiricalrun/r2-uploader");
13
- const adm_zip_1 = __importDefault(require("adm-zip"));
13
+ const zip_1 = require("@empiricalrun/r2-uploader/zip");
14
14
  const fs_1 = __importDefault(require("fs"));
15
15
  const path_1 = __importDefault(require("path"));
16
16
  const logger_1 = require("../logger");
17
17
  const cmd_1 = require("./cmd");
18
+ function buildMappingPatterns(urlMappings) {
19
+ return Object.entries(urlMappings).map(([resourcePath, url]) => {
20
+ const resourceFileName = resourcePath.replace(/^resources\//, "");
21
+ const escaped = resourceFileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22
+ const regex = new RegExp(`[^"]*${escaped}`, "g");
23
+ return { regex, resourceFileName, url };
24
+ });
25
+ }
26
+ function applyMappingPatterns(content, patterns) {
27
+ let modified = content;
28
+ for (const { regex, resourceFileName, url } of patterns) {
29
+ if (!modified.includes(resourceFileName))
30
+ continue;
31
+ modified = modified.replace(regex, url);
32
+ }
33
+ return modified;
34
+ }
18
35
  async function runPlaywrightMergeReports(options) {
19
36
  const { blobDir, outputDir, cwd } = options;
20
37
  logger_1.logger.debug(`[Merge Reports] Running playwright merge-reports`);
@@ -38,16 +55,15 @@ async function runPlaywrightMergeReports(options) {
38
55
  return { success: false };
39
56
  }
40
57
  }
41
- function extractUrlMappingsFromBlobs(blobDir) {
58
+ async function extractUrlMappingsFromBlobs(blobDir) {
42
59
  const combinedMap = {};
43
60
  const files = fs_1.default.readdirSync(blobDir);
44
61
  for (const fileName of files.filter((f) => f.endsWith(".zip"))) {
45
62
  const zipPath = path_1.default.join(blobDir, fileName);
46
63
  try {
47
- const zip = new adm_zip_1.default(zipPath);
48
- const urlsEntry = zip.getEntry("_empirical_urls.json");
49
- if (urlsEntry) {
50
- const content = JSON.parse(urlsEntry.getData().toString("utf8"));
64
+ const buffer = await (0, zip_1.readZipEntry)(zipPath, "_empirical_urls.json");
65
+ if (buffer) {
66
+ const content = JSON.parse(buffer.toString("utf8"));
51
67
  Object.assign(combinedMap, content);
52
68
  logger_1.logger.debug(`[Merge Reports] Extracted ${Object.keys(content).length} URL mappings from ${fileName}`);
53
69
  }
@@ -65,8 +81,11 @@ async function patchMergedHtmlReport(htmlFilePath, urlMappings) {
65
81
  return;
66
82
  }
67
83
  let htmlContent;
84
+ const startTime = Date.now();
85
+ logger_1.logger.info(`[Merge Reports] Starting HTML patch...`);
68
86
  try {
69
87
  htmlContent = await fs_1.default.promises.readFile(htmlFilePath, "utf8");
88
+ logger_1.logger.info(`[Merge Reports] HTML file read: ${(htmlContent.length / 1024 / 1024).toFixed(2)} MB in ${Date.now() - startTime}ms`);
70
89
  }
71
90
  catch (error) {
72
91
  logger_1.logger.error(`[Merge Reports] Failed to read HTML file:`, error);
@@ -83,28 +102,29 @@ async function patchMergedHtmlReport(htmlFilePath, urlMappings) {
83
102
  const tempDir = fs_1.default.mkdtempSync(path_1.default.join(htmlDir, "merge-patch-"));
84
103
  const zipPath = path_1.default.join(tempDir, "archive.zip");
85
104
  try {
105
+ let stepTime = Date.now();
86
106
  await fs_1.default.promises.writeFile(zipPath, Buffer.from(base64, "base64"));
87
- const zip = new adm_zip_1.default(zipPath);
88
- zip.extractAllTo(tempDir, true);
107
+ await (0, zip_1.extractZipToDirectory)(zipPath, tempDir);
89
108
  await fs_1.default.promises.unlink(zipPath);
109
+ logger_1.logger.info(`[Merge Reports] Zip extracted in ${Date.now() - stepTime}ms`);
90
110
  const jsonFiles = (await fs_1.default.promises.readdir(tempDir)).filter((f) => f.endsWith(".json"));
111
+ logger_1.logger.info(`[Merge Reports] Patching ${jsonFiles.length} JSON files with ${Object.keys(urlMappings).length} mappings`);
112
+ const mappingPatterns = buildMappingPatterns(urlMappings);
113
+ stepTime = Date.now();
91
114
  for (const file of jsonFiles) {
92
115
  const filePath = path_1.default.join(tempDir, file);
93
116
  const content = await fs_1.default.promises.readFile(filePath, "utf8");
94
- let modified = content;
95
- for (const [resourcePath, url] of Object.entries(urlMappings)) {
96
- const resourceFileName = resourcePath.replace(/^resources\//, "");
97
- const regex = new RegExp(`[^"]*${resourceFileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g");
98
- modified = modified.replace(regex, url);
99
- }
117
+ const modified = applyMappingPatterns(content, mappingPatterns);
100
118
  if (modified !== content) {
101
119
  await fs_1.default.promises.writeFile(filePath, modified, "utf8");
102
120
  logger_1.logger.debug(`[Merge Reports] Patched ${file}`);
103
121
  }
104
122
  }
105
- const newZip = new adm_zip_1.default();
106
- newZip.addLocalFolder(tempDir);
107
- const newBase64 = newZip.toBuffer().toString("base64");
123
+ logger_1.logger.info(`[Merge Reports] JSON patching completed in ${Date.now() - stepTime}ms`);
124
+ stepTime = Date.now();
125
+ const newBuffer = await (0, zip_1.createZipFromDirectory)(tempDir);
126
+ const newBase64 = newBuffer.toString("base64");
127
+ logger_1.logger.info(`[Merge Reports] New zip created in ${Date.now() - stepTime}ms`);
108
128
  let updatedHtml;
109
129
  if (oldFormatMatch) {
110
130
  updatedHtml = htmlContent.replace(/(window\.playwrightReportBase64\s*=\s*")(?:data:application\/zip;base64,)?[^"]*(")/, `$1data:application/zip;base64,${newBase64}$2`);
@@ -128,13 +148,10 @@ async function patchSummaryJson(jsonFilePath, urlMappings) {
128
148
  return;
129
149
  }
130
150
  try {
131
- let content = await fs_1.default.promises.readFile(jsonFilePath, "utf8");
132
- for (const [resourcePath, url] of Object.entries(urlMappings)) {
133
- const resourceFileName = resourcePath.replace(/^resources\//, "");
134
- const regex = new RegExp(`[^"]*${resourceFileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g");
135
- content = content.replace(regex, url);
136
- }
137
- await fs_1.default.promises.writeFile(jsonFilePath, content, "utf8");
151
+ const content = await fs_1.default.promises.readFile(jsonFilePath, "utf8");
152
+ const mappingPatterns = buildMappingPatterns(urlMappings);
153
+ const modified = applyMappingPatterns(content, mappingPatterns);
154
+ await fs_1.default.promises.writeFile(jsonFilePath, modified, "utf8");
138
155
  logger_1.logger.info(`[Merge Reports] summary.json patched successfully`);
139
156
  }
140
157
  catch (error) {
@@ -200,7 +217,7 @@ async function mergeReports(options) {
200
217
  logger_1.logger.error(`[Merge Reports] Blob directory does not exist: ${blobDir}`);
201
218
  return { success: false };
202
219
  }
203
- const urlMappings = extractUrlMappingsFromBlobs(blobDir);
220
+ const urlMappings = await extractUrlMappingsFromBlobs(blobDir);
204
221
  const { success } = await runPlaywrightMergeReports({
205
222
  blobDir,
206
223
  outputDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/test-run",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -26,18 +26,16 @@
26
26
  },
27
27
  "author": "Empirical Team <hey@empirical.run>",
28
28
  "dependencies": {
29
- "adm-zip": "^0.5.16",
30
29
  "async-retry": "^1.3.3",
31
30
  "commander": "^12.1.0",
32
31
  "console-log-level": "^1.4.1",
33
32
  "dotenv": "^16.4.5",
34
33
  "minimatch": "^10.0.1",
35
34
  "ts-morph": "^23.0.0",
36
- "@empiricalrun/r2-uploader": "^0.6.0"
35
+ "@empiricalrun/r2-uploader": "^0.7.0"
37
36
  },
38
37
  "devDependencies": {
39
38
  "@playwright/test": "1.53.2",
40
- "@types/adm-zip": "^0.5.7",
41
39
  "@types/async-retry": "^1.4.8",
42
40
  "@types/console-log-level": "^1.4.5",
43
41
  "@types/node": "^22.5.5",
@@ -1 +1 @@
1
- {"root":["./src/dashboard.ts","./src/glob-matcher.ts","./src/index.ts","./src/logger.ts","./src/bin/index.ts","./src/bin/merge-reports.ts","./src/lib/cmd.ts","./src/lib/merge-reports.ts","./src/lib/run-all-tests.ts","./src/lib/run-specific-test.ts","./src/lib/memfs/read-hello-world.ts","./src/stdout-parser/index.ts","./src/types/index.ts","./src/utils/config-parser.ts","./src/utils/config.ts","./src/utils/index.ts"],"version":"5.8.3"}
1
+ {"root":["./src/dashboard.ts","./src/glob-matcher.ts","./src/index.ts","./src/logger.ts","./src/bin/index.ts","./src/bin/merge-reports.ts","./src/lib/cancellation-watcher.ts","./src/lib/cmd.ts","./src/lib/merge-reports.ts","./src/lib/run-all-tests.ts","./src/lib/run-specific-test.ts","./src/lib/memfs/read-hello-world.ts","./src/stdout-parser/index.ts","./src/types/index.ts","./src/utils/config-parser.ts","./src/utils/config.ts","./src/utils/index.ts"],"version":"5.8.3"}