Atlas › Test

TestWriteCurrentReport

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

Package
go.temporal.io/server/tools/testrunner
Suite / test hierarchy
TestWriteCurrentReport
Test
TestWriteCurrentReport
Introduced at
testrunner.go ×6 Frontier kind: Joint frontier
Covered ranges
56
Covered lines
188
Covered files
3

Covered source

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

go.temporal.io/server/tools/testrunner/junit.go 105 covered LOC · 30 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
84 }
85
86 > func (j *junitReport) write() error { junit.go
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
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
99 > return nil
100 }
101
215 }
216
217 > func (j *junitReport) collectTestCases() map[string]struct{} { junit.go
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
225 }
226
227 > func (j *junitReport) collectTestCaseFailures() []string { junit.go
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
234 > if tc.Failure != nil {
235 > failures = append(failures, tc.Name)
236 > }
237 }
238 }
239
240 // Sort lexicographically
241 > slices.Sort(failures) junit.go
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
247 leafFailures = append(leafFailures, failures[i])
248 }
249 }
250 > if len(failures) > 0 { junit.go
251 > leafFailures = append(leafFailures, failures[len(failures)-1])
252 > }
253
254 > return leafFailures junit.go
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) junit.go
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)
289 }
290 }
291 > if len(missing) > 0 { junit.go
292 reportingErrs = append(reportingErrs, fmt.Errorf(
293 "expected rerun of all failures from previous attempt, missing: %v", missing))
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) junit.go
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. junit.go
318 > continue
319 }
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/log.go 47 covered LOC · 19 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 log.go
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.
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. log.go
380 > failLine = line
381 > continue
382 }
383 > if !strings.Contains(lines[i], "Error Trace:") { log.go
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
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
397 > if failLine != "" {
398 > out = append(out, "", failLine) log.go
399 > }
400 > return strings.Join(out, "\n"), true log.go
401 }
402 return "", false
403 }
404
405 > func endOfAssertionBlock(lines []string, start int) int { log.go
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
411 > }
412 > if strings.HasPrefix(line, "Test:") { log.go
413 > sawTestLine = true log.go
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
418 return i
419 }
go.temporal.io/server/tools/testrunner/testrunner.go 36 covered LOC · 7 ranges

Open complete file

81 }
82
83 > func newRunner() *runner { testrunner.go
84 > return &runner{
85 > attempts: make([]*attempt, 0),
86 > maxAttempts: 1,
87 > }
88 > }
89
90 // nolint:revive,cognitive-complexity
185 }
186
187 > func (r *runner) newAttempt() *attempt { testrunner.go
188 > a := &attempt{
189 > runner: r,
190 > number: len(r.attempts) + 1,
191 > coverProfilePath: fmt.Sprintf(
192 > "%v_%v%v",
193 > strings.TrimSuffix(r.coverProfilePath, codeCoverageExtension),
194 > len(r.attempts),
195 > codeCoverageExtension),
196 > junitReport: &junitReport{
197 > path: filepath.Join(os.TempDir(), fmt.Sprintf("temporalio-temporal-%s-junit.xml", uuid.NewString())),
198 > },
199 > }
200 > r.attempts = append(r.attempts, a)
201 > return a
202 > }
203
204 > func (r *runner) allReports() []*junitReport { testrunner.go
205 > var reports []*junitReport
206 > for _, a := range r.attempts {
207 > reports = append(reports, a.junitReport)
208 > }
209 > return reports
210 }
211
301 // Reporting errors (e.g. unexpected missing reruns) are intentionally ignored
302 // here; they are only checked for the final write at the end of runTests.
303 > func (r *runner) writeCurrentReport() { testrunner.go
304 > reports := r.allReports()
305 > if len(reports) == 0 {
306 return
307 }
308 > merged, err := mergeReports(reports) testrunner.go
309 > if err != nil {
310 log.Printf("warning: failed to merge reports for intermediate write: %v", err)
311 return
312 }
313 > if len(r.alerts) > 0 { testrunner.go
314 merged.appendAlertsSuite(r.alerts)
315 }
316 > merged.path = r.junitOutputPath testrunner.go
317 > if err := merged.write(); err != nil {
318 log.Printf("warning: failed to write intermediate report: %v", err)
319 }