go.temporal.io/server/tools/testrunner/log.go
444 LOC · 251 covered · 193 uncovered · 77 ranges · 48 concepts · 23 introducers · 31 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
package testrunner
import (
"fmt"
"io"
"slices"
"strings"
"github.com/maruel/panicparse/v2/stack"
)
const (
// noFailureDetails is returned by parseFailureDetails when no recognisable
// failure block is found.
noFailureDetails = "(error details not found)"
goTestFailLinePrefix = "--- FAIL:"
)
// parseTestTimeouts parses the stdout of a test run and returns the stacktrace and names of tests that timed out.
lines := strings.Split(strings.ReplaceAll(stdout, "\r\n", "\n"), "\n")
for i := 0; i < len(lines); i++ {
line := lines[i]
if strings.HasPrefix(line, "FAIL") {
// ignore
} else if strings.HasPrefix(line, "panic: test timed out after") {
// parse names of tests that timed out
for {
i++
line = strings.TrimSpace(lines[i])
if strings.HasPrefix(line, "Test") {
timedoutTests = append(timedoutTests, strings.Split(line, " ")[0])
}
if line == "" {
break
}
}
// collect stracktrace
stacktrace += line + "\n"
}
}
len(timedoutTests), strings.Join(timedoutTests, "\n\t"), testOnlyStacktrace(stacktrace))
return
}
// testOnlyStacktrace removes all but the test stacktraces from the full stacktrace.
var res string
snap, _, err := stack.ScanSnapshot(strings.NewReader(stacktrace), io.Discard, stack.DefaultOpts())
if err != nil && err != io.EOF {
return fmt.Sprintf("failed to parse stacktrace: %v", err)
}
return "failed to find a stacktrace"
}
for _, goroutine := range snap.Goroutines {
shouldPrint := slices.ContainsFunc(goroutine.Stack.Calls, func(call stack.Call) bool {
return strings.HasSuffix(call.RemoteSrcPath, "_test.go")
})
if shouldPrint {
res += fmt.Sprintf("\tgoroutine %d [%v]:\n", goroutine.ID, goroutine.State)
for _, call := range goroutine.Stack.Calls {
file := call.RemoteSrcPath
res += fmt.Sprintf("\t\t%s:%d\n", file, call.Line)
}
res += "\n"
}
}
}
// alert captures a prominent issue detected from stdout/stderr of test runs.
type alert struct {
Type failureType
Summary string
Details string
Tests []string
}
// primaryTestName returns a single representative test name for an alert.
// Preference order:
// 1) Fully-qualified test name containing ".Test"
// 2) First detected test name
if len(tests) == 0 {
return ""
}
if strings.Contains(t, ".Test") {
}
}
}
// preferFullyQualifiedTestName returns the best display name for a test.
// If the primary name is not fully-qualified (e.g., "TestXxx"), but a
// fully-qualified variant exists in the list (e.g., "pkg/path.TestXxx"),
// this returns the fully-qualified variant.
func preferFullyQualifiedTestName(tests []string) string {
primary := primaryTestName(tests)
if primary == "" || strings.Contains(primary, ".Test") {
return primary
}
// Try to find an FQN that ends with "."+primary
suffix := "." + primary
for _, t := range tests {
if strings.HasSuffix(t, suffix) {
return t
}
}
return primary
}
// parseAlerts scans a gotestsum/go test stdout stream and extracts high-priority
// alerts such as data races and panics. It returns a slice of alerts in the
// order they were encountered.
lines := strings.Split(strings.ReplaceAll(stdout, "\r\n", "\n"), "\n")
var alerts []alert
for i := 0; i < len(lines); i++ {
line := lines[i]
if a, next, ok := tryParseDataRace(lines, i, line); ok {
alerts = append(alerts, a)
i = next
continue
}
alerts = append(alerts, a)
i = next
continue
}
alerts = append(alerts, a)
i = next
continue
}
}
}
// extractTestNames tries to identify Go test function names from a log block.
// It looks for fully-qualified names like pkg.TestXxx(...) and Go test failure lines.
var tests []string
seen := make(map[string]struct{})
for line := range strings.SplitSeq(block, "\n") {
l := strings.TrimSpace(line)
if l == "" {
continue
}
addUniqueTest(&tests, seen, name)
continue
}
addUniqueTest(&tests, seen, name)
continue
}
addUniqueTest(&tests, seen, name)
}
}
}
// addUniqueTest appends name to tests if not already seen.
if _, ok := seen[name]; ok {
}
*tests = append(*tests, name)
}
// parseTripleDashTestName parses Go test failure lines and returns the test name if present.
if !strings.HasPrefix(line, goTestFailLinePrefix) {
}
name, _, _ = strings.Cut(name, " ")
if !strings.HasPrefix(name, "Test") {
return "", false
}
}
// parseFullyQualifiedTestName extracts names like "pkg/path.TestName" from a line.
idx := strings.Index(line, ".Test")
if idx < 0 {
return "", false
}
// Include the package/path qualifier preceding ".Test"
if sp := strings.LastIndex(line[:idx], " "); sp >= 0 {
start = sp + 1
}
return line[start : idx+p], true
}
return "", false
}
// parsePlainTestName extracts a leading "TestName(" form.
if !strings.HasPrefix(line, "Test") || !strings.Contains(line, "(") {
return "", false
}
name := line
if p := strings.Index(name, "("); p > 0 {
name = name[:p]
}
return name, true
}
// tryParseDataRace parses a data race alert at position i if present.
if !strings.HasPrefix(line, "WARNING: DATA RACE") {
return alert{}, i, false
}
start := findRaceBlockStart(lines, i)
// Merge contiguous race-report sections into a single alert. The Go race
// detector may emit multiple "WARNING: DATA RACE" blocks back-to-back,
// each wrapped by a line of ==================. Treat adjacent sections as
// a single logical alert until we either hit a test boundary or a race
// boundary that is not followed by another race section.
block, end := collectBlock(lines, start, func(curLine string, idx, start int) bool {
// Stop at PASS/FAIL boundaries always.
if isTestResultBoundary(curLine) {
return true
}
// If we hit a race boundary after we've started, only stop if the next
// non-current line does not continue the race report.
if idx+1 < len(lines) {
next := strings.TrimSpace(lines[idx+1])
if isRaceBoundary(next) || strings.HasPrefix(next, "WARNING: DATA RACE") {
return false
}
}
}
})
Type: failureTypeDataRace,
Summary: "Data race detected",
Details: block,
Tests: extractTestNames(block),
}, end, true
}
// tryParsePanic parses a non-timeout panic alert at position i if present.
if !strings.HasPrefix(line, "panic: ") || strings.HasPrefix(line, "panic: test timed out after") {
return alert{}, i, false
}
block, end := collectBlock(lines, i, shouldStopOnTestBoundary)
return alert{
Type: failureTypePanic,
Summary: strings.TrimSpace(strings.TrimPrefix(line, "panic: ")),
Details: block,
Tests: extractTestNames(block),
}, end, true
}
// tryParseFatal parses a runtime fatal error alert at position i if present.
if !strings.HasPrefix(line, "fatal error: ") {
return alert{}, i, false
}
block, end := collectBlock(lines, i, shouldStopOnTestBoundary)
return alert{
Type: failureTypeFatal,
Summary: strings.TrimSpace(strings.TrimPrefix(line, "fatal error: ")),
Details: block,
Tests: extractTestNames(block),
}, end, true
}
// findRaceBlockStart searches upward for the race report delimiter.
start := i
for j := i - 1; j >= 0; j-- {
if isRaceBoundary(lines[j]) {
start = j
break
}
}
}
// collectBlock builds a block from start until the stop condition is met.
func collectBlock(lines []string, start int, stop func(line string, idx, start int) bool) (string, int) {
log.go ×27
var b strings.Builder
for j := start; j < len(lines); j++ {
b.WriteString(lines[j])
b.WriteByte('\n')
if stop(lines[j], j, start) {
return b.String(), j
}
}
return b.String(), len(lines) - 1
}
return strings.HasPrefix(strings.TrimSpace(line), "==================")
}
return strings.HasPrefix(line, "FAIL") || strings.HasPrefix(line, "PASS")
}
return isTestResultBoundary(line)
}
// parseFailedTestsFromOutput extracts failing test names from gotestsum stdout.
// It looks for Go test failure lines produced as tests complete, and is
// used when the test binary was killed externally before producing a JUnit XML.
var failed []string
seen := make(map[string]struct{})
for line := range strings.SplitSeq(strings.ReplaceAll(stdout, "\r\n", "\n"), "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, goTestFailLinePrefix) {
continue
}
addUniqueTest(&failed, seen, name)
}
}
}
// parseFailureDetails extracts the actionable part of a JUnit failure Data block.
lines := normalizedFailureLines(data)
// Prefer assertion blocks because they contain the useful testify failure
// detail and can be selected from the end while ignoring trailing logs.
if block, ok := findLastAssertionFailureBlock(lines); ok {
}
// Some failures, such as gomock errors, do not include an Error Trace.
// Fall back to the last Go test output block in those cases.
}
}
lines := strings.Split(strings.ReplaceAll(data, "\r\n", "\n"), "\n")
for len(lines) > 0 {
t := strings.TrimSpace(lines[len(lines)-1])
if t != "" && t != "FAIL" {
}
}
}
var failLine string
for i := len(lines) - 1; i >= 0; i-- {
if failLine == "" && strings.HasPrefix(line, goTestFailLinePrefix) {
failLine = line
continue
}
continue
}
// Include the nearest preceding line when present. For testify this is
// the file header; for await failures this is the attempt marker.
for prev := i - 1; prev >= 0; prev-- {
if strings.TrimSpace(lines[prev]) != "" {
start = prev
break
}
}
if failLine != "" {
}
}
}
sawTestLine := false
for i := start; i < len(lines); i++ {
line := strings.TrimSpace(lines[i])
if line == "" || strings.HasPrefix(line, goTestFailLinePrefix) {
}
continue
}
// Logs written after the Test line are not part of the assertion block.
}
}
}
for start := len(lines) - 1; start >= 0; start-- {
continue
}
for end < len(lines) && !isTestOutputLine(lines[end]) && lines[end] != "" {
end++
}
return start, end, true
}
}
// isTestOutputLine reports whether line is a Go test-framework output line,
// i.e. " file.go:N: …" — exactly 4 spaces then a non-whitespace character.
// Testify assertion content is indented further (8+ spaces or tabs), so this
// distinguishes log entries from assertion block content.
return len(line) > 4 && line[:4] == " " && line[4] != ' ' && line[4] != '\t'
}