Move tests, remove old .ts files

This commit is contained in:
Chris Wanstrath 2026-04-02 15:40:32 -07:00
parent eef0f4d2dc
commit aba7f9676d
19 changed files with 5 additions and 726 deletions

View File

@ -66,14 +66,14 @@ Transcript-based shell integration test runner. Bun + TypeScript.
1. `src/parse.ts` — update types (`Directive`, `ShoutFile`, `Command`) and both parsers (`parse` + `parseSetup`) 1. `src/parse.ts` — update types (`Directive`, `ShoutFile`, `Command`) and both parsers (`parse` + `parseSetup`)
2. `src/parse.test.ts` — unit tests for parsing the new syntax in both `.shout` and setup file contexts 2. `src/parse.test.ts` — unit tests for parsing the new syntax in both `.shout` and setup file contexts
3. `src/cli/index.ts` — wire up the parsed result in `runOne` (directive resolution, command merging, result handling) 3. `src/cli/index.ts` — wire up the parsed result in `runOne` (directive resolution, command merging, result handling)
4. `test/*.shout` — integration test file exercising the feature end-to-end 4. `tests/*.shout` — integration test file exercising the feature end-to-end
5. `CLAUDE.md` — update `.shout file format` section 5. `CLAUDE.md` — update `.shout file format` section
6. `README.md` — update Directives section 6. `README.md` — update Directives section
7. `web/index.html` — add or update a section on the website 7. `web/index.html` — add or update a section on the website
8. Run `bun test` and `bun run src/cli/index.ts test test/` to verify 8. Run `cargo test` to verify
## Style ## Style
- Strict TypeScript, Bun runtime - Strict TypeScript, Bun runtime
- No classes — plain functions and types - No classes — plain functions and types
- Tests in `src/*.test.ts`, example `.shout` files in `test/` - Integration tests in `tests/shout.rs`, example `.shout` files in `tests/`

View File

@ -1,31 +0,0 @@
{
"name": "@because/shout",
"version": "0.0.19",
"description": "shell output tester",
"module": "src/index.ts",
"type": "module",
"files": [
"src"
],
"exports": {
".": "./src/index.ts"
},
"bin": {
"shout": "src/cli/index.ts"
},
"scripts": {
"check": "bunx tsc --noEmit",
"cli:link": "ln -sf $(pwd)/src/cli/index.ts ~/.bun/bin/shout",
"test": "bun test"
},
"devDependencies": {
"@types/bun": "latest",
"@types/diff": "^8.0.0",
"typescript": "^5.9.3"
},
"dependencies": {
"commander": "14.0.3",
"diff": "^8.0.3",
"ansis": "*"
}
}

View File

