Skip to content

Commit 85a2255

Browse files
committed
Import tests and implement many features
1 parent d18cbc8 commit 85a2255

25 files changed

+7354
-1143
lines changed

src/commands/grep/grep.advanced.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ describe("grep advanced", () => {
150150
},
151151
});
152152
const result = await env.exec("grep -A1 match /test.txt");
153-
expect(result.stdout).toBe("match1\nb\nmatch2\nd\n");
153+
// Separator between non-contiguous groups (GNU grep behavior)
154+
expect(result.stdout).toBe("match1\nb\n--\nmatch2\nd\n");
154155
expect(result.exitCode).toBe(0);
155156
});
156157

src/commands/rg/gitignore.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export class GitignoreParser {
204204
/**
205205
* Hierarchical gitignore manager
206206
*
207-
* Loads .gitignore files from the root down to the current directory,
207+
* Loads .gitignore and .ignore files from the root down to the current directory,
208208
* applying patterns in order (child patterns override parent patterns).
209209
*/
210210
export class GitignoreManager {
@@ -218,7 +218,7 @@ export class GitignoreManager {
218218
}
219219

220220
/**
221-
* Load all .gitignore files from root to the specified path
221+
* Load all .gitignore and .ignore files from root to the specified path
222222
*/
223223
async load(targetPath: string): Promise<void> {
224224
// Build list of directories from root to target
@@ -232,16 +232,19 @@ export class GitignoreManager {
232232
current = parent;
233233
}
234234

235-
// Load .gitignore from each directory
235+
// Load .gitignore and .ignore from each directory
236+
// ripgrep loads them in order: .gitignore, then .ignore (ignore can override)
236237
for (const dir of dirs) {
237-
const gitignorePath = this.fs.resolvePath(dir, ".gitignore");
238-
try {
239-
const content = await this.fs.readFile(gitignorePath);
240-
const parser = new GitignoreParser(dir);
241-
parser.parse(content);
242-
this.parsers.push(parser);
243-
} catch {
244-
// No .gitignore in this directory
238+
for (const filename of [".gitignore", ".ignore"]) {
239+
const ignorePath = this.fs.resolvePath(dir, filename);
240+
try {
241+
const content = await this.fs.readFile(ignorePath);
242+
const parser = new GitignoreParser(dir);
243+
parser.parse(content);
244+
this.parsers.push(parser);
245+
} catch {
246+
// No ignore file in this directory
247+
}
245248
}
246249
}
247250
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Imported ripgrep Tests
2+
3+
These tests are transliterated from [ripgrep](https://github.com/BurntSushi/ripgrep), the original Rust-based search tool.
4+
5+
## Source
6+
7+
Tests were imported from the ripgrep test suite:
8+
- https://github.com/BurntSushi/ripgrep/tree/master/tests
9+
10+
## Files
11+
12+
| File | Source | Description |
13+
|------|--------|-------------|
14+
| `binary.test.ts` | `tests/binary.rs` | Binary file detection and handling |
15+
| `feature.test.ts` | `tests/feature.rs` | Feature tests from GitHub issues |
16+
| `misc.test.ts` | `tests/misc.rs` | Miscellaneous behavior tests |
17+
| `regression.test.ts` | `tests/regression.rs` | Regression tests from bug reports |
18+
19+
## Skipped Test Files
20+
21+
- `json.rs` - JSON output format not implemented
22+
- `multiline.rs` - Multiline matching not implemented
23+
24+
## Skipped Individual Tests
25+
26+
Some tests are skipped due to implementation differences:
27+
- `.ignore` file support (we only support `.gitignore`)
28+
- Complex `-A/-B/-C` flag precedence rules
29+
- Word boundary behavior with non-word characters
30+
31+
## License
32+
33+
ripgrep is licensed under the MIT license. See the [ripgrep repository](https://github.com/BurntSushi/ripgrep) for details.
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/**
2+
* Tests imported from ripgrep: tests/binary.rs
3+
*
4+
* These tests cover binary file detection and handling.
5+
* ripgrep skips binary files by default (files containing NUL bytes).
6+
*
7+
* Note: Many ripgrep binary tests involve --mmap, --binary, and --text flags
8+
* which we don't fully support. This file contains applicable tests.
9+
*/
10+
import { describe, expect, it } from "vitest";
11+
import { Bash } from "../../../Bash.js";
12+
13+
// Simple binary content with NUL byte
14+
const BINARY_CONTENT = "hello\x00world\n";
15+
const TEXT_CONTENT = "hello world\n";
16+
17+
describe("rg binary: basic detection", () => {
18+
it("should skip binary files by default in directory search", async () => {
19+
const bash = new Bash({
20+
cwd: "/home/user",
21+
files: {
22+
"/home/user/text.txt": TEXT_CONTENT,
23+
"/home/user/binary.bin": BINARY_CONTENT,
24+
},
25+
});
26+
const result = await bash.exec("rg hello");
27+
expect(result.exitCode).toBe(0);
28+
// Should only find match in text file, not binary
29+
expect(result.stdout).toBe("text.txt:1:hello world\n");
30+
expect(result.stdout).not.toContain("binary.bin");
31+
});
32+
33+
it("should skip binary files when searching single explicit file", async () => {
34+
const bash = new Bash({
35+
cwd: "/home/user",
36+
files: {
37+
"/home/user/binary.bin": BINARY_CONTENT,
38+
},
39+
});
40+
const result = await bash.exec("rg hello binary.bin");
41+
expect(result.exitCode).toBe(1);
42+
expect(result.stdout).toBe("");
43+
});
44+
45+
it("should detect binary in first 8KB of file", async () => {
46+
const bash = new Bash({
47+
cwd: "/home/user",
48+
files: {
49+
// NUL byte early in file
50+
"/home/user/early.bin": `\x00${"a".repeat(100)}pattern\n`,
51+
},
52+
});
53+
const result = await bash.exec("rg pattern");
54+
expect(result.exitCode).toBe(1);
55+
});
56+
57+
it("should not detect binary if NUL after 8KB sample", async () => {
58+
const bash = new Bash({
59+
cwd: "/home/user",
60+
files: {
61+
// NUL byte after 8KB - won't be detected in sample
62+
"/home/user/late.txt": `pattern\n${"a".repeat(9000)}\x00end\n`,
63+
},
64+
});
65+
const result = await bash.exec("rg pattern");
66+
expect(result.exitCode).toBe(0);
67+
expect(result.stdout).toContain("pattern");
68+
});
69+
});
70+
71+
describe("rg binary: with count flag", () => {
72+
it("should not count matches in binary files", async () => {
73+
const bash = new Bash({
74+
cwd: "/home/user",
75+
files: {
76+
"/home/user/text.txt": "match\nmatch\n",
77+
"/home/user/binary.bin": "match\x00match\n",
78+
},
79+
});
80+
const result = await bash.exec("rg -c match");
81+
expect(result.exitCode).toBe(0);
82+
expect(result.stdout).toBe("text.txt:2\n");
83+
});
84+
});
85+
86+
describe("rg binary: with files-with-matches flag", () => {
87+
it("should not list binary files with -l", async () => {
88+
const bash = new Bash({
89+
cwd: "/home/user",
90+
files: {
91+
"/home/user/text.txt": "findme\n",
92+
"/home/user/binary.bin": "findme\x00\n",
93+
},
94+
});
95+
const result = await bash.exec("rg -l findme");
96+
expect(result.exitCode).toBe(0);
97+
expect(result.stdout).toBe("text.txt\n");
98+
});
99+
});
100+
101+
describe("rg binary: mixed content", () => {
102+
it("should only search text files in mixed directory", async () => {
103+
const bash = new Bash({
104+
cwd: "/home/user",
105+
files: {
106+
"/home/user/readme.md": "documentation\n",
107+
"/home/user/image.png": "\x89PNG\r\n\x1a\n\x00\x00\x00",
108+
"/home/user/script.sh": "echo documentation\n",
109+
},
110+
});
111+
const result = await bash.exec("rg documentation");
112+
expect(result.exitCode).toBe(0);
113+
expect(result.stdout).toContain("readme.md");
114+
expect(result.stdout).toContain("script.sh");
115+
expect(result.stdout).not.toContain("image.png");
116+
});
117+
118+
it("should handle multiple binary and text files", async () => {
119+
const bash = new Bash({
120+
cwd: "/home/user",
121+
files: {
122+
"/home/user/a.txt": "test\n",
123+
"/home/user/b.bin": "test\x00\n",
124+
"/home/user/c.txt": "test\n",
125+
"/home/user/d.bin": "test\x00\n",
126+
},
127+
});
128+
const result = await bash.exec("rg test");
129+
expect(result.exitCode).toBe(0);
130+
expect(result.stdout).toBe("a.txt:1:test\nc.txt:1:test\n");
131+
});
132+
});
133+
134+
describe("rg binary: edge cases", () => {
135+
it("should handle file with only NUL bytes", async () => {
136+
const bash = new Bash({
137+
cwd: "/home/user",
138+
files: {
139+
"/home/user/nulls.bin": "\x00\x00\x00\x00",
140+
},
141+
});
142+
const result = await bash.exec("rg anything");
143+
expect(result.exitCode).toBe(1);
144+
});
145+
146+
it("should handle NUL at start of file", async () => {
147+
const bash = new Bash({
148+
cwd: "/home/user",
149+
files: {
150+
"/home/user/start.bin": "\x00hello world\n",
151+
},
152+
});
153+
const result = await bash.exec("rg hello");
154+
expect(result.exitCode).toBe(1);
155+
});
156+
157+
it("should handle NUL at end of file", async () => {
158+
const bash = new Bash({
159+
cwd: "/home/user",
160+
files: {
161+
"/home/user/end.bin": "hello world\n\x00",
162+
},
163+
});
164+
const result = await bash.exec("rg hello");
165+
expect(result.exitCode).toBe(1);
166+
});
167+
168+
it("should handle multiple NUL bytes", async () => {
169+
const bash = new Bash({
170+
cwd: "/home/user",
171+
files: {
172+
"/home/user/multi.bin": "a\x00b\x00c\x00d\n",
173+
},
174+
});
175+
const result = await bash.exec("rg '[a-d]'");
176+
expect(result.exitCode).toBe(1);
177+
});
178+
});
179+
180+
describe("rg binary: common binary file types", () => {
181+
it("should skip files with common binary signatures", async () => {
182+
const bash = new Bash({
183+
cwd: "/home/user",
184+
files: {
185+
// PNG signature
186+
"/home/user/image.png": "\x89PNG\r\n\x1a\n\x00data",
187+
// PDF signature (simplified)
188+
"/home/user/doc.pdf": "%PDF-1.4\n\x00binary",
189+
// ZIP signature
190+
"/home/user/archive.zip": "PK\x03\x04\x00\x00data",
191+
// Text file for comparison
192+
"/home/user/text.txt": "data\n",
193+
},
194+
});
195+
const result = await bash.exec("rg data");
196+
expect(result.exitCode).toBe(0);
197+
expect(result.stdout).toBe("text.txt:1:data\n");
198+
});
199+
});
200+
201+
describe("rg binary: with other flags", () => {
202+
it("should work with -i flag", async () => {
203+
const bash = new Bash({
204+
cwd: "/home/user",
205+
files: {
206+
"/home/user/text.txt": "HELLO world\n",
207+
"/home/user/binary.bin": "HELLO\x00world\n",
208+
},
209+
});
210+
const result = await bash.exec("rg -i hello");
211+
expect(result.exitCode).toBe(0);
212+
expect(result.stdout).toBe("text.txt:1:HELLO world\n");
213+
});
214+
215+
it("should work with -v flag", async () => {
216+
const bash = new Bash({
217+
cwd: "/home/user",
218+
files: {
219+
"/home/user/text.txt": "keep\nremove\nkeep\n",
220+
"/home/user/binary.bin": "keep\x00remove\n",
221+
},
222+
});
223+
const result = await bash.exec("rg -v remove");
224+
expect(result.exitCode).toBe(0);
225+
expect(result.stdout).toBe("text.txt:1:keep\ntext.txt:3:keep\n");
226+
});
227+
228+
it("should work with -w flag", async () => {
229+
const bash = new Bash({
230+
cwd: "/home/user",
231+
files: {
232+
"/home/user/text.txt": "foo bar\nfoobar\n",
233+
"/home/user/binary.bin": "foo bar\x00\n",
234+
},
235+
});
236+
const result = await bash.exec("rg -w foo");
237+
expect(result.exitCode).toBe(0);
238+
expect(result.stdout).toBe("text.txt:1:foo bar\n");
239+
});
240+
241+
it("should work with context flags", async () => {
242+
const bash = new Bash({
243+
cwd: "/home/user",
244+
files: {
245+
"/home/user/text.txt": "before\nmatch\nafter\n",
246+
"/home/user/binary.bin": "before\x00match\nafter\n",
247+
},
248+
});
249+
const result = await bash.exec("rg -C1 match");
250+
expect(result.exitCode).toBe(0);
251+
expect(result.stdout).toContain("before");
252+
expect(result.stdout).toContain("match");
253+
expect(result.stdout).toContain("after");
254+
expect(result.stdout).not.toContain("binary.bin");
255+
});
256+
257+
it("should work with -m flag", async () => {
258+
const bash = new Bash({
259+
cwd: "/home/user",
260+
files: {
261+
"/home/user/text.txt": "match\nmatch\nmatch\n",
262+
"/home/user/binary.bin": "match\x00match\n",
263+
},
264+
});
265+
const result = await bash.exec("rg -m1 match");
266+
expect(result.exitCode).toBe(0);
267+
expect(result.stdout).toBe("text.txt:1:match\n");
268+
});
269+
});
270+
271+
describe("rg binary: subdirectories", () => {
272+
it("should skip binary files in subdirectories", async () => {
273+
const bash = new Bash({
274+
cwd: "/home/user",
275+
files: {
276+
"/home/user/src/code.ts": "export const x = 1;\n",
277+
"/home/user/assets/image.bin": "export\x00data\n",
278+
"/home/user/lib/util.ts": "export function foo() {}\n",
279+
},
280+
});
281+
const result = await bash.exec("rg export");
282+
expect(result.exitCode).toBe(0);
283+
expect(result.stdout).toContain("src/code.ts");
284+
expect(result.stdout).toContain("lib/util.ts");
285+
expect(result.stdout).not.toContain("assets/image.bin");
286+
});
287+
});

0 commit comments

Comments
 (0)