Skip to content

Commit 3c63df2

Browse files
author
Dave Bartolomeo
committed
Unit tests for SplitBuffer
1 parent c972a5c commit 3c63df2

File tree

2 files changed

+104
-4
lines changed

2 files changed

+104
-4
lines changed

extensions/ql-vscode/src/common/split-stream.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { StringDecoder } from "string_decoder";
44
/**
55
* Buffer to hold state used when splitting a text stream into lines.
66
*/
7-
class SplitBuffer {
7+
export class SplitBuffer {
88
private readonly decoder = new StringDecoder("utf8");
99
private readonly maxSeparatorLength: number;
1010
private buffer = "";
1111
private searchIndex = 0;
12+
private ended = false;
1213

1314
constructor(private readonly separators: readonly string[]) {
1415
this.maxSeparatorLength = separators
@@ -29,7 +30,7 @@ class SplitBuffer {
2930
*/
3031
public end(): void {
3132
this.buffer += this.decoder.end();
32-
this.buffer += this.separators[0]; // Append a separator to the end to ensure the last line is returned.
33+
this.ended = true;
3334
}
3435

3536
/**
@@ -56,7 +57,14 @@ class SplitBuffer {
5657
* line is available.
5758
*/
5859
public getNextLine(): string | undefined {
59-
while (this.searchIndex <= this.buffer.length - this.maxSeparatorLength) {
60+
// If we haven't received all of the input yet, don't search too close to the end of the buffer,
61+
// or we could match a separator that's split across two chunks. For example, we could see "\r"
62+
// at the end of the buffer and match that, even though we were about to receive a "\n" right
63+
// after it.
64+
const maxSearchIndex = this.ended
65+
? this.buffer.length - 1
66+
: this.buffer.length - this.maxSeparatorLength;
67+
while (this.searchIndex <= maxSearchIndex) {
6068
for (const separator of this.separators) {
6169
if (SplitBuffer.startsWith(this.buffer, separator, this.searchIndex)) {
6270
const line = this.buffer.slice(0, this.searchIndex);
@@ -68,7 +76,15 @@ class SplitBuffer {
6876
this.searchIndex++;
6977
}
7078

71-
return undefined;
79+
if (this.ended && this.buffer.length > 0) {
80+
// If we still have some text left in the buffer, return it as the last line.
81+
const line = this.buffer;
82+
this.buffer = "";
83+
this.searchIndex = 0;
84+
return line;
85+
} else {
86+
return undefined;
87+
}
7288
}
7389
}
7490

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { LINE_ENDINGS, SplitBuffer } from "../../../src/common/split-stream";
2+
3+
interface Chunk {
4+
chunk: string;
5+
lines: string[];
6+
}
7+
8+
function checkLines(
9+
buffer: SplitBuffer,
10+
expectedLinesForChunk: string[],
11+
chunkIndex: number | "end",
12+
): void {
13+
expectedLinesForChunk.forEach((expectedLine, lineIndex) => {
14+
const line = buffer.getNextLine();
15+
const location = `[chunk ${chunkIndex}, line ${lineIndex}]: `;
16+
expect(location + line).toEqual(location + expectedLine);
17+
});
18+
expect(buffer.getNextLine()).toBeUndefined();
19+
}
20+
21+
function testSplitBuffer(chunks: Chunk[], endLines: string[]): void {
22+
const buffer = new SplitBuffer(LINE_ENDINGS);
23+
chunks.forEach((chunk, chunkIndex) => {
24+
buffer.addChunk(Buffer.from(chunk.chunk, "utf-8"));
25+
checkLines(buffer, chunk.lines, chunkIndex);
26+
});
27+
buffer.end();
28+
checkLines(buffer, endLines, "end");
29+
}
30+
31+
describe("split buffer", () => {
32+
it("should handle a one-chunk string with no terminator", async () => {
33+
// Won't return the line until we call `end()`.
34+
testSplitBuffer([{ chunk: "some text", lines: [] }], ["some text"]);
35+
});
36+
37+
it("should handle a one-chunk string with a one-byte terminator", async () => {
38+
// Won't return the line until we call `end()` because the actual terminator is shorter than the
39+
// longest terminator.
40+
testSplitBuffer([{ chunk: "some text\n", lines: [] }], ["some text"]);
41+
});
42+
43+
it("should handle a one-chunk string with a two-byte terminator", async () => {
44+
testSplitBuffer([{ chunk: "some text\r\n", lines: ["some text"] }], []);
45+
});
46+
47+
it("should handle a multi-chunk string with terminators at the end of each chunk", async () => {
48+
testSplitBuffer(
49+
[
50+
{ chunk: "first line\n", lines: [] }, // Waiting for second potential terminator byte
51+
{ chunk: "second line\r", lines: ["first line"] }, // Waiting for second potential terminator byte
52+
{ chunk: "third line\r\n", lines: ["second line", "third line"] }, // No wait, because we're at the end
53+
],
54+
[],
55+
);
56+
});
57+
58+
it("should handle a multi-chunk string with terminators at random offsets", async () => {
59+
testSplitBuffer(
60+
[
61+
{ chunk: "first line\nsecond", lines: ["first line"] },
62+
{
63+
chunk: " line\rthird line",
64+
lines: ["second line"],
65+
},
66+
{ chunk: "\r\n", lines: ["third line"] },
67+
],
68+
[],
69+
);
70+
});
71+
72+
it("should handle a terminator split between chunks", async () => {
73+
testSplitBuffer(
74+
[
75+
{ chunk: "first line\r", lines: [] },
76+
{
77+
chunk: "\nsecond line",
78+
lines: ["first line"],
79+
},
80+
],
81+
["second line"],
82+
);
83+
});
84+
});

0 commit comments

Comments
 (0)