A long run's logs pushed the --- FAIL lines out of the last 200 lines the summary was named from, so a failed check could not say which test failed. Pick the lines that name a failure as the output streams, keep them whole at the end of the verdict's report, and say the whole output in the build's log.
286 lines
7.9 KiB
Go
286 lines
7.9 KiB
Go
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 <package> 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<package>` 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
|
|
}
|