Compare commits
4 Commits
d3c9178958
...
191317ae22
| Author | SHA1 | Date | |
|---|---|---|---|
| 191317ae22 | |||
| 813b32ab78 | |||
| 7df105d02a | |||
| 1ddee9ea9e |
|
|
@ -36,6 +36,7 @@ Transcript-based shell integration test runner. Bun + TypeScript.
|
||||||
- `...` inline = matches any characters on that line
|
- `...` inline = matches any characters on that line
|
||||||
- `[N]` on last line of expected output = assert exit code N
|
- `[N]` on last line of expected output = assert exit code N
|
||||||
- `[*]` = assert any non-zero exit code; default expects 0
|
- `[*]` = assert any non-zero exit code; default expects 0
|
||||||
|
- `$#` comment line = not executed, no output expected (e.g. `$# start the server`)
|
||||||
- `#` after a command = comment (stripped); `#` in expected output is literal
|
- `#` after a command = comment (stripped); `#` in expected output is literal
|
||||||
- `@env KEY=VALUE` before first command = set environment variable
|
- `@env KEY=VALUE` before first command = set environment variable
|
||||||
- `@setup path.shout` before first command = prepend commands (and `@env`) from another file
|
- `@setup path.shout` before first command = prepend commands (and `@env`) from another file
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -30,6 +30,16 @@ ls: missing: No such file or directory
|
||||||
|
|
||||||
`[1]` after the expected output matches the exit code.
|
`[1]` after the expected output matches the exit code.
|
||||||
|
|
||||||
|
`$#` is a comment line — not executed, no output expected:
|
||||||
|
|
||||||
|
```
|
||||||
|
$# start the server
|
||||||
|
$ my-server &
|
||||||
|
$# now test it
|
||||||
|
$ curl localhost:8080
|
||||||
|
OK
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@because/shout",
|
"name": "@because/shout",
|
||||||
"version": "0.0.13",
|
"version": "0.0.14",
|
||||||
"description": "shell output tester",
|
"description": "shell output tester",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,38 @@ describe("parse", () => {
|
||||||
expect(result.commands[0]!.expected).toEqual(["hi"])
|
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", () => {
|
test("no directives returns empty array", () => {
|
||||||
const result = parse("test.shout", "$ echo hi\nhi\n")
|
const result = parse("test.shout", "$ echo hi\nhi\n")
|
||||||
expect(result.directives).toEqual([])
|
expect(result.directives).toEqual([])
|
||||||
|
|
|
||||||
18
src/parse.ts
18
src/parse.ts
|
|
@ -32,6 +32,11 @@ function stripComment(line: string): string {
|
||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A line like "$# ..." or "$ # ..." — a comment, not a real command */
|
||||||
|
export function isCommentLine(line: string): boolean {
|
||||||
|
return line.startsWith("$#") || (line.startsWith("$ ") && stripComment(line.slice(2)) === "")
|
||||||
|
}
|
||||||
|
|
||||||
function parseExitCode(lines: string[]): {
|
function parseExitCode(lines: string[]): {
|
||||||
lines: string[]
|
lines: string[]
|
||||||
exitCode: number | "*" | null
|
exitCode: number | "*" | null
|
||||||
|
|
@ -134,7 +139,18 @@ export function parse(path: string, content: string): ShoutFile {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.startsWith("\\$ ") && current) {
|
if (isCommentLine(line)) {
|
||||||
|
// Comment line like "$# ..." or "$ # ..." — finalize current and skip
|
||||||
|
seenCommand = true
|
||||||
|
if (current) {
|
||||||
|
const trimmed = trimTrailingEmpty(current.expected)
|
||||||
|
const { lines: expectedLines, exitCode } = parseExitCode(trimmed)
|
||||||
|
current.expected = trimTrailingEmpty(expectedLines)
|
||||||
|
current.exitCode = exitCode
|
||||||
|
commands.push(current)
|
||||||
|
}
|
||||||
|
current = null
|
||||||
|
} else if (line.startsWith("\\$ ") && current) {
|
||||||
// Escaped dollar-space: literal expected output starting with "$ "
|
// Escaped dollar-space: literal expected output starting with "$ "
|
||||||
current.expected.push(line.slice(1))
|
current.expected.push(line.slice(1))
|
||||||
} else if (line.startsWith("$ ")) {
|
} else if (line.startsWith("$ ")) {
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,19 @@ describe("runFile", () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("strips ANSI color codes from output", async () => {
|
||||||
|
const file = makeFile([
|
||||||
|
{ command: `printf '\\033[31mred\\033[0m and \\033[1;32mbold green\\033[0m'` },
|
||||||
|
])
|
||||||
|
const result = await runFile(file, defaultOpts)
|
||||||
|
try {
|
||||||
|
expect(result.results[0]?.actual).toEqual(["red and bold green"])
|
||||||
|
expect(result.results[0]?.exitCode).toBe(0)
|
||||||
|
} finally {
|
||||||
|
await cleanupTmpDir(result.tmpDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("background process output does not leak into subsequent commands", async () => {
|
test("background process output does not leak into subsequent commands", async () => {
|
||||||
const file = makeFile([
|
const file = makeFile([
|
||||||
// Start a background process that writes to stdout after a delay
|
// Start a background process that writes to stdout after a delay
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,13 @@ function parseSentinelOutput(
|
||||||
return { outputs, exitCodes }
|
return { outputs, exitCodes }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
const ANSI_REGEX = /[\u001b\u009b][\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g
|
||||||
|
|
||||||
|
function stripAnsi(line: string): string {
|
||||||
|
return line.replace(ANSI_REGEX, "")
|
||||||
|
}
|
||||||
|
|
||||||
function trimTrailingEmpty(lines: string[]): string[] {
|
function trimTrailingEmpty(lines: string[]): string[] {
|
||||||
let end = lines.length
|
let end = lines.length
|
||||||
while (end > 0 && lines[end - 1] === "") end--
|
while (end > 0 && lines[end - 1] === "") end--
|
||||||
|
|
@ -253,7 +260,7 @@ export async function runFile(
|
||||||
|
|
||||||
const results: CommandResult[] = file.commands.map((cmd, i) => ({
|
const results: CommandResult[] = file.commands.map((cmd, i) => ({
|
||||||
command: cmd,
|
command: cmd,
|
||||||
actual: outputs[i] ?? [],
|
actual: (outputs[i] ?? []).map(stripAnsi),
|
||||||
exitCode: exitCodes[i] ?? 1,
|
exitCode: exitCodes[i] ?? 1,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { CommandResult } from "./run.ts"
|
import type { CommandResult } from "./run.ts"
|
||||||
import type { ShoutFile } from "./parse.ts"
|
import type { ShoutFile } from "./parse.ts"
|
||||||
import { matchOutput, matchLine } from "./match.ts"
|
import { matchOutput, matchLine } from "./match.ts"
|
||||||
|
import { isCommentLine } from "./parse.ts"
|
||||||
|
|
||||||
export function rewriteFile(
|
export function rewriteFile(
|
||||||
file: ShoutFile,
|
file: ShoutFile,
|
||||||
|
|
@ -15,7 +16,10 @@ export function rewriteFile(
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i]!
|
const line = lines[i]!
|
||||||
|
|
||||||
if (line.startsWith("$ ") && !line.startsWith("\\$ ")) {
|
if (isCommentLine(line)) {
|
||||||
|
// Preserve comment lines as-is
|
||||||
|
output.push(line)
|
||||||
|
} else if (line.startsWith("$ ") && !line.startsWith("\\$ ")) {
|
||||||
// Emit the command line as-is
|
// Emit the command line as-is
|
||||||
output.push(line)
|
output.push(line)
|
||||||
|
|
||||||
|
|
@ -28,7 +32,7 @@ export function rewriteFile(
|
||||||
|
|
||||||
// Skip past old expected output lines in the original
|
// Skip past old expected output lines in the original
|
||||||
let j = i + 1
|
let j = i + 1
|
||||||
while (j < lines.length && !(lines[j]!.startsWith("$ ") && !lines[j]!.startsWith("\\$ "))) {
|
while (j < lines.length && !isCommentLine(lines[j]!) && !(lines[j]!.startsWith("$ ") && !lines[j]!.startsWith("\\$ "))) {
|
||||||
j++
|
j++
|
||||||
}
|
}
|
||||||
// Collect old expected lines (before trimming trailing blanks for separator)
|
// Collect old expected lines (before trimming trailing blanks for separator)
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,12 @@
|
||||||
<span class="output">Homebrew 5</span><span class="wildcard">...</span></code></pre>
|
<span class="output">Homebrew 5</span><span class="wildcard">...</span></code></pre>
|
||||||
<p><code class="wildcard">...</code> matches anything — inline or across lines.</p>
|
<p><code class="wildcard">...</code> matches anything — inline or across lines.</p>
|
||||||
<p><code class="exit-code">[1]</code> asserts the exit code.</p>
|
<p><code class="exit-code">[1]</code> asserts the exit code.</p>
|
||||||
|
<p><code class="dim">$#</code> starts a comment line — not executed, no output expected.</p>
|
||||||
|
<pre><code><span class="prompt">$</span><span class="comment"># start the server</span>
|
||||||
|
<span class="prompt">$</span> <span class="cmd">my-server &</span>
|
||||||
|
<span class="prompt">$</span><span class="comment"># now test it</span>
|
||||||
|
<span class="prompt">$</span> <span class="cmd">curl localhost:8080</span>
|
||||||
|
<span class="output">OK</span></code></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user