package builder import ( "bytes" "fmt" "regexp" "strings" ) // **What failed is picked from the whole of a check's output, as it streams** (novox/hq issue 460). // // A repository's own check kept only the last reportLines lines of what it printed, and named what failed // from those. A long store-backed run prints its logs after the failure: the `--- FAIL:` line of mesh-controller // #218 was pushed out of the tail by the package's own log lines, and the verdict said only // "FAIL 268.072s" — the test that failed could not be named, and the fix was a guess. #220's failure // came a few dozen lines before the end, inside the tail, and was named. The length of what came after the // failure decided it, nothing else. // // So every line is looked at as it is written, and the lines that name what failed are kept whole, wherever // in the output they are: each `--- FAIL:` line with its indented message lines, a `panic:` with its first // frames, a `WARNING: DATA RACE` report, each `FAIL\t` line, a build error with its package, and // the files gofmt lists. The rest is cut as before. // Bounds on what is picked, so a run that fails everywhere still travels in a verdict: the controller cuts a // report to its last 60 KiB (maxCheckReport), and what failed is said at the report's end, so it stays inside. const ( // failureLines and failureBytes bound everything picked from one run. failureLines = 400 failureBytes = 32 << 10 // failureLineBytes bounds one picked line: a message that pastes a whole document is cut, said. failureLineBytes = 1 << 10 // The lines kept after the line that opens each kind of block. failMessageLines = 30 // a --- FAIL's messages panicLines = 40 // a panic's first frames raceLines = 80 // a data race report, to its closing rule buildLines = 40 // a package's build errors gofmtLines = 50 // the files gofmt lists // failedNames bounds how many further failing tests the summary names after the first. failedNames = 5 ) // goError is a compiler's or vet's line: a Go file, a line, and what is wrong there. var goError = regexp.MustCompile(`^\S+\.go:\d+(:\d+)?: `) // raceRule is the line the race detector opens and closes its report with. const raceRule = "==================" type failureBlock int const ( noBlock failureBlock = iota inFail inPanic inRace inBuild inGofmt ) // failures keeps the lines of a check's output that name what failed. It is an io.Writer, fed the same // bytes as the report's tail. type failures struct { partial []byte kept []string size int dropped int block failureBlock indent int // a --- FAIL line's indentation: its messages are indented deeper left int // lines the open block may still keep headers []string tests []string // every failing test's --- FAIL line, in order lastRule bool // the line before was the race detector's rule, which opens its report } func (f *failures) Write(p []byte) (int, error) { f.partial = append(f.partial, p...) for { i := bytes.IndexByte(f.partial, '\n') if i < 0 { break } f.line(strings.TrimRight(string(f.partial[:i]), "\r")) f.partial = f.partial[i+1:] } // A line with no end, longer than any line is kept, is judged by its start. if len(f.partial) > failureLineBytes*4 { f.line(string(f.partial)) f.partial = nil } return len(p), nil } // flush judges what is left of a last line with no newline. func (f *failures) flush() { if len(f.partial) > 0 { f.line(strings.TrimRight(string(f.partial), "\r")) f.partial = nil } } func indentOf(line string) int { return len(line) - len(strings.TrimLeft(line, " \t")) } func (f *failures) keep(line string) { if f.size >= failureBytes || len(f.kept) >= failureLines { f.dropped++ return } if len(line) > failureLineBytes { line = line[:failureLineBytes] + fmt.Sprintf(" … (%d bytes more)", len(line)-failureLineBytes) } f.kept = append(f.kept, line) f.size += len(line) + 1 } func (f *failures) open(block failureBlock, lines int) { f.block, f.left = block, lines } func (f *failures) line(line string) { trimmed := strings.TrimSpace(line) rule := trimmed == raceRule // A line that opens a block ends whatever block was open. if f.opens(line, trimmed) { f.lastRule = rule return } switch f.block { case inFail: if trimmed != "" && indentOf(line) > f.indent && f.left > 0 { f.left-- f.keep(line) f.lastRule = rule return } case inPanic: if f.left > 0 && !strings.HasPrefix(line, "FAIL") && !strings.HasPrefix(line, "ok \t") && !strings.HasPrefix(line, "exit status") { f.left-- f.keep(line) f.lastRule = rule return } case inRace: if f.left > 0 { f.left-- f.keep(line) if rule { f.block = noBlock } f.lastRule = rule return } case inBuild: if f.left > 0 && trimmed != "" && !strings.HasPrefix(line, "FAIL") && !strings.HasPrefix(line, "ok \t") && !strings.HasPrefix(line, "? \t") { f.left-- f.keep(line) f.lastRule = rule return } case inGofmt: if f.left > 0 && trimmed != "" { f.left-- f.keep(line) f.lastRule = rule return } } f.block = noBlock switch { case strings.HasPrefix(line, "# "): // A package's name before its build errors — kept only when an error follows it. f.headers = append(f.headers, line) if len(f.headers) > 4 { f.headers = f.headers[1:] } case len(f.headers) > 0 && goError.MatchString(line): for _, h := range f.headers { f.keep(h) } f.headers = nil f.keep(line) f.open(inBuild, buildLines) case strings.HasPrefix(line, "FAIL\t"): f.headers = nil f.keep(line) default: f.headers = nil } f.lastRule = rule } // opens keeps a line that opens a block of what failed, and opens the block. False for any other line. func (f *failures) opens(line, trimmed string) bool { switch { case strings.HasPrefix(trimmed, "--- FAIL:"): f.headers = nil f.keep(line) f.tests = append(f.tests, trimmed) f.indent = indentOf(line) f.open(inFail, failMessageLines) case strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:"): f.headers = nil f.keep(line) f.open(inPanic, panicLines) case trimmed == "WARNING: DATA RACE": f.headers = nil if f.lastRule { f.keep(raceRule) } f.keep(line) f.open(inRace, raceLines) case strings.HasPrefix(trimmed, "not gofmt'd:"): f.headers = nil f.keep(line) f.open(inGofmt, gofmtLines) default: return false } return true } // String is every line picked, in the order the run printed them, and how many more were left out. func (f *failures) String() string { f.flush() s := strings.Join(f.kept, "\n") if f.dropped > 0 { s += fmt.Sprintf("\n… %d more line(s) naming what failed, past the %d lines or %d KiB kept", f.dropped, failureLines, failureBytes>>10) } return s } // said is what the repository layer's summary says failed: the first thing that failed, whole, then the // further failing tests by name. Empty when nothing was picked. func (f *failures) said() string { picked := f.String() if picked == "" { return "" } first := whatFailed(picked) var more []string for _, t := range f.tests { if t == first { continue } // A subtest's parent fails with it; its name is in the subtest's. name := strings.Fields(strings.TrimPrefix(t, "--- FAIL:")) if len(name) > 0 { more = append(more, name[0]) } } more = withoutParents(more) switch { case len(more) == 0: return first case len(more) > failedNames: return fmt.Sprintf("%s; and %s and %d more", first, strings.Join(more[:failedNames], ", "), len(more)-failedNames) default: return first + "; and " + strings.Join(more, ", ") } } // withoutParents drops a test whose subtest is also named. func withoutParents(names []string) []string { var out []string for _, n := range names { parent := false for _, m := range names { if strings.HasPrefix(m, n+"/") { parent = true break } } if !parent { out = append(out, n) } } return out }