Skip to content

Commit 03f895b

Browse files
committed
Align json output
1 parent 85a2255 commit 03f895b

File tree

7 files changed

+432
-181
lines changed

7 files changed

+432
-181
lines changed

src/commands/rg/imported-tests/README.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,30 @@ Tests were imported from the ripgrep test suite:
1313
|------|--------|-------------|
1414
| `binary.test.ts` | `tests/binary.rs` | Binary file detection and handling |
1515
| `feature.test.ts` | `tests/feature.rs` | Feature tests from GitHub issues |
16-
| `misc.test.ts` | `tests/misc.rs` | Miscellaneous behavior tests |
16+
| `json.test.ts` | `tests/json.rs` | JSON output format |
17+
| `misc.test.ts` | `tests/misc.rs` | Miscellaneous behavior tests + gzip |
18+
| `multiline.test.ts` | `tests/multiline.rs` | Multiline matching tests |
1719
| `regression.test.ts` | `tests/regression.rs` | Regression tests from bug reports |
1820

19-
## Skipped Test Files
21+
## Skipped Tests
2022

21-
- `json.rs` - JSON output format not implemented
22-
- `multiline.rs` - Multiline matching not implemented
23+
Some tests are skipped due to implementation differences:
2324

24-
## Skipped Individual Tests
25+
### json.rs
26+
- `notutf8`, `notutf8_file` - Non-UTF8 file handling not supported
27+
- `crlf`, `r1095_*` - `--crlf` flag not implemented
28+
- `r1412_*` - Requires PCRE2 look-behind
2529

26-
Some tests are skipped due to implementation differences:
30+
### multiline.rs
31+
- Tests using `\p{Any}` Unicode property (not supported in JavaScript regex)
32+
- `--multiline-dotall` flag (not implemented)
33+
34+
### misc.rs
35+
- `compressed_*` for bzip2, xz, lz4, lzma, brotli, zstd, compress (only gzip supported)
36+
37+
### General
2738
- `.ignore` file support (we only support `.gitignore`)
28-
- Complex `-A/-B/-C` flag precedence rules
29-
- Word boundary behavior with non-word characters
39+
- Context messages in JSON output (`-A/-B/-C` context not output as separate messages)
3040

