Skip to content

Commit 06abb77

Browse files
authored
Use go test -json in updateFailing, detect baseline errors (#2347)
1 parent 20bf4fc commit 06abb77

File tree

2 files changed

+114
-24
lines changed

2 files changed

+114
-24
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,8 @@ jobs:
264264
- name: Regenerate fourslash tests and update failing test list
265265
run: |
266266
set -x
267-
echo "" > ./internal/fourslash/_scripts/failingTests.txt
268267
npm run convertfourslash >/dev/null 2>&1 || true
269-
npx hereby test >/dev/null || true
270-
npx hereby baseline-accept || true
271268
npm run updatefailing >/dev/null 2>&1 || true
272-
npx hereby baseline-accept || true
273269
rm -rf testdata/baselines/reference/fourslash || true
274270
npx hereby test >/dev/null || true
275271
npx hereby baseline-accept || true
Lines changed: 114 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,137 @@
11
import * as cp from "child_process";
22
import * as fs from "fs";
33
import path from "path";
4+
import * as readline from "readline";
45
import which from "which";
56

67
const failingTestsPath = path.join(import.meta.dirname, "failingTests.txt");
78
const crashingTestsPath = path.join(import.meta.dirname, "crashingTests.txt");
89

9-
function main() {
10+
interface TestEvent {
11+
Time?: string;
12+
Action: string;
13+
Package?: string;
14+
Test?: string;
15+
Output?: string;
16+
Elapsed?: number;
17+
}
18+
19+
async function main() {
1020
const go = which.sync("go");
11-
let testOutput: string;
21+
22+
let testProcess: cp.ChildProcess;
1223
try {
1324
// Run tests with TSGO_FOURSLASH_IGNORE_FAILING=1 to run all tests including those in failingTests.txt
14-
testOutput = cp.execFileSync(go, ["test", "-v", "./internal/fourslash/tests/gen"], {
15-
encoding: "utf-8",
25+
testProcess = cp.spawn(go, ["test", "-json", "./internal/fourslash/tests/gen"], {
26+
stdio: ["ignore", "pipe", "pipe"],
1627
env: { ...process.env, TSGO_FOURSLASH_IGNORE_FAILING: "1" },
1728
});
1829
}
1930
catch (error) {
20-
testOutput = (error as { stdout: string; }).stdout as string;
31+
throw new Error("Failed to spawn test process: " + error);
2132
}
22-
const panicRegex = /^panic/m;
23-
if (panicRegex.test(testOutput)) {
24-
throw new Error("Unrecovered panic detected in tests\n" + testOutput);
33+
34+
if (!testProcess.stdout || !testProcess.stderr) {
35+
throw new Error("Test process stdout or stderr is null");
2536
}
26-
const failRegex = /--- FAIL: ([\S]+)/gm;
37+
2738
const failingTests: string[] = [];
28-
const crashingRegex = /^=== (?:NAME|CONT) ([\S]+)\n.*InternalError.*$/gm;
2939
const crashingTests: string[] = [];
30-
let match;
40+
const testOutputs = new Map<string, string[]>();
41+
const allOutputs: string[] = [];
42+
let hadPanic = false;
3143

32-
while ((match = failRegex.exec(testOutput)) !== null) {
33-
failingTests.push(match[1]);
34-
}
35-
while ((match = crashingRegex.exec(testOutput)) !== null) {
36-
crashingTests.push(match[1]);
37-
}
44+
const rl = readline.createInterface({
45+
input: testProcess.stdout,
46+
crlfDelay: Infinity,
47+
});
48+
49+
rl.on("line", line => {
50+
try {
51+
const event: TestEvent = JSON.parse(line);
52+
53+
// Collect output for each test
54+
if (event.Action === "output" && event.Output) {
55+
allOutputs.push(event.Output);
56+
if (event.Test) {
57+
if (!testOutputs.has(event.Test)) {
58+
testOutputs.set(event.Test, []);
59+
}
60+
testOutputs.get(event.Test)!.push(event.Output);
61+
}
62+
63+
// Check for panics
64+
if (/^panic/m.test(event.Output)) {
65+
hadPanic = true;
66+
}
67+
}
68+
69+
// Process failed tests
70+
if (event.Action === "fail" && event.Test) {
71+
const outputs = testOutputs.get(event.Test) || [];
3872

39-
fs.writeFileSync(failingTestsPath, failingTests.sort((a, b) => a.localeCompare(b, "en-US")).join("\n") + "\n", "utf-8");
40-
fs.writeFileSync(crashingTestsPath, crashingTests.sort((a, b) => a.localeCompare(b, "en-US")).join("\n") + "\n", "utf-8");
73+
// Check if this is a crashing test (contains InternalError)
74+
const hasCrash = outputs.some(line => line.includes("InternalError"));
75+
if (hasCrash) {
76+
crashingTests.push(event.Test);
77+
}
78+
79+
// A test is only considered a baseline-only failure if ALL error messages
80+
// are baseline-related. Any non-baseline error message means it's a real failure.
81+
const baselineMessagePatterns = [
82+
/^\s*baseline\.go:\d+: the baseline file .* has changed\./,
83+
/^\s*baseline\.go:\d+: new baseline created at /,
84+
/^\s*baseline\.go:\d+: the baseline file .* does not exist in the TypeScript submodule/,
85+
/^\s*baseline\.go:\d+: the baseline file .* does not match the reference in the TypeScript submodule/,
86+
];
87+
88+
// Check each output line that looks like an error message
89+
// Error messages from Go tests typically contain ".go:" with a line number
90+
const errorLines = outputs.filter(line => /^\s*\w+\.go:\d+:/.test(line));
91+
92+
// If there are no error lines, it's a real failure.
93+
// If all error lines match baseline patterns, it's a baseline-only failure
94+
const isBaselineOnlyFailure = errorLines.length > 0 &&
95+
errorLines.every(line => baselineMessagePatterns.some(pattern => pattern.test(line)));
96+
97+
if (!isBaselineOnlyFailure) {
98+
failingTests.push(event.Test);
99+
}
100+
}
101+
}
102+
catch (e) {
103+
// Not JSON, possibly stderr or other output - ignore
104+
}
105+
});
106+
107+
testProcess.stderr.on("data", data => {
108+
// Check stderr for panics too
109+
const output = data.toString();
110+
allOutputs.push(output);
111+
if (/^panic/m.test(output)) {
112+
hadPanic = true;
113+
}
114+
});
115+
116+
await new Promise<void>((resolve, reject) => {
117+
testProcess.on("close", code => {
118+
if (hadPanic) {
119+
reject(new Error("Unrecovered panic detected in tests\n" + allOutputs.join("")));
120+
return;
121+
}
122+
123+
fs.writeFileSync(failingTestsPath, failingTests.sort((a, b) => a.localeCompare(b, "en-US")).join("\n") + "\n", "utf-8");
124+
fs.writeFileSync(crashingTestsPath, crashingTests.sort((a, b) => a.localeCompare(b, "en-US")).join("\n") + "\n", "utf-8");
125+
resolve();
126+
});
127+
128+
testProcess.on("error", error => {
129+
reject(error);
130+
});
131+
});
41132
}
42133

43-
main();
134+
main().catch(error => {
135+
console.error("Error:", error);
136+
process.exit(1);
137+
});

0 commit comments

Comments
 (0)