@@ -37,6 +37,7 @@ type Runner struct {
3737 UseShuffle bool // Enable test shuffling. -shuffle=on flag.
3838 ShuffleSeed string // Set seed for test shuffling -shuffle={seed} flag. Must be used with UseShuffle.
3939 FailFast bool // Stop on first test failure.
40+ RerunFailed int // Number of additional runs for tests that initially fail.
4041 SkipTests []string // Test names to exclude.
4142 SelectTests []string // Test names to include.
4243 CollectRawOutput bool // Set to true to collect test output for later inspection.
@@ -48,11 +49,13 @@ type Runner struct {
4849
4950// RunTestPackages executes the tests for each provided package and aggregates all results.
5051// It returns all test results and any error encountered during testing.
52+ // RunTestPackages executes the tests for each provided package and aggregates all results.
5153func (r * Runner ) RunTestPackages (packages []string ) (* reports.TestReport , error ) {
5254 var jsonFilePaths []string
55+ // Initial runs.
5356 for _ , p := range packages {
5457 for i := 0 ; i < r .RunCount ; i ++ {
55- if r .CollectRawOutput { // Collect raw output for debugging
58+ if r .CollectRawOutput { // Collect raw output for debugging.
5659 if r .rawOutputs == nil {
5760 r .rawOutputs = make (map [string ]* bytes.Buffer )
5861 }
@@ -73,10 +76,23 @@ func (r *Runner) RunTestPackages(packages []string) (*reports.TestReport, error)
7376 }
7477 }
7578
79+ // Parse initial results.
7680 results , err := r .parseTestResults (jsonFilePaths )
7781 if err != nil {
7882 return nil , fmt .Errorf ("failed to parse test results: %w" , err )
7983 }
84+
85+ // Rerun failing tests (only the unique tests).
86+ if r .RerunFailed > 0 {
87+ rerunResults , err := r .rerunFailedTests (results )
88+ if err != nil {
89+ return nil , fmt .Errorf ("failed to rerun failing tests: %w" , err )
90+ }
91+
92+ // Merge rerun results with initial results.
93+ mergeTestResults (& results , rerunResults )
94+ }
95+
8096 report := & reports.TestReport {
8197 GoProject : r .prettyProjectPath ,
8298 RaceDetection : r .UseRace ,
@@ -718,3 +734,111 @@ func prettyProjectPath(projectPath string) (string, error) {
718734
719735 return "" , fmt .Errorf ("module path not found in go.mod" )
720736}
737+
738+ func (r * Runner ) rerunFailedTests (results []reports.TestResult ) ([]reports.TestResult , error ) {
739+ var failingTests []reports.TestResult
740+ for _ , tr := range results {
741+ if ! tr .Skipped && tr .PassRatio < 1 {
742+ failingTests = append (failingTests , tr )
743+ }
744+ }
745+
746+ if r .Verbose {
747+ log .Info ().Msgf ("Rerunning failing tests: %v" , failingTests )
748+ }
749+
750+ var rerunResults []reports.TestResult
751+
752+ // Rerun each failing test up to RerunFailed times
753+ for i := 0 ; i < r .RerunFailed ; i ++ {
754+ for _ , fTest := range failingTests {
755+ testCmd := r .buildGoTestCommandForTest (fTest )
756+
757+ if r .Verbose {
758+ log .Info ().Msgf ("Rerun iteration %d for %s: %v" , i + 1 , fTest .TestName , testCmd )
759+ }
760+
761+ jsonFilePath , _ , err := r .runCmd (testCmd , i )
762+ if err != nil {
763+ return nil , fmt .Errorf ("error on rerunCmd for test %s: %w" , fTest .TestName , err )
764+ }
765+
766+ additionalResults , err := r .parseTestResults ([]string {jsonFilePath })
767+ if err != nil {
768+ return nil , fmt .Errorf ("failed to parse rerun results: %w" , err )
769+ }
770+
771+ // Collect these rerun results in a slice; we'll merge them later.
772+ rerunResults = append (rerunResults , additionalResults ... )
773+ }
774+ }
775+
776+ return rerunResults , nil
777+ }
778+
779+ // buildGoTestCommandForTest builds a `go test` command specifically
780+ // for one failing test, using TestPackage and TestName in the -run argument.
781+ func (r * Runner ) buildGoTestCommandForTest (t reports.TestResult ) []string {
782+ cmd := []string {
783+ "go" , "test" ,
784+ t .TestPackage ,
785+ "-run" , fmt .Sprintf ("^%s$" , t .TestName ), // Run exactly this test
786+ "-json" , // Example flag, adjust as needed
787+ }
788+
789+ // Add verbosity if requested
790+ if r .Verbose {
791+ cmd = append (cmd , "-v" )
792+ }
793+
794+ // Add any additional flags or args required by your setup here
795+
796+ return cmd
797+ }
798+
799+ // appendUnique appends s to slice if not already present.
800+ func appendUnique (slice []string , s string ) []string {
801+ for _ , v := range slice {
802+ if v == s {
803+ return slice
804+ }
805+ }
806+ return append (slice , s )
807+ }
808+
809+ // mergeTestResults merges additional test results into the existing results slice.
810+ func mergeTestResults (mainResults * []reports.TestResult , additional []reports.TestResult ) {
811+ for _ , add := range additional {
812+ found := false
813+ for i , main := range * mainResults {
814+ if main .TestName == add .TestName && main .TestPackage == add .TestPackage {
815+ // Merge top-level stats
816+ (* mainResults )[i ].Runs += add .Runs
817+ (* mainResults )[i ].Successes += add .Successes
818+ (* mainResults )[i ].Failures += add .Failures
819+ (* mainResults )[i ].Skips += add .Skips
820+
821+ // Merge durations
822+ (* mainResults )[i ].Durations = append ((* mainResults )[i ].Durations , add .Durations ... )
823+
824+ // Because PassedOutputs and FailedOutputs are now []string:
825+ (* mainResults )[i ].PassedOutputs = append ((* mainResults )[i ].PassedOutputs , add .PassedOutputs ... )
826+ (* mainResults )[i ].FailedOutputs = append ((* mainResults )[i ].FailedOutputs , add .FailedOutputs ... )
827+
828+ // Update pass ratio
829+ if (* mainResults )[i ].Runs > 0 {
830+ (* mainResults )[i ].PassRatio = float64 ((* mainResults )[i ].Successes ) / float64 ((* mainResults )[i ].Runs )
831+ } else {
832+ (* mainResults )[i ].PassRatio = - 1.0
833+ }
834+
835+ found = true
836+ break
837+ }
838+ }
839+ // If we didn't find a match, append this as a new test result
840+ if ! found {
841+ * mainResults = append (* mainResults , add )
842+ }
843+ }
844+ }
0 commit comments