3141
## License
3242

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/**
2+
* JSON output tests imported from ripgrep
3+
*
4+
* Source: https://github.com/BurntSushi/ripgrep/blob/master/tests/json.rs
5+
*
6+
* Not implemented (tests not imported):
7+
* - notutf8, notutf8_file: Non-UTF8 file handling not supported
8+
* - crlf, r1095_missing_crlf, r1095_crlf_empty_match: --crlf flag not implemented
9+
* - r1412_look_behind_match_missing: Requires PCRE2 look-behind
10+
*/
11+
12+
import { describe, expect, it } from "vitest";
13+
import { Bash } from "../../../Bash.js";
14+
15+
const SHERLOCK = `For the Doctor Watsons of this world, as opposed to the Sherlock
16+
Holmeses, success in the province of detective work must always
17+
be, to a very large extent, the result of luck. Sherlock Holmes
18+
can extract a clew from a wisp of straw or a flake of cigar ash;
19+
but Doctor Watson has to have it taken out for him and dusted,
20+
and exhibited clearly, with a label attached.
21+
`;
22+
23+
interface JsonMessage {
24+
type: "begin" | "end" | "match" | "context" | "summary";
25+
data: Record<string, unknown>;
26+
}
27+
28+
function parseJsonLines(output: string): JsonMessage[] {
29+
return output
30+
.trim()
31+
.split("\n")
32+
.filter((line) => line.length > 0)
33+
.map((line) => JSON.parse(line) as JsonMessage);
34+
}
35+
36+
describe("rg json: basic", () => {
37+
it("basic: JSON output structure", async () => {
38+
const bash = new Bash({
39+
cwd: "/home/user",
40+
files: {
41+
"/home/user/sherlock": SHERLOCK,
42+
},
43+
});
44+
const result = await bash.exec("rg --json 'Sherlock Holmes' sherlock");
45+
expect(result.exitCode).toBe(0);
46+
47+
const msgs = parseJsonLines(result.stdout);
48+
49+
// Check begin message
50+
expect(msgs[0].type).toBe("begin");
51+
expect(msgs[0].data.path).toEqual({ text: "sherlock" });
52+
53+
// Check match message
54+
expect(msgs[1].type).toBe("match");
55+
expect(msgs[1].data.path).toEqual({ text: "sherlock" });
56+
expect(msgs[1].data.lines).toEqual({
57+
text: "be, to a very large extent, the result of luck. Sherlock Holmes\n",
58+
});
59+
expect(msgs[1].data.line_number).toBe(3);
60+
expect(msgs[1].data.absolute_offset).toBe(129);
61+
const submatches = msgs[1].data.submatches as Array<{
62+
match: { text: string };
63+
start: number;
64+
end: number;
65+
}>;
66+
expect(submatches.length).toBe(1);
67+
expect(submatches[0].match).toEqual({ text: "Sherlock Holmes" });
68+
expect(submatches[0].start).toBe(48);
69+
expect(submatches[0].end).toBe(63);
70+
71+
// Check end message
72+
expect(msgs[2].type).toBe("end");
73+
expect(msgs[2].data.path).toEqual({ text: "sherlock" });
74+
expect(msgs[2].data.binary_offset).toBeNull();
75+
76+
// Check summary message
77+
expect(msgs[3].type).toBe("summary");
78+
const stats = msgs[3].data.stats as { searches_with_match: number };
79+
expect(stats.searches_with_match).toBe(1);
80+
});
81+
82+
it("replacement: JSON output with replacement text", async () => {
83+
const bash = new Bash({
84+
cwd: "/home/user",
85+
files: {
86+
"/home/user/sherlock": SHERLOCK,
87+
},
88+
});
89+
const result = await bash.exec(
90+
"rg --json 'Sherlock Holmes' -r 'John Watson' sherlock",
91+
);
92+
expect(result.exitCode).toBe(0);
93+
94+
const msgs = parseJsonLines(result.stdout);
95+
expect(msgs[1].type).toBe("match");
96+
const submatches = msgs[1].data.submatches as Array<{
97+
match: { text: string };
98+
replacement: { text: string };
99+
}>;
100+
expect(submatches[0].replacement).toEqual({ text: "John Watson" });
101+
});
102+
103+
it("quiet_stats: JSON with --quiet shows only summary", async () => {
104+
const bash = new Bash({
105+
cwd: "/home/user",
106+
files: {
107+
"/home/user/sherlock": SHERLOCK,
108+
},
109+
});
110+
const result = await bash.exec(
111+
"rg --json --quiet 'Sherlock Holmes' sherlock",
112+
);
113+
expect(result.exitCode).toBe(0);
114+
// ripgrep behavior: --quiet --json outputs only the summary
115+
const msgs = parseJsonLines(result.stdout);
116+
expect(msgs.length).toBe(1);
117+
expect(msgs[0].type).toBe("summary");
118+
const stats = msgs[0].data.stats as { searches_with_match: number };
119+
expect(stats.searches_with_match).toBe(1);
120+
});
121+
});
122+
123+
describe("rg json: multiple matches", () => {
124+
it("should output all matches with correct submatches", async () => {
125+
const bash = new Bash({
126+
cwd: "/home/user",
127+
files: {
128+
"/home/user/test.txt": "foo bar foo baz foo\n",
129+
},
130+
});
131+
const result = await bash.exec("rg --json foo test.txt");
132+
expect(result.exitCode).toBe(0);
133+
134+
const msgs = parseJsonLines(result.stdout);
135+
const match = msgs.find((m) => m.type === "match");
136+
expect(match).toBeDefined();
137+
138+
const submatches = match?.data.submatches as Array<{
139+
match: { text: string };
140+
start: number;
141+
end: number;
142+
}>;
143+
expect(submatches.length).toBe(3);
144+
expect(submatches[0]).toEqual({ match: { text: "foo" }, start: 0, end: 3 });
145+
expect(submatches[1]).toEqual({
146+
match: { text: "foo" },
147+
start: 8,
148+
end: 11,
149+
});
150+
expect(submatches[2]).toEqual({
151+
match: { text: "foo" },
152+
start: 16,
153+
end: 19,
154+
});
155+
});
156+
157+
it("should output multiple files with begin/end messages", async () => {
158+
const bash = new Bash({
159+
cwd: "/home/user",
160+
files: {
161+
"/home/user/a.txt": "hello\n",
162+
"/home/user/b.txt": "hello\n",
163+
},
164+
});
165+
const result = await bash.exec("rg --json hello");
166+
expect(result.exitCode).toBe(0);
167+
168+
const msgs = parseJsonLines(result.stdout);
169+
170+
// Should have begin/match/end for each file plus summary
171+
const begins = msgs.filter((m) => m.type === "begin");
172+
const ends = msgs.filter((m) => m.type === "end");
173+
const matches = msgs.filter((m) => m.type === "match");
174+
const summaries = msgs.filter((m) => m.type === "summary");
175+
176+
expect(begins.length).toBe(2);
177+
expect(ends.length).toBe(2);
178+
expect(matches.length).toBe(2);
179+
expect(summaries.length).toBe(1);
180+
});
181+
});
182+
183+
describe("rg json: edge cases", () => {
184+
it("should output summary even with no matches", async () => {
185+
const bash = new Bash({
186+
cwd: "/home/user",
187+
files: {
188+
"/home/user/test.txt": "hello world\n",
189+
},
190+
});
191+
const result = await bash.exec("rg --json notfound");
192+
expect(result.exitCode).toBe(1);
193+
194+
// Always outputs summary with stats
195+
const msgs = parseJsonLines(result.stdout);
196+
const summary = msgs.find((m) => m.type === "summary");
197+
expect(summary).toBeDefined();
198+
const stats = summary?.data.stats as { searches_with_match: number };
199+
expect(stats.searches_with_match).toBe(0);
200+
});
201+
202+
it("should handle empty file with summary", async () => {
203+
const bash = new Bash({
204+
cwd: "/home/user",
205+
files: {
206+
"/home/user/empty.txt": "",
207+
},
208+
});
209+
const result = await bash.exec("rg --json foo empty.txt");
210+
expect(result.exitCode).toBe(1);
211+
212+
const msgs = parseJsonLines(result.stdout);
213+
const summary = msgs.find((m) => m.type === "summary");
214+
expect(summary).toBeDefined();
215+
const stats = summary?.data.stats as {
216+
searches_with_match: number;
217+
bytes_searched: number;
218+
};
219+
expect(stats.searches_with_match).toBe(0);
220+
expect(stats.bytes_searched).toBe(0);
221+
});
222+
});

