golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/logparser/parse.go (about)

     1  // Copyright 2022 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 logparser parses build.golang.org dashboard logs.
     6  package logparser
     7  
     8  import (
     9  	"regexp"
    10  	"strconv"
    11  	"strings"
    12  )
    13  
    14  // A Fail is a single failure mentioned in a dashboard log.
    15  // (There can be multiple failures in a single log.)
    16  type Fail struct {
    17  	Section string
    18  	Pkg     string
    19  	Test    string
    20  	Mode    string
    21  	Output  string
    22  	Snippet string
    23  }
    24  
    25  // compileRE matches compiler errors, with file:line:[col:] at the start of the line.
    26  var compileRE = regexp.MustCompile(`^[a-zA-Z0-9_./\\]+:\d+:(\d+:)? `)
    27  
    28  // runningRE matches the buildlet :: Running messages,
    29  // which are displayed at the start of each operation the buildlet does.
    30  var runningRE = regexp.MustCompile(`:: Running [^ ]+ with args \[([^\[\]]+)\] and env`)
    31  
    32  // stringRE matches a single Go quoted string.
    33  var stringRE = regexp.MustCompile(`"([^"\\]|\\.)*"`)
    34  
    35  var testExecEnv = []string{
    36  	"# GOARCH:",
    37  	"# CPU:",
    38  	"# GOOS:",
    39  	"# OS Version:",
    40  }
    41  
    42  // Parse parses a build log, returning all the failures it finds.
    43  // It always returns at least one failure.
    44  func Parse(log string) []*Fail {
    45  	// Some logs have \r\n lines.
    46  	log = strings.ReplaceAll(log, "\r", "")
    47  
    48  	// Parsing proceeds line at a time, tracking the "section" of the build
    49  	// we are currently in.
    50  	// When we see lines that might be output associated with a failure,
    51  	// we accumulate them in the hold slice.
    52  	// When we see a line that definitely indicates a failure, we start a
    53  	// new failure f, append it to fails, and also append the lines associated
    54  	// with f to lines. (That is, fails[i]'s output lines are lines[i].)
    55  	// When we see a line that definitely indicates a non-failure, such as
    56  	// an "ok" result from go test, we "flush" the current failure, meaning
    57  	// we set f to nil so there's no current failure anymore, and we clear
    58  	// the hold slice as well. Before clearing the hold slice, we check to
    59  	// see if there are any compile failures in it that should be extracted.
    60  	var (
    61  		section string
    62  		hold    []string
    63  		fails   []*Fail
    64  		lines   [][]string
    65  		f       *Fail
    66  	)
    67  
    68  	// flush is called when we've reached a non-failing test in the output,
    69  	// meaning that any failing test is over. It clears f and the hold space,
    70  	// but it also processes the hold space to find any build failures lurking.
    71  	flush := func() {
    72  		// Any unattributed compile-failure-looking lines turn into a build failure.
    73  		for _, line := range hold {
    74  			if compileRE.MatchString(line) {
    75  				f = &Fail{
    76  					Section: section,
    77  					Mode:    "build",
    78  				}
    79  				fails = append(fails, f)
    80  				lines = append(lines, hold)
    81  				hold = nil
    82  				break
    83  			}
    84  		}
    85  		f = nil
    86  		hold = hold[:0]
    87  	}
    88  
    89  	// Process the log, a line at a time.
    90  Lines:
    91  	for i, line := range strings.SplitAfter(log, "\n") {
    92  		// first line is "<builder> at <hash> ..."; ignore.
    93  		if i == 0 && strings.Contains(line, " at ") {
    94  			continue
    95  		}
    96  		// ignore empty string left by Split at end of slice
    97  		if line == "" {
    98  			continue
    99  		}
   100  		// ignore "go: downloading" chatter.
   101  		if strings.HasPrefix(line, "go: downloading ") {
   102  			continue
   103  		}
   104  		// replace lines with trailing spaces with empty line
   105  		if strings.TrimSpace(line) == "" {
   106  			line = "\n"
   107  			if len(hold) == 0 && f == nil {
   108  				continue
   109  			}
   110  		}
   111  
   112  		if strings.TrimSpace(line) == "XXXBANNERXXX:Test execution environment." {
   113  			continue
   114  		}
   115  
   116  		// :: Running line says what command the buildlet is running now.
   117  		// Use this as the initial section.
   118  		if strings.HasPrefix(line, ":: Running") {
   119  			flush()
   120  			if m := runningRE.FindStringSubmatch(line); m != nil {
   121  				var args []string
   122  				for _, q := range stringRE.FindAllString(m[1], -1) {
   123  					s, _ := strconv.Unquote(q)
   124  					args = append(args, s)
   125  				}
   126  				args[0] = args[0][strings.LastIndex(args[0], `/`)+1:]
   127  				args[0] = args[0][strings.LastIndex(args[0], `\`)+1:]
   128  				section = strings.Join(args, " ")
   129  			}
   130  			continue
   131  		}
   132  
   133  		// cmd/dist in the main repo prints Building lines during bootstrap.
   134  		// Use them as sections.
   135  		if strings.HasPrefix(line, "Building ") {
   136  			flush()
   137  			section = strings.TrimSpace(line)
   138  			continue
   139  		}
   140  
   141  		// cmd/dist prints ##### lines between test sections in the main repo.
   142  		if strings.HasPrefix(line, "##### ") {
   143  			flush()
   144  			if p := strings.TrimSpace(line[len("##### "):]); p != "" {
   145  				section = p
   146  				continue
   147  			}
   148  		}
   149  
   150  		// skipped or passing test from go test marks end of previous failure
   151  		if strings.HasPrefix(line, "?   \t") || strings.HasPrefix(line, "ok  \t") {
   152  			flush()
   153  			continue
   154  		}
   155  
   156  		// test binaries print FAIL at the end of execution, as does "go test".
   157  		// If we've already found a specific failure, these are noise and can be dropped.
   158  		if line == "FAIL\n" && len(fails) > 0 {
   159  			continue
   160  		}
   161  
   162  		// --- FAIL: marks package testing's report of a failed test.
   163  		// Lines may have been printed above it, during test execution;
   164  		// they are picked up from the hold slice for inclusion in the report.
   165  		if strings.HasPrefix(line, "--- FAIL: ") {
   166  			if fields := strings.Fields(line); len(fields) >= 3 {
   167  				// Found start of test function failure.
   168  				f = &Fail{
   169  					Section: section,
   170  					Test:    fields[2],
   171  					Mode:    "test",
   172  				}
   173  				if strings.HasPrefix(section, "../") {
   174  					f.Pkg = strings.TrimPrefix(section, "../")
   175  				}
   176  				fails = append(fails, f)
   177  				// Include held lines printed above the --- FAIL
   178  				// since they could have been printed from the test.
   179  				lines = append(lines, append(hold, line))
   180  				hold = nil
   181  				continue
   182  			}
   183  		}
   184  
   185  		// During go test, build failures are reported starting with a "# pkg/path" or
   186  		// "# pkg/path [foo.test]" line. We have to distinguish these from the # lines
   187  		// printed during the "../test" part of all.bash, and we have to distinguish them
   188  		// from the # key: value lines printed in the # Test execution environment section.
   189  		if strings.HasPrefix(line, "# ") && strings.Contains(line, ":") {
   190  			for _, env := range testExecEnv {
   191  				if strings.HasPrefix(line, env) {
   192  					continue Lines
   193  				}
   194  			}
   195  		}
   196  		if strings.HasPrefix(line, "# ") && section != "../test" {
   197  			if fields := strings.Fields(line); len(fields) >= 2 {
   198  				flush()
   199  				// Found start of build failure.
   200  				f = &Fail{
   201  					Section: section,
   202  					Pkg:     fields[1],
   203  					Mode:    "build",
   204  				}
   205  				fails = append(fails, f)
   206  				lines = append(lines, nil)
   207  				continue
   208  			}
   209  		}
   210  
   211  		// In the ../test phase, run.go prints "go run run.go" lines for each failing test.
   212  		if strings.HasPrefix(line, "# go run run.go -- ") {
   213  			f = &Fail{
   214  				Section: section,
   215  				Pkg:     strings.TrimSpace(strings.TrimPrefix(line, "# go run run.go -- ")),
   216  				Mode:    "test",
   217  			}
   218  			fails = append(fails, f)
   219  			lines = append(lines, append(hold, line))
   220  			hold = nil
   221  			continue
   222  		}
   223  
   224  		// go test prints "FAIL\tpkg\t0.1s\n" after a failing test's output has been printed.
   225  		// We've seen the failing test cases already but didn't know what package they were from.
   226  		// Update them. If there is no active failure, it could be that the test panicked or
   227  		// otherwise exited without printing the usual test case failures.
   228  		// Create a new Fail in that case, recording whatever output we did see (from the hold slice).
   229  		//
   230  		// In the ../test phase, run.go prints "FAIL\ttestcase.go 0.1s" (space not tab).
   231  		// For those, we don't need to update any test cases.
   232  		//
   233  		if strings.HasPrefix(line, "FAIL\t") {
   234  			if fields := strings.Fields(line); len(fields) >= 3 {
   235  				if strings.Contains(line, "[build failed]") {
   236  					flush()
   237  					continue
   238  				}
   239  				// Found test package failure line printed by cmd/go after test output.
   240  				pkg := fields[1]
   241  				if f != nil && f.Section == "../test" {
   242  					// already collecting
   243  				} else if f != nil {
   244  					for i := len(fails) - 1; i >= 0 && fails[i].Test != ""; i-- {
   245  						fails[i].Pkg = pkg
   246  					}
   247  				} else {
   248  					f = &Fail{
   249  						Section: section,
   250  						Pkg:     pkg,
   251  						Mode:    "test",
   252  					}
   253  					fails = append(fails, f)
   254  					lines = append(lines, hold)
   255  					hold = nil
   256  				}
   257  				flush()
   258  				continue
   259  			}
   260  		}
   261  		if f == nil {
   262  			hold = append(hold, line)
   263  		} else {
   264  			lines[len(fails)-1] = append(lines[len(fails)-1], line)
   265  		}
   266  	}
   267  
   268  	// If we didn't find any failures in the log, at least grab the current hold slice.
   269  	// It's not much, but it's something.
   270  	if len(fails) == 0 {
   271  		f = &Fail{
   272  			Section: section,
   273  		}
   274  		fails = append(fails, f)
   275  		lines = append(lines, hold)
   276  		hold = nil
   277  	}
   278  	flush()
   279  
   280  	// Now that we have the full output for each failure,
   281  	// build the Output and Snippet fields.
   282  	for i, f := range fails {
   283  		// Trim trailing blank lines.
   284  		out := lines[i]
   285  		for len(out) > 0 && strings.TrimSpace(out[len(out)-1]) == "" {
   286  			out = out[:len(out)-1]
   287  		}
   288  		f.Output = strings.Join(out, "")
   289  		f.Snippet = strings.Join(shorten(out, true), "")
   290  		if f.Test == "" && strings.Contains(f.Output, "\n\ngoroutine ") {
   291  			// If a test binary panicked, it doesn't report what test was running.
   292  			// Figure that out by parsing the goroutine stacks.
   293  			findRunningTest(f, out)
   294  		}
   295  	}
   296  
   297  	return fails
   298  }
   299  
   300  var goroutineStack = regexp.MustCompile(`^goroutine \d+ \[(.*)\]:$`)
   301  
   302  // findRunningTest looks at the test output to find the running test goroutine,
   303  // extracts the test name from it, and then updates f.Test.
   304  func findRunningTest(f *Fail, lines []string) {
   305  	goroutineStart := -1 // index of current goroutine's "goroutine N" line.
   306  Scan:
   307  	for i, line := range lines {
   308  		s := strings.TrimSpace(line)
   309  		if s == "" { // blank line marks end of goroutine stack
   310  			goroutineStart = -1
   311  			continue
   312  		}
   313  		if goroutineStack.MatchString(s) {
   314  			goroutineStart = i
   315  		}
   316  
   317  		// If we find testing.tRunner on the stack, the frame above it is a test.
   318  		// But in the case of tests using t.Parallel, what usually happens is that
   319  		// many tests are blocked in t.Parallel and one is actually running.
   320  		// Take the first that hasn't called t.Parallel.
   321  		if goroutineStart >= 0 && strings.HasPrefix(s, "testing.tRunner(") && i > 2 {
   322  			// Frame above tRunner should be a test.
   323  			testLine := strings.TrimSpace(lines[i-2])
   324  			if name, _, ok := strings.Cut(testLine, "("); ok {
   325  				if _, test, ok := strings.Cut(name, ".Test"); ok {
   326  					// Ignore this goroutine if it is blocked in t.Parallel.
   327  					for _, line := range lines[goroutineStart+1 : i] {
   328  						if strings.HasPrefix(strings.TrimSpace(line), "testing.(*T).Parallel(") {
   329  							goroutineStart = -1
   330  							continue Scan
   331  						}
   332  					}
   333  					test, _, _ = strings.Cut(test, ".func")
   334  					f.Test = "Test" + test
   335  
   336  					// Append the stack trace down to tRunner,
   337  					// but without the goroutine line, and then re-shorten the snippet.
   338  					// We pass false to shorten to discard all the other goroutines:
   339  					// we've found the one we want, and we deleted its goroutine header
   340  					// so that shorten won't remove it.
   341  					var big []string
   342  					big = append(big, lines...)
   343  					big = append(big, "\n")
   344  					big = append(big, lines[goroutineStart+1:i+1]...)
   345  					f.Snippet = strings.Join(shorten(big, false), "")
   346  					return
   347  				}
   348  			}
   349  		}
   350  	}
   351  }
   352  
   353  // shorten shortens the output lines to form a snippet.
   354  func shorten(lines []string, keepRunning bool) []string {
   355  	// First, remove most goroutine stacks.
   356  	// Those are often irrelevant and easy to drop from the snippet.
   357  	// If keepRunning is true, we keep the [running] goroutines.
   358  	// If keepRunning is false, we keep no goroutine stacks at all.
   359  	{
   360  		var keep []string
   361  		var wasBlank bool
   362  		var inGoroutine bool
   363  		for _, line := range lines {
   364  			s := strings.TrimSpace(line)
   365  			if inGoroutine {
   366  				if s == "" {
   367  					inGoroutine = false
   368  					wasBlank = true
   369  				}
   370  				continue
   371  			}
   372  			if wasBlank {
   373  				if m := goroutineStack.FindStringSubmatch(s); m != nil && (!keepRunning || m[1] != "running") {
   374  					inGoroutine = true
   375  					continue
   376  				}
   377  			}
   378  			keep = append(keep, line)
   379  			wasBlank = s == ""
   380  		}
   381  		lines = keep
   382  	}
   383  
   384  	// Remove trailing blank lines.
   385  	for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
   386  		lines = lines[:len(lines)-1]
   387  	}
   388  
   389  	// If we have more than 30 lines, make the snippet by taking the first 10,
   390  	// the last 10, and possibly a middle 10. The middle 10 is included when
   391  	// the interior lines (between the first and last 10) contain an important-looking
   392  	// message like "panic:" or "--- FAIL:". The middle 10 start at the important-looking line.
   393  	// such as
   394  	if len(lines) > 30 {
   395  		var keep []string
   396  		keep = append(keep, lines[:10]...)
   397  		dots := true
   398  		for i := 10; i < len(lines)-10; i++ {
   399  			s := strings.TrimSpace(lines[i])
   400  			if strings.HasPrefix(s, "panic:") || strings.HasPrefix(s, "fatal error:") || strings.HasPrefix(s, "--- FAIL:") || strings.Contains(s, ": internal compiler error:") {
   401  				if i > 10 {
   402  					keep = append(keep, "...\n")
   403  				}
   404  				end := i + 10
   405  				if end >= len(lines)-10 {
   406  					dots = false
   407  					end = len(lines) - 10
   408  				}
   409  				keep = append(keep, lines[i:end]...)
   410  				break
   411  			}
   412  		}
   413  		if dots {
   414  			keep = append(keep, "...\n")
   415  		}
   416  		keep = append(keep, lines[len(lines)-10:]...)
   417  		lines = keep
   418  	}
   419  
   420  	return lines
   421  }