Skip to content

Commit 11f2d3c

Browse files
authored
Skip cleanup 30 (#19)
* Fix skipped tests * Comparison tests with fixtures * Support locking for safe recording * lint * Record on CI * Record on CI * Record on CI * lock
1 parent f13cd6c commit 11f2d3c

39 files changed

+3763
-49
lines changed

.github/workflows/comparison-tests.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,13 @@ jobs:
2525

2626
- name: Run comparison tests
2727
run: pnpm test:comparison
28+
29+
- name: Run comparison tests in record mode
30+
run: pnpm test:comparison:record
31+
32+
- name: diff
33+
run: git diff
34+
35+
# Fail if there are any diffs
36+
- name: Fail if there are any diffs
37+
run: if [ -n "$(git diff --name-only)" ]; then exit 1; fi

CLAUDE.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ pnpm knip # Check for unused exports/dependencies
1818
# Testing
1919
pnpm test:run # Run ALL tests (including spec tests)
2020
pnpm test:unit # Run unit tests only (fast, no comparison/spec)
21-
pnpm test:comparison # Run comparison tests only
21+
pnpm test:comparison # Run comparison tests only (uses fixtures)
22+
pnpm test:comparison:record # Re-record comparison test fixtures
2223

2324
# Excluding spec tests (spec tests have known failures)
2425
pnpm test:run --exclude src/spec-tests
@@ -149,11 +150,32 @@ Commands go in `src/commands/<name>/` with:
149150
### Testing Strategy
150151

151152
- **Unit tests**: Fast, isolated tests for specific functionality
152-
- **Comparison tests**: Run same script in just-bash and real bash, compare output
153+
- **Comparison tests**: Compare just-bash output against recorded bash fixtures (see `src/comparison-tests/README.md`)
153154
- **Spec tests** (`src/spec-tests/`): Bash specification conformance (may have known failures)
154155

155156
Prefer comparison tests when uncertain about bash behavior. Keep test files under 300 lines.
156157

158+
### Comparison Tests (Fixture System)
159+
160+
Comparison tests use pre-recorded bash outputs stored in `src/comparison-tests/fixtures/`. This eliminates platform differences (macOS vs Linux). See `src/comparison-tests/README.md` for details.
161+
162+
```bash
163+
# Run comparison tests (uses fixtures, no real bash needed)
164+
pnpm test:comparison
165+
166+
# Re-record fixtures (skips locked fixtures)
167+
RECORD_FIXTURES=1 pnpm test:run src/comparison-tests/mytest.comparison.test.ts
168+
169+
# Force re-record including locked fixtures
170+
RECORD_FIXTURES=force pnpm test:comparison
171+
```
172+
173+
When adding comparison tests:
174+
1. Write the test using `setupFiles()` and `compareOutputs()`
175+
2. Run with `RECORD_FIXTURES=1` to generate fixtures
176+
3. Commit both the test file and the generated fixture JSON
177+
4. If manually adjusting for Linux behavior, add `"locked": true` to the fixture
178+
157179
## Development Guidelines
158180

159181
- Read AGENTS.md

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"test:dist": "vitest run src/cli/just-bash.bundle.test.ts",
7777
"test:unit": "vitest run --config vitest.unit.config.ts",
7878
"test:comparison": "vitest run --config vitest.comparison.config.ts",
79+
"test:comparison:record": "RECORD_FIXTURES=1 vitest run --config vitest.comparison.config.ts",
7980
"shell": "npx tsx src/cli/shell.ts",
8081
"dev:exec": "npx tsx src/cli/exec.ts"
8182
},

src/commands/printf/printf.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,13 @@ describe("printf", () => {
129129
expect(result.exitCode).toBe(0);
130130
});
131131

132-
it.skip("should handle non-numeric for %d", async () => {
133-
// TODO: Bash returns exit 0 with warning, our shell returns exit 1
132+
it("should handle non-numeric for %d", async () => {
133+
// Bash returns exit 1 with warning and outputs 0
134134
const env = new Bash();
135135
const result = await env.exec('printf "%d" notanumber');
136136
expect(result.stdout).toBe("0");
137-
expect(result.exitCode).toBe(0);
137+
expect(result.stderr).toContain("invalid number");
138+
expect(result.exitCode).toBe(1);
138139
});
139140
});
140141

src/commands/sed/sed.limits.test.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ describe("SED Execution Limits", () => {
2323
expect(result.exitCode).toBe(ExecutionLimitError.EXIT_CODE);
2424
});
2525

