Atlas › Test

TestRenderSummaryFromReports_Markdown_RendersTrimmedFailureBody

Exact test identity: go.temporal.io/server/tools/testrunner/TestRenderSummaryFromReports_Markdown_RendersTrimmedFailureBody

Package
go.temporal.io/server/tools/testrunner
Suite / test hierarchy
TestRenderSummaryFromReports_Markdown_RendersTrimmedFailureBody
Test
TestRenderSummaryFromReports_Markdown_RendersTrimmedFailureBody
Introduced at
TestRenderSummaryFromReports_Markdown_RendersTrimmedFailureBody Frontier kind: Test frontier
Covered ranges
46
Covered lines
144
Covered files
3

Covered source

Expand a file to inspect source; the > gutter marks covered lines.

go.temporal.io/server/tools/testrunner/junit.go 55 covered LOC · 15 ranges

Open complete file

38 }
39
40 > func (j *junitReport) read() error { junit.go
41 > f, err := os.Open(j.path)
42 > if err != nil {
43 return fmt.Errorf("failed to open junit report file: %w", err)
44 }
45 > defer f.Close() junit.go
46 >
47 > decoder := xml.NewDecoder(f)
48 > if err = decoder.Decode(&j.Testsuites); err != nil {
49 return fmt.Errorf("failed to read junit report file: %w", err)
50 }
51 > return nil junit.go
52 }
53
255 }
256
257 > func mergeReports(reports []*junitReport) (*junitReport, error) { junit.go
258 > if len(reports) == 0 {
259 return nil, errors.New("no reports to merge")
260 }
261
262 > var reportingErrs []error junit.go
263 > var combined junit.Testsuites
264 > combined.XMLName = reports[0].XMLName
265 > combined.Name = reports[0].Name
266 >
267 > for i, report := range reports {
268 > combined.Tests += report.Tests
269 > combined.Errors += report.Errors
270 > combined.Failures += report.Failures
271 > combined.Skipped += report.Skipped
272 > combined.Disabled += report.Disabled
273 > combined.Time += report.Time
274 >
275 > // If the report is for a retry ...
276 > var suffix string
277 > if i > 0 {
278 suffix = fmt.Sprintf(" (retry %d)", i)
279 if i == len(reports)-1 {
295 }
296
297 > for _, suite := range report.Suites { junit.go
298 > if len(suite.Testcases) == 0 {
299 continue
300 }
301
302 > newSuite := suite // shallow copy junit.go
303 > newSuite.Name += suffix
304 > newSuite.Testcases = make([]junit.Testcase, 0, len(suite.Testcases))
305 >
306 > // Sort test cases by name.
307 > slices.SortFunc(suite.Testcases, func(a, b junit.Testcase) int {
308 return strings.Compare(a.Name, b.Name)
309 })
310
311 // Collect test cases.
312 > for j := range len(suite.Testcases) { junit.go
313 > testCase := suite.Testcases[j]
314 > // Check if this is a parent test case (ie prefix of next subtest).
315 > // Use testCase.Name+"/" to avoid matching iteration suffixes like #01.
316 > if j != len(suite.Testcases)-1 && strings.HasPrefix(suite.Testcases[j+1].Name, testCase.Name+"/") {
317 // Discard test case parents since they provide no value.
318 continue
320
321 // Parse failure details from Failure.Data, if present.
322 > if testCase.Failure != nil && testCase.Failure.Data != "" { junit.go
323 > if details := parseFailureDetails(testCase.Failure.Data); details != noFailureDetails {
324 > testCase.Failure.Data = details junit.go
325 > }
326 }
327
328 // Failure.Type carries the canonical kind in merged JUnit.
329 > if testCase.Failure != nil { junit.go
330 > if suite.Name == alertsSuiteName {
331 if testCase.Failure.Type == "" {
332 testCase.Failure.Type = testCase.Failure.Message
333 }
334 > } else { junit.go
335 > testCase.Failure.Type = string(failureTypeFailed)
336 > }
337 }
338 > testCase.Name += suffix junit.go
339 > newSuite.Testcases = append(newSuite.Testcases, testCase)
340 }
341 > combined.Suites = append(combined.Suites, newSuite) junit.go
342 }
343 }
344
345 > return &junitReport{ junit.go
346 > Testsuites: combined,
347 > reportingErrs: reportingErrs,
348 > }, nil
349 }
350
go.temporal.io/server/tools/testrunner/summary.go 52 covered LOC · 16 ranges

Open complete file

17 }
18
19 > func newSummaryFromReports(reports []*junitReport) summary { summary.go
20 > return summary{
21 > Rows: newSummaryRowsFromReports(reports),
22 > }
23 > }
24
25 // Markdown renders the GitHub step summary HTML and enforces both the total
26 // summary budget and per-row detail truncation.
27 > func (s summary) Markdown() string { summary.go
28 > if len(s.Rows) == 0 {
29 return ""
30 }
31
32 > var sb strings.Builder summary.go
33 > sb.WriteString("<table>\n<tr><th>Kind</th><th>Test</th></tr>\n")
34 >
35 > // Reserve bytes for the closing tag so we can always finish the table.
36 > const tableClose = "</table>\n"
37 > budget := summaryMarkdownMaxBytes - sb.Len() - len(tableClose)
38 >
39 > written := 0
40 > for _, row := range s.Rows {
41 > rendered := row.Markdown()
42 > if len(rendered) > budget {
43 omitted := len(s.Rows) - written
44 fmt.Fprintf(&sb, "<tr><td colspan=\"2\">… %d failure(s) not shown — see full output in job logs</td></tr>\n", omitted)
45 break
46 }
47 > sb.WriteString(rendered) summary.go
48 > budget -= len(rendered)
49 > written++
50 }
51
52 > sb.WriteString(tableClose) summary.go
53 > return sb.String()
54 }
55
66 }
67
68 > func newSummaryRowsFromReports(reports []*junitReport) []summaryRow { summary.go
69 > var rows []summaryRow
70 > for _, report := range reports {
71 > for _, suite := range report.Suites {
72 > for _, tc := range suite.Testcases { summary.go
73 > if tc.Failure == nil {
74 continue
75 }
76 > rows = append(rows, newSummaryRow(failureType(tc.Failure.Type), tc.Name, tc.Failure.Data)) summary.go
77 }
78 }
79 }
80 > slices.SortFunc(rows, func(a, b summaryRow) int { summary.go
81 if byName := strings.Compare(a.Name, b.Name); byName != 0 {
82 return byName
87 return strings.Compare(a.Details, b.Details)
88 })
89 > return rows summary.go
90 }
91
92 > func newSummaryRow(kind failureType, name string, details string) summaryRow { summary.go
93 > if len(details) > summaryMaxDetailBytes {
94 headBytes := (summaryMaxDetailBytes - len(summaryTruncatedMarker)) / 2
95 tailBytes := summaryMaxDetailBytes - len(summaryTruncatedMarker) - headBytes
96 details = details[:headBytes] + summaryTruncatedMarker + details[len(details)-tailBytes:]
97 }
98 > return summaryRow{ summary.go
99 > Kind: kind,
100 > Name: name,
101 > Details: details,
102 > Final: strings.Contains(name, "(final)"),
103 > }
104 }
105
106 // Markdown renders one summary table row.
107 > func (row summaryRow) Markdown() string { summary.go
108 > kind := string(row.Kind)
109 > if row.Final {
110 kind = "❌ " + kind
111 }
112
113 > var sb strings.Builder summary.go
114 > fmt.Fprintf(&sb, "<tr><td>%s</td><td>", html.EscapeString(kind))
115 > if row.Details != "" {
116 > escaped := html.EscapeString(row.Details)
117 > if strings.Contains(row.Details, summaryTruncatedMarker) {
118 escaped += "\n… (truncated — see full output in job logs)"
119 }
120 > fmt.Fprintf(&sb, "<details><summary>%s</summary><pre>%s</pre></details>", summary.go
121 > html.EscapeString(row.Name), escaped)
122 } else {
123 sb.WriteString(html.EscapeString(row.Name))
124 }
125 > sb.WriteString("</td></tr>\n") summary.go
126 > return sb.String()
127 }
go.temporal.io/server/tools/testrunner/log.go 37 covered LOC · 15 ranges

Open complete file

344
345 // parseFailureDetails extracts the actionable part of a JUnit failure Data block.
346 > func parseFailureDetails(data string) string { log.go
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
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
357 > return strings.Join(lines[start:end], "\n") log.go
358 > }
359 return noFailureDetails
360 }
361
362 > func normalizedFailureLines(data string) []string { log.go
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
368 }
369 > lines = lines[:len(lines)-1] log.go
370 }
371 > return lines log.go
372 }
373
374 > func findLastAssertionFailureBlock(lines []string) (string, bool) { log.go
375 > var failLine string
376 > for i := len(lines) - 1; i >= 0; i-- {
377 > line := strings.TrimSpace(lines[i]) log.go
378 > if failLine == "" && strings.HasPrefix(line, goTestFailLinePrefix) {
379 // Keep the final Go test failure line because it carries the test duration.
380 failLine = line
381 continue
382 }
383 > if !strings.Contains(lines[i], "Error Trace:") { log.go
384 > continue
385 }
386
400 return strings.Join(out, "\n"), true
401 }
402 > return "", false log.go
403 }
404
422 }
423
424 > func findLastTestOutputFailureBlock(lines []string) (start, end int, ok bool) { log.go
425 > for start := len(lines) - 1; start >= 0; start-- {
426 > if !isTestOutputLine(lines[start]) { log.go
427 > continue
428 }
429 > end := start + 1 log.go
430 > for end < len(lines) && !isTestOutputLine(lines[end]) && lines[end] != "" {
431 > end++
432 > }
433 > return start, end, true
434 }
435 return 0, 0, false
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
443 > return len(line) > 4 && line[:4] == " " && line[4] != ' ' && line[4] != '\t'
444 > }