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
}
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
}
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
}
335
>
testCase.Failure.Type = string(failureTypeFailed)
336
>
}
337
}
339
>
newSuite.Testcases = append(newSuite.Testcases, testCase)
340
}
341
>
combined.Suites = append(combined.Suites, newSuite)
junit.go
342
}
343
}
344
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
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
}
48
>
budget -= len(rendered)
49
>
written++
50
}
51
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
})
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
}
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
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
}
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" {
368
}
369
>
lines = lines[:len(lines)-1]
log.go
370
}
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
}
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
}
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
>
}