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.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filego.temporal.io/server/tools/testrunner/junit.go · 369 LOCtestrunner/junit.gosummary.go ×1 · 2 introduced LOCsummary.go ×1testrunner.go ×6 · 30 introduced LOCtestrunner.go ×6TestRenderSummaryFromReports_JSON · 0 introduced LOCTestRenderSummaryFromRep…TestNewSummaryFromReports_KeepsFailureAndAlertRows · 0 introduced LOCTestNewSummaryFromReport…TestMergeReports_MissingRerun · 0 introduced LOCTestMergeReports_Missing…TestMergeReports_MultipleReports · 0 introduced LOCTestMergeReports_Multipl…TestRenderSummaryFromReports_Markdown_RendersTrimmedFailureBody · 0 introduced LOCTestRenderSummaryFromRep…TestNewSummaryFromReports_RendersAssertionFailureRow · 0 introduced LOCTestNewSummaryFromReport…TestNewSummaryFromReports_RendersTrimmedFailureBodyRow · 0 introduced LOCTestNewSummaryFromReport…log.go ×27 · 113 introduced LOClog.go ×27junit.go ×1 · 2 introduced LOCjunit.go ×1junit.go ×2 · 5 introduced LOCjunit.go ×2TestMergeReports_IterationSuffixPreserved · 0 introduced LOCTestMergeReports_Iterati…junit.go ×4 · 18 introduced LOCjunit.go ×4TestMergeReports_PreservesOriginalFailureDataWhenExtractionFindsNothing · 0 introduced LOCTestMergeReports_Preserv…junit.go ×1 · 1 introduced LOCjunit.go ×1junit.go ×1 · 2 introduced LOCjunit.go ×1junit.go ×1 · 2 introduced LOCjunit.go ×1junit.go ×1 · 3 introduced LOCjunit.go ×1report_with_alerts, report_with_special_characters · 0 introduced LOCreport_with_alerts, repo…junit.go ×10 · 42 introduced LOCjunit.go ×10TestAppendAlertsSuite · 0 introduced LOCTestAppendAlertsSuitejunit.go ×1 · 2 introduced LOCjunit.go ×1log.go ×1 · 2 introduced LOClog.go ×1log.go ×2 · 4 introduced LOClog.go ×2log.go ×1 · 1 introduced LOClog.go ×1log.go ×1 · 2 introduced LOClog.go ×1junit.go ×1 · 2 introduced LOCjunit.go ×1log.go ×2 · 7 introduced LOClog.go ×2log.go ×7 · 17 introduced LOClog.go ×7junit.go ×7 · 42 introduced LOCjunit.go ×7log.go ×1 · 2 introduced LOClog.go ×1returns_sentinel_when_no_output_exists · 0 introduced LOCreturns_sentinel_when_no…DeduplicatesDuplicateLines · 0 introduced LOCDeduplicatesDuplicateLin…log.go ×1 · 3 introduced LOClog.go ×1log.go ×2 · 2 introduced LOClog.go ×2log.go ×1 · 3 introduced LOClog.go ×1log.go ×3 · 7 introduced LOClog.go ×3log.go ×3 · 4 introduced LOClog.go ×3log.go ×3 · 5 introduced LOClog.go ×3log.go ×7 · 41 introduced LOClog.go ×7log.go ×1 · 2 introduced LOClog.go ×1junit.go ×3 · 13 introduced LOCjunit.go ×3log.go ×1 · 1 introduced LOClog.go ×1log.go ×1 · 2 introduced LOClog.go ×1log.go ×4 · 15 introduced LOClog.go ×4log.go ×3 · 6 introduced LOClog.go ×3log.go ×2 · 8 introduced LOClog.go ×2TestAppendAlertsSuite · introduced test · go.temporal.io/server/tools/testrunner/TestAppendAlertsSuiteTestAppendAlertsSuiteTestAppendAlertsSuite_TruncatesLargeDetails · introduced test · go.temporal.io/server/tools/testrunner/TestAppendAlertsSuite_TruncatesLargeDetailsTestAppendAlertsSuite_Tr…report_with_alerts · introduced test · go.temporal.io/server/tools/testrunner/TestJUnitXMLWellFormed/report_with_alertsreport_with_alertsreport_with_special_characters · introduced test · go.temporal.io/server/tools/testrunner/TestJUnitXMLWellFormed/report_with_special_charactersreport_with_special_char…TestMergeReports_IterationSuffixPreserved · introduced test · go.temporal.io/server/tools/testrunner/TestMergeReports_IterationSuffixPreservedTestMergeReports_Iterati…TestMergeReports_MissingRerun · introduced test · go.temporal.io/server/tools/testrunner/TestMergeReports_MissingRerunTestMergeReports_Missing…TestMergeReports_MultipleReports · introduced test · go.temporal.io/server/tools/testrunner/TestMergeReports_MultipleReportsTestMergeReports_Multipl…TestMergeReports_PreservesAlertFailureMessage · introduced test · go.temporal.io/server/tools/testrunner/TestMergeReports_PreservesAlertFailureMessageTestMergeReports_Preserv…TestMergeReports_PreservesOriginalFailureDataWhenExtractionFindsNothing · introduced test · go.temporal.io/server/tools/testrunner/TestMergeReports_PreservesOriginalFailureDataWhenExtractionFindsNothingTestMergeReports_Preserv…TestMergeReports_SingleReport · introduced test · go.temporal.io/server/tools/testrunner/TestMergeReports_SingleReportTestMergeReports_SingleR…TestNewSummaryFromReports_KeepsFailureAndAlertRows · introduced test · go.temporal.io/server/tools/testrunner/TestNewSummaryFromReports_KeepsFailureAndAlertRowsTestNewSummaryFromReport…TestNewSummaryFromReports_RendersAssertionFailureRow · introduced test · go.temporal.io/server/tools/testrunner/TestNewSummaryFromReports_RendersAssertionFailureRowTestNewSummaryFromReport…TestNewSummaryFromReports_RendersTrimmedFailureBodyRow · introduced test · go.temporal.io/server/tools/testrunner/TestNewSummaryFromReports_RendersTrimmedFailureBodyRowTestNewSummaryFromReport…TestParseAlerts_DataRaceAndPanic · introduced test · go.temporal.io/server/tools/testrunner/TestParseAlerts_DataRaceAndPanicTestParseAlerts_DataRace…DeduplicatesDuplicateLines · introduced test · go.temporal.io/server/tools/testrunner/TestParseFailedTestsFromOutput/DeduplicatesDuplicateLinesDeduplicatesDuplicateLin…ExtractsFailedTestNames · introduced test · go.temporal.io/server/tools/testrunner/TestParseFailedTestsFromOutput/ExtractsFailedTestNamesExtractsFailedTestNamesReturnsEmptyOnEmptyInput · introduced test · go.temporal.io/server/tools/testrunner/TestParseFailedTestsFromOutput/ReturnsEmptyOnEmptyInputReturnsEmptyOnEmptyInputReturnsEmptyWhenNoFailures · introduced test · go.temporal.io/server/tools/testrunner/TestParseFailedTestsFromOutput/ReturnsEmptyWhenNoFailuresReturnsEmptyWhenNoFailur…extracts_testify_block · introduced test · go.temporal.io/server/tools/testrunner/TestParseFailureDetails/extracts_testify_blockextracts_testify_blockkeeps_last_await_attempt_without_trailing_logs · introduced test · go.temporal.io/server/tools/testrunner/TestParseFailureDetails/keeps_last_await_attempt_without_trailing_logskeeps_last_await_attempt…keeps_testify_file_header_line · introduced test · go.temporal.io/server/tools/testrunner/TestParseFailureDetails/keeps_testify_file_header_linekeeps_testify_file_heade…returns_sentinel_when_no_output_exists · introduced test · go.temporal.io/server/tools/testrunner/TestParseFailureDetails/returns_sentinel_when_no_output_existsreturns_sentinel_when_no…stops_before_logs_printed_after_assertion_block · introduced test · go.temporal.io/server/tools/testrunner/TestParseFailureDetails/stops_before_logs_printed_after_assertion_blockstops_before_logs_printe…strips_trailing_FAIL_marker · introduced test · go.temporal.io/server/tools/testrunner/TestParseFailureDetails/strips_trailing_FAIL_markerstrips_trailing_FAIL_mar…uses_last_failure_block_when_multiple_exist · introduced test · go.temporal.io/server/tools/testrunner/TestParseFailureDetails/uses_last_failure_block_when_multiple_existuses_last_failure_block_…uses_last_gomock_style_block · introduced test · go.temporal.io/server/tools/testrunner/TestParseFailureDetails/uses_last_gomock_style_blockuses_last_gomock_style_b…TestParseTestTimeouts · introduced test · go.temporal.io/server/tools/testrunner/TestParseTestTimeoutsTestParseTestTimeoutsTestRenderSummaryFromReports_JSON · introduced test · go.temporal.io/server/tools/testrunner/TestRenderSummaryFromReports_JSONTestRenderSummaryFromRep…TestRenderSummaryFromReports_Markdown_RendersFailureRows · introduced test · go.temporal.io/server/tools/testrunner/TestRenderSummaryFromReports_Markdown_RendersFailureRowsTestRenderSummaryFromRep…TestRenderSummaryFromReports_Markdown_RendersTrimmedFailureBody · introduced test · go.temporal.io/server/tools/testrunner/TestRenderSummaryFromReports_Markdown_RendersTrimmedFailureBodyTestRenderSummaryFromRep…TestWriteCurrentReport · introduced test · go.temporal.io/server/tools/testrunner/TestWriteCurrentReportTestWriteCurrentReportFocused file · go.temporal.io/server/tools/testrunner/log.go · 444 LOCtestrunner/log.go

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.

