115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
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",
|
|
directives: [],
|
|
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")
|
|
// Find state after the comm field (which is wrapped in parens and may contain spaces)
|
|
const state = stat.charAt(stat.lastIndexOf(")") + 2)
|
|
return state !== "Z" && state !== "X"
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
describe("runFile", () => {
|
|
test("basic command", async () => {
|
|
const file = makeFile([{ command: "echo hello" }])
|
|
const result = await runFile(file, defaultOpts)
|
|
try {
|
|
expect(result.results[0]?.actual).toEqual(["hello"])
|
|
expect(result.results[0]?.exitCode).toBe(0)
|
|
} finally {
|
|
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)
|
|
try {
|
|
const pid = parseInt(result.results[0]?.actual[0] ?? "", 10)
|
|
expect(pid).toBeGreaterThan(0)
|
|
await new Promise(r => setTimeout(r, 100))
|
|
expect(await isProcessRunning(pid)).toBe(false)
|
|
} finally {
|
|
await cleanupTmpDir(result.tmpDir)
|
|
}
|
|
})
|
|
|
|
test("strips ANSI color codes from output", async () => {
|
|
const file = makeFile([
|
|
{ command: `printf '\\033[31mred\\033[0m and \\033[1;32mbold green\\033[0m'` },
|
|
])
|
|
const result = await runFile(file, defaultOpts)
|
|
try {
|
|
expect(result.results[0]?.actual).toEqual(["red and bold green"])
|
|
expect(result.results[0]?.exitCode).toBe(0)
|
|
} finally {
|
|
await cleanupTmpDir(result.tmpDir)
|
|
}
|
|
})
|
|
|
|
test("background process output does not leak into subsequent commands", async () => {
|
|
const file = makeFile([
|
|
// Start a background process that writes to stdout after a delay
|
|
{ command: "{ sleep 0.1; echo LEAKED; } &" },
|
|
// Wait long enough for the background output to appear
|
|
{ command: "sleep 0.3; echo clean" },
|
|
])
|
|
const result = await runFile(file, defaultOpts)
|
|
try {
|
|
// The background command itself should have no output
|
|
expect(result.results[0]?.actual).toEqual([])
|
|
// The sleep command should only see its own output, not the background "LEAKED"
|
|
expect(result.results[1]?.actual).toEqual(["clean"])
|
|
} finally {
|
|
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)
|
|
try {
|
|
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)
|
|
}
|
|
} finally {
|
|
await cleanupTmpDir(result.tmpDir)
|
|
}
|
|
})
|
|
})
|