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. 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...] Usage: shout test [options] [files...]

View File

@ -12,8 +12,26 @@ import type { TestResult } from "../format.ts"
import { parseDuration } from "../duration.ts" import { parseDuration } from "../duration.ts"
import { rewriteFile } from "../update.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[]> { async function findShoutFiles(paths: string[]): Promise<string[]> {
const files: string[] = [] const explicit: string[] = []
const discovered: string[] = []
for (const p of paths) { for (const p of paths) {
const abs = resolve(p) const abs = resolve(p)
@ -22,7 +40,7 @@ async function findShoutFiles(paths: string[]): Promise<string[]> {
: null : null
if (stat && abs.endsWith(".shout")) { if (stat && abs.endsWith(".shout")) {
files.push(abs) explicit.push(abs)
continue continue
} }
@ -31,16 +49,17 @@ async function findShoutFiles(paths: string[]): Promise<string[]> {
const entries = await readdir(abs, { recursive: true }) const entries = await readdir(abs, { recursive: true })
for (const entry of entries) { for (const entry of entries) {
if (entry.endsWith(".shout")) { if (entry.endsWith(".shout")) {
files.push(resolve(abs, entry)) discovered.push(resolve(abs, entry))
} }
} }
} catch { } catch {
// If not a directory, try as file anyway // 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" import pkg from "../../package.json"
@ -57,7 +76,7 @@ program
.option("-u, --update", "Rewrite expected output in-place with actual output") .option("-u, --update", "Rewrite expected output in-place with actual output")
.option("-k, --keep", "Keep temp directories after run") .option("-k, --keep", "Keep temp directories after run")
.option("--clean-env", "Start with empty environment") .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("--timeout <dur>", "Per-command timeout", "10s")
.option("-v, --verbose", "Print each command as it runs") .option("-v, --verbose", "Print each command as it runs")
.option("--port-from <n>", "Auto-assign $PORT starting from <n>") .option("--port-from <n>", "Auto-assign $PORT starting from <n>")