26-
// TODO: t command with loop needs better substitution tracking
27-
// The t command branches on successful substitution, but s/./&/ replaces
28-
// a character with itself, which doesn't count as "successful" in our impl
29-
it.skip("should protect against test loop (t command)", async () => {
26+
it("should protect against test loop (t command)", async () => {
3027
const env = new Bash();
3128
// Substitution that always succeeds + t branch = infinite loop
3229
const result = await env.exec(
@@ -152,8 +149,7 @@ describe("SED Execution Limits", () => {
152149
expect(result.exitCode).toBeDefined();
153150
});
154151

155-
// TODO: Nested braces parsing not implemented in our sed
156-
it.skip("should handle deeply nested braces", async () => {
152+
it("should handle deeply nested braces", async () => {
157153
const env = new Bash();
158154
// Nested command blocks
159155
const result = await env.exec(`echo "test" | sed '{ { { p } } }'`);

src/comparison-tests/README.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# Comparison Tests
2+
3+
Comparison tests validate that just-bash produces the same output as real bash. They use a **fixture-based system** that records bash outputs once and replays them during tests, eliminating platform-specific differences.
4+
5+
## How It Works
6+
7+
1. **Fixtures** are JSON files containing recorded bash outputs (`src/comparison-tests/fixtures/*.fixtures.json`)
8+
2. **Tests** run commands in just-bash and compare against the recorded fixtures
9+
3. **Record mode** runs real bash and saves outputs to fixtures
10+
11+
## Running Tests
12+
13+
```bash
14+
# Run all comparison tests (uses fixtures, no real bash needed)
15+
pnpm test:comparison
16+
17+
# Run a specific test file
18+
pnpm test:run src/comparison-tests/ls.comparison.test.ts
19+
20+
# Re-record fixtures (runs real bash, skips locked fixtures)
21+
pnpm test:comparison:record
22+
# Or: RECORD_FIXTURES=1 pnpm test:comparison
23+
24+
# Force re-record ALL fixtures including locked ones
25+
RECORD_FIXTURES=force pnpm test:comparison
26+
```
27+
28+
## Adding New Tests
29+
30+
### 1. Add the test case
31+
32+
```typescript
33+
// src/comparison-tests/mycommand.comparison.test.ts
34+
import { afterEach, beforeEach, describe, it } from "vitest";
35+
import {
36+
cleanupTestDir,
37+
compareOutputs,
38+
createTestDir,
39+
setupFiles,
40+
} from "./test-helpers.js";
41+
42+
describe("mycommand - Real Bash Comparison", () => {
43+
let testDir: string;
44+
45+
beforeEach(async () => {
46+
testDir = await createTestDir();
47+
});
48+
49+
afterEach(async () => {
50+
await cleanupTestDir(testDir);
51+
});
52+
53+
it("should do something", async () => {
54+
const env = await setupFiles(testDir, {
55+
"input.txt": "hello world\n",
56+
});
57+
await compareOutputs(env, testDir, "mycommand input.txt");
58+
});
59+
});
60+
```
61+
62+
### 2. Record the fixture
63+
64+
```bash
65+
RECORD_FIXTURES=1 pnpm test:run src/comparison-tests/mycommand.comparison.test.ts
66+
```
67+
68+
This creates `src/comparison-tests/fixtures/mycommand.comparison.fixtures.json`.
69+
70+
### 3. Commit both the test and fixture file
71+
72+
## Updating Fixtures
73+
74+
When bash behavior changes or you need to update expected outputs:
75+
76+
```bash
77+
# Re-record specific test file
78+
RECORD_FIXTURES=1 pnpm test:run src/comparison-tests/ls.comparison.test.ts
79+
80+
# Re-record all fixtures
81+
pnpm test:comparison:record
82+
```
83+
84+
## Handling Platform Differences
85+
86+
The fixture system solves platform differences (macOS vs Linux):
87+
88+
1. **Record once** on any platform
89+
2. **Manually adjust** the fixture to match desired behavior (usually Linux)
90+
3. **Lock the fixture** to prevent accidental overwriting
91+
4. Tests then pass on all platforms
92+
93+
Example: `ls -R` outputs differently on macOS vs Linux:
94+
- macOS: `dir\nfile.txt\n...`
95+
- Linux: `.:\ndir\nfile.txt\n...` (includes ".:" header)
96+
97+
We record on macOS, then edit the fixture to use Linux behavior since our implementation follows Linux.
98+
99+
## Locked Fixtures
100+
101+
Fixtures that have been manually adjusted for platform-specific behavior should be marked as **locked** to prevent accidental overwriting when re-recording:
102+
103+
```json
104+
{
105+
"fixture_id": {
106+
"command": "ls -R",
107+
"files": { ... },
108+
"stdout": ".:\ndir\nfile.txt\n...",
109+
"stderr": "",
110+
"exitCode": 0,
111+
"locked": true
112+
}
113+
}
114+
```
115+
116+
When recording:
117+
- `RECORD_FIXTURES=1` skips locked fixtures and reports them
118+
- `RECORD_FIXTURES=force` overwrites all fixtures including locked ones
119+
120+
Currently locked fixtures:
121+
- `ls -R` - Uses Linux-style output with ".:" header
122+
- `cat -n` with multiple files - Uses continuous line numbering (Linux behavior)
123+
124+
## API Reference
125+
126+
### `setupFiles(testDir, files)`
127+
128+
Sets up test files in both real filesystem and BashEnv.
129+
130+
```typescript
131+
const env = await setupFiles(testDir, {
132+
"file.txt": "content",
133+
"dir/nested.txt": "nested content",
134+
});
135+
```
136+
137+
### `compareOutputs(env, testDir, command, options?)`
138+
139+
Compares just-bash output against recorded fixture.
140+
141+
```typescript
142+
// Basic usage
143+
await compareOutputs(env, testDir, "cat file.txt");
144+
145+
// With options
146+
await compareOutputs(env, testDir, "wc -l file.txt", {
147+
normalizeWhitespace: true, // For BSD/GNU whitespace differences
148+
compareExitCode: false, // Skip exit code comparison
149+
});
150+
```
151+
152+
### `runRealBash(command, cwd)`
153+
154+
Runs a command in real bash (for tests that need direct bash access).
155+
156+
```typescript
157+
const result = await runRealBash("echo hello", testDir);
158+
// result: { stdout, stderr, exitCode }
159+
```
160+
161+
## Fixture File Format
162+
163+
```json
164+
{
165+
"fixture_id_hash": {
166+
"command": "ls -la",
167+
"files": {
168+
"file.txt": "content"
169+
},
170+
"stdout": "file.txt\n",
171+
"stderr": "",
172+
"exitCode": 0
173+
}
174+
}
175+
```
176+
177+
The fixture ID is a hash of (command + files), ensuring each unique test case has its own fixture entry.
178+
179+
## Best Practices
180+
181+
1. **Keep tests focused** - One behavior per test
182+
2. **Use meaningful file content** - Makes debugging easier
183+
3. **Test edge cases** - Empty files, special characters, etc.
184+
4. **Use `normalizeWhitespace`** for commands with platform-specific formatting (wc, column widths)
185+
5. **Commit fixtures** - They're part of the test suite
186+
6. **Re-record when needed** - If you change test files/commands, re-record the fixtures

src/comparison-tests/cat.comparison.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
cleanupTestDir,
44
compareOutputs,
55
createTestDir,
6-
isLinux,
76
setupFiles,
87
} from "./test-helpers.js";
98

@@ -62,8 +61,8 @@ describe("cat command - Real Bash Comparison", () => {
6261
});
6362

6463
// Linux cat -n continues line numbers across files, macOS resets per file
65-
// BashEnv follows Linux behavior, so skip on macOS
66-
it.skipIf(!isLinux)("should match -n with multiple files", async () => {
64+
// BashEnv follows Linux behavior - fixture uses Linux output
65+
it("should match -n with multiple files", async () => {
6766
const env = await setupFiles(testDir, {
6867
"a.txt": "file a line 1\nfile a line 2\n",
6968
"b.txt": "file b line 1\n",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"2cab91f70ea1cb84": {
3+
"command": "alias notexists || echo failed",
4+
"files": {},
5+
"stdout": "failed\n",
6+
"stderr": "/bin/bash: line 1: alias: notexists: not found\n",
7+
"exitCode": 0,
8+
"locked": true
9+
},
10+
"b7f201402670991c": {
11+
"command": "alias greet='echo hi'; unalias greet; alias greet || echo removed",
12+
"files": {},
13+
"stdout": "removed\n",
14+
"stderr": "/bin/bash: line 1: alias: greet: not found\n",
15+
"exitCode": 0,
16+
"locked": true
17+
},
18+
"c2151098e11aee6e": {
19+
"command": "unalias nonexistent || echo not_found",
20+
"files": {},
21+
"stdout": "not_found\n",
22+
"stderr": "/bin/bash: line 1: unalias: nonexistent: not found\n",
23+
"exitCode": 0,
24+
"locked": true
25+
}
26+
}

0 commit comments

Comments
 (0)