Skip to content

Commit defeb71

Browse files
committed
fix
1 parent d0e7884 commit defeb71

File tree

5 files changed

+302
-4
lines changed

5 files changed

+302
-4
lines changed

src/spec-tests/cases/command-sub.test.sh

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ echo status=$?
4545
## OK mksh status: 1
4646
4747
#### Command sub with here doc
48-
## SKIP: Here-doc as command prefix (<<EOF cmd) not supported
4948
echo $(<<EOF tac
5049
one
5150
two
@@ -54,7 +53,6 @@ EOF
5453
## stdout: two one
5554
5655
#### Here doc with pipeline
57-
## SKIP: Here-doc as command prefix (<<EOF cmd) not supported
5856
<<EOF tac | tr '\n' 'X'
5957
one
6058
two

src/spec-tests/cases/here-doc.test.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ three
145145
## END
146146

147147
#### Two here docs -- first is ignored; second ones wins!
148-
## SKIP: Here-doc edge cases not implemented
149148
<<EOF1 cat <<EOF2
150149
hello
151150
EOF1

src/spec-tests/runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
type ParsedSpecFile,
1313
type TestCase,
1414
} from "./parser.js";
15-
import { testHelperCommands } from "./test-helpers.js";
15+
import { testHelperCommands } from "./spec-helpers.js";
1616

