Add @teardown directive support
This commit is contained in:
parent
191317ae22
commit
e8bc4be382
|
|
@ -39,9 +39,13 @@ Transcript-based shell integration test runner. Bun + TypeScript.
|
|||
- `$#` comment line = not executed, no output expected (e.g. `$# start the server`)
|
||||
- `#` after a command = comment (stripped); `#` in expected output is literal
|
||||
- `@env KEY=VALUE` before first command = set environment variable
|
||||
- `@teardown <command>` before first command = run command after all test commands
|
||||
- Runs regardless of pass/fail
|
||||
- Teardown failures produce warnings but don't affect test results
|
||||
- Can appear in both `.shout` files and setup files
|
||||
- `@setup path.shout` before first command = prepend commands (and `@env`) from another file
|
||||
- Setup files use a plain format: each line is a command (no `$ ` prefix), `#` lines are comments, blank lines ignored
|
||||
- Setup files can contain `@env` directives but not `@setup` (no nesting)
|
||||
- Setup files can contain `@env` and `@teardown` directives but not `@setup` (no nesting)
|
||||
- User file `@env` overrides setup file `@env`
|
||||
- Setup command failures abort the test with an error
|
||||
- Each file runs in a fresh temp dir with a single `/bin/sh` session
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ program
|
|||
const setupEnvVars: Record<string, string> = {}
|
||||
const userEnvVars: Record<string, string> = {}
|
||||
const setupCommands: Command[] = []
|
||||
const teardownCommands: Command[] = [...parsed.teardownCommands]
|
||||
for (const d of parsed.directives) {
|
||||
if (d.type === "setup") {
|
||||
const setupPath = resolve(dirname(filePath), d.path)
|
||||
|
|
@ -120,6 +121,7 @@ program
|
|||
if (sd.type === "env") setupEnvVars[sd.key] = sd.value
|
||||
}
|
||||
setupCommands.push(...setupParsed.commands)
|
||||
teardownCommands.push(...setupParsed.teardownCommands)
|
||||
} else if (d.type === "env") {
|
||||
userEnvVars[d.key] = d.value
|
||||
}
|
||||
|
|
@ -128,7 +130,7 @@ program
|
|||
if (!("PORT" in userEnvVars) && !("PORT" in setupEnvVars)) {
|
||||
envVars["PORT"] = String(port)
|
||||
}
|
||||
const merged: ShoutFile = { ...parsed, commands: [...setupCommands, ...parsed.commands] }
|
||||
const merged: ShoutFile = { ...parsed, commands: [...setupCommands, ...parsed.commands, ...teardownCommands] }
|
||||
|
||||
const fileResult = await runFile(merged, {
|
||||
cleanEnv: opts.cleanEnv ?? false,
|
||||
|
|
@ -166,7 +168,21 @@ program
|
|||
}
|
||||
}
|
||||
|
||||
const fileOwnResults = fileResult.results.slice(setupCommands.length)
|
||||
const fileOwnResults = fileResult.results.slice(
|
||||
setupCommands.length,
|
||||
setupCommands.length + parsed.commands.length,
|
||||
)
|
||||
|
||||
// Warn on teardown failures
|
||||
const teardownResults = fileResult.results.slice(setupCommands.length + parsed.commands.length)
|
||||
for (let i = 0; i < teardownResults.length; i++) {
|
||||
const r = teardownResults[i]
|
||||
if (r && r.exitCode !== 0) {
|
||||
process.stderr.write(
|
||||
ansis.yellow(`warning: teardown command failed (exit ${r.exitCode}): $ ${teardownCommands[i]!.command}\n`),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const testResult = evaluateFile(
|
||||
parsed.path,
|
||||
|
|
|
|||
|
|
@ -164,6 +164,22 @@ describe("parse", () => {
|
|||
expect(result.directives).toEqual([])
|
||||
})
|
||||
|
||||
test("@teardown in .shout file", () => {
|
||||
const result = parse("test.shout", "@teardown rm -f /tmp/test.db\n$ echo hi\nhi\n")
|
||||
expect(result.teardownCommands).toHaveLength(1)
|
||||
expect(result.teardownCommands[0]!.command).toBe("rm -f /tmp/test.db")
|
||||
expect(result.commands).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("@teardown with @setup in .shout file", () => {
|
||||
const content = "@setup setup.shout\n@teardown rm -f /tmp/test.db\n@env PORT=3000\n$ echo hi\nhi\n"
|
||||
const result = parse("test.shout", content)
|
||||
expect(result.directives).toHaveLength(2)
|
||||
expect(result.directives[0]!.type).toBe("setup")
|
||||
expect(result.teardownCommands).toHaveLength(1)
|
||||
expect(result.teardownCommands[0]!.command).toBe("rm -f /tmp/test.db")
|
||||
})
|
||||
|
||||
test("unknown directive throws", () => {
|
||||
expect(() => parse("test.shout", "@evn PORT=3000\n$ echo hi\n")).toThrow(
|
||||
"test.shout:1: unknown directive: @evn PORT=3000",
|
||||
|
|
@ -205,6 +221,32 @@ describe("parseSetup", () => {
|
|||
)
|
||||
})
|
||||
|
||||
test("@teardown commands in setup file", () => {
|
||||
const result = parseSetup("setup.shout", "export FOO=bar\n@teardown rm -f /tmp/test.db\n")
|
||||
expect(result.commands).toHaveLength(1)
|
||||
expect(result.commands[0]!.command).toBe("export FOO=bar")
|
||||
expect(result.teardownCommands).toHaveLength(1)
|
||||
expect(result.teardownCommands[0]!.command).toBe("rm -f /tmp/test.db")
|
||||
})
|
||||
|
||||
test("@teardown strips trailing comment", () => {
|
||||
const result = parseSetup("setup.shout", "@teardown rm -f /tmp/test.db # cleanup\n")
|
||||
expect(result.teardownCommands[0]!.command).toBe("rm -f /tmp/test.db")
|
||||
})
|
||||
|
||||
test("@teardown with empty command throws", () => {
|
||||
expect(() => parseSetup("setup.shout", "@teardown \n")).toThrow(
|
||||
"setup.shout:1: @teardown requires a command",
|
||||
)
|
||||
})
|
||||
|
||||
test("multiple @teardown commands", () => {
|
||||
const result = parseSetup("setup.shout", "@teardown rm -f a.db\n@teardown rm -f b.db\n")
|
||||
expect(result.teardownCommands).toHaveLength(2)
|
||||
expect(result.teardownCommands[0]!.command).toBe("rm -f a.db")
|
||||
expect(result.teardownCommands[1]!.command).toBe("rm -f b.db")
|
||||
})
|
||||
|
||||
test("commands have no expected output", () => {
|
||||
const result = parseSetup("setup.shout", "echo hello\n")
|
||||
expect(result.commands[0]!.expected).toEqual([])
|
||||
|
|
|
|||
31
src/parse.ts
31
src/parse.ts
|
|
@ -14,6 +14,7 @@ export type ShoutFile = {
|
|||
path: string
|
||||
commands: Command[]
|
||||
directives: Directive[]
|
||||
teardownCommands: Command[]
|
||||
}
|
||||
|
||||
function stripComment(line: string): string {
|
||||
|
|
@ -76,6 +77,7 @@ export function parseSetup(path: string, content: string): ShoutFile {
|
|||
}
|
||||
|
||||
const commands: Command[] = []
|
||||
const teardownCommands: Command[] = []
|
||||
const directives: Directive[] = []
|
||||
|
||||
for (let i = 0; i < rawLines.length; i++) {
|
||||
|
|
@ -87,6 +89,18 @@ export function parseSetup(path: string, content: string): ShoutFile {
|
|||
if (line.startsWith("@env ")) {
|
||||
const { key, value } = parseEnvDirective(path, line, i + 1)
|
||||
directives.push({ type: "env", key, value, line: i + 1 })
|
||||
} else if (line.startsWith("@teardown ")) {
|
||||
const cmd = line.slice(10)
|
||||
if (!cmd.trim()) {
|
||||
throw new Error(`${path}:${i + 1}: @teardown requires a command`)
|
||||
}
|
||||
teardownCommands.push({
|
||||
line: i + 1,
|
||||
raw: line,
|
||||
command: stripComment(cmd),
|
||||
expected: [],
|
||||
exitCode: null,
|
||||
})
|
||||
} else if (line.startsWith("@setup ")) {
|
||||
throw new Error(`${path}:${i + 1}: @setup not allowed in setup files`)
|
||||
} else {
|
||||
|
|
@ -104,7 +118,7 @@ export function parseSetup(path: string, content: string): ShoutFile {
|
|||
})
|
||||
}
|
||||
|
||||
return { path, commands, directives }
|
||||
return { path, commands, directives, teardownCommands }
|
||||
}
|
||||
|
||||
export function parse(path: string, content: string): ShoutFile {
|
||||
|
|
@ -116,6 +130,7 @@ export function parse(path: string, content: string): ShoutFile {
|
|||
}
|
||||
|
||||
const commands: Command[] = []
|
||||
const teardownCommands: Command[] = []
|
||||
const directives: Directive[] = []
|
||||
let current: Command | null = null
|
||||
let seenCommand = false
|
||||
|
|
@ -130,6 +145,18 @@ export function parse(path: string, content: string): ShoutFile {
|
|||
throw new Error(`${path}:${i + 1}: @setup requires a file path`)
|
||||
}
|
||||
directives.push({ type: "setup", path: setupPath, line: i + 1 })
|
||||
} else if (line.startsWith("@teardown ")) {
|
||||
const cmd = line.slice(10)
|
||||
if (!cmd.trim()) {
|
||||
throw new Error(`${path}:${i + 1}: @teardown requires a command`)
|
||||
}
|
||||
teardownCommands.push({
|
||||
line: i + 1,
|
||||
raw: line,
|
||||
command: stripComment(cmd),
|
||||
expected: [],
|
||||
exitCode: null,
|
||||
})
|
||||
} else if (line.startsWith("@env ")) {
|
||||
const { key, value } = parseEnvDirective(path, line, i + 1)
|
||||
directives.push({ type: "env", key, value, line: i + 1 })
|
||||
|
|
@ -183,5 +210,5 @@ export function parse(path: string, content: string): ShoutFile {
|
|||
commands.push(current)
|
||||
}
|
||||
|
||||
return { path, commands, directives }
|
||||
return { path, commands, directives, teardownCommands }
|
||||
}
|
||||
|
|
|
|||
2
test/teardown-setup.shout
Normal file
2
test/teardown-setup.shout
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export READY=yes
|
||||
@teardown rm -f "$SHOUT_PROJECT_DIR/data/test.cleanup.db"
|
||||
4
test/teardown.shout
Normal file
4
test/teardown.shout
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
@setup teardown-setup.shout
|
||||
|
||||
$ touch marker.txt && ls marker.txt
|
||||
marker.txt
|
||||
Loading…
Reference in New Issue
Block a user