Skip to content

Commit 782be4b

Browse files
committed
Refactor Flakeguard test execution flow: enhance initial run handling, improve rerun logic, and update logging for clarity. Adjusted command output messages for better user guidance and added checks for command-line argument usage to prevent unreliable test reruns.
1 parent 61bfc9d commit 782be4b

File tree

1 file changed

+117
-46
lines changed

1 file changed

+117
-46
lines changed

tools/flakeguard/cmd/run.go

Lines changed: 117 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,13 @@ var RunTestsCmd = &cobra.Command{
108108
testRunner := initializeRunner(cfg)
109109

110110
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
111-
s.Suffix = " Running tests..."
111+
s.Suffix = " Running initial tests..."
112112
s.Start()
113113

114114
var mainResults []reports.TestResult
115115
var runErr error
116116
if len(cfg.TestCmds) > 0 {
117-
s.Suffix = " Running custom test command..."
117+
s.Suffix = " Running custom test command(s)..."
118118
mainResults, runErr = testRunner.RunTestCmd(cfg.TestCmds)
119119
} else {
120120
s.Suffix = " Running test packages..."
@@ -123,7 +123,7 @@ var RunTestsCmd = &cobra.Command{
123123
s.Stop()
124124

125125
if runErr != nil {
126-
exitHandler.logErrorAndExit(runErr, "Error running tests")
126+
exitHandler.logErrorAndExit(runErr, "Error running initial tests")
127127
}
128128
if len(mainResults) == 0 {
129129
exitHandler.logMsgAndExit(zerolog.ErrorLevel, "No tests were run.", ErrorExitCode)
@@ -142,12 +142,32 @@ var RunTestsCmd = &cobra.Command{
142142
}
143143

144144
if cfg.RerunFailedCount > 0 {
145-
handleReruns(exitHandler, testRunner, mainReport, cfg, goProject)
145+
// Process the initial run, potentially display logs, and decide if reruns are needed.
146+
failedTests, proceedToRerun := handleInitialRunResults(exitHandler, mainReport, cfg, goProject)
147+
if !proceedToRerun {
148+
// handleInitialRunResults will have set the exit code and printed messages.
149+
exitHandler.flush() // Exit based on initial run results.
150+
return
151+
}
152+
153+
// Execute the reruns and generate the rerun report.
154+
rerunResults, rerunReport, err := executeAndReportReruns(exitHandler, testRunner, failedTests, cfg, goProject)
155+
if err != nil {
156+
// executeAndReportReruns logs the error and sets the exit code.
157+
exitHandler.flush()
158+
return
159+
}
160+
161+
// Evaluate the final outcome after reruns.
162+
evaluateRerunOutcome(exitHandler, rerunReport, rerunResults, cfg)
163+
146164
} else {
165+
// Handle the case where reruns are disabled.
147166
handleNoReruns(exitHandler, mainReport, cfg)
148167
}
149168

150-
exitHandler.code = 0
169+
// If we reach here without an early exit, it implies success (or flaky tests handled by handleNoReruns).
170+
// The exit code will be set by the specific handlers.
151171
exitHandler.flush()
152172
},
153173
}
@@ -303,49 +323,75 @@ func getJSONOutputPaths(dir string) ([]string, error) {
303323
return paths, nil
304324
}
305325

306-
// handleReruns manages the process of rerunning failed tests and reporting results.
307-
func handleReruns(exitHandler *summaryAndExit, testRunner *runner.Runner, mainReport *reports.TestReport, cfg *runConfig, goProject string) {
326+
// handleInitialRunResults processes the results of the initial test run.
327+
// It prints summaries and logs for failed tests.
328+
// It returns the list of failed tests and a boolean indicating whether to proceed with reruns.
329+
func handleInitialRunResults(exitHandler *summaryAndExit, mainReport *reports.TestReport, cfg *runConfig, goProject string) ([]reports.TestResult, bool) {
308330
failedTests := reports.FilterTests(mainReport.Results, func(tr reports.TestResult) bool {
309-
return !tr.Skipped && tr.PassRatio < 1.0 // Rerun only tests that failed completely or partially in the main run
331+
// Consider a test "failed" initially if it wasn't skipped and didn't pass all its runs.
332+
return !tr.Skipped && tr.PassRatio < 1.0
310333
})
311334

312335
if len(failedTests) == 0 {
313336
log.Info().Msg("All tests passed the initial run. No tests to rerun.")
314-
fmt.Fprint(&exitHandler.buffer, "\nFlakeguard Summary\n")
337+
fmt.Fprint(&exitHandler.buffer, "\nFlakeguard Initial Run Summary\n")
315338
reports.RenderTestReport(&exitHandler.buffer, *mainReport, false, false)
316-
exitHandler.code = 0
317-
exitHandler.flush()
318-
return
339+
exitHandler.code = 0 // Success
340+
return nil, false // Do not proceed to rerun
319341
}
320342

343+
// Print summary of initially failed tests
344+
fmt.Fprint(&exitHandler.buffer, "\nFailed Tests Summary (Initial Run):\n\n")
345+
reports.PrintTestResultsTable(&exitHandler.buffer, failedTests, false, false, true, false, false, false)
346+
fmt.Fprintln(&exitHandler.buffer)
347+
348+
// Check for the problematic 'go test file.go' case within --test-cmd
349+
foundCommandLineArgs := false
321350
if len(cfg.TestCmds) > 0 {
322-
foundCommandLineArgs := false
323351
for _, test := range failedTests {
324352
if test.TestPackage == "command-line-arguments" {
325353
foundCommandLineArgs = true
326354
break
327355
}
328356
}
357+
}
329358

330-
if foundCommandLineArgs {
331-
warningMsg := "WARNING: Skipping all reruns because 'go test <file.go>' was detected within --test-cmd. " +
332-
"Flakeguard cannot reliably rerun these tests as it loses the original directory context. " +
333-
"Results are based on the initial run only. To enable reruns, use 'go test . -run TestPattern' instead of 'go test <file.go>' within your --test-cmd."
334-
log.Warn().Msg(warningMsg)
335-
fmt.Fprint(&exitHandler.buffer, "\nFailed Tests On The First Run:\n\n")
336-
reports.PrintTestResultsTable(&exitHandler.buffer, failedTests, false, false, true, false, false, false)
337-
fmt.Fprintf(&exitHandler.buffer, "\n\n%s\n", warningMsg)
338-
handleNoReruns(exitHandler, mainReport, cfg)
339-
return
340-
}
359+
if foundCommandLineArgs {
360+
// Log warning but don't print logs as context might be unreliable
361+
warningMsg := "WARNING: Skipping initial failure logs and all reruns because 'go test <file.go>' was detected within --test-cmd. " +
362+
"Flakeguard cannot reliably determine log context or rerun these tests. " +
363+
"Results are based on the initial run only. To enable logs and reruns, use 'go test . -run TestPattern' instead of 'go test <file.go>' within your --test-cmd."
364+
log.Warn().Msg(warningMsg)
365+
fmt.Fprintf(&exitHandler.buffer, "\n%s\n", warningMsg)
366+
// Treat this as if reruns were disabled, determining exit code based on MinPassRatio
367+
handleNoReruns(exitHandler, mainReport, cfg)
368+
return failedTests, false // Do not proceed to rerun
341369
}
342370

343-
fmt.Fprint(&exitHandler.buffer, "\nFailed Tests On The First Run:\n\n")
344-
reports.PrintTestResultsTable(&exitHandler.buffer, failedTests, false, false, true, false, false, false)
345-
fmt.Fprintln(&exitHandler.buffer)
371+
// Print logs for initially failed tests (using gotestsum style)
372+
fmt.Fprint(&exitHandler.buffer, "\nLogs from Initial Run for Failed Tests:\n\n")
373+
failedOnlyReport, err := reports.NewTestReport(failedTests,
374+
reports.WithGoProject(goProject), // Include for context
375+
reports.WithJSONOutputPaths(mainReport.JSONOutputPaths),
376+
// Potentially add other relevant options if PrintGotestsumOutput needs them
377+
)
378+
if err != nil {
379+
log.Warn().Err(err).Msg("Failed to create temporary report for initial failed test logs. Skipping log output.")
380+
} else {
381+
// Use "testname" grouping for potentially better focus on individual test failures
382+
err = failedOnlyReport.PrintGotestsumOutput(&exitHandler.buffer, "testname")
383+
if err != nil {
384+
log.Warn().Err(err).Msg("Error printing gotestsum output for initially failed tests")
385+
}
386+
fmt.Fprintln(&exitHandler.buffer) // Add a newline for spacing after logs
387+
}
346388

347-
log.Info().Int("count", len(failedTests)).Int("rerun_count", cfg.RerunFailedCount).Msg("Rerunning failed tests...")
389+
log.Info().Int("count", len(failedTests)).Int("rerun_count", cfg.RerunFailedCount).Msg("Proceeding to rerun failed tests...")
390+
return failedTests, true // Proceed to rerun
391+
}
348392

393+
// executeAndReportReruns performs the rerun of failed tests and generates a report.
394+
func executeAndReportReruns(exitHandler *summaryAndExit, testRunner *runner.Runner, failedTests []reports.TestResult, cfg *runConfig, goProject string) ([]reports.TestResult, *reports.TestReport, error) {
349395
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
350396
s.Suffix = " Rerunning failed tests..."
351397
s.Start()
@@ -354,41 +400,49 @@ func handleReruns(exitHandler *summaryAndExit, testRunner *runner.Runner, mainRe
354400

355401
if err != nil {
356402
exitHandler.logErrorAndExit(err, "Error rerunning failed tests")
403+
return nil, nil, err // Return error to signal failure
357404
}
358405

359406
rerunReport, err := reports.NewTestReport(rerunResults,
360407
reports.WithGoProject(goProject),
361408
reports.WithCodeOwnersPath(cfg.CodeownersPath),
362-
reports.WithMaxPassRatio(1),
409+
reports.WithMaxPassRatio(1), // Pass ratio for reruns is effectively 1 (did it pass at least once?)
363410
reports.WithExcludedTests(cfg.SkipTests),
364411
reports.WithSelectedTests(cfg.SelectTests),
365412
reports.WithJSONOutputPaths(rerunJsonOutputPaths),
366413
)
367414
if err != nil {
368415
exitHandler.logErrorAndExit(err, "Error creating rerun test report")
416+
return rerunResults, nil, err // Return error
369417
}
370418

371-
fmt.Fprint(&exitHandler.buffer, "\nTests After Rerun:\n\n")
419+
fmt.Fprint(&exitHandler.buffer, "Tests After Rerun:")
372420
reports.PrintTestResultsTable(&exitHandler.buffer, rerunResults, false, false, true, true, true, true)
373421
fmt.Fprintln(&exitHandler.buffer)
374422

375423
// Save the rerun test report to file
376424
if cfg.RerunResultsPath != "" && len(rerunResults) > 0 {
377425
if err := reports.SaveTestResultsToFile(rerunResults, cfg.RerunResultsPath); err != nil {
378426
log.Error().Err(err).Str("path", cfg.RerunResultsPath).Msg("Error saving rerun test results to file")
427+
// Don't treat this as a fatal error for the overall run
379428
} else {
380429
log.Info().Str("path", cfg.RerunResultsPath).Msg("Rerun test report saved")
381430
}
382431
}
383432

433+
return rerunResults, &rerunReport, nil // Success
434+
}
435+
436+
// evaluateRerunOutcome determines the final exit code based on persistently failing tests after reruns.
437+
func evaluateRerunOutcome(exitHandler *summaryAndExit, rerunReport *reports.TestReport, rerunResults []reports.TestResult, cfg *runConfig) {
384438
// Filter tests that still failed after reruns (0 successes)
385439
failedAfterRerun := reports.FilterTests(rerunResults, func(tr reports.TestResult) bool {
386440
return !tr.Skipped && tr.Successes == 0
387441
})
388442

389443
if len(failedAfterRerun) > 0 {
390-
fmt.Fprint(&exitHandler.buffer, "\nPersistently Failing Test Logs:\n\n")
391-
err := rerunReport.PrintGotestsumOutput(&exitHandler.buffer, "pkgname")
444+
fmt.Fprint(&exitHandler.buffer, "Persistently Failing Test Logs (After Reruns):")
445+
err := rerunReport.PrintGotestsumOutput(&exitHandler.buffer, "testname") // Use testname grouping
392446
if err != nil {
393447
log.Error().Err(err).Msg("Error printing gotestsum output for persistently failing tests")
394448
}
@@ -399,43 +453,54 @@ func handleReruns(exitHandler *summaryAndExit, testRunner *runner.Runner, mainRe
399453
})
400454
} else {
401455
log.Info().Msg("All initially failing tests passed at least once after reruns.")
402-
exitHandler.code = 0
403-
exitHandler.flush()
456+
exitHandler.code = 0 // Success
457+
// No need to call flush here, the main function will do it.
404458
}
405459
}
406460

407-
// handleNoReruns determines the outcome when reruns are disabled.
461+
// handleNoReruns determines the outcome when reruns are disabled or skipped.
408462
func handleNoReruns(exitHandler *summaryAndExit, mainReport *reports.TestReport, cfg *runConfig) {
409463
flakyTests := reports.FilterTests(mainReport.Results, func(tr reports.TestResult) bool {
464+
// A test is flaky if it wasn't skipped and its pass ratio is below the threshold.
410465
return !tr.Skipped && tr.PassRatio < cfg.MinPassRatio
411466
})
412467

413-
fmt.Fprint(&exitHandler.buffer, "\nFlakeguard Summary\n")
414-
reports.RenderTestReport(&exitHandler.buffer, *mainReport, false, false)
468+
// Print the final summary report only if it wasn't printed already (e.g., in the no-initial-failures case)
469+
// We check if the buffer already contains the "Flakeguard Initial Run Summary" header
470+
if !bytes.Contains(exitHandler.buffer.Bytes(), []byte("Flakeguard Initial Run Summary")) {
471+
fmt.Fprint(&exitHandler.buffer, "Flakeguard Summary (No Reruns Performed)")
472+
reports.RenderTestReport(&exitHandler.buffer, *mainReport, false, false)
473+
}
415474

416475
if len(flakyTests) > 0 {
417476
// Create a new report with only flaky tests to get their gotestsum output
477+
// We need to ensure JSON paths are associated correctly for log retrieval.
418478
flakyReport, err := reports.NewTestReport(flakyTests,
419479
reports.WithJSONOutputPaths(mainReport.JSONOutputPaths),
480+
// Add other necessary options if PrintGotestsumOutput depends on them
420481
)
421482
if err != nil {
422-
log.Error().Err(err).Msg("Error creating flaky tests report")
483+
log.Error().Err(err).Msg("Error creating flaky tests report for log printing")
423484
} else {
424-
fmt.Fprint(&exitHandler.buffer, "\nFlaky Test Logs:\n\n")
425-
err := flakyReport.PrintGotestsumOutput(&exitHandler.buffer, "pkgname")
485+
fmt.Fprint(&exitHandler.buffer, "Flaky Test Logs (Based on Initial Run):")
486+
err := flakyReport.PrintGotestsumOutput(&exitHandler.buffer, "testname") // Use testname grouping
426487
if err != nil {
427488
log.Error().Err(err).Msg("Error printing gotestsum output for flaky tests")
428489
}
429490
}
430491

431-
exitHandler.logMsgAndExit(zerolog.InfoLevel, "Found flaky tests.", FlakyTestsExitCode, map[string]interface{}{
492+
exitHandler.logMsgAndExit(zerolog.InfoLevel, "Found flaky tests based on initial run.", FlakyTestsExitCode, map[string]interface{}{
432493
"flaky_count": len(flakyTests),
433494
"stability_threshold": fmt.Sprintf("%.0f%%", cfg.MinPassRatio*100),
434495
})
435496
} else {
436-
log.Info().Msg("All tests passed stability requirements.")
437-
exitHandler.code = 0
438-
exitHandler.flush()
497+
// If no tests were flaky according to the threshold, it's a success.
498+
// The exit code might already be 0 if set by handleInitialRunResults.
499+
if exitHandler.code != 0 { // Only log success if not already handled (e.g., by the command-line-args warning path)
500+
log.Info().Msg("All tests passed stability requirements based on the initial run.")
501+
exitHandler.code = 0
502+
}
503+
// No need to call flush here, the main function will do it.
439504
}
440505
}
441506

@@ -446,7 +511,9 @@ func init() {
446511
RunTestsCmd.Flags().String("test-packages-json", "", "JSON-encoded string of test packages")
447512
RunTestsCmd.Flags().StringSlice("test-packages", nil, "Comma-separated list of test packages to run")
448513
RunTestsCmd.Flags().StringArray("test-cmd", nil,
449-
"Optional custom test command(s) (e.g. 'go test -json ./... -v'), which must produce 'go test -json' output. Can be specified multiple times.",
514+
"Optional custom test command(s) (e.g. 'go test -json ./... -v'), which must produce 'go test -json' output. "+
515+
"Avoid 'go test <file.go>' syntax as it prevents reliable log output and reruns. Use 'go test . -run TestName' instead. "+
516+
"Can be specified multiple times.",
450517
)
451518
RunTestsCmd.Flags().StringSlice("skip-tests", nil, "Comma-separated list of test names (regex supported by `go test -skip`) to skip")
452519
RunTestsCmd.Flags().StringSlice("select-tests", nil, "Comma-separated list of test names (regex supported by `go test -run`) to specifically run")
@@ -479,8 +546,12 @@ func checkDependencies(projectPath string) error {
479546
cmd.Stderr = &out // Capture stderr as well
480547

481548
if err := cmd.Run(); err != nil {
549+
// Don't block execution, just warn, as sometimes tidy fails for unrelated reasons
550+
log.Warn().Err(err).Str("output", out.String()).Msg("Dependency check ('go mod tidy') failed. Continuing execution, but dependencies might be inconsistent.")
551+
// return fmt.Errorf("dependency check ('go mod tidy') failed: %w
482552
return fmt.Errorf("dependency check ('go mod tidy') failed: %w\n%s", err, out.String())
553+
} else {
554+
log.Debug().Msg("'go mod tidy' completed successfully.")
483555
}
484-
log.Debug().Msg("'go mod tidy' completed successfully.")
485556
return nil
486557
}

0 commit comments

Comments
 (0)