src/commands/rg/imported-tests/misc.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,3 +547,56 @@ describe("rg misc: smart case", () => {
547547
expect(result.stdout).not.toContain("Test\n");
548548
});
549549
});
550+
551+
// Gzip tests from ripgrep misc.rs (compressed_gzip, etc.)
552+
// Note: Only gzip is supported, not bzip2, xz, lz4, lzma, brotli, zstd, or compress
553+
describe("rg misc: compressed files (-z)", () => {
554+
// Dynamically import gzipSync to avoid issues
555+
const { gzipSync } = require("node:zlib");
556+
557+
it("compressed_gzip: should search in gzip compressed files with -z", async () => {
558+
const compressed = gzipSync(Buffer.from(SHERLOCK));
559+
const bash = new Bash({
560+
cwd: "/home/user",
561+
files: {
562+
"/home/user/sherlock.gz": compressed,
563+
},
564+
});
565+
const result = await bash.exec("rg -z Sherlock sherlock.gz");
566+
expect(result.exitCode).toBe(0);
567+
expect(result.stdout).toContain("Sherlock");
568+
});
569+
570+
it("should search gzip files recursively with -z", async () => {
571+
const compressed = gzipSync(Buffer.from("hello world\n"));
572+
const bash = new Bash({
573+
cwd: "/home/user",
574+
files: {
575+
"/home/user/test.gz": compressed,
576+
"/home/user/plain.txt": "hello there\n",
577+
},
578+
});
579+
const result = await bash.exec("rg -z hello");
580+
expect(result.exitCode).toBe(0);
581+
expect(result.stdout).toContain("test.gz");
582+
expect(result.stdout).toContain("plain.txt");
583+
});
584+
585+
it("should not decompress without -z flag", async () => {
586+
const compressed = gzipSync(Buffer.from("hello world\n"));
587+
const bash = new Bash({
588+
cwd: "/home/user",
589+
files: {
590+
"/home/user/test.gz": compressed,
591+
"/home/user/plain.txt": "hello there\n",
592+
},
593+
});
594+
const result = await bash.exec("rg hello");
595+
expect(result.exitCode).toBe(0);
596+
// Should only find the plain text file
597+
expect(result.stdout).toBe("plain.txt:1:hello there\n");
598+
});
599+
600+
// Note: compressed_failing_gzip test not implemented - we don't validate gzip magic bytes
601+
// before attempting decompression
602+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Multiline tests imported from ripgrep
3+
*
4+
* Source: https://github.com/BurntSushi/ripgrep/blob/master/tests/multiline.rs
5+
*
6+
* Note: Tests using \p{Any} Unicode property or --multiline-dotall are skipped
7+
* as JavaScript regex doesn't support Unicode properties and we don't implement
8+
* --multiline-dotall.
9+
*/
10+
11+
import { describe, expect, it } from "vitest";
12+
import { Bash } from "../../../Bash.js";
13+
14+
describe("rg multiline: basic overlapping matches", () => {
15+
// This tests that multiline matches that span multiple lines, but where
16+
// multiple matches may begin and end on the same line work correctly.
17+
it("overlap1: multiline matches spanning lines", async () => {
18+
const bash = new Bash({
19+
cwd: "/home/user",
20+
files: {
21+
"/home/user/test": "xxx\nabc\ndefxxxabc\ndefxxx\nxxx",
22+
},
23+
});
24+
const result = await bash.exec("rg -n -U 'abc\\ndef' test");
25+
expect(result.exitCode).toBe(0);
26+
expect(result.stdout).toBe("2:abc\n3:defxxxabc\n4:defxxx\n");
27+
});
28+
29+
// Like overlap1, but tests the case where one match ends at precisely the same
30+
// location at which the next match begins.
31+
it("overlap2: adjacent multiline matches", async () => {
32+
const bash = new Bash({
33+
cwd: "/home/user",
34+
files: {
35+
"/home/user/test": "xxx\nabc\ndefabc\ndefxxx\nxxx",
36+
},
37+
});
38+
const result = await bash.exec("rg -n -U 'abc\\ndef' test");
39+
expect(result.exitCode).toBe(0);
40+
expect(result.stdout).toBe("2:abc\n3:defabc\n4:defxxx\n");
41+
});
42+
});
43+
44+
describe("rg multiline: dot behavior", () => {
45+
const SHERLOCK = `For the Doctor Watsons of this world, as opposed to the Sherlock
46+
Holmeses, success in the province of detective work must always
47+
be, to a very large extent, the result of luck. Sherlock Holmes
48+
can extract a clew from a wisp of straw or a flake of cigar ash;
49+
but Doctor Watson has to have it taken out for him and dusted,
50+
and exhibited clearly, with a label attached.
51+
`;
52+
53+
// Tests that even in a multiline search, a '.' does not match a newline.
54+
it("dot_no_newline: dot does not match newline in multiline mode", async () => {
55+
const bash = new Bash({
56+
cwd: "/home/user",
57+
files: {
58+
"/home/user/sherlock": SHERLOCK,
59+
},
60+
});
61+
// Pattern tries to match "of this world" followed by any chars and "detective work"
62+
// With standard multiline (no dotall), . doesn't match \n, so this should fail
63+
const result = await bash.exec(
64+
"rg -n -U 'of this world.+detective work' sherlock",
65+
);
66+
expect(result.exitCode).toBe(1);
67+
expect(result.stdout).toBe("");
68+
});
69+
70+
// NOTE: dot_all test is skipped - requires --multiline-dotall flag which is not implemented
71+
});
72+
73+
// NOTE: The following tests from multiline.rs are skipped because they use
74+
// \p{Any} which is a Unicode property not supported in JavaScript regex:
75+
// - only_matching
76+
// - vimgrep
77+
// - stdin
78+
// - context

0 commit comments

Comments
 (0)