@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 +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/lib/cancellation-watcher.d.ts +5 -0
- package/dist/lib/cancellation-watcher.d.ts.map +1 -0
- package/dist/lib/cancellation-watcher.js +49 -0
- package/dist/lib/cmd.d.ts +3 -0
- package/dist/lib/cmd.d.ts.map +1 -1
- package/dist/lib/cmd.js +41 -3
- package/dist/lib/merge-reports.d.ts +1 -1
- package/dist/lib/merge-reports.d.ts.map +1 -1
- package/dist/lib/merge-reports.js +42 -25
- package/package.json +2 -4
- package/tsconfig.tsbuildinfo +1 -1
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;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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 @@
|
|
|
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;
|
package/dist/lib/cmd.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cmd.d.ts","sourceRoot":"","sources":["../../src/lib/cmd.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
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
|
-
//
|
|
48
|
-
detached:
|
|
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":"
|
|
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
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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.
|
|
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.
|
|
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",
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -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"}
|