@ -1,333 +0,0 @@
#!/usr/bin/env bun
import { readdir, readFile, writeFile } from "node:fs/promises"
import { resolve, relative, dirname } from "node:path"
import { program } from "commander"
import ansis from "ansis"
import { parse, parseSetup, type Command, type ShoutFile } from "../parse.ts"
import { runFile, cleanupTmpDir, type CommandResult } from "../run.ts"
import { evaluateFile, formatFailure, formatSummary } from "../format.ts"
import type { TestResult } from "../format.ts"
import { matchOutput } from "../match.ts"
import { parseDuration } from "../duration.ts"
import { rewriteFile } from "../update.ts"
async function filterGitignored(files: string[]): Promise<string[]> {
if (files.length === 0) return files
try {
const proc = Bun.spawn(["git", "check-ignore", "--stdin"], {
stdin: new Blob([files.join("\n")]),
stdout: "pipe",
stderr: "ignore",
})
const output = await new Response(proc.stdout).text()
await proc.exited
const ignored = new Set(output.trim().split("\n").filter(Boolean))
return files.filter(f => !ignored.has(f))
} catch {
return files
}
}
async function findShoutFiles(paths: string[]): Promise<string[]> {
const explicit: string[] = []
const discovered: string[] = []
for (const p of paths) {
const abs = resolve(p)
const stat = await Bun.file(abs).exists()
? Bun.file(abs)
: null
if (stat && abs.endsWith(".shout")) {
explicit.push(abs)
continue
}
// Try as directory
try {
const entries = await readdir(abs, { recursive: true })
for (const entry of entries) {
if (entry.endsWith(".shout")) {
discovered.push(resolve(abs, entry))
}
}
} catch {
// If not a directory, try as file anyway
if (abs.endsWith(".shout")) explicit.push(abs)
}
}
const filtered = await filterGitignored(discovered)
return [...explicit, ...filtered].sort()
}
import pkg from "../../package.json"
program
.name("shout")
.description("$ shell output tester")
.version(pkg.version)
program
.command("test")
.description("Run .shout test files")
.argument("[files...]", "Files or directories to test")
.option("-u, --update", "Rewrite expected output in-place with actual output")
.option("-k, --keep", "Keep temp directories after run")
.option("--clean-env", "Start with empty environment")
.option("--path <path>", "Prepend <path> to PATH (repeatable)", (val: string, acc: string[]) => [...acc, val], [])
.option("--timeout <dur>", "Per-command timeout", "10s")
.option("-v, --verbose", "Print each command as it runs")
.option("--port-from <n>", "Auto-assign $PORT starting from <n>", "5400")
.option("-t, --filter <pattern>", "Only run files matching <pattern> (substring match)")
.option("--parallel", "Run files in parallel")
.action(async (fileArgs: string[], opts) => {
const timeoutMs = parseDuration(opts.timeout)
const paths = fileArgs.length > 0 ? fileArgs : ["."]
let files = await findShoutFiles(paths)
const start = performance.now()
const results: TestResult[] = []
const cwd = process.cwd()
if (opts.filter) {
const pattern = opts.filter
files = files.filter(f => relative(cwd, f).includes(pattern))
}
if (files.length === 0) {
console.error(opts.filter ? `No .shout files matching "${opts.filter}"` : "No .shout files found")
process.exit(1)
}
const portFrom = parseInt(opts.portFrom, 10)
if (Number.isNaN(portFrom)) {
console.error("--port-from must be an integer")
process.exit(1)
}
let nextPort = portFrom
const runOne = async (filePath: string, port: number) => {
const content = await readFile(filePath, "utf-8")
const parsed = parse(relative(cwd, filePath), content)
// Resolve directives in a single pass. Setup @env is collected separately
// so that the user file's @env always takes precedence.
const envVars: Record<string, string> = {}
const setupEnvVars: Record<string, string> = {}
const userEnvVars: Record<string, string> = {}
const setupMacros: Record<string, string> = {}
const userMacros: 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)
const setupContent = await readFile(setupPath, "utf-8")
const setupParsed = parseSetup(relative(cwd, setupPath), setupContent)
for (const sd of setupParsed.directives) {
if (sd.type === "env") setupEnvVars[sd.key] = sd.value
else if (sd.type === "def") setupMacros[sd.name] = sd.body
}
setupCommands.push(...setupParsed.commands)
teardownCommands.push(...setupParsed.teardownCommands)
} else if (d.type === "env") {
userEnvVars[d.key] = d.value
} else if (d.type === "def") {
userMacros[d.name] = d.body
}
}
Object.assign(envVars, setupEnvVars, userEnvVars)
if (!("PORT" in userEnvVars) && !("PORT" in setupEnvVars)) {
envVars["PORT"] = String(port)
}
const macros: Record<string, string> = Object.assign({}, setupMacros, userMacros)
const expandMacro = (cmd: Command): Command => {
const body = macros[cmd.command]
return body !== undefined ? { ...cmd, command: body } : cmd
}
const merged: ShoutFile = {
...parsed,
commands: [...setupCommands, ...parsed.commands, ...teardownCommands].map(expandMacro),
}
const setupLen = setupCommands.length
const userLen = parsed.commands.length
const printDot = (result: CommandResult) => {
const { command, actual, exitCode } = result
const outputMatches = matchOutput(command.expected, actual)
let exitCodeMismatch = false
if (command.exitCode === null) {
exitCodeMismatch = exitCode !== 0
} else if (command.exitCode === "*") {
exitCodeMismatch = exitCode === 0
} else {
exitCodeMismatch = exitCode !== command.exitCode
}
if (outputMatches && !exitCodeMismatch) {
process.stdout.write(ansis.green("."))
} else {
process.stdout.write(ansis.red("F"))
}
}
const fileResult = await runFile(merged, {
cleanEnv: opts.cleanEnv ?? false,
pathDirs: opts.path,
envVars,
sourceDir: resolve(dirname(filePath)),
projectDir: cwd,
timeout: timeoutMs,
onCommand: opts.verbose
? (cmd) => process.stderr.write(ansis.dim(` $ ${cmd.command}\n`))
: undefined,
onCommandResult: (index, result) => {
if (index >= setupLen && index < setupLen + userLen) {
printDot(result)
}
},
})
// Check setup commands for failures
for (let i = 0; i < setupCommands.length; i++) {
const r = fileResult.results[i]
const expected = setupCommands[i]!.exitCode
const ok = expected === null
? r?.exitCode === 0
: expected === "*"
? r?.exitCode !== 0
: r?.exitCode === expected
if (!ok) {
if (opts.keep) {
process.stderr.write(`${fileResult.tmpDir}\n`)
} else {
await cleanupTmpDir(fileResult.tmpDir)
}
return evaluateFile(
parsed.path,
[],
`setup command failed (exit ${r?.exitCode ?? "?"}): $ ${setupCommands[i]!.command}`,
)
}
}
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,
fileOwnResults,
fileResult.error,
)
if (opts.update && fileOwnResults.length > 0) {
const updated = rewriteFile(parsed, fileOwnResults, content)
if (updated !== content) {
await writeFile(filePath, updated)
}
}
if (opts.keep) {
process.stderr.write(`${fileResult.tmpDir}\n`)
} else {
await cleanupTmpDir(fileResult.tmpDir)
}
return testResult
}
const printErrorDot = (r: TestResult) => {
if (r.error) {
process.stdout.write(ansis.red("F"))
}
}
if (opts.parallel) {
const promises = files.map(async f => {
const r = await runOne(f, nextPort++)
printErrorDot(r)
return r
})
results.push(...await Promise.all(promises))
process.stdout.write("\n")
} else {
for (const filePath of files) {
const r = await runOne(filePath, nextPort++)
printErrorDot(r)
results.push(r)
}
process.stdout.write("\n")
}
// Print failures
const failures = results.filter(r => !r.passed)
if (failures.length > 0) {
console.log()
for (const f of failures) {
console.log(formatFailure(f))
console.log()
}
}
const elapsed = performance.now() - start
const singleFile = files.length === 1 ? relative(cwd, files[0]) : undefined
console.log(formatSummary(results, elapsed, singleFile))
process.exit(failures.length > 0 ? 1 : 0)
})
program
.command("version")
.description("Print the version")
.action(() => {
console.log(pkg.version)
})
program
.command("example")
.description("Print an example .shout file")
.action(() => {
console.log(`# Example .shout file
$ echo hello
hello
$ echo "one"; echo "two"; echo "three"
one
...
three
$ cat nonexistent
cat: nonexistent: ...
[1]
$ true
[0]`)
})
program
.command("upgrade")
.description("Upgrade to the latest version")
.action(async () => {
const result = await Bun.spawn(["bun", "install", "-g", "@because/shout@latest"], {
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
}).exited
process.exit(result)
})
program.parse()