1717
export interface TestResult {
1818
testCase: TestCase;

src/spec-tests/spec-helpers.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Test helper commands for spec tests
3+
* These replace the Python scripts used in the original Oil shell tests
4+
*
5+
* NOTE: Standard Unix commands (tac, od, hostname) are now in src/commands/
6+
*/
7+
8+
import { defineCommand } from "../custom-commands.js";
9+
import type { Command } from "../types.js";
10+
11+
// argv.py - prints arguments in Python repr() format: ['arg1', "arg with '"]
12+
// Python uses single quotes by default, double quotes when string contains single quotes
13+
export const argvCommand: Command = defineCommand("argv.py", async (args) => {
14+
const formatted = args.map((arg) => {
15+
const hasSingleQuote = arg.includes("'");
16+
const hasDoubleQuote = arg.includes('"');
17+
18+
if (hasSingleQuote && !hasDoubleQuote) {
19+
// Use double quotes when string contains single quotes but no double quotes
20+
const escaped = arg.replace(/\\/g, "\\\\");
21+
return `"${escaped}"`;
22+
}
23+
// Default: use single quotes (escape single quotes and backslashes)
24+
const escaped = arg.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
25+
return `'${escaped}'`;
26+
});
27+
return { stdout: `[${formatted.join(", ")}]\n`, stderr: "", exitCode: 0 };
28+
});
29+
30+
// printenv.py - prints environment variable values, one per line
31+
// Prints "None" for variables that are not set (matching Python's printenv.py)
32+
export const printenvCommand: Command = defineCommand(
33+
"printenv.py",
34+
async (args, ctx) => {
35+
const output = args.map((name) => ctx.env[name] ?? "None").join("\n");
36+
return {
37+
stdout: output ? `${output}\n` : "",
38+
stderr: "",
39+
exitCode: 0,
40+
};
41+
},
42+
);
43+
44+
// stdout_stderr.py - outputs to both stdout and stderr
45+
export const stdoutStderrCommand: Command = defineCommand(
46+
"stdout_stderr.py",
47+
async () => {
48+
return { stdout: "STDOUT\n", stderr: "STDERR\n", exitCode: 0 };
49+
},
50+
);
51+
52+
// read_from_fd.py - reads from a file descriptor (simplified - reads from stdin)
53+
export const readFromFdCommand: Command = defineCommand(
54+
"read_from_fd.py",
55+
async (args, ctx) => {
56+
// In real bash, this reads from a specific FD. Here we just return stdin or empty.
57+
const fd = args[0] || "0";
58+
if (fd === "0" && ctx.stdin) {
59+
return { stdout: ctx.stdin, stderr: "", exitCode: 0 };
60+
}
61+
return { stdout: "", stderr: "", exitCode: 0 };
62+
},
63+
);
64+
65+
/** All test helper commands (Python script replacements) */
66+
export const testHelperCommands: Command[] = [
67+
argvCommand,
68+
printenvCommand,
69+
stdoutStderrCommand,
70+
readFromFdCommand,
71+
];

src/spec-tests/spec.test.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/**
2+
* Vitest runner for Oils spec tests
3+
*
4+
* This runs the imported spec tests from the Oils project against BashEnv.
5+
*/
6+
7+
import * as fs from "node:fs";
8+
import * as path from "node:path";
9+
import { describe, expect, it } from "vitest";
10+
import { parseSpecFile } from "./parser.js";
11+
import { runTestCase } from "./runner.js";
12+
13+
const CASES_DIR = path.join(__dirname, "cases");
14+
15+
// All available test files - dynamically loaded
16+
const ALL_TEST_FILES = fs
17+
.readdirSync(CASES_DIR)
18+
.filter((f) => f.endsWith(".test.sh"))
19+
.sort();
20+
21+
// Tests to skip entirely (interactive, requires real shell, etc.)
22+
const SKIP_FILES = new Set([
23+
// Interactive shell tests - require TTY
24+
"interactive.test.sh",
25+
"interactive-parse.test.sh",
26+
"prompt.test.sh",
27+
"builtin-history.test.sh",
28+
"builtin-fc.test.sh",
29+
"builtin-bind.test.sh",
30+
"builtin-completion.test.sh",
31+
32+
// Process/job control - requires real processes
33+
"background.test.sh",
34+
"builtin-process.test.sh",
35+
"builtin-kill.test.sh",
36+
"builtin-trap.test.sh",
37+
"builtin-trap-bash.test.sh",
38+
"builtin-trap-err.test.sh",
39+
"builtin-times.test.sh",
40+
"process-sub.test.sh",
41+
42+
// Shell-specific features not implemented
43+
"alias.test.sh",
44+
"xtrace.test.sh",
45+
"builtin-dirs.test.sh",
46+
"sh-usage.test.sh",
47+
48+
// ZSH-specific tests
49+
"zsh-assoc.test.sh",
50+
"zsh-idioms.test.sh",
51+
52+
// BLE (bash line editor) specific
53+
"ble-features.test.sh",
54+
"ble-idioms.test.sh",
55+
"ble-unset.test.sh",
56+
57+
// Tests that require external commands or real filesystem
58+
"nul-bytes.test.sh",
59+
"unicode.test.sh",
60+
61+
// Meta/introspection tests
62+
"introspect.test.sh",
63+
"print-source-code.test.sh",
64+
"serialize.test.sh",
65+
"spec-harness-bug.test.sh",
66+
67+
// Known differences / divergence docs (not real tests)
68+
"known-differences.test.sh",
69+
"divergence.test.sh",
70+
71+
// Toysh-specific
72+
"toysh.test.sh",
73+
"toysh-posix.test.sh",
74+
75+
// Blog/exploration tests (not spec tests)
76+
"blog1.test.sh",
77+
"blog2.test.sh",
78+
"blog-other1.test.sh",
79+
"explore-parsing.test.sh",
80+
81+
// Extended globbing - not implemented
82+
"extglob-match.test.sh",
83+
"extglob-files.test.sh",
84+
"globstar.test.sh",
85+
"globignore.test.sh",
86+
"nocasematch-match.test.sh",
87+
88+
// Advanced features not implemented
89+
"builtin-getopts.test.sh", // getopts builtin
90+
"nameref.test.sh", // nameref/declare -n
91+
"var-ref.test.sh", // ${!var} indirect references
92+
"regex.test.sh", // =~ regex matching
93+
"sh-options.test.sh", // shopt options
94+
"sh-options-bash.test.sh",
95+
96+
// Bash-specific builtins not implemented
97+
"builtin-bash.test.sh",
98+
"builtin-type-bash.test.sh",
99+
"builtin-vars.test.sh",
100+
"builtin-meta.test.sh",
101+
"builtin-meta-assign.test.sh",
102+
103+
// Advanced array features
104+
"array-assoc.test.sh", // associative arrays
105+
"array-sparse.test.sh", // sparse arrays
106+
"array-compat.test.sh",
107+
"array-literal.test.sh",
108+
"array-assign.test.sh",
109+
110+
// Complex assignment features
111+
"assign-extended.test.sh",
112+
"assign-deferred.test.sh",
113+
"assign-dialects.test.sh",
114+
115+
// Advanced arithmetic
116+
"arith-dynamic.test.sh",
117+
118+
// Complex redirect features
119+
"redirect-multi.test.sh",
120+
"redirect-command.test.sh",
121+
"redir-order.test.sh",
122+
123+
// Other advanced features
124+
"command-sub-ksh.test.sh",
125+
"vars-bash.test.sh",
126+
"var-op-bash.test.sh",
127+
"type-compat.test.sh",
128+
"shell-grammar.test.sh",
129+
"shell-bugs.test.sh",
130+
"nix-idioms.test.sh",
131+
"paren-ambiguity.test.sh",
132+
"fatal-errors.test.sh",
133+
"for-expr.test.sh",
134+
"glob-bash.test.sh",
135+
"bool-parse.test.sh",
136+
"arg-parse.test.sh",
137+
"append.test.sh",
138+
"bugs.test.sh",
139+
]);
140+
141+
const TEST_FILES = ALL_TEST_FILES.filter((f) => !SKIP_FILES.has(f));
142+
143+
/**
144+
* Truncate script for test name display
145+
*/
146+
function truncateScript(script: string, maxLen = 60): string {
147+
// Normalize whitespace and get first meaningful line(s)
148+
const normalized = script
149+
.split("\n")
150+
.map((l) => l.trim())
151+
.filter((l) => l && !l.startsWith("#"))
152+
.join(" | ");
153+
154+
if (normalized.length <= maxLen) {
155+
return normalized;
156+
}
157+
return `${normalized.slice(0, maxLen - 3)}...`;
158+
}
159+
160+
/**
161+
* Format error message for debugging
162+
*/
163+
function formatError(result: Awaited<ReturnType<typeof runTestCase>>): string {
164+
const lines: string[] = [];
165+
166+
// Show error message first (especially important for UNEXPECTED PASS)
167+
if (result.error) {
168+
lines.push(result.error);
169+
lines.push("");
170+
}
171+
172+
if (result.expectedStdout !== null || result.actualStdout !== undefined) {
173+
lines.push("STDOUT:");
174+
lines.push(` expected: ${JSON.stringify(result.expectedStdout ?? "")}`);
175+
lines.push(` actual: ${JSON.stringify(result.actualStdout ?? "")}`);
176+
}
177+
178+
if (result.expectedStderr !== null || result.actualStderr) {
179+
lines.push("STDERR:");
180+
lines.push(` expected: ${JSON.stringify(result.expectedStderr ?? "")}`);
181+
lines.push(` actual: ${JSON.stringify(result.actualStderr ?? "")}`);
182+
}
183+
184+
if (result.expectedStatus !== null || result.actualStatus !== undefined) {
185+
lines.push("STATUS:");
186+
lines.push(` expected: ${result.expectedStatus ?? "(not checked)"}`);
187+
lines.push(` actual: ${result.actualStatus}`);
188+
}
189+
190+
lines.push("");
191+
lines.push("SCRIPT:");
192+
lines.push(result.testCase.script);
193+
194+
return lines.join("\n");
195+
}
196+
197+
describe("Oils Spec Tests", () => {
198+
for (const fileName of TEST_FILES) {
199+
const filePath = path.join(CASES_DIR, fileName);
200+
201+
describe(fileName, () => {
202+
// Parse must succeed - this is not optional
203+
const content = fs.readFileSync(filePath, "utf-8");
204+
const specFile = parseSpecFile(content, filePath);
205+
206+
// Must have test cases
207+
if (specFile.testCases.length === 0) {
208+
throw new Error(`No test cases found in ${fileName}`);
209+
}
210+
211+
for (const testCase of specFile.testCases) {
212+
// Include truncated script in test name for easier debugging
213+
const scriptPreview = truncateScript(testCase.script);
214+
const testName = `[L${testCase.lineNumber}] ${testCase.name}: ${scriptPreview}`;
215+
216+
it(testName, async () => {
217+
const result = await runTestCase(testCase);
218+
219+
if (result.skipped) {
220+
return;
221+
}
222+
223+
if (!result.passed) {
224+
expect.fail(formatError(result));
225+
}
226+
});
227+
}
228+
});
229+
}
230+
});

0 commit comments

Comments
 (0)