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 }