mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-07 13:08:54 -07:00
fix(tests): guard empty arrays, cap output, kill orphaned children
- bash 3.2: guard BLOCKERS/PATH_ARGS empty-array expansion under set -u - run-coverage-lane: surface collectLaneFiles errors instead of throwing - run-test-lane: cap per-file output at 1 MB; track and SIGKILL children on SIGINT/SIGTERM
This commit is contained in:
+12
-2
@@ -255,8 +255,18 @@ write_summary_files() {
|
|||||||
local lane_lines
|
local lane_lines
|
||||||
lane_lines=$(printf '%s\n' "${SELECTED_LANES[@]}")
|
lane_lines=$(printf '%s\n' "${SELECTED_LANES[@]}")
|
||||||
printf '%s\n' "$lane_lines" >"$ARTIFACT_DIR/lanes.txt"
|
printf '%s\n' "$lane_lines" >"$ARTIFACT_DIR/lanes.txt"
|
||||||
printf '%s\n' "${BLOCKERS[@]}" >"$ARTIFACT_DIR/blockers.txt"
|
# bash 3.2 raises "unbound variable" under set -u when expanding an empty
|
||||||
printf '%s\n' "${PATH_ARGS[@]}" >"$ARTIFACT_DIR/requested-paths.txt"
|
# array, so guard on length (matching the idiom used elsewhere here).
|
||||||
|
if [[ ${#BLOCKERS[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${BLOCKERS[@]}" >"$ARTIFACT_DIR/blockers.txt"
|
||||||
|
else
|
||||||
|
: >"$ARTIFACT_DIR/blockers.txt"
|
||||||
|
fi
|
||||||
|
if [[ ${#PATH_ARGS[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${PATH_ARGS[@]}" >"$ARTIFACT_DIR/requested-paths.txt"
|
||||||
|
else
|
||||||
|
: >"$ARTIFACT_DIR/requested-paths.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
ARTIFACT_DIR_ENV="$ARTIFACT_DIR" \
|
ARTIFACT_DIR_ENV="$ARTIFACT_DIR" \
|
||||||
SESSION_ID_ENV="$SESSION_ID" \
|
SESSION_ID_ENV="$SESSION_ID" \
|
||||||
|
|||||||
@@ -214,7 +214,13 @@ function runCoverageLane(): number {
|
|||||||
rmSync(shardRoot, { recursive: true, force: true });
|
rmSync(shardRoot, { recursive: true, force: true });
|
||||||
mkdirSync(shardRoot, { recursive: true });
|
mkdirSync(shardRoot, { recursive: true });
|
||||||
|
|
||||||
const files = collectLaneFiles(repoRoot, laneName);
|
let files: string[];
|
||||||
|
try {
|
||||||
|
files = collectLaneFiles(repoRoot, laneName);
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
const reports: string[] = [];
|
const reports: string[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,6 +11,28 @@ import { collectLaneFiles } from './test-lanes.ts';
|
|||||||
|
|
||||||
const repoRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
const repoRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
||||||
|
|
||||||
|
// Cap per-file buffered output so a long or noisy test cannot grow the string
|
||||||
|
// without bound and exhaust memory.
|
||||||
|
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
||||||
|
|
||||||
|
// Track spawned `bun test` children so we can kill them if the runner is
|
||||||
|
// interrupted, avoiding orphaned in-flight test processes.
|
||||||
|
const activeChildren = new Set();
|
||||||
|
|
||||||
|
function terminateChildren() {
|
||||||
|
for (const child of activeChildren) {
|
||||||
|
child.kill('SIGKILL');
|
||||||
|
}
|
||||||
|
activeChildren.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
||||||
|
process.on(signal, () => {
|
||||||
|
terminateChildren();
|
||||||
|
process.exit(130);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
const options = { lane: undefined, jobs: 1, timeoutSecs: 300, singleProcess: false };
|
const options = { lane: undefined, jobs: 1, timeoutSecs: 300, singleProcess: false };
|
||||||
for (let index = 0; index < argv.length; index += 1) {
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
@@ -34,20 +56,32 @@ function parseArgs(argv) {
|
|||||||
function runFile(file, timeoutSecs) {
|
function runFile(file, timeoutSecs) {
|
||||||
return new Promise((resolvePromise) => {
|
return new Promise((resolvePromise) => {
|
||||||
const child = spawn('bun', ['test', `./${file}`], { cwd: repoRoot });
|
const child = spawn('bun', ['test', `./${file}`], { cwd: repoRoot });
|
||||||
|
activeChildren.add(child);
|
||||||
let output = '';
|
let output = '';
|
||||||
|
let truncated = false;
|
||||||
let timedOut = false;
|
let timedOut = false;
|
||||||
child.stdout.on('data', (chunk) => (output += chunk));
|
const append = (chunk) => {
|
||||||
child.stderr.on('data', (chunk) => (output += chunk));
|
if (truncated) return;
|
||||||
|
output += chunk;
|
||||||
|
if (output.length > MAX_OUTPUT_BYTES) {
|
||||||
|
output = `${output.slice(0, MAX_OUTPUT_BYTES)}\n[output truncated at ${MAX_OUTPUT_BYTES} bytes]\n`;
|
||||||
|
truncated = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
child.stdout.on('data', append);
|
||||||
|
child.stderr.on('data', append);
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
timedOut = true;
|
timedOut = true;
|
||||||
child.kill('SIGKILL');
|
child.kill('SIGKILL');
|
||||||
}, timeoutSecs * 1000);
|
}, timeoutSecs * 1000);
|
||||||
child.on('close', (code) => {
|
child.on('close', (code) => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
|
activeChildren.delete(child);
|
||||||
resolvePromise({ file, code: timedOut ? 124 : (code ?? 1), output, timedOut });
|
resolvePromise({ file, code: timedOut ? 124 : (code ?? 1), output, timedOut });
|
||||||
});
|
});
|
||||||
child.on('error', (error) => {
|
child.on('error', (error) => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
|
activeChildren.delete(child);
|
||||||
resolvePromise({ file, code: 1, output: String(error), timedOut: false });
|
resolvePromise({ file, code: 1, output: String(error), timedOut: false });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user