@@ -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 , "\n Flakeguard Summary\n " )
337+ fmt .Fprint (& exitHandler .buffer , "\n Flakeguard 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 , "\n Failed 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 , "\n Failed 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 , "\n Failed 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 , "\n Logs 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 , "\n Tests 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 , "\n Persistently 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 .
408462func 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 , "\n Flakeguard 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 , "\n Flaky 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