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  }