fix: handle empty junit failure lists

This commit is contained in:
Peter Steinberger 2026-05-04 21:51:30 +01:00
parent def3b5d9ce
commit a6ddbcbe2a
No known key found for this signature in database
5 changed files with 71 additions and 3 deletions

View File

@ -19,6 +19,7 @@
### Fixed
- Fixed `crabbox run --junit` so all-passing JUnit files record results instead of leaving the coordinator run stuck when the failure list is empty.
- Fixed native Windows `--shell` runs so multi-statement PowerShell scripts keep their quotes instead of being re-parsed by a nested PowerShell process.
- Removed the static macOS managed-login path so static host VNC cannot be mistaken for a Crabbox-created external instance.
- Excluded macOS AppleDouble `._*` sidecar files from default sync manifests so native Windows archives do not transfer invalid TypeScript/package sidecars.

View File

@ -49,7 +49,11 @@ func parseJUnitResults(files map[string]string) (*TestResultSummary, error) {
if len(files) == 0 {
return nil, nil
}
summary := &TestResultSummary{Format: "junit", Files: make([]string, 0, len(files))}
summary := &TestResultSummary{
Format: "junit",
Files: make([]string, 0, len(files)),
Failed: []TestFailure{},
}
for name, data := range files {
trimmed := strings.TrimSpace(data)
if trimmed == "" {

View File

@ -18,6 +18,24 @@ func TestParseJUnitResults(t *testing.T) {
}
}
func TestParseJUnitResultsInitializesEmptyFailureList(t *testing.T) {
results, err := parseJUnitResults(map[string]string{"junit.xml": `<testsuite name="pkg" tests="1" failures="0" errors="0" skipped="0" time="0.1">
<testcase classname="pkg.TestThing" name="passes"/>
</testsuite>`})
if err != nil {
t.Fatal(err)
}
if results == nil {
t.Fatal("results nil")
}
if results.Failed == nil {
t.Fatalf("failed slice is nil: %#v", results)
}
if len(results.Failed) != 0 {
t.Fatalf("failed=%#v", results.Failed)
}
}
func TestParseMarkedFiles(t *testing.T) {
files := parseMarkedFiles("\n__CRABBOX_RESULT_FILE__:a.xml\n<a/>\n__CRABBOX_RESULT_FILE__:b.xml\n<b/>\n")
if files["a.xml"] != "<a/>" || files["b.xml"] != "<b/>" {

View File

@ -1527,12 +1527,14 @@ function phaseForRunEvent(event: RunEventRecord): string {
}
function boundedTestResults(results: TestResultSummary): TestResultSummary {
const files = Array.isArray(results.files) ? results.files : [];
const failed = Array.isArray(results.failed) ? results.failed : [];
return {
...results,
files: results.files
files: files
.slice(0, MAX_RESULT_FILES)
.map((file) => truncateString(file, MAX_RESULT_STRING_BYTES)),
failed: results.failed.slice(0, MAX_RESULT_FAILURES).map(boundedTestFailure),
failed: failed.slice(0, MAX_RESULT_FAILURES).map(boundedTestFailure),
};
}

View File

@ -825,6 +825,49 @@ describe("fleet run history", () => {
expect(await logs.text()).toBe("ok\n");
});
it("accepts Go nil slices in passing test results", async () => {
const fleet = testFleet();
const create = await fleet.fetch(
request("POST", "/v1/runs", {
body: {
leaseID: "cbx_000000000001",
provider: "aws",
class: "beast",
serverType: "c7a.48xlarge",
command: ["go", "test", "./..."],
},
}),
);
expect(create.status).toBe(201);
const { run } = (await create.json()) as { run: { id: string } };
const finish = await fleet.fetch(
request("POST", `/v1/runs/${run.id}/finish`, {
body: {
exitCode: 0,
log: "ok\n",
results: {
format: "junit",
files: null,
suites: 1,
tests: 1,
failures: 0,
errors: 0,
skipped: 0,
timeSeconds: 0.001,
failed: null,
},
},
}),
);
expect(finish.status).toBe(200);
const finished = (await finish.json()) as {
run: { results?: { files: string[]; failed: unknown[] } };
};
expect(finished.run.results?.files).toEqual([]);
expect(finished.run.results?.failed).toEqual([]);
});
it("records chunked run logs so failures do not disappear from long output", async () => {
const storage = new MemoryStorage();
const fleet = testFleet(storage);