View File

@ -1,354 +0,0 @@
There are no merge conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) in the file you provided. The content is already clean. Here it is as-is:
import { describe, expect, test } from "bun:test"
import { parse, parseSetup } from "./parse.ts"
describe("parse", () => {
test("simple command with output", () => {
const result = parse("test.shout", "$ echo hello\nhello\n")
expect(result.commands).toHaveLength(1)
expect(result.commands[0]!.command).toBe("echo hello")
expect(result.commands[0]!.expected).toEqual(["hello"])
expect(result.commands[0]!.exitCode).toBeNull()
})
test("multiple commands", () => {
const content = "$ echo one\none\n\n$ echo two\ntwo\n"
const result = parse("test.shout", content)
expect(result.commands).toHaveLength(2)
expect(result.commands[0]!.expected).toEqual(["one"])
expect(result.commands[1]!.expected).toEqual(["two"])
})
test("command with no expected output", () => {
const result = parse("test.shout", "$ export FOO=bar\n$ echo $FOO\nbar\n")
expect(result.commands).toHaveLength(2)
expect(result.commands[0]!.expected).toEqual([])
expect(result.commands[1]!.expected).toEqual(["bar"])
})
test("strips trailing comment from command", () => {
const result = parse("test.shout", '$ echo hello # a comment\nhello\n')
expect(result.commands[0]!.command).toBe("echo hello")
expect(result.commands[0]!.raw).toBe("$ echo hello # a comment")
})
test("preserves # inside quotes", () => {
const result = parse("test.shout", '$ echo "keep # this"\nkeep # this\n')
expect(result.commands[0]!.command).toBe('echo "keep # this"')
})
test("exit code [N]", () => {
const result = parse("test.shout", "$ false\n[1]\n")
expect(result.commands[0]!.exitCode).toBe(1)
expect(result.commands[0]!.expected).toEqual([])
})
test("exit code [*]", () => {
const result = parse("test.shout", "$ false\noops\n[*]\n")
expect(result.commands[0]!.exitCode).toBe("*")
expect(result.commands[0]!.expected).toEqual(["oops"])
})
test("exit code [42] with output", () => {
const result = parse("test.shout", "$ sh -c 'echo err && exit 42'\nerr\n[42]\n")
expect(result.commands[0]!.exitCode).toBe(42)
expect(result.commands[0]!.expected).toEqual(["err"])
})
test("blank lines in expected output", () => {
const content = '$ echo -e "a\\n\\nb"\na\n\nb\n'
const result = parse("test.shout", content)
expect(result.commands[0]!.expected).toEqual(["a", "", "b"])
})
test("trailing newline ignored", () => {
const a = parse("test.shout", "$ echo hi\nhi\n")
const b = parse("test.shout", "$ echo hi\nhi")
expect(a.commands[0]!.expected).toEqual(b.commands[0]!.expected)
})
test("@env directive", () => {
const result = parse("test.shout", "@env PORT=3000\n$ echo $PORT\n3000\n")
expect(result.directives).toEqual([
{ type: "env", key: "PORT", value: "3000", line: 1 },
])
expect(result.commands).toHaveLength(1)
})
test("@env with value containing =", () => {
const result = parse("test.shout", "@env FOO=bar=baz\n$ echo $FOO\n")
expect(result.directives[0]).toEqual(
{ type: "env", key: "FOO", value: "bar=baz", line: 1 },
)
})
test("@setup directive", () => {
const result = parse("test.shout", "@setup shared/setup.shout\n$ echo hi\nhi\n")
expect(result.directives).toEqual([
{ type: "setup", path: "shared/setup.shout", line: 1 },
])
expect(result.commands).toHaveLength(1)
})
test("multiple directives", () => {
const content = "@setup setup.shout\n@env PORT=3000\n@env NODE_ENV=test\n\n$ echo hi\nhi\n"
const result = parse("test.shout", content)
expect(result.directives).toHaveLength(3)
expect(result.directives[0]!.type).toBe("setup")
expect(result.directives[1]).toEqual({ type: "env", key: "PORT", value: "3000", line: 2 })
expect(result.directives[2]).toEqual({ type: "env", key: "NODE_ENV", value: "test", line: 3 })
})
test("@ lines after first command are expected output", () => {
const result = parse("test.shout", "$ cat config\n@env PORT=3000\n")
expect(result.directives).toEqual([])
expect(result.commands[0]!.expected).toEqual(["@env PORT=3000"])
})
test("escaped dollar sign in expected output", () => {
const content = "$ echo '$ hello'\n\\$ hello\n"
const result = parse("test.shout", content)
expect(result.commands).toHaveLength(1)
expect(result.commands[0]!.expected).toEqual(["$ hello"])
})
test("multiple escaped dollar signs", () => {
const content = "$ printf '$ a\\n$ b\\n'\n\\$ a\n\\$ b\n"
const result = parse("test.shout", content)
expect(result.commands).toHaveLength(1)
expect(result.commands[0]!.expected).toEqual(["$ a", "$ b"])
})
test("escaped dollar before first command is ignored", () => {
const content = "\\$ not a command\n$ echo hi\nhi\n"
const result = parse("test.shout", content)
// \$ before any command — no current command to attach to, so skipped
expect(result.commands).toHaveLength(1)
expect(result.commands[0]!.expected).toEqual(["hi"])
})
test("$# comment line is skipped", () => {
const result = parse("test.shout", "$# start the server\n$ echo hi\nhi\n")
expect(result.commands).toHaveLength(1)
expect(result.commands[0]!.command).toBe("echo hi")
})
test("$# comment between commands", () => {
const result = parse("test.shout", "$ echo one\none\n$# now do two\n$ echo two\ntwo\n")
expect(result.commands).toHaveLength(2)
expect(result.commands[0]!.expected).toEqual(["one"])
expect(result.commands[1]!.expected).toEqual(["two"])
})
test("$# comment with space after hash", () => {
const result = parse("test.shout", "$ # server setup\n$ echo hi\nhi\n")
expect(result.commands).toHaveLength(1)
expect(result.commands[0]!.command).toBe("echo hi")
})
test("$# comment as last line", () => {
const result = parse("test.shout", "$ echo hi\nhi\n$# done\n")
expect(result.commands).toHaveLength(1)
expect(result.commands[0]!.expected).toEqual(["hi"])
})
test("output after $# comment is ignored", () => {
const result = parse("test.shout", "$ echo hi\nhi\n$# comment\nstray line\n$ echo bye\nbye\n")
expect(result.commands).toHaveLength(2)
expect(result.commands[0]!.expected).toEqual(["hi"])
expect(result.commands[1]!.expected).toEqual(["bye"])
})
test("no directives returns empty array", () => {
const result = parse("test.shout", "$ echo hi\nhi\n")
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",
)
})
test("@def simple macro", () => {
const result = parse("test.shout", "@def greet echo hello\n$ greet\nhello\n")
expect(result.directives).toEqual([
{ type: "def", name: "greet", body: "echo hello", line: 1 },
])
expect(result.commands).toHaveLength(1)
expect(result.commands[0]!.command).toBe("greet")
})
test("@def with backslash continuation", () => {
const content = "@def multi echo one; \\\n echo two\n$ multi\none\ntwo\n"
const result = parse("test.shout", content)
expect(result.directives).toEqual([
{ type: "def", name: "multi", body: "echo one;\necho two", line: 1 },
])
expect(result.commands).toHaveLength(1)
})
test("@def with body starting on continuation line", () => {
const content = "@def serve \\\n python3 -m http.server\n$ serve\n"
const result = parse("test.shout", content)
expect(result.directives).toEqual([
{ type: "def", name: "serve", body: "python3 -m http.server", line: 1 },
])
})
test("@def multiple macros", () => {
const content = "@def foo echo foo\n@def bar echo bar\n$ foo\nfoo\n"
const result = parse("test.shout", content)
expect(result.directives).toHaveLength(2)
expect(result.directives[0]).toEqual({ type: "def", name: "foo", body: "echo foo", line: 1 })
expect(result.directives[1]).toEqual({ type: "def", name: "bar", body: "echo bar", line: 2 })
})
test("@def without body throws", () => {
expect(() => parse("test.shout", "@def greet\n$ echo hi\n")).toThrow(
"test.shout:1: @def requires a name and body",
)
})
test("@def with whitespace-only body throws", () => {
expect(() => parse("test.shout", "@def greet \n$ echo hi\n")).toThrow(
"test.shout:1: @def requires a name and body",
)
})
test("@def continuation consuming $ line throws", () => {
expect(() => parse("test.shout", "@def foo echo a \\\n$ echo real\n")).toThrow(
"test.shout:2: @def continuation consumed a command or directive line",
)
})
test("@def continuation consuming @ directive throws", () => {
expect(() => parse("test.shout", "@def foo echo a \\\n@env PORT=3000\n")).toThrow(
"test.shout:2: @def continuation consumed a command or directive line",
)
})
test("@def trailing backslash with no continuation throws", () => {
expect(() => parse("test.shout", "@def foo echo a \\\n")).toThrow(
"test.shout:1: @def ends with \\ but has no continuation line",
)
})
test("@def continuation consuming blank line throws", () => {
expect(() => parse("test.shout", "@def foo echo a \\\n\n$ echo real\n")).toThrow(
"test.shout:2: @def continuation consumed a command or directive line",
)
})
test("@def continuation consuming comment line throws", () => {
expect(() => parse("test.shout", "@def foo echo a \\\n# comment\n$ echo real\n")).toThrow(
"test.shout:2: @def continuation consumed a command or directive line",
)
})
test("@def after first command is expected output", () => {
const result = parse("test.shout", "$ cat file\n@def foo bar\n")
expect(result.directives).toEqual([])
expect(result.commands[0]!.expected).toEqual(["@def foo bar"])
})
})
describe("parseSetup", () => {
test("plain commands without $ prefix", () => {
const result = parseSetup("setup.shout", "export FOO=bar\necho hello\n")
expect(result.commands).toHaveLength(2)
expect(result.commands[0]!.command).toBe("export FOO=bar")
expect(result.commands[1]!.command).toBe("echo hello")
})
test("@env directives", () => {
const result = parseSetup("setup.shout", "@env PORT=3000\nexport FOO=bar\n")
expect(result.directives).toEqual([
{ type: "env", key: "PORT", value: "3000", line: 1 },
])
expect(result.commands).toHaveLength(1)
})
test("blank lines and comments are ignored", () => {
const result = parseSetup("setup.shout", "# set up env\nexport FOO=bar\n\nexport BAZ=qux\n")
expect(result.commands).toHaveLength(2)
expect(result.commands[0]!.command).toBe("export FOO=bar")
expect(result.commands[1]!.command).toBe("export BAZ=qux")
})
test("strips trailing comments from commands", () => {
const result = parseSetup("setup.shout", "export FOO=bar # set foo\n")
expect(result.commands[0]!.command).toBe("export FOO=bar")
})
test("@setup in setup file throws", () => {
expect(() => parseSetup("setup.shout", "@setup other.shout\n")).toThrow(
"setup.shout:1: @setup not allowed in setup files",
)
})
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([])
expect(result.commands[0]!.exitCode).toBeNull()
})
test("@def in setup file", () => {
const result = parseSetup("setup.shout", "@def greet echo hello\nexport FOO=bar\n")
expect(result.directives).toEqual([
{ type: "def", name: "greet", body: "echo hello", line: 1 },
])
expect(result.commands).toHaveLength(1)
})
test("@def with backslash continuation in setup file", () => {
const result = parseSetup("setup.shout", "@def multi echo one; \\\n echo two\n")
expect(result.directives).toEqual([
{ type: "def", name: "multi", body: "echo one;\necho two", line: 1 },
])
})
})

View File

@ -1,3 +0,0 @@
The TypeScript content you pasted doesn't contain any merge conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`). The file appears to be clean and complete.
I found merge conflicts in other files in the repo, but none in a `parse.ts` file. Could you clarify which file has the conflict, or re-paste the content with the conflict markers visible? It's possible they were stripped during message formatting.

View File

@ -9,8 +9,8 @@ fn shout() -> Command {
#[test] #[test]
fn test_suite_passes() { fn test_suite_passes() {
let status = shout() let status = shout()
.args(["test", "test/"]) .args(["test", "tests/"])
.status() .status()
.unwrap(); .unwrap();
assert!(status.success(), "shout test test/ exited with {status}"); assert!(status.success(), "shout test tests/ exited with {status}");
} }