Compare commits
No commits in common. "490414bd4a4869500c48ce8df94c552e39c80c21" and "040c3a8c47968c84bc2dd21d01f0c1aaa30d4d35" have entirely different histories.
490414bd4a
...
040c3a8c47
|
|
@ -11,7 +11,7 @@ Transcript-based shell integration test runner. Bun + TypeScript.
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
- `src/parse.ts` — parses `.shout` files into `ShoutFile` (list of `Command`)
|
- `src/parse.ts` — parses `.shout` files into `ShoutFile` (list of `Command`)
|
||||||
- `src/run.ts` — executes commands via `Bun.spawn(["setsid", "/bin/sh"])`, captures output with sentinels
|
- `src/run.ts` — executes commands via `Bun.spawn(["/bin/sh"])`, captures output with sentinels
|
||||||
- `src/match.ts` — wildcard-aware output matching and diff generation
|
- `src/match.ts` — wildcard-aware output matching and diff generation
|
||||||
- `src/format.ts` — evaluates pass/fail, formats failures and summary
|
- `src/format.ts` — evaluates pass/fail, formats failures and summary
|
||||||
- `src/update.ts` — rewrites `.shout` files with actual output (`--update` mode)
|
- `src/update.ts` — rewrites `.shout` files with actual output (`--update` mode)
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
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")
|
|
||||||
// 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("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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
12
src/run.ts
12
src/run.ts
|
|
@ -132,7 +132,8 @@ export async function runFile(
|
||||||
env["PATH"] = options.pathDirs.join(":") + ":" + (env["PATH"] ?? "")
|
env["PATH"] = options.pathDirs.join(":") + ":" + (env["PATH"] ?? "")
|
||||||
}
|
}
|
||||||
|
|
||||||
const proc = Bun.spawn(["setsid", "/bin/sh"], {
|
try {
|
||||||
|
const proc = Bun.spawn(["/bin/sh"], {
|
||||||
stdin: "pipe",
|
stdin: "pipe",
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
|
|
@ -140,7 +141,6 @@ export async function runFile(
|
||||||
env,
|
env,
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
|
||||||
proc.stdin.write(script)
|
proc.stdin.write(script)
|
||||||
proc.stdin.end()
|
proc.stdin.end()
|
||||||
|
|
||||||
|
|
@ -174,10 +174,6 @@ export async function runFile(
|
||||||
tmpDir,
|
tmpDir,
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: err instanceof Error ? err.message : String(err),
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
if (proc.pid) {
|
|
||||||
try { process.kill(-proc.pid, "SIGKILL") } catch {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,9 +184,8 @@ async function readWithTimeout(
|
||||||
const reader = stream.getReader()
|
const reader = stream.getReader()
|
||||||
const chunks: Uint8Array[] = []
|
const chunks: Uint8Array[] = []
|
||||||
|
|
||||||
let timerId: ReturnType<typeof setTimeout>
|
|
||||||
const timeout = new Promise<never>((_, reject) =>
|
const timeout = new Promise<never>((_, reject) =>
|
||||||
timerId = setTimeout(() => reject(new Error("Timeout reading output")), timeoutMs),
|
setTimeout(() => reject(new Error("Timeout reading output")), timeoutMs),
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -200,7 +195,6 @@ async function readWithTimeout(
|
||||||
if (value) chunks.push(value)
|
if (value) chunks.push(value)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timerId!)
|
|
||||||
reader.releaseLock()
|
reader.releaseLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user