Compare commits

..

4 Commits

2 changed files with 51 additions and 6 deletions

View File

@ -44,6 +44,32 @@ $ shout test
Each line in a `.shout` file is run sequentially, unless `--parallel` is passed.
## Directives
Directives go at the top of a `.shout` file, before any commands.
### `@env`
Set environment variables for the test:
```
@env GREETING=hello
@env TARGET=world
$ echo "$GREETING $TARGET"
hello world
```
### `@setup`
Prepend commands (and `@env` directives) from another `.shout` file:
```
@setup setup-shared.shout
```
Setup commands run first and their failures abort the test. Setup files cannot themselves contain `@setup` — no nesting. If both the setup file and the user file define the same `@env`, the user file wins.
```
Usage: shout test [options] [files...]

View File

@ -12,8 +12,26 @@ import type { TestResult } from "../format.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 files: string[] = []
const explicit: string[] = []
const discovered: string[] = []
for (const p of paths) {
const abs = resolve(p)
@ -22,7 +40,7 @@ async function findShoutFiles(paths: string[]): Promise<string[]> {
: null
if (stat && abs.endsWith(".shout")) {
files.push(abs)
explicit.push(abs)
continue
}
@ -31,16 +49,17 @@ async function findShoutFiles(paths: string[]): Promise<string[]> {
const entries = await readdir(abs, { recursive: true })
for (const entry of entries) {
if (entry.endsWith(".shout")) {
files.push(resolve(abs, entry))
discovered.push(resolve(abs, entry))
}
}
} catch {
// If not a directory, try as file anyway
if (abs.endsWith(".shout")) files.push(abs)
if (abs.endsWith(".shout")) explicit.push(abs)
}
}
return files.sort()
const filtered = await filterGitignored(discovered)
return [...explicit, ...filtered].sort()
}
import pkg from "../../package.json"
@ -57,7 +76,7 @@ program
.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("--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>")