From 6180a8f7e969f3b0536dfd133a5a368df1ac4482 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Thu, 2 Apr 2026 15:40:32 -0700 Subject: [PATCH] Move tests, remove old .ts files --- CLAUDE.md | 6 +- package.json | 31 -- src/cli/index.ts | 333 --------------------- src/parse.test.ts | 354 ----------------------- src/parse.ts | 3 - {test => tests}/basic.shout | 0 {test => tests}/comments.shout | 0 {test => tests}/def-override.shout | 0 {test => tests}/def-setup.shout | 0 {test => tests}/def-shared.shout | 0 {test => tests}/def.shout | 0 {test => tests}/dollar-sign-output.shout | 0 {test => tests}/env.shout | 0 {test => tests}/features.shout | 0 {test => tests}/setup-shared.shout | 0 {test => tests}/setup-user.shout | 0 tests/shout.rs | 4 +- {test => tests}/teardown-setup.shout | 0 {test => tests}/teardown.shout | 0 19 files changed, 5 insertions(+), 726 deletions(-) delete mode 100644 package.json delete mode 100755 src/cli/index.ts delete mode 100644 src/parse.test.ts delete mode 100644 src/parse.ts rename {test => tests}/basic.shout (100%) rename {test => tests}/comments.shout (100%) rename {test => tests}/def-override.shout (100%) rename {test => tests}/def-setup.shout (100%) rename {test => tests}/def-shared.shout (100%) rename {test => tests}/def.shout (100%) rename {test => tests}/dollar-sign-output.shout (100%) rename {test => tests}/env.shout (100%) rename {test => tests}/features.shout (100%) rename {test => tests}/setup-shared.shout (100%) rename {test => tests}/setup-user.shout (100%) rename {test => tests}/teardown-setup.shout (100%) rename {test => tests}/teardown.shout (100%) diff --git a/CLAUDE.md b/CLAUDE.md index fe92f7b..731916a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`) 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) -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 6. `README.md` — update Directives section 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 - Strict TypeScript, Bun runtime - 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/` diff --git a/package.json b/package.json deleted file mode 100644 index dcaef0c..0000000 --- a/package.json +++ /dev/null @@ -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": "*" - } -} diff --git a/src/cli/index.ts b/src/cli/index.ts deleted file mode 100755 index b7da8bb..0000000 --- a/src/cli/index.ts +++ /dev/null @@ -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 { - 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 { - 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 ", "Prepend to PATH (repeatable)", (val: string, acc: string[]) => [...acc, val], []) - .option("--timeout ", "Per-command timeout", "10s") - .option("-v, --verbose", "Print each command as it runs") - .option("--port-from ", "Auto-assign $PORT starting from ", "5400") - .option("-t, --filter ", "Only run files matching (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 = {} - const setupEnvVars: Record = {} - const userEnvVars: Record = {} - const setupMacros: Record = {} - const userMacros: Record = {} - 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 = 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() diff --git a/src/parse.test.ts b/src/parse.test.ts deleted file mode 100644 index 4afe174..0000000 --- a/src/parse.test.ts +++ /dev/null @@ -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 }, - ]) - }) -}) diff --git a/src/parse.ts b/src/parse.ts deleted file mode 100644 index e5212e2..0000000 --- a/src/parse.ts +++ /dev/null @@ -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. diff --git a/test/basic.shout b/tests/basic.shout similarity index 100% rename from test/basic.shout rename to tests/basic.shout diff --git a/test/comments.shout b/tests/comments.shout similarity index 100% rename from test/comments.shout rename to tests/comments.shout diff --git a/test/def-override.shout b/tests/def-override.shout similarity index 100% rename from test/def-override.shout rename to tests/def-override.shout diff --git a/test/def-setup.shout b/tests/def-setup.shout similarity index 100% rename from test/def-setup.shout rename to tests/def-setup.shout diff --git a/test/def-shared.shout b/tests/def-shared.shout similarity index 100% rename from test/def-shared.shout rename to tests/def-shared.shout diff --git a/test/def.shout b/tests/def.shout similarity index 100% rename from test/def.shout rename to tests/def.shout diff --git a/test/dollar-sign-output.shout b/tests/dollar-sign-output.shout similarity index 100% rename from test/dollar-sign-output.shout rename to tests/dollar-sign-output.shout diff --git a/test/env.shout b/tests/env.shout similarity index 100% rename from test/env.shout rename to tests/env.shout diff --git a/test/features.shout b/tests/features.shout similarity index 100% rename from test/features.shout rename to tests/features.shout diff --git a/test/setup-shared.shout b/tests/setup-shared.shout similarity index 100% rename from test/setup-shared.shout rename to tests/setup-shared.shout diff --git a/test/setup-user.shout b/tests/setup-user.shout similarity index 100% rename from test/setup-user.shout rename to tests/setup-user.shout diff --git a/tests/shout.rs b/tests/shout.rs index 3cf18e2..e9b9314 100644 --- a/tests/shout.rs +++ b/tests/shout.rs @@ -9,8 +9,8 @@ fn shout() -> Command { #[test] fn test_suite_passes() { let status = shout() - .args(["test", "test/"]) + .args(["test", "tests/"]) .status() .unwrap(); - assert!(status.success(), "shout test test/ exited with {status}"); + assert!(status.success(), "shout test tests/ exited with {status}"); } diff --git a/test/teardown-setup.shout b/tests/teardown-setup.shout similarity index 100% rename from test/teardown-setup.shout rename to tests/teardown-setup.shout diff --git a/test/teardown.shout b/tests/teardown.shout similarity index 100% rename from test/teardown.shout rename to tests/teardown.shout