go.temporal.io/server/tools/testrunner/junit.go

369 LOC · 200 covered · 169 uncovered · 51 ranges · 45 concepts · 20 introducers · 30 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/log.go · 444 LOCtestrunner/log.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…testrunner.go ×5 · 7 introduced LOCtestrunner.go ×5TestMergeReports_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 ×4testrunner.go ×1 · 3 introduced LOCtestrunner.go ×1testrunner.go ×8 · 18 introduced LOCtestrunner.go ×8TestMergeReports_PreservesOriginalFailureDataWhenExtractionFindsNothing · 0 introduced LOCTestMergeReports_Preserv…junit.go ×1 · 1 introduced LOCjunit.go ×1junit.go ×1 · 2 introduced LOCjunit.go ×1testrunner.go ×2 · 5 introduced LOCtestrunner.go ×2junit.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 LOCTestAppendAlertsSuiteTestRenderSummaryFromReports_Markdown_RendersAlertRow · 0 introduced LOCTestRenderSummaryFromRep…junit.go ×1 · 2 introduced LOCjunit.go ×1junit.go ×1 · 2 introduced LOCjunit.go ×1junit.go ×7 · 42 introduced LOCjunit.go ×7summary.go ×1 · 3 introduced LOCsummary.go ×1summary.go ×1 · 3 introduced LOCsummary.go ×1summary.go ×2 · 3 introduced LOCsummary.go ×2junit.go ×1 · 1 introduced LOCjunit.go ×1summary.go ×1 · 2 introduced LOCsummary.go ×1basic_report · 0 introduced LOCbasic_reportjunit.go ×5 · 17 introduced LOCjunit.go ×5summary.go ×4 · 11 introduced LOCsummary.go ×4log.go ×1 · 2 introduced LOClog.go ×1junit.go ×1 · 18 introduced LOCjunit.go ×1junit.go ×3 · 13 introduced LOCjunit.go ×3junit.go ×3 · 9 introduced LOCjunit.go ×3junit.go ×3 · 10 introduced LOCjunit.go ×3junit.go ×3 · 8 introduced LOCjunit.go ×3junit.go ×1 · 7 introduced LOCjunit.go ×1TestAppendAlertsSuite · introduced test · go.temporal.io/server/tools/testrunner/TestAppendAlertsSuiteTestAppendAlertsSuiteTestAppendAlertsSuite_TruncatesLargeDetails · introduced test · go.temporal.io/server/tools/testrunner/TestAppendAlertsSuite_TruncatesLargeDetailsTestAppendAlertsSuite_Tr…TestGenerateJUnitReportForTimedoutTests · introduced test · go.temporal.io/server/tools/testrunner/TestGenerateJUnitReportForTimedoutTestsTestGenerateJUnitReportF…basic_report · introduced test · go.temporal.io/server/tools/testrunner/TestJUnitXMLWellFormed/basic_reportbasic_reportreport_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_EmptyWhenNoFailures · introduced test · go.temporal.io/server/tools/testrunner/TestNewSummaryFromReports_EmptyWhenNoFailuresTestNewSummaryFromReport…TestNewSummaryFromReports_KeepsFailureAndAlertRows · introduced test · go.temporal.io/server/tools/testrunner/TestNewSummaryFromReports_KeepsFailureAndAlertRowsTestNewSummaryFromReport…TestNewSummaryFromReports_MergesMultipleReports · introduced test · go.temporal.io/server/tools/testrunner/TestNewSummaryFromReports_MergesMultipleReportsTestNewSummaryFromReport…TestNewSummaryFromReports_RendersAlertRow · introduced test · go.temporal.io/server/tools/testrunner/TestNewSummaryFromReports_RendersAlertRowTestNewSummaryFromReport…TestNewSummaryFromReports_RendersAssertionFailureRow · introduced test · go.temporal.io/server/tools/testrunner/TestNewSummaryFromReports_RendersAssertionFailureRowTestNewSummaryFromReport…TestNewSummaryFromReports_RendersTrimmedFailureBodyRow · introduced test · go.temporal.io/server/tools/testrunner/TestNewSummaryFromReports_RendersTrimmedFailureBodyRowTestNewSummaryFromReport…TestNode · introduced test · go.temporal.io/server/tools/testrunner/TestNodeTestNodeTestParseAlerts_DataRaceAndPanic · introduced test · go.temporal.io/server/tools/testrunner/TestParseAlerts_DataRaceAndPanicTestParseAlerts_DataRace…TestReadJUnitReport · introduced test · go.temporal.io/server/tools/testrunner/TestReadJUnitReportTestReadJUnitReportTestRenderSummaryFromReports_JSON · introduced test · go.temporal.io/server/tools/testrunner/TestRenderSummaryFromReports_JSONTestRenderSummaryFromRep…TestRenderSummaryFromReports_Markdown_EmptyWhenNoFailures · introduced test · go.temporal.io/server/tools/testrunner/TestRenderSummaryFromReports_Markdown_EmptyWhenNoFailuresTestRenderSummaryFromRep…TestRenderSummaryFromReports_Markdown_RendersAlertRow · introduced test · go.temporal.io/server/tools/testrunner/TestRenderSummaryFromReports_Markdown_RendersAlertRowTestRenderSummaryFromRep…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…TestRunnerPrintSummary · introduced test · go.temporal.io/server/tools/testrunner/TestRunnerPrintSummaryTestRunnerPrintSummaryTestRunnerPrintSummarySkipsEmptySummary · introduced test · go.temporal.io/server/tools/testrunner/TestRunnerPrintSummarySkipsEmptySummaryTestRunnerPrintSummarySk…TestRunnerReportCrash · introduced test · go.temporal.io/server/tools/testrunner/TestRunnerReportCrashTestRunnerReportCrashTestWriteCurrentReport · introduced test · go.temporal.io/server/tools/testrunner/TestWriteCurrentReportTestWriteCurrentReportFocused file · go.temporal.io/server/tools/testrunner/junit.go · 369 LOCtestrunner/junit.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 "encoding/xml"
5 "errors"
6 "fmt"
7 "iter"
8 "log"
9 "os"
10 "slices"
11 "strings"
12
13 "github.com/jstemmer/go-junit-report/v2/junit"
14 )
15
16 // alertsSuiteName is the JUnit suite name used for structural alerts (data
17 // races, panics, fatal errors).
18 const alertsSuiteName = "ALERTS"
19
20 const junitAlertDetailsMaxBytes = 64 * 1024
21
22 type failureType string
23
24 const (
25 // failureTypeFailed marks a failed assertion.
26 failureTypeFailed failureType = "Failed"
27 failureTypeTimeout failureType = "TIMEOUT"
28 failureTypeCrash failureType = "CRASH"
29 failureTypeDataRace failureType = "DATA RACE"
30 failureTypePanic failureType = "PANIC"
31 failureTypeFatal failureType = "FATAL"
32 )
33
34 type junitReport struct {
35 junit.Testsuites
36 path string
37 reportingErrs []error
38 }
39
40 > func (j *junitReport) read() error { junit.go ×3
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 ×3
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 ×3
52 }
53
54 // generateReport builds a JUnit report for failures that the runner
55 // derives itself, such as timeouts and crashes. Failure.Type stores the
56 // canonical failure type (for example TIMEOUT or CRASH), and Failure.Data is
57 // intentionally left empty.
58 > func generateReport(names []string, suffix string, kind failureType) *junitReport { junit.go ×1
59 > var testcases []junit.Testcase
60 > for _, name := range names {
61 > testcases = append(testcases, junit.Testcase{
62 > Name: fmt.Sprintf("%s (%s)", name, suffix),
63 > Failure: generateFailure(kind, ""),
64 > })
65 > }
66 > return &junitReport{
67 > Testsuites: junit.Testsuites{
68 > Suites: []junit.Testsuite{
69 > {
70 > Name: "suite",
71 > Testcases: testcases,
72 > },
73 > },
74 > },
75 > }
76 }
77
78 > func generateFailure(kind failureType, data string) *junit.Result { junit.go ×1
79 > return &junit.Result{
80 > Message: string(kind),
81 > Type: string(kind),
82 > Data: data,
83 > }
84 > }
85
86 > func (j *junitReport) write() error { junit.go ×3
87 > f, err := os.Create(j.path)
88 > if err != nil {
89 return fmt.Errorf("failed to open junit report file: %w", err)
90 }
91 > defer f.Close() junit.go ×3
92 >
93 > encoder := xml.NewEncoder(f)
94 > encoder.Indent("", " ")
95 > if err = encoder.Encode(j.Testsuites); err != nil {
96 return fmt.Errorf("failed to write junit report file: %w", err)
97 }
98 > log.Printf("wrote junit report to %s", j.path) junit.go ×3
99 > return nil
100 }
101
102 // appendSyntheticFailure adds a failure entry under a "testrunner" suite for
103 // events outside any real testcase (e.g. timeout killing gotestsum pre-write).
104 func (j *junitReport) appendSyntheticFailure(name string, kind failureType, detail string) {
105 tc := junit.Testcase{
106 Name: name,
107 Failure: generateFailure(kind, detail),
108 }
109 // Reuse an existing testrunner suite if one is already present.
110 for i := range j.Suites {
111 if j.Suites[i].Name == "testrunner" {
112 j.Suites[i].Testcases = append(j.Suites[i].Testcases, tc)
113 j.Suites[i].Failures++
114 j.Suites[i].Tests++
115 j.Tests++
116 j.Failures++
117 return
118 }
119 }
120 j.Suites = append(j.Suites, junit.Testsuite{
121 Name: "testrunner",
122 Failures: 1,
123 Tests: 1,
124 Testcases: []junit.Testcase{tc},
125 })
126 j.Tests++
127 j.Failures++
128 }
129
130 // appendAlertsSuite adds a synthetic JUnit suite summarizing high-priority alerts
131 // (data races, panics, fatals) so that CI surfaces them prominently.
132 > func (j *junitReport) appendAlertsSuite(alerts []alert) { junit.go ×7
133 > // Deduplicate by type+details to avoid noisy repeats across retries.
134 > alerts = dedupeAlerts(alerts)
135 > if len(alerts) == 0 {
136 return
137 }
138
139 // Convert alerts to JUnit test cases.
140 > var cases []junit.Testcase junit.go ×7
141 > for _, a := range alerts {
142 > name := fmt.Sprintf("%s: %s", a.Type, a.Summary)
143 > if p := primaryTestName(a.Tests); p != "" {
144 > name = fmt.Sprintf("%s — in %s", name, p)
145 > }
146 > var sb strings.Builder
147 > if a.Details != "" {
148 > sb.WriteString(truncateAlertDetails(sanitizeXML(a.Details)))
149 > sb.WriteByte('\n')
150 > }
151 > if len(a.Tests) > 0 {
152 > fmt.Fprintf(&sb, "Detected in tests:\n\t%s", strings.Join(a.Tests, "\n\t"))
153 > }
154 > f := generateFailure(a.Type, strings.TrimRight(sb.String(), "\n"))
155 > cases = append(cases, junit.Testcase{
156 > Name: name,
157 > Failure: f,
158 > })
159 }
160
161 // Append the alerts suite to the report.
162 > suite := junit.Testsuite{ junit.go ×7
163 > Name: alertsSuiteName,
164 > Failures: len(cases),
165 > Tests: len(cases),
166 > Testcases: cases,
167 > }
168 > j.Suites = append(j.Suites, suite)
169 > j.Failures += suite.Failures
170 > j.Tests += suite.Tests
171 }
172
173 // sanitizeXML removes characters that are invalid in XML 1.0. Go's xml.Encoder
174 // escapes <, >, & etc., but control characters other than \t, \n, \r are not
175 // legal XML and cause parsers to reject the document.
176 > func sanitizeXML(s string) string { junit.go ×7
177 > return strings.Map(func(r rune) rune {
178 > switch r {
179 > case '\t', '\n', '\r':
180 > return r
181 case 0xFFFE, 0xFFFF:
182 return -1 // Reserved Unicode noncharacters; disallowed in XML 1.0.
183 }
184 > if r < 0x20 { junit.go ×7
185 // 0x20 is space; lower code points are ASCII control characters.
186 return -1
187 }
188 > return r junit.go ×7
189 }, s)
190 }
191
192 // truncateAlertDetails keeps alert payloads from bloating the JUnit artifact.
193 > func truncateAlertDetails(s string) string { junit.go ×7
194 > if len(s) <= junitAlertDetailsMaxBytes {
195 > return s junit.go ×1
196 > }
197 > const marker = "\n... (truncated) ...\n" junit.go ×1
198 > return s[:junitAlertDetailsMaxBytes-len(marker)] + marker
199 }
200
201 // dedupeAlerts removes duplicate alerts (e.g., repeated across retries) based
202 // on type and details while preserving the first-seen order.
203 > func dedupeAlerts(alerts []alert) []alert { junit.go ×3
204 > seen := make(map[string]struct{}, len(alerts))
205 > var out []alert
206 > for _, a := range alerts {
207 > key := string(a.Type) + "\n" + a.Details
208 > if _, ok := seen[key]; ok {
209 > continue log.go ×27
210 }
211 > seen[key] = struct{}{} junit.go ×3
212 > out = append(out, a)
213 }
214 > return out junit.go ×3
215 }
216
217 > func (j *junitReport) collectTestCases() map[string]struct{} { junit.go ×4
218 > cases := make(map[string]struct{})
219 > for _, suite := range j.Suites {
220 > for _, tc := range suite.Testcases {
221 > cases[tc.Name] = struct{}{}
222 > }
223 }
224 > return cases junit.go ×4
225 }
226
227 > func (j *junitReport) collectTestCaseFailures() []string { junit.go ×5
228 > var failures []string
229 > for _, suite := range j.Suites {
230 > if suite.Failures == 0 {
231 continue
232 }
233 > for _, tc := range suite.Testcases { junit.go ×5
234 > if tc.Failure != nil {
235 > failures = append(failures, tc.Name)
236 > }
237 }
238 }
239
240 // Sort lexicographically
241 > slices.Sort(failures) junit.go ×5
242 >
243 > // Find leaf failures using the simplified algorithm
244 > var leafFailures []string
245 > for i := 0; i < len(failures)-1; i++ {
246 > if !strings.HasPrefix(failures[i+1], failures[i]+"/") { junit.go ×1
247 leafFailures = append(leafFailures, failures[i])
248 }
249 }
250 > if len(failures) > 0 { junit.go ×5
251 > leafFailures = append(leafFailures, failures[len(failures)-1])
252 > }
253
254 > return leafFailures junit.go ×5
255 }
256
257 > func mergeReports(reports []*junitReport) (*junitReport, error) { junit.go ×10
258 > if len(reports) == 0 {
259 return nil, errors.New("no reports to merge")
260 }
261
262 > var reportingErrs []error junit.go ×10
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) junit.go ×4
279 > if i == len(reports)-1 {
280 > suffix += " (final)"
281 > }
282 > prevFailures := reports[i-1].collectTestCaseFailures()
283 > currCases := report.collectTestCases()
284 >
285 > var missing []string
286 > for _, f := range prevFailures {
287 > if _, ok := currCases[f]; !ok {
288 > missing = append(missing, f) junit.go ×2
289 > }
290 }
291 > if len(missing) > 0 { junit.go ×4
292 > reportingErrs = append(reportingErrs, fmt.Errorf( junit.go ×2
293 > "expected rerun of all failures from previous attempt, missing: %v", missing))
294 > }
295 }
296
297 > for _, suite := range report.Suites { junit.go ×10
298 > if len(suite.Testcases) == 0 {
299 continue
300 }
301
302 > newSuite := suite // shallow copy junit.go ×10
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) junit.go ×1
309 > })
310
311 // Collect test cases.
312 > for j := range len(suite.Testcases) { junit.go ×10
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. junit.go ×1
318 > continue
319 }
320
321 // Parse failure details from Failure.Data, if present.
322 > if testCase.Failure != nil && testCase.Failure.Data != "" { junit.go ×10
323 > if details := parseFailureDetails(testCase.Failure.Data); details != noFailureDetails {
324 > testCase.Failure.Data = details junit.go ×1
325 > }
326 }
327
328 // Failure.Type carries the canonical kind in merged JUnit.
329 > if testCase.Failure != nil { junit.go ×10
330 > if suite.Name == alertsSuiteName {
331 > if testCase.Failure.Type == "" { junit.go ×1
332 testCase.Failure.Type = testCase.Failure.Message
333 }
334 > } else { junit.go ×1
335 > testCase.Failure.Type = string(failureTypeFailed)
336 > }
337 }
338 > testCase.Name += suffix junit.go ×10
339 > newSuite.Testcases = append(newSuite.Testcases, testCase)
340 }
341 > combined.Suites = append(combined.Suites, newSuite) junit.go ×10
342 }
343 }
344
345 > return &junitReport{ junit.go ×10
346 > Testsuites: combined,
347 > reportingErrs: reportingErrs,
348 > }, nil
349 }
350
351 type node struct {
352 children map[string]node
353 }
354
355 > func (n node) visitor(path ...string) func(yield func(string, node) bool) { junit.go ×3
356 > return func(yield func(string, node) bool) {
357 > for name, child := range n.children {
358 > path := append(path, name)
359 > if !yield(strings.Join(path, "/"), child) {
360 return
361 }
362 > child.visitor(path...)(yield) junit.go ×3
363 }
364 }
365 }
366
367 > func (n node) walk() iter.Seq2[string, node] { junit.go ×3
368 > return n.visitor()
369 > }