Skip to content

Commit bf677b0

Browse files
committed
wip
1 parent 4919de9 commit bf677b0

File tree

1 file changed

+125
-1
lines changed

1 file changed

+125
-1
lines changed

tools/flakeguard/runner/runner.go

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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.
5153
func (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

Comments
 (0)