diff --git a/modules/mesh-build-go/cmd/mesh-build-go/levels.go b/modules/mesh-build-go/cmd/mesh-build-go/levels.go index d247f1a9..71af4d52 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/levels.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/levels.go @@ -10,6 +10,7 @@ import ( "encoding/json" "fmt" "regexp" + "slices" "sort" "strings" "time" @@ -52,6 +53,10 @@ type TestTime struct { // Failure is a test that failed, or a package that failed with no test to name (a build that failed, a TestMain that // panicked); Output is the last lines it wrote, up to outputBytes (issue 1901), and Cause the first line of what the // test reported as its error (t.Error, t.Fatal), without its file and line, when go test said which lines those were. +// **A failure that is not one test's assertion carries its real cause too** (issue 1901, its third meeting): the +// "panic:" line (a timeout's with the tests it found running), a "fatal error:" or "signal:" line, a build's first +// error, or in words that the test program exited, while which tests ran or after its tests passed. Test then names the +// tests that were running when the program stopped. A "PASS" or "ok" line is never a failure's words. type Failure struct { Package string `json:"package"` Test string `json:"test,omitempty"` @@ -126,11 +131,18 @@ type reader struct { causes map[string]string // the first error line of a test, by the same key times []TestTime failedIn map[string]bool // packages with a failed test + // running is the tests that started and have not ended, by package, in the order they started: when a package + // fails with no failed test, these were running when its test program stopped (issue 1901). + running map[string][]string + // passed says a package's test program printed "PASS" (its tests passed) before it failed; built says its test + // program failed to build. + passed, built map[string]bool } func newReader(res *TestResult, dir string) *reader { return &reader{res: res, dir: dir, pkgs: map[string]*PackageResult{}, builds: map[string][]string{}, - pkgOut: map[string][]string{}, testOut: map[string][]string{}, causes: map[string]string{}, failedIn: map[string]bool{}} + pkgOut: map[string][]string{}, testOut: map[string][]string{}, causes: map[string]string{}, failedIn: map[string]bool{}, + running: map[string][]string{}, passed: map[string]bool{}, built: map[string]bool{}} } // read takes go test's output. Lines that are not events are kept for a failure that has nothing else to say. @@ -178,12 +190,21 @@ func (r *reader) event(ev event) { p := r.pkg(ev.Package) switch ev.Action { case "output": - r.pkgOut[ev.Package] = keep(r.pkgOut[ev.Package], ev.Output) + // go test's own summary of a package ("PASS", "FAIL", "ok ", "FAIL ") is never a failure's + // words: a "PASS" before a "FAIL " says only that the tests passed and the program failed after them. + switch l := strings.TrimSpace(ev.Output); { + case l == "PASS": + r.passed[ev.Package] = true + case summaryLine(l): + default: + r.pkgOut[ev.Package] = keep(r.pkgOut[ev.Package], ev.Output) + } case "pass": p.Passed, p.Elapsed = true, ev.Elapsed case "fail": p.Passed, p.Elapsed = false, ev.Elapsed if ev.FailedBuild != "" { + r.built[ev.Package] = true r.pkgOut[ev.Package] = append(r.builds[ev.FailedBuild], r.pkgOut[ev.Package]...) } case "skip": @@ -197,7 +218,10 @@ func (r *reader) event(ev event) { if ev.OutputType == "error" && r.causes[key] == "" { r.causes[key] = errorWords(ev.Output) } + case "run": + r.running[ev.Package] = append(r.running[ev.Package], ev.Test) case "pass", "fail", "skip": + r.running[ev.Package] = slices.DeleteFunc(r.running[ev.Package], func(t string) bool { return t == ev.Test }) p := r.pkg(ev.Package) p.Tests++ r.res.Tests++ @@ -207,16 +231,21 @@ func (r *reader) event(ev event) { r.times = append(r.times, TestTime{Package: ev.Package, Test: ev.Test, Elapsed: ev.Elapsed}) if ev.Action == "fail" { r.failedIn[ev.Package] = true - r.res.Failed = append(r.res.Failed, Failure{Package: ev.Package, Test: ev.Test, Output: clip(r.testOut[key], r.causes[key]), Cause: r.causes[key]}) + cause := r.causes[key] + if cause == "" { + cause = causeOf(r.testOut[key]) + } + r.res.Failed = append(r.res.Failed, Failure{Package: ev.Package, Test: ev.Test, Output: clip(r.testOut[key], cause), Cause: cause}) } } } } // clip joins the last lines of a failure's output that fit outputBytes, each cut to outputLineBytes, and says how many -// earlier lines it left out. **When it leaves lines out, its first line is still a real one** (issue 1901/AC3): the failure's -// cause, or failing that the first non-empty line of the output, then the marker. An older checker takes only the first -// non-empty line of the output, and a marker there would be less than the older pair kept. +// earlier lines it left out. **Its first line is the failure's cause** (issue 1901/AC3, AC4), put ahead when the kept +// lines do not start with it, or, with no cause and lines left out, the first non-empty line of the output, then the +// marker. An older checker takes only the first non-empty line of the output, and a marker there would be less than the +// older pair kept. func clip(lines []string, cause string) string { size, from := 0, len(lines) for from > 0 { @@ -227,15 +256,15 @@ func clip(lines []string, cause string) string { size += n from-- } - kept := make([]string, 0, len(lines)-from+1) + kept := make([]string, 0, len(lines)-from+2) + head := strings.TrimSpace(cause) + for i := 0; head == "" && i < from; i++ { + head = strings.TrimSpace(lines[i]) + } + if head != "" && (from > 0 || len(lines) == 0 || strings.TrimSpace(lines[from]) != head) { + kept = append(kept, cutLine(head)) + } if from > 0 { - head := strings.TrimSpace(cause) - for i := 0; head == "" && i < from; i++ { - head = strings.TrimSpace(lines[i]) - } - if head != "" { - kept = append(kept, cutLine(head)) - } kept = append(kept, fmt.Sprintf("... (%d earlier lines)", from)) } for _, l := range lines[from:] { @@ -244,6 +273,86 @@ func clip(lines []string, cause string) string { return strings.Join(kept, "\n") } +// summaryLine says a line is go test's summary of a package: "PASS", "FAIL", "ok