golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/greplogs/internal/logparse/failure.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package logparse 6 7 import ( 8 "fmt" 9 "regexp" 10 "strconv" 11 "strings" 12 "sync" 13 ) 14 15 // Failure records a failure extracted from an all.bash log. 16 type Failure struct { 17 // Package is the Go package of this failure. In the case of a 18 // testing.T failure, this will be the package of the test. 19 Package string 20 21 // Test identifies the failed test function. If this is not a 22 // testing.T failure, this will be "". 23 Test string 24 25 // Message is the summarized failure message. This will be one 26 // line of text. 27 Message string 28 29 // FullMessage is a substring of the log that captures the 30 // entire failure message. It may be many lines long. 31 FullMessage string 32 33 // Function is the fully qualified name of the function where 34 // this failure happened, if known. This helps distinguish 35 // between generic errors like "out of bounds" and is more 36 // stable for matching errors than file/line. 37 Function string 38 39 // File is the source file where this failure happened, if 40 // known. 41 File string 42 43 // Line is the source line where this failure happened, if 44 // known. 45 Line int 46 47 // OS and Arch are the GOOS and GOARCH of this failure. 48 OS, Arch string 49 } 50 51 func (f Failure) String() string { 52 s := f.Package 53 if f.Test != "" { 54 s += "." + f.Test 55 } 56 if f.Function != "" || f.File != "" { 57 if s != "" { 58 s += " " 59 } 60 if f.Function != "" { 61 s += "at " + f.Function 62 } else { 63 s += "at " + f.File 64 if f.Line != 0 { 65 s += fmt.Sprintf(":%d", f.Line) 66 } 67 } 68 } 69 if s != "" { 70 s += ": " 71 } 72 s += f.Message 73 return s 74 } 75 76 func (f *Failure) canonicalMessage() string { 77 // Do we need to do anything to the message? 78 for _, c := range f.Message { 79 if '0' <= c && c <= '9' { 80 goto rewrite 81 } 82 } 83 return f.Message 84 85 rewrite: 86 // Canonicalize any "word" of the message containing numbers. 87 // 88 // TODO: "Escape" any existing … to make this safe as a key 89 // for later use with canonicalFields (direct use is 90 // unimportant). 91 return numberWords.ReplaceAllString(f.Message, "…") 92 } 93 94 // numberWords matches words that consist of both letters and 95 // digits. Since this is meant to canonicalize numeric fields 96 // of error messages, we accept any Unicode letter, but only 97 // digits 0-9. We match the whole word to catch things like 98 // hexadecimal and temporary file names. 99 var numberWords = regexp.MustCompile(`\pL*[0-9][\pL0-9]*`) 100 101 var ( 102 linesStar = `(?:.*\n)*?` 103 linesPlus = `(?:.*\n)+?` 104 105 // failPkg matches the FAIL line for a package. 106 // 107 // In case of failure the Android wrapper prints "exitcode=1" without a newline, 108 // so for logs prior to the fix for https://golang.org/issue/49317 we need to 109 // strip that from the beginning of the line. 110 failPkg = `(?m:^(?:exitcode=1)?FAIL[ \t]+(\S+))` 111 112 // logTruncated matches the "log truncated" line injected by the coordinator. 113 logTruncated = `(?:\n\.\.\. log truncated \.\.\.)` 114 115 endOfTest = `(?:` + failPkg + `|` + logTruncated + `)` 116 117 canonLine = regexp.MustCompile(`\r+\n`) 118 119 // testingHeader matches the beginning of the go test std 120 // section. On Plan 9 there used to be just one #. 121 testingHeader = regexp.MustCompile(`^#+ Testing packages`) 122 123 // sectionHeader matches the header of each testing section 124 // printed by go tool dist test. 125 sectionHeader = regexp.MustCompile(`^##### (.*)`) 126 127 // testingFailed matches a testing.T failure. This may be a 128 // T.Error or a recovered panic. There was a time when the 129 // test name included GOMAXPROCS (like how benchmark names 130 // do), so we strip that out. 131 testingFailed = regexp.MustCompile(`^--- FAIL: ([^-\s]+).*\n(` + linesStar + `)` + endOfTest) 132 133 // testingError matches the file name and message of the last 134 // T.Error in a testingFailed log. 135 testingError = regexp.MustCompile(`(?:.*\n)*\t([^:]+):([0-9]+): (.*)\n`) 136 137 // testingPanic matches a recovered panic in a testingFailed 138 // log. 139 testingPanic = regexp.MustCompile(`panic: (.*?)(?: \[recovered\])`) 140 141 // gotestFailed matches a $GOROOT/test failure. 142 gotestFailed = regexp.MustCompile(`^# go run run\.go.*\n(` + linesPlus + `)` + endOfTest) 143 144 // buildFailed matches build failures from the testing package. 145 buildFailed = regexp.MustCompile(`^` + failPkg + `\s+\[build failed\]`) 146 147 // timeoutPanic1 matches a test timeout detected by the testing package. 148 timeoutPanic1 = regexp.MustCompile(`^panic: test timed out after .*\n(` + linesStar + `)` + endOfTest) 149 150 // timeoutPanic2 matches a test timeout detected by go test. 151 timeoutPanic2 = regexp.MustCompile(`^\*\*\* Test killed.*ran too long\n` + endOfTest) 152 153 // coordinatorTimeout matches a test timeout detected by the 154 // coordinator, for both non-sharded and sharded tests. 155 coordinatorTimeout = regexp.MustCompile(`(?m)^Build complete.*Result: error: timed out|^Test "[^"]+" ran over [0-9a-z]+ limit`) 156 157 // tbEntry is a regexp string that matches a single 158 // function/line number entry in a traceback. Group 1 matches 159 // the fully qualified function name. Groups 2 and 3 match the 160 // file name and line number. 161 // Most entries have trailing stack metadata for each frame, 162 // but inlined calls, lacking a frame, may omit that metadata. 163 tbEntry = `(\S+)\(.*\)\n\t(.*):([0-9]+)(?: .*)?\n` 164 165 // runtimeFailed matches a runtime throw or testing package 166 // panic. Matching the panic is fairly loose because in some 167 // cases a "fatal error:" can be preceded by a "panic:" if 168 // we've started the panic and then realize we can't (e.g., 169 // sigpanic). Also gather up the "runtime:" prints preceding a 170 // throw. 171 runtimeFailed = regexp.MustCompile(`^(?:runtime:.*\n)*.*(?:panic: |fatal error: )(.*)`) 172 runtimeLiterals = []string{"runtime:", "panic:", "fatal error:"} 173 runtimeFailedTrailer = regexp.MustCompile(`^(?:exit status.*\n)?(?:\*\*\* Test killed.*\n)?` + endOfTest + `?`) 174 175 // apiCheckerFailed matches an API checker failure. 176 apiCheckerFailed = regexp.MustCompile(`^Error running API checker: (.*)`) 177 178 // goodLine matches known-good lines so we can ignore them 179 // before doing more aggressive/fuzzy failure extraction. 180 goodLine = regexp.MustCompile(`^#|^ok\s|^\?\s|^Benchmark|^PASS|^=== |^--- `) 181 182 // testingUnknownFailed matches the last line of some unknown 183 // failure detected by the testing package. 184 testingUnknownFailed = regexp.MustCompile(`^` + endOfTest) 185 186 // miscFailed matches the log.Fatalf in go tool dist test when 187 // a test fails. We use this as a last resort, mostly to pick 188 // up failures in sections that don't use the testing package. 189 miscFailed = regexp.MustCompile(`^.*Failed: (?:exit status|test failed)`) 190 ) 191 192 // An extractCache speeds up failure extraction from multiple logs by 193 // caching known lines. It is *not* thread-safe, so we track it in a 194 // sync.Pool. 195 type extractCache struct { 196 boringLines map[string]bool 197 } 198 199 var extractCachePool sync.Pool 200 201 func init() { 202 extractCachePool.New = func() interface{} { 203 return &extractCache{make(map[string]bool)} 204 } 205 } 206 207 // Extract parses the failures from all.bash log m. 208 func Extract(m string, os, arch string) ([]*Failure, error) { 209 fs := []*Failure{} 210 testingStarted := false 211 section := "" 212 sectionHeaderFailures := 0 // # failures at section start 213 unknown := []string{} 214 cache := extractCachePool.Get().(*extractCache) 215 defer extractCachePool.Put(cache) 216 217 // Canonicalize line endings. Note that some logs have a mix 218 // of line endings and some somehow have multiple \r's. 219 m = canonLine.ReplaceAllString(m, "\n") 220 221 var s []string 222 matcher := newMatcher(m) 223 consume := func(r *regexp.Regexp) bool { 224 matched := matcher.consume(r) 225 s = matcher.groups 226 if matched && !strings.HasSuffix(s[0], "\n") { 227 // Consume the rest of the line. 228 matcher.line() 229 } 230 return matched 231 } 232 firstBadLine := func() string { 233 for _, u := range unknown { 234 if len(u) > 0 { 235 return u 236 } 237 } 238 return "" 239 } 240 241 for !matcher.done() { 242 // Check for a cached result. 243 line, nextLinePos := matcher.peekLine() 244 isGoodLine, cached := cache.boringLines[line] 245 246 // Process the line. 247 isKnown := true 248 switch { 249 case cached: 250 matcher.pos = nextLinePos 251 if !isGoodLine { 252 // This line is known to not match any 253 // regexps. Follow the default case. 254 isKnown = false 255 unknown = append(unknown, line) 256 } 257 258 case consume(testingHeader): 259 testingStarted = true 260 261 case consume(sectionHeader): 262 section = s[1] 263 sectionHeaderFailures = len(fs) 264 265 case consume(testingFailed): 266 f := &Failure{ 267 Test: s[1], 268 Package: s[3], 269 FullMessage: s[0], 270 Message: "unknown testing.T failure", 271 } 272 273 // TODO: Can have multiple errors per FAIL: 274 // ../fetchlogs/rev/2015-03-24T19:51:21-41f9c43/linux-arm64-canonical 275 276 sError := testingError.FindStringSubmatch(s[2]) 277 sPanic := testingPanic.FindStringSubmatch(s[2]) 278 if sError != nil { 279 f.File, f.Line, f.Message = sError[1], atoi(sError[2]), sError[3] 280 } else if sPanic != nil { 281 f.Function, f.File, f.Line = panicWhere(s[2]) 282 f.Message = sPanic[1] 283 } 284 285 fs = append(fs, f) 286 287 case consume(gotestFailed): 288 fs = append(fs, &Failure{ 289 Package: "test/" + s[2], 290 FullMessage: s[0], 291 Message: firstLine(s[1]), 292 }) 293 294 case consume(buildFailed): 295 // This may have an accompanying compiler 296 // crash, but it's interleaved with other "ok" 297 // lines, so it's hard to find. 298 fs = append(fs, &Failure{ 299 FullMessage: s[0], 300 Message: "build failed", 301 Package: s[1], 302 }) 303 304 case consume(timeoutPanic1): 305 fs = append(fs, &Failure{ 306 Test: testFromTraceback(s[1]), 307 FullMessage: s[0], 308 Message: "test timed out", 309 Package: s[2], 310 }) 311 312 case consume(timeoutPanic2): 313 tb := strings.Join(unknown, "\n") 314 fs = append(fs, &Failure{ 315 Test: testFromTraceback(tb), 316 FullMessage: tb + "\n" + s[0], 317 Message: "test timed out", 318 Package: s[1], 319 }) 320 321 case matcher.lineHasLiteral(runtimeLiterals...) && consume(runtimeFailed): 322 start := matcher.matchPos 323 msg := s[1] 324 pkg := "testing" 325 if strings.Contains(s[0], "fatal error:") { 326 pkg = "runtime" 327 } 328 traceback := consumeTraceback(matcher) 329 matcher.consume(runtimeFailedTrailer) 330 fn, file, line := panicWhere(traceback) 331 fs = append(fs, &Failure{ 332 Package: pkg, 333 FullMessage: matcher.str[start:matcher.pos], 334 Message: msg, 335 Function: fn, 336 File: file, 337 Line: line, 338 }) 339 340 case consume(apiCheckerFailed): 341 fs = append(fs, &Failure{ 342 Package: "API checker", 343 FullMessage: s[0], 344 Message: s[1], 345 }) 346 347 case consume(goodLine): 348 // Ignore. Just cache and clear unknown. 349 cache.boringLines[line] = true 350 351 case consume(testingUnknownFailed): 352 fs = append(fs, &Failure{ 353 Package: s[1], 354 FullMessage: s[0], 355 Message: "unknown failure: " + firstBadLine(), 356 }) 357 358 case len(fs) == sectionHeaderFailures && consume(miscFailed): 359 fs = append(fs, &Failure{ 360 Package: section, 361 FullMessage: s[0], 362 Message: "unknown failure: " + firstBadLine(), 363 }) 364 365 default: 366 isKnown = false 367 unknown = append(unknown, line) 368 cache.boringLines[line] = false 369 matcher.pos = nextLinePos 370 } 371 372 // Clear unknown lines on any known line. 373 if isKnown { 374 unknown = unknown[:0] 375 } 376 } 377 378 // TODO: FullMessages for these. 379 if len(fs) == 0 && strings.Contains(m, "no space left on device") { 380 fs = append(fs, &Failure{ 381 Message: "build failed (no space left on device)", 382 }) 383 } 384 if len(fs) == 0 && coordinatorTimeout.MatchString(m) { 385 // all.bash was killed by coordinator. 386 fs = append(fs, &Failure{ 387 Message: "build failed (timed out)", 388 }) 389 } 390 if len(fs) == 0 && strings.Contains(m, "Failed to schedule") { 391 // Test sharding failed. 392 fs = append(fs, &Failure{ 393 Message: "build failed (failed to schedule)", 394 }) 395 } 396 if len(fs) == 0 && strings.Contains(m, "nosplit stack overflow") { 397 fs = append(fs, &Failure{ 398 Message: "build failed (nosplit stack overflow)", 399 }) 400 } 401 402 // If the same (message, where) shows up in more than five 403 // packages, it's probably a systemic issue, so collapse it 404 // down to one failure with no package. 405 type dedup struct { 406 packages map[string]bool 407 kept bool 408 } 409 msgDedup := map[Failure]*dedup{} 410 failureMap := map[*Failure]*dedup{} 411 maxCount := 0 412 for _, f := range fs { 413 key := Failure{ 414 Message: f.canonicalMessage(), 415 Function: f.Function, 416 File: f.File, 417 Line: f.Line, 418 } 419 420 d := msgDedup[key] 421 if d == nil { 422 d = &dedup{packages: map[string]bool{}} 423 msgDedup[key] = d 424 } 425 d.packages[f.Package] = true 426 if len(d.packages) > maxCount { 427 maxCount = len(d.packages) 428 } 429 failureMap[f] = d 430 } 431 if maxCount >= 5 { 432 fsn := []*Failure{} 433 for _, f := range fs { 434 d := failureMap[f] 435 if len(d.packages) < 5 { 436 fsn = append(fsn, f) 437 } else if !d.kept { 438 d.kept = true 439 f.Test, f.Package = "", "" 440 fsn = append(fsn, f) 441 } 442 } 443 fs = fsn 444 } 445 446 // Check if we even got as far as testing. Note that there was 447 // a period when we didn't print the "testing" header, so as 448 // long as we found failures, we don't care if we found the 449 // header. 450 if !testingStarted && len(fs) == 0 { 451 fs = append(fs, &Failure{ 452 Message: "toolchain build failed", 453 }) 454 } 455 456 for _, f := range fs { 457 f.OS, f.Arch = os, arch 458 459 // Clean up package. For misc/cgo tests, this will be 460 // something like 461 // _/tmp/buildlet-scatch825855615/go/misc/cgo/test. 462 if strings.HasPrefix(f.Package, "_/tmp/") { 463 f.Package = strings.SplitN(f.Package, "/", 4)[3] 464 } 465 466 // Trim trailing newlines from FullMessage. 467 f.FullMessage = strings.TrimRight(f.FullMessage, "\n") 468 } 469 return fs, nil 470 } 471 472 func atoi(s string) int { 473 v, err := strconv.Atoi(s) 474 if err != nil { 475 panic("expected number, got " + s) 476 } 477 return v 478 } 479 480 // firstLine returns the first line from s, not including the line 481 // terminator. 482 func firstLine(s string) string { 483 if i := strings.Index(s, "\n"); i >= 0 { 484 return s[:i] 485 } 486 return s 487 } 488 489 var ( 490 tracebackStart = regexp.MustCompile(`^(goroutine [0-9]+.*:|runtime stack:)\n`) 491 tracebackEntry = regexp.MustCompile(`^` + tbEntry) 492 ) 493 494 // consumeTraceback consumes a traceback from m. 495 func consumeTraceback(m *matcher) string { 496 // Find the beginning of the traceback. 497 for !m.done() && !m.peek(tracebackStart) { 498 m.line() 499 } 500 501 start := m.pos 502 loop: 503 for !m.done() { 504 switch { 505 case m.hasPrefix("\n") || m.hasPrefix("\t") || 506 m.hasPrefix("goroutine ") || m.hasPrefix("runtime stack:") || 507 m.hasPrefix("created by "): 508 m.line() 509 510 case m.consume(tracebackEntry): 511 // Do nothing. 512 513 default: 514 break loop 515 } 516 } 517 return m.str[start:m.pos] 518 } 519 520 var ( 521 // testFromTracebackRe matches a traceback entry from a 522 // function named Test* in a file named *_test.go. It ignores 523 // "created by" lines. 524 testFromTracebackRe = regexp.MustCompile(`\.(Test[^(\n]+)\(.*\n.*_test\.go`) 525 526 panicWhereRe = regexp.MustCompile(`(?m:^)` + tbEntry) 527 ) 528 529 // testFromTraceback attempts to return the test name from a 530 // traceback. 531 func testFromTraceback(tb string) string { 532 s := testFromTracebackRe.FindStringSubmatch(tb) 533 if s == nil { 534 return "" 535 } 536 return s[1] 537 } 538 539 // panicWhere attempts to return the fully qualified name, source 540 // file, and line number of the panicking function in traceback tb. 541 func panicWhere(tb string) (fn string, file string, line int) { 542 m := matcher{str: tb} 543 for m.consume(panicWhereRe) { 544 fn := m.groups[1] 545 546 // Ignore functions involved in panic handling. 547 if strings.HasPrefix(fn, "runtime.panic") || fn == "runtime.throw" || fn == "runtime.sigpanic" { 548 continue 549 } 550 return fn, m.groups[2], atoi(m.groups[3]) 551 } 552 return "", "", 0 553 }