1 package testrunner
2
3 import (
4 "fmt"
5 "io"
6 "slices"
7 "strings"
8
9 "github.com/maruel/panicparse/v2/stack"
10 )
11
12 const (
13 // noFailureDetails is returned by parseFailureDetails when no recognisable
14 // failure block is found.
15 noFailureDetails = "(error details not found)"
16 goTestFailLinePrefix = "--- FAIL:"
17 )
18
19 // parseTestTimeouts parses the stdout of a test run and returns the stacktrace and names of tests that timed out.
20 > func parseTestTimeouts(stdout string) (stacktrace string, timedoutTests []string) { log.go ×7
21 > lines := strings.Split(strings.ReplaceAll(stdout, "\r\n", "\n"), "\n")
22 > for i := 0; i < len(lines); i++ {
23 > line := lines[i]
24 > if strings.HasPrefix(line, "FAIL") {
25 > // ignore
26 > } else if strings.HasPrefix(line, "panic: test timed out after") {
27 > // parse names of tests that timed out
28 > for {
29 > i++
30 > line = strings.TrimSpace(lines[i])
31 > if strings.HasPrefix(line, "Test") {
32 > timedoutTests = append(timedoutTests, strings.Split(line, " ")[0])
33 > }
34 > if line == "" {
35 > break
36 }
37 }
38 > } else if len(timedoutTests) > 0 { log.go ×7
39 > // collect stracktrace
40 > stacktrace += line + "\n"
41 > }
42 }
43
44 > stacktrace = fmt.Sprintf("%d timed out test(s):\n\t%v\n\n%v", log.go ×7
45 > len(timedoutTests), strings.Join(timedoutTests, "\n\t"), testOnlyStacktrace(stacktrace))
46 > return
47 }
48
49 // testOnlyStacktrace removes all but the test stacktraces from the full stacktrace.
50 > func testOnlyStacktrace(stacktrace string) string { log.go ×7
51 > var res string
52 > snap, _, err := stack.ScanSnapshot(strings.NewReader(stacktrace), io.Discard, stack.DefaultOpts())
53 > if err != nil && err != io.EOF {
54 return fmt.Sprintf("failed to parse stacktrace: %v", err)
55 }
56 > if snap == nil { log.go ×7
57 return "failed to find a stacktrace"
58 }
59 > res = "abridged stacktrace:\n" log.go ×7
60 > for _, goroutine := range snap.Goroutines {
61 > shouldPrint := slices.ContainsFunc(goroutine.Stack.Calls, func(call stack.Call) bool {
62 > return strings.HasSuffix(call.RemoteSrcPath, "_test.go")
63 > })
64 > if shouldPrint {
65 > res += fmt.Sprintf("\tgoroutine %d [%v]:\n", goroutine.ID, goroutine.State)
66 > for _, call := range goroutine.Stack.Calls {
67 > file := call.RemoteSrcPath
68 > res += fmt.Sprintf("\t\t%s:%d\n", file, call.Line)
69 > }
70 > res += "\n"
71 }
72 }
73 > return res log.go ×7
74 }
75
76 // alert captures a prominent issue detected from stdout/stderr of test runs.
77 type alert struct {
78 Type failureType
79 Summary string
80 Details string
81 Tests []string
82 }
83
84 // primaryTestName returns a single representative test name for an alert.
85 // Preference order:
86 // 1) Fully-qualified test name containing ".Test"
87 // 2) First detected test name
88 > func primaryTestName(tests []string) string { junit.go ×3
89 > if len(tests) == 0 {
90 return ""
91 }
92 > for _, t := range tests { junit.go ×3
93 > if strings.Contains(t, ".Test") {
94 > return t log.go ×1
95 > }
96 }
97 > return tests[0] junit.go ×7
98 }
99
100 // preferFullyQualifiedTestName returns the best display name for a test.
101 // If the primary name is not fully-qualified (e.g., "TestXxx"), but a
102 // fully-qualified variant exists in the list (e.g., "pkg/path.TestXxx"),
103 // this returns the fully-qualified variant.
104 func preferFullyQualifiedTestName(tests []string) string {
105 primary := primaryTestName(tests)
106 if primary == "" || strings.Contains(primary, ".Test") {
107 return primary
108 }
109 // Try to find an FQN that ends with "."+primary
110 suffix := "." + primary
111 for _, t := range tests {
112 if strings.HasSuffix(t, suffix) {
113 return t
114 }
115 }
116 return primary
117 }
118
119 // parseAlerts scans a gotestsum/go test stdout stream and extracts high-priority
120 // alerts such as data races and panics. It returns a slice of alerts in the
121 // order they were encountered.
122 > func parseAlerts(stdout string) []alert { log.go ×27
123 > lines := strings.Split(strings.ReplaceAll(stdout, "\r\n", "\n"), "\n")
124 > var alerts []alert
125 >
126 > for i := 0; i < len(lines); i++ {
127 > line := lines[i]
128 >
129 > if a, next, ok := tryParseDataRace(lines, i, line); ok {
130 > alerts = append(alerts, a)
131 > i = next
132 > continue
133 }
134 > if a, next, ok := tryParsePanic(lines, i, line); ok { log.go ×27
135 > alerts = append(alerts, a)
136 > i = next
137 > continue
138 }
139 > if a, next, ok := tryParseFatal(lines, i, line); ok { log.go ×27
140 alerts = append(alerts, a)
141 i = next
142 continue
143 }
144 }
145
146 > return alerts log.go ×27
147 }
148
149 // extractTestNames tries to identify Go test function names from a log block.
150 // It looks for fully-qualified names like pkg.TestXxx(...) and Go test failure lines.
151 > func extractTestNames(block string) []string { log.go ×27
152 > var tests []string
153 > seen := make(map[string]struct{})
154 > for line := range strings.SplitSeq(block, "\n") {
155 > l := strings.TrimSpace(line)
156 > if l == "" {
157 > continue
158 }
159 > if name, ok := parseTripleDashTestName(l); ok { log.go ×27
160 addUniqueTest(&tests, seen, name)
161 continue
162 }
163 > if name, ok := parseFullyQualifiedTestName(l); ok { log.go ×27
164 > addUniqueTest(&tests, seen, name)
165 > continue
166 }
167 > if name, ok := parsePlainTestName(l); ok { log.go ×27
168 addUniqueTest(&tests, seen, name)
169 }
170 }
171 > return tests log.go ×27
172 }
173
174 // addUniqueTest appends name to tests if not already seen.
175 > func addUniqueTest(tests *[]string, seen map[string]struct{}, name string) { log.go ×3
176 > if _, ok := seen[name]; ok {
177 > return log.go ×1
178 > }
179 > seen[name] = struct{}{} log.go ×3
180 > *tests = append(*tests, name)
181 }
182
183 // parseTripleDashTestName parses Go test failure lines and returns the test name if present.
184 > func parseTripleDashTestName(line string) (string, bool) { log.go ×3
185 > if !strings.HasPrefix(line, goTestFailLinePrefix) {
186 > return "", false log.go ×27
187 > }
188 > name := strings.TrimSpace(strings.TrimPrefix(line, goTestFailLinePrefix)) log.go ×3
189 > name, _, _ = strings.Cut(name, " ")
190 > if !strings.HasPrefix(name, "Test") {
191 return "", false
192 }
193 > return name, true log.go ×3
194 }
195
196 // parseFullyQualifiedTestName extracts names like "pkg/path.TestName" from a line.
197 > func parseFullyQualifiedTestName(line string) (string, bool) { log.go ×27
198 > idx := strings.Index(line, ".Test")
199 > if idx < 0 {
200 > return "", false
201 > }
202 // Include the package/path qualifier preceding ".Test"
203 > start := 0 log.go ×27
204 > if sp := strings.LastIndex(line[:idx], " "); sp >= 0 {
205 start = sp + 1
206 }
207 > if p := strings.Index(line[idx:], "("); p > 0 { log.go ×27
208 > return line[start : idx+p], true
209 > }
210 return "", false
211 }
212
213 // parsePlainTestName extracts a leading "TestName(" form.
214 > func parsePlainTestName(line string) (string, bool) { log.go ×27
215 > if !strings.HasPrefix(line, "Test") || !strings.Contains(line, "(") {
216 > return "", false
217 > }
218 name := line
219 if p := strings.Index(name, "("); p > 0 {
220 name = name[:p]
221 }
222 return name, true
223 }
224
225 // tryParseDataRace parses a data race alert at position i if present.
226 > func tryParseDataRace(lines []string, i int, line string) (alert, int, bool) { log.go ×27
227 > if !strings.HasPrefix(line, "WARNING: DATA RACE") {
228 > return alert{}, i, false
229 > }
230 > start := findRaceBlockStart(lines, i)
231 > // Merge contiguous race-report sections into a single alert. The Go race
232 > // detector may emit multiple "WARNING: DATA RACE" blocks back-to-back,
233 > // each wrapped by a line of ==================. Treat adjacent sections as
234 > // a single logical alert until we either hit a test boundary or a race
235 > // boundary that is not followed by another race section.
236 > block, end := collectBlock(lines, start, func(curLine string, idx, start int) bool {
237 > // Stop at PASS/FAIL boundaries always.
238 > if isTestResultBoundary(curLine) {
239 return true
240 }
241 // If we hit a race boundary after we've started, only stop if the next
242 // non-current line does not continue the race report.
243 > if idx > start && isRaceBoundary(curLine) { log.go ×27
244 > if idx+1 < len(lines) {
245 > next := strings.TrimSpace(lines[idx+1])
246 > if isRaceBoundary(next) || strings.HasPrefix(next, "WARNING: DATA RACE") {
247 > return false
248 > }
249 }
250 > return true log.go ×27
251 }
252 > return false log.go ×27
253 })
254 > return alert{ log.go ×27
255 > Type: failureTypeDataRace,
256 > Summary: "Data race detected",
257 > Details: block,
258 > Tests: extractTestNames(block),
259 > }, end, true
260 }
261
262 // tryParsePanic parses a non-timeout panic alert at position i if present.
263 > func tryParsePanic(lines []string, i int, line string) (alert, int, bool) { log.go ×27
264 > if !strings.HasPrefix(line, "panic: ") || strings.HasPrefix(line, "panic: test timed out after") {
265 > return alert{}, i, false
266 > }
267 > block, end := collectBlock(lines, i, shouldStopOnTestBoundary)
268 > return alert{
269 > Type: failureTypePanic,
270 > Summary: strings.TrimSpace(strings.TrimPrefix(line, "panic: ")),
271 > Details: block,
272 > Tests: extractTestNames(block),
273 > }, end, true
274 }
275
276 // tryParseFatal parses a runtime fatal error alert at position i if present.
277 > func tryParseFatal(lines []string, i int, line string) (alert, int, bool) { log.go ×27
278 > if !strings.HasPrefix(line, "fatal error: ") {
279 > return alert{}, i, false
280 > }
281 block, end := collectBlock(lines, i, shouldStopOnTestBoundary)
282 return alert{
283 Type: failureTypeFatal,
284 Summary: strings.TrimSpace(strings.TrimPrefix(line, "fatal error: ")),
285 Details: block,
286 Tests: extractTestNames(block),
287 }, end, true
288 }
289
290 // findRaceBlockStart searches upward for the race report delimiter.
291 > func findRaceBlockStart(lines []string, i int) int { log.go ×27
292 > start := i
293 > for j := i - 1; j >= 0; j-- {
294 > if isRaceBoundary(lines[j]) {
295 > start = j
296 > break
297 }
298 }
299 > return start log.go ×27
300 }
301
302 // collectBlock builds a block from start until the stop condition is met.
303 > func collectBlock(lines []string, start int, stop func(line string, idx, start int) bool) (string, int) { log.go ×27
304 > var b strings.Builder
305 > for j := start; j < len(lines); j++ {
306 > b.WriteString(lines[j])
307 > b.WriteByte('\n')
308 > if stop(lines[j], j, start) {
309 > return b.String(), j
310 > }
311 }
312 return b.String(), len(lines) - 1
313 }
314
315 > func isRaceBoundary(line string) bool { log.go ×27
316 > return strings.HasPrefix(strings.TrimSpace(line), "==================")
317 > }
318
319 > func isTestResultBoundary(line string) bool { log.go ×27
320 > return strings.HasPrefix(line, "FAIL") || strings.HasPrefix(line, "PASS")
321 > }
322
323 > func shouldStopOnTestBoundary(line string, _ int, _ int) bool { log.go ×27
324 > return isTestResultBoundary(line)
325 > }
326
327 // parseFailedTestsFromOutput extracts failing test names from gotestsum stdout.
328 // It looks for Go test failure lines produced as tests complete, and is
329 // used when the test binary was killed externally before producing a JUnit XML.
330 > func parseFailedTestsFromOutput(stdout string) []string { log.go ×2
331 > var failed []string
332 > seen := make(map[string]struct{})
333 > for line := range strings.SplitSeq(strings.ReplaceAll(stdout, "\r\n", "\n"), "\n") {
334 > line = strings.TrimSpace(line)
335 > if !strings.HasPrefix(line, goTestFailLinePrefix) {
336 > continue
337 }
338 > if name, ok := parseTripleDashTestName(line); ok { log.go ×3
339 > addUniqueTest(&failed, seen, name)
340 > }
341 }
342 > return failed log.go ×2
343 }
344
345 // parseFailureDetails extracts the actionable part of a JUnit failure Data block.
346 > func parseFailureDetails(data string) string { log.go ×4
347 > lines := normalizedFailureLines(data)
348 >
349 > // Prefer assertion blocks because they contain the useful testify failure
350 > // detail and can be selected from the end while ignoring trailing logs.
351 > if block, ok := findLastAssertionFailureBlock(lines); ok {
352 > return block log.go ×7
353 > }
354 // Some failures, such as gomock errors, do not include an Error Trace.
355 // Fall back to the last Go test output block in those cases.
356 > if start, end, ok := findLastTestOutputFailureBlock(lines); ok { log.go ×3
357 > return strings.Join(lines[start:end], "\n") log.go ×2
358 > }
359 > return noFailureDetails log.go ×2
360 }
361
362 > func normalizedFailureLines(data string) []string { log.go ×4
363 > lines := strings.Split(strings.ReplaceAll(data, "\r\n", "\n"), "\n")
364 > for len(lines) > 0 {
365 > t := strings.TrimSpace(lines[len(lines)-1])
366 > if t != "" && t != "FAIL" {
367 > break log.go ×3
368 }
369 > lines = lines[:len(lines)-1] log.go ×1
370 }
371 > return lines log.go ×4
372 }
373
374 > func findLastAssertionFailureBlock(lines []string) (string, bool) { log.go ×4
375 > var failLine string
376 > for i := len(lines) - 1; i >= 0; i-- {
377 > line := strings.TrimSpace(lines[i]) log.go ×3
378 > if failLine == "" && strings.HasPrefix(line, goTestFailLinePrefix) {
379 > // Keep the final Go test failure line because it carries the test duration. log.go ×1
380 > failLine = line
381 > continue
382 }
383 > if !strings.Contains(lines[i], "Error Trace:") { log.go ×3
384 > continue
385 }
386
387 // Include the nearest preceding line when present. For testify this is
388 // the file header; for await failures this is the attempt marker.
389 > start := i log.go ×7
390 > for prev := i - 1; prev >= 0; prev-- {
391 > if strings.TrimSpace(lines[prev]) != "" {
392 > start = prev
393 > break
394 }
395 }
396 > out := append([]string{}, lines[start:endOfAssertionBlock(lines, i+1)]...) log.go ×7
397 > if failLine != "" {
398 > out = append(out, "", failLine) log.go ×2
399 > }
400 > return strings.Join(out, "\n"), true log.go ×7
401 }
402 > return "", false log.go ×3
403 }
404
405 > func endOfAssertionBlock(lines []string, start int) int { log.go ×7
406 > sawTestLine := false
407 > for i := start; i < len(lines); i++ {
408 > line := strings.TrimSpace(lines[i])
409 > if line == "" || strings.HasPrefix(line, goTestFailLinePrefix) {
410 > return i log.go ×2
411 > }
412 > if strings.HasPrefix(line, "Test:") { log.go ×7
413 > sawTestLine = true log.go ×1
414 > continue
415 }
416 // Logs written after the Test line are not part of the assertion block.
417 > if sawTestLine && isTestOutputLine(lines[i]) { log.go ×7
418 > return i log.go ×1
419 > }
420 }
421 > return len(lines) log.go ×1
422 }
423
424 > func findLastTestOutputFailureBlock(lines []string) (start, end int, ok bool) { log.go ×3
425 > for start := len(lines) - 1; start >= 0; start-- {
426 > if !isTestOutputLine(lines[start]) { log.go ×1
427 > continue
428 }
429 > end := start + 1 log.go ×2
430 > for end < len(lines) && !isTestOutputLine(lines[end]) && lines[end] != "" {
431 > end++
432 > }
433 > return start, end, true
434 }
435 > return 0, 0, false log.go ×2
436 }
437
438 // isTestOutputLine reports whether line is a Go test-framework output line,
439 // i.e. " file.go:N: …" — exactly 4 spaces then a non-whitespace character.
440 // Testify assertion content is indented further (8+ spaces or tabs), so this
441 // distinguishes log entries from assertion block content.
442 > func isTestOutputLine(line string) bool { log.go ×1
443 > return len(line) > 4 && line[:4] == " " && line[4] != ' ' && line[4] != '\t'
444 > }