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.
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 (
"encoding/xml"
"errors"
"fmt"
"iter"
"log"
"os"
"slices"
"strings"
"github.com/jstemmer/go-junit-report/v2/junit"
)
// alertsSuiteName is the JUnit suite name used for structural alerts (data
// races, panics, fatal errors).
const alertsSuiteName = "ALERTS"
const junitAlertDetailsMaxBytes = 64 * 1024
type failureType string
const (
// failureTypeFailed marks a failed assertion.
failureTypeFailed failureType = "Failed"
failureTypeTimeout failureType = "TIMEOUT"
failureTypeCrash failureType = "CRASH"
failureTypeDataRace failureType = "DATA RACE"
failureTypePanic failureType = "PANIC"
failureTypeFatal failureType = "FATAL"
)
type junitReport struct {
junit.Testsuites
path string
reportingErrs []error
}
f, err := os.Open(j.path)
if err != nil {
return fmt.Errorf("failed to open junit report file: %w", err)
}
decoder := xml.NewDecoder(f)
if err = decoder.Decode(&j.Testsuites); err != nil {
return fmt.Errorf("failed to read junit report file: %w", err)
}
}
// generateReport builds a JUnit report for failures that the runner
// derives itself, such as timeouts and crashes. Failure.Type stores the
// canonical failure type (for example TIMEOUT or CRASH), and Failure.Data is
// intentionally left empty.
func generateReport(names []string, suffix string, kind failureType) *junitReport {
junit.go ×1
var testcases []junit.Testcase
for _, name := range names {
testcases = append(testcases, junit.Testcase{
Name: fmt.Sprintf("%s (%s)", name, suffix),
Failure: generateFailure(kind, ""),
})
}
return &junitReport{
Testsuites: junit.Testsuites{
Suites: []junit.Testsuite{
{
Name: "suite",
Testcases: testcases,
},
},
},
}
}
return &junit.Result{
Message: string(kind),
Type: string(kind),
Data: data,
}
}
f, err := os.Create(j.path)
if err != nil {
return fmt.Errorf("failed to open junit report file: %w", err)
}
encoder := xml.NewEncoder(f)
encoder.Indent("", " ")
if err = encoder.Encode(j.Testsuites); err != nil {
return fmt.Errorf("failed to write junit report file: %w", err)
}
return nil
}
// appendSyntheticFailure adds a failure entry under a "testrunner" suite for
// events outside any real testcase (e.g. timeout killing gotestsum pre-write).
func (j *junitReport) appendSyntheticFailure(name string, kind failureType, detail string) {
tc := junit.Testcase{
Name: name,
Failure: generateFailure(kind, detail),
}
// Reuse an existing testrunner suite if one is already present.
for i := range j.Suites {
if j.Suites[i].Name == "testrunner" {
j.Suites[i].Testcases = append(j.Suites[i].Testcases, tc)
j.Suites[i].Failures++
j.Suites[i].Tests++
j.Tests++
j.Failures++
return
}
}
j.Suites = append(j.Suites, junit.Testsuite{
Name: "testrunner",
Failures: 1,
Tests: 1,
Testcases: []junit.Testcase{tc},
})
j.Tests++
j.Failures++
}
// appendAlertsSuite adds a synthetic JUnit suite summarizing high-priority alerts
// (data races, panics, fatals) so that CI surfaces them prominently.
// Deduplicate by type+details to avoid noisy repeats across retries.
alerts = dedupeAlerts(alerts)
if len(alerts) == 0 {
return
}
// Convert alerts to JUnit test cases.
for _, a := range alerts {
name := fmt.Sprintf("%s: %s", a.Type, a.Summary)
if p := primaryTestName(a.Tests); p != "" {
name = fmt.Sprintf("%s — in %s", name, p)
}
var sb strings.Builder
if a.Details != "" {
sb.WriteString(truncateAlertDetails(sanitizeXML(a.Details)))
sb.WriteByte('\n')
}
if len(a.Tests) > 0 {
fmt.Fprintf(&sb, "Detected in tests:\n\t%s", strings.Join(a.Tests, "\n\t"))
}
f := generateFailure(a.Type, strings.TrimRight(sb.String(), "\n"))
cases = append(cases, junit.Testcase{
Name: name,
Failure: f,
})
}
// Append the alerts suite to the report.
Name: alertsSuiteName,
Failures: len(cases),
Tests: len(cases),
Testcases: cases,
}
j.Suites = append(j.Suites, suite)
j.Failures += suite.Failures
j.Tests += suite.Tests
}
// sanitizeXML removes characters that are invalid in XML 1.0. Go's xml.Encoder
// escapes <, >, & etc., but control characters other than \t, \n, \r are not
// legal XML and cause parsers to reject the document.
return strings.Map(func(r rune) rune {
switch r {
case '\t', '\n', '\r':
return r
case 0xFFFE, 0xFFFF:
return -1 // Reserved Unicode noncharacters; disallowed in XML 1.0.
}
// 0x20 is space; lower code points are ASCII control characters.
return -1
}
}, s)
}
// truncateAlertDetails keeps alert payloads from bloating the JUnit artifact.
if len(s) <= junitAlertDetailsMaxBytes {
}
return s[:junitAlertDetailsMaxBytes-len(marker)] + marker
}
// dedupeAlerts removes duplicate alerts (e.g., repeated across retries) based
// on type and details while preserving the first-seen order.
seen := make(map[string]struct{}, len(alerts))
var out []alert
for _, a := range alerts {
key := string(a.Type) + "\n" + a.Details
if _, ok := seen[key]; ok {
}
out = append(out, a)
}
}
cases := make(map[string]struct{})
for _, suite := range j.Suites {
for _, tc := range suite.Testcases {
cases[tc.Name] = struct{}{}
}
}
}
var failures []string
for _, suite := range j.Suites {
if suite.Failures == 0 {
continue
}
if tc.Failure != nil {
failures = append(failures, tc.Name)
}
}
}
// Sort lexicographically
// Find leaf failures using the simplified algorithm
var leafFailures []string
for i := 0; i < len(failures)-1; i++ {
leafFailures = append(leafFailures, failures[i])
}
}
leafFailures = append(leafFailures, failures[len(failures)-1])
}
}
if len(reports) == 0 {
return nil, errors.New("no reports to merge")
}
var combined junit.Testsuites
combined.XMLName = reports[0].XMLName
combined.Name = reports[0].Name
for i, report := range reports {
combined.Tests += report.Tests
combined.Errors += report.Errors
combined.Failures += report.Failures
combined.Skipped += report.Skipped
combined.Disabled += report.Disabled
combined.Time += report.Time
// If the report is for a retry ...
var suffix string
if i > 0 {
if i == len(reports)-1 {
suffix += " (final)"
}
prevFailures := reports[i-1].collectTestCaseFailures()
currCases := report.collectTestCases()
var missing []string
for _, f := range prevFailures {
if _, ok := currCases[f]; !ok {
}
}
"expected rerun of all failures from previous attempt, missing: %v", missing))
}
}
if len(suite.Testcases) == 0 {
continue
}
newSuite.Name += suffix
newSuite.Testcases = make([]junit.Testcase, 0, len(suite.Testcases))
// Sort test cases by name.
slices.SortFunc(suite.Testcases, func(a, b junit.Testcase) int {
})
// Collect test cases.
testCase := suite.Testcases[j]
// Check if this is a parent test case (ie prefix of next subtest).
// Use testCase.Name+"/" to avoid matching iteration suffixes like #01.
if j != len(suite.Testcases)-1 && strings.HasPrefix(suite.Testcases[j+1].Name, testCase.Name+"/") {
continue
}
// Parse failure details from Failure.Data, if present.
if details := parseFailureDetails(testCase.Failure.Data); details != noFailureDetails {
}
}
// Failure.Type carries the canonical kind in merged JUnit.
if suite.Name == alertsSuiteName {
testCase.Failure.Type = testCase.Failure.Message
}
testCase.Failure.Type = string(failureTypeFailed)
}
}
newSuite.Testcases = append(newSuite.Testcases, testCase)
}
}
}
Testsuites: combined,
reportingErrs: reportingErrs,
}, nil
}
type node struct {
children map[string]node
}
return func(yield func(string, node) bool) {
for name, child := range n.children {
path := append(path, name)
if !yield(strings.Join(path, "/"), child) {
return
}
}
}
}
return n.visitor()
}