@@ -94,7 +94,7 @@ func extractFailures(suites *junit.Testsuites, artifactName string, runID int64,
9494
9595// extractAllTestRuns extracts all test runs (including successes) from parsed JUnit data
9696// Used for calculating failure rates
97- func extractAllTestRuns (suites * junit.Testsuites , runID int64 ) []TestRun {
97+ func extractAllTestRuns (suites * junit.Testsuites , runID int64 , jobID string ) []TestRun {
9898 var runs []TestRun
9999
100100 for _ , suite := range suites .Suites {
@@ -105,6 +105,7 @@ func extractAllTestRuns(suites *junit.Testsuites, runID int64) []TestRun {
105105 Failed : testcase .Failure != nil ,
106106 Skipped : testcase .Skipped != nil ,
107107 RunID : runID ,
108+ JobID : jobID ,
108109 }
109110 runs = append (runs , run )
110111 }
@@ -237,6 +238,25 @@ func convertToReports(grouped map[string][]TestFailure, testRunCounts map[string
237238 return reports
238239}
239240
241+ // filterParentTests removes top-level test names from grouped when subtests of
242+ // that parent were observed in testRunCounts. A top-level failure whose subtests
243+ // ran in other CI jobs is already captured (with a correct denominator) in the
244+ // Flaky Suites section, so including it in the per-test table produces a
245+ // misleading 1/1 entry.
246+ func filterParentTests (grouped map [string ][]TestFailure , testRunCounts map [string ]int ) {
247+ suitePrefix := make (map [string ]bool , len (testRunCounts ))
248+ for name := range testRunCounts {
249+ if idx := strings .IndexByte (name , '/' ); idx >= 0 {
250+ suitePrefix [name [:idx ]] = true
251+ }
252+ }
253+ for testName := range grouped {
254+ if ! strings .Contains (testName , "/" ) && suitePrefix [testName ] {
255+ delete (grouped , testName )
256+ }
257+ }
258+ }
259+
240260// isFinalRetry returns true if the test name has the "(final)" suffix,
241261// indicating the test runner exhausted all retries.
242262func isFinalRetry (testName string ) bool {
@@ -341,23 +361,36 @@ func identifyCIBreakers(failures []TestFailure) (map[string][]TestFailure, map[s
341361 return ciBreakers , ciBreakCount
342362}
343363
364+ // jobKey returns a string that uniquely identifies a single job execution.
365+ // When a real JobID is available it is globally unique; otherwise we fall back
366+ // to the RunID so that the set still grows correctly across CI runs.
367+ func jobKey (runID int64 , jobID string ) string {
368+ if jobID != "" && jobID != "unknown" {
369+ return jobID
370+ }
371+ return fmt .Sprintf ("%d" , runID )
372+ }
373+
344374// generateSuiteReports creates per-suite flake breakdown from all failures and test runs.
345- // Suite flake rate = % of workflow runs where the suite had at least one non-retry failure.
375+ // Suite flake rate = % of job executions where the suite had at least one non-retry failure.
346376func generateSuiteReports (allFailures []TestFailure , allTestRuns []TestRun ) []SuiteReport {
347- // Track unique workflow runs per suite (denominator)
348- suiteRuns := make (map [string ]map [int64 ]bool )
377+ // Track unique job executions per suite (denominator).
378+ // Each matrix shard / DB-config combination is a separate job execution even
379+ // though it shares the same workflow RunID, so we key by JobID (falling back
380+ // to RunID when JobID is unavailable).
381+ suiteRuns := make (map [string ]map [string ]bool )
349382 for _ , run := range allTestRuns {
350383 if run .Skipped || ! isGoTestSuite (run .SuiteName ) {
351384 continue
352385 }
353386 if suiteRuns [run .SuiteName ] == nil {
354- suiteRuns [run .SuiteName ] = make (map [int64 ]bool )
387+ suiteRuns [run .SuiteName ] = make (map [string ]bool )
355388 }
356- suiteRuns [run.SuiteName ][run.RunID ] = true
389+ suiteRuns [run .SuiteName ][jobKey ( run .RunID , run . JobID ) ] = true
357390 }
358391
359- // Track workflow runs with non-retry failures per suite (numerator)
360- suiteFailedRuns := make (map [string ]map [int64 ]bool )
392+ // Track job executions with non-retry failures per suite (numerator)
393+ suiteFailedRuns := make (map [string ]map [string ]bool )
361394 suiteLastFailure := make (map [string ]time.Time )
362395 for _ , failure := range allFailures {
363396 if ! isGoTestSuite (failure .SuiteName ) {
@@ -368,9 +401,9 @@ func generateSuiteReports(allFailures []TestFailure, allTestRuns []TestRun) []Su
368401 continue
369402 }
370403 if suiteFailedRuns [failure .SuiteName ] == nil {
371- suiteFailedRuns [failure .SuiteName ] = make (map [int64 ]bool )
404+ suiteFailedRuns [failure .SuiteName ] = make (map [string ]bool )
372405 }
373- suiteFailedRuns [failure.SuiteName ][failure.RunID ] = true
406+ suiteFailedRuns [failure .SuiteName ][jobKey ( failure .RunID , failure . JobID ) ] = true
374407 if failure .Timestamp .After (suiteLastFailure [failure .SuiteName ]) {
375408 suiteLastFailure [failure .SuiteName ] = failure .Timestamp
376409 }
0 commit comments