Add tests and kill process group on cleanup
This commit is contained in:
parent
56d982db17
commit
04abf17d60
82
src/run.test.ts
Normal file
82
src/run.test.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { readFile } from "node:fs/promises"
|
||||
|
||||
import { runFile, cleanupTmpDir } from "./run.ts"
|
||||
import type { ShoutFile } from "./parse.ts"
|
||||
|
||||
function makeFile(commands: { command: string; expected?: string[] }[]): ShoutFile {
|
||||
return {
|
||||
path: "test.shout",
|
||||
commands: commands.map((c, i) => ({
|
||||
line: i + 1,
|
||||
raw: `$ ${c.command}`,
|
||||
command: c.command,
|
||||
expected: c.expected ?? [],
|
||||
exitCode: null,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
const defaultOpts = {
|
||||
cleanEnv: false,
|
||||
timeout: 5000,
|
||||
verbose: false,
|
||||
}
|
||||
|
||||
async function isProcessRunning(pid: number): Promise<boolean> {
|
||||
try {
|
||||
const stat = await readFile(`/proc/${pid}/stat`, "utf-8")
|
||||
// State is the 3rd field: R=running, S=sleeping, Z=zombie, etc.
|
||||
const state = stat.split(" ")[2]
|
||||
return state !== "Z" && state !== "X"
|
||||
} catch {
|
||||
// /proc entry doesn't exist — process is fully gone
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
describe("runFile", () => {
|
||||
test("basic command", async () => {
|
||||
const file = makeFile([{ command: "echo hello" }])
|
||||
const result = await runFile(file, defaultOpts)
|
||||
expect(result.results[0]?.actual).toEqual(["hello"])
|
||||
expect(result.results[0]?.exitCode).toBe(0)
|
||||
await cleanupTmpDir(result.tmpDir)
|
||||
})
|
||||
|
||||
test("cleans up backgrounded processes after exit", async () => {
|
||||
const file = makeFile([
|
||||
{ command: "sleep 300 >/dev/null 2>&1 & echo $!" },
|
||||
])
|
||||
|
||||
const result = await runFile(file, defaultOpts)
|
||||
const pid = parseInt(result.results[0]?.actual[0] ?? "", 10)
|
||||
expect(pid).toBeGreaterThan(0)
|
||||
|
||||
// Give the SIGTERM a moment to be delivered
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
|
||||
expect(await isProcessRunning(pid)).toBe(false)
|
||||
await cleanupTmpDir(result.tmpDir)
|
||||
})
|
||||
|
||||
test("cleans up multiple backgrounded processes", async () => {
|
||||
const file = makeFile([
|
||||
{ command: "sleep 300 >/dev/null 2>&1 & P1=$!; sleep 300 >/dev/null 2>&1 & P2=$!; echo $P1 $P2" },
|
||||
])
|
||||
|
||||
const result = await runFile(file, defaultOpts)
|
||||
const pids = (result.results[0]?.actual[0] ?? "").split(" ").map(Number)
|
||||
expect(pids).toHaveLength(2)
|
||||
expect(pids[0]).toBeGreaterThan(0)
|
||||
expect(pids[1]).toBeGreaterThan(0)
|
||||
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
|
||||
for (const pid of pids) {
|
||||
expect(await isProcessRunning(pid)).toBe(false)
|
||||
}
|
||||
|
||||
await cleanupTmpDir(result.tmpDir)
|
||||
})
|
||||
})
|
||||
26
src/run.ts
26
src/run.ts
|
|
@ -132,15 +132,15 @@ export async function runFile(
|
|||
env["PATH"] = options.pathDirs.join(":") + ":" + (env["PATH"] ?? "")
|
||||
}
|
||||
|
||||
try {
|
||||
const proc = Bun.spawn(["/bin/sh"], {
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
cwd: tmpDir,
|
||||
env,
|
||||
})
|
||||
const proc = Bun.spawn(["setsid", "/bin/sh"], {
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
cwd: tmpDir,
|
||||
env,
|
||||
})
|
||||
|
||||
try {
|
||||
proc.stdin.write(script)
|
||||
proc.stdin.end()
|
||||
|
||||
|
|
@ -174,6 +174,16 @@ export async function runFile(
|
|||
tmpDir,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
}
|
||||
} finally {
|
||||
// Kill the shell's process group to clean up backgrounded children
|
||||
if (proc.pid) {
|
||||
try {
|
||||
process.kill(-proc.pid, "SIGTERM")
|
||||
} catch {}
|
||||
setTimeout(() => {
|
||||
try { process.kill(-proc.pid, "SIGKILL") } catch {}
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user