

























@@ -8,6 +8,7 @@ const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."
88const DEFAULT_E2E_IMAGE = "openclaw-docker-e2e:local";
99const DEFAULT_PARALLELISM = 4;
1010const DEFAULT_FAILURE_TAIL_LINES = 80;
11+const DEFAULT_LANE_TIMEOUT_MS = 120 * 60 * 1000;
11121213const lanes = [
1314["live-models", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-models"],
@@ -62,6 +63,13 @@ function parsePositiveInt(raw, fallback, label) {
6263return parsed;
6364}
646566+function parseBool(raw, fallback) {
67+if (raw === undefined || raw === "") {
68+return fallback;
69+}
70+return !/^(?:0|false|no)$/i.test(raw);
71+}
72+6573function utcStampForPath() {
6674return new Date().toISOString().replaceAll("-", "").replaceAll(":", "").replace(/\..*$/, "Z");
6775}
@@ -86,14 +94,29 @@ function commandEnv(extra = {}) {
8694};
8795}
889689-function runShellCommand({ command, env, label, logFile }) {
97+function runShellCommand({ command, env, label, logFile, timeoutMs }) {
9098return new Promise((resolve) => {
9199const child = spawn("bash", ["-lc", command], {
92100cwd: ROOT_DIR,
93101 env,
94102stdio: logFile ? ["ignore", "pipe", "pipe"] : "inherit",
95103});
96104activeChildren.add(child);
105+let timedOut = false;
106+let killTimer;
107+const timeoutTimer =
108+timeoutMs > 0
109+ ? setTimeout(() => {
110+timedOut = true;
111+if (stream) {
112+stream.write(`\n==> [${label}] timeout after ${timeoutMs}ms; sending SIGTERM\n`);
113+}
114+child.kill("SIGTERM");
115+killTimer = setTimeout(() => child.kill("SIGKILL"), 10_000);
116+killTimer.unref?.();
117+}, timeoutMs)
118+ : undefined;
119+timeoutTimer?.unref?.();
9712098121let stream;
99122if (logFile) {
@@ -105,13 +128,19 @@ function runShellCommand({ command, env, label, logFile }) {
105128}
106129107130child.on("close", (status, signal) => {
131+if (timeoutTimer) {
132+clearTimeout(timeoutTimer);
133+}
134+if (killTimer) {
135+clearTimeout(killTimer);
136+}
108137activeChildren.delete(child);
109138const exitCode = typeof status === "number" ? status : signal ? 128 : 1;
110139if (stream) {
111140stream.write(`\n==> [${label}] finished: ${utcStamp()} status=${exitCode}\n`);
112141stream.end();
113142}
114-resolve({ status: exitCode, signal });
143+resolve({ signal, status: exitCode, timedOut });
115144});
116145});
117146}
@@ -137,7 +166,7 @@ function laneEnv(name, baseEnv, logDir) {
137166return env;
138167}
139168140-async function runLane(lane, baseEnv, logDir) {
169+async function runLane(lane, baseEnv, logDir, timeoutMs) {
141170const [name, command] = lane;
142171const logFile = path.join(logDir, `${name}.log`);
143172const env = laneEnv(name, baseEnv, logDir);
@@ -153,31 +182,41 @@ async function runLane(lane, baseEnv, logDir) {
153182);
154183console.log(`==> [${name}] start`);
155184const startedAt = Date.now();
156-const result = await runShellCommand({ command, env, label: name, logFile });
185+const result = await runShellCommand({ command, env, label: name, logFile, timeoutMs });
157186const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000);
158187if (result.status === 0) {
159188console.log(`==> [${name}] pass ${elapsedSeconds}s`);
160189} else {
161-console.error(`==> [${name}] fail status=${result.status} ${elapsedSeconds}s log=${logFile}`);
190+const timeoutLabel = result.timedOut ? " timeout" : "";
191+console.error(
192+`==> [${name}] fail${timeoutLabel} status=${result.status} ${elapsedSeconds}s log=${logFile}`,
193+);
162194}
163195return {
164196 command,
165197 logFile,
166198 name,
167199status: result.status,
200+timedOut: result.timedOut,
168201};
169202}
170203171-async function runLanePool(poolLanes, baseEnv, logDir, parallelism) {
204+async function runLanePool(poolLanes, baseEnv, logDir, parallelism, options) {
172205const failures = [];
173206let nextIndex = 0;
174207175208async function worker() {
176209while (nextIndex < poolLanes.length) {
210+if (options.failFast && failures.length > 0) {
211+return;
212+}
177213const lane = poolLanes[nextIndex++];
178-const result = await runLane(lane, baseEnv, logDir);
214+const result = await runLane(lane, baseEnv, logDir, options.timeoutMs);
179215if (result.status !== 0) {
180216failures.push(result);
217+if (options.failFast) {
218+return;
219+}
181220}
182221}
183222}
@@ -187,12 +226,15 @@ async function runLanePool(poolLanes, baseEnv, logDir, parallelism) {
187226return failures;
188227}
189228190-async function runLanesSerial(serialLanes, baseEnv, logDir) {
229+async function runLanesSerial(serialLanes, baseEnv, logDir, options) {
191230const failures = [];
192231for (const lane of serialLanes) {
193-const result = await runLane(lane, baseEnv, logDir);
232+const result = await runLane(lane, baseEnv, logDir, options.timeoutMs);
194233if (result.status !== 0) {
195234failures.push(result);
235+if (options.failFast) {
236+break;
237+}
196238}
197239}
198240return failures;
@@ -242,6 +284,12 @@ async function main() {
242284DEFAULT_FAILURE_TAIL_LINES,
243285"OPENCLAW_DOCKER_ALL_FAILURE_TAIL_LINES",
244286);
287+const laneTimeoutMs = parsePositiveInt(
288+process.env.OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS,
289+DEFAULT_LANE_TIMEOUT_MS,
290+"OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS",
291+);
292+const failFast = parseBool(process.env.OPENCLAW_DOCKER_ALL_FAIL_FAST, true);
245293const runId = process.env.OPENCLAW_DOCKER_ALL_RUN_ID || utcStampForPath();
246294const logDir = path.resolve(
247295process.env.OPENCLAW_DOCKER_ALL_LOG_DIR ||
@@ -258,6 +306,8 @@ async function main() {
258306259307console.log(`==> Docker test logs: ${logDir}`);
260308console.log(`==> Parallelism: ${parallelism}`);
309+console.log(`==> Lane timeout: ${laneTimeoutMs}ms`);
310+console.log(`==> Fail fast: ${failFast ? "yes" : "no"}`);
261311console.log(`==> Live-test bundled plugin deps: ${baseEnv.OPENCLAW_DOCKER_BUILD_EXTENSIONS}`);
262312263313await runForeground("Build shared live-test image once", "pnpm test:docker:live-build", baseEnv);
@@ -267,14 +317,15 @@ async function main() {
267317baseEnv,
268318);
269319270-const failures = await runLanePool(lanes, baseEnv, logDir, parallelism);
320+const options = { failFast, timeoutMs: laneTimeoutMs };
321+const failures = await runLanePool(lanes, baseEnv, logDir, parallelism, options);
271322if (failures.length > 0) {
272323await printFailureSummary(failures, tailLines);
273324process.exit(1);
274325}
275326276327console.log("==> Running provider-sensitive Docker lanes exclusively");
277-failures.push(...(await runLanesSerial(exclusiveLanes, baseEnv, logDir)));
328+failures.push(...(await runLanesSerial(exclusiveLanes, baseEnv, logDir, options)));
278329if (failures.length > 0) {
279330await printFailureSummary(failures, tailLines);
280331process.exit(1);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。