@@ -2,11 +2,9 @@ package runner
22
33import (
44 "bufio"
5- "bytes"
65 "encoding/json"
76 "errors"
87 "fmt"
9- "io"
108 "os"
119 "os/exec"
1210 "path/filepath"
@@ -21,6 +19,11 @@ import (
2119 "github.com/smartcontractkit/chainlink-testing-framework/tools/flakeguard/reports"
2220)
2321
22+ const (
23+ RawOutputDir = "./flakeguard_raw_output"
24+ RawOutputTransformedDir = "./flakeguard_raw_output_transformed"
25+ )
26+
2427var (
2528 startPanicRe = regexp .MustCompile (`^panic:` )
2629 startRaceRe = regexp .MustCompile (`^WARNING: DATA RACE` )
@@ -40,44 +43,33 @@ type Runner struct {
4043 FailFast bool // Stop on first test failure.
4144 SkipTests []string // Test names to exclude.
4245 SelectTests []string // Test names to include.
43- CollectRawOutput bool // Set to true to collect test output for later inspection.
4446 OmitOutputsOnSuccess bool // Set to true to omit test outputs on success.
4547 MaxPassRatio float64 // Maximum pass ratio threshold for a test to be considered flaky.
4648 IgnoreParentFailuresOnSubtests bool // Ignore failures in parent tests when only subtests fail.
47- rawOutputs map [string ]* bytes.Buffer
49+ rawOutputFiles []string // Raw output files for each test run.
50+ transformedOutputFiles []string // Transformed output files for each test run.
4851}
4952
5053// RunTestPackages executes the tests for each provided package and aggregates all results.
5154// It returns all test results and any error encountered during testing.
5255// RunTestPackages executes the tests for each provided package and aggregates all results.
5356func (r * Runner ) RunTestPackages (packages []string ) ([]reports.TestResult , error ) {
54- var jsonFilePaths []string
5557 // Initial runs.
5658 for _ , p := range packages {
57- for i := 0 ; i < r .RunCount ; i ++ {
58- if r .CollectRawOutput { // Collect raw output for debugging.
59- if r .rawOutputs == nil {
60- r .rawOutputs = make (map [string ]* bytes.Buffer )
61- }
62- if _ , exists := r .rawOutputs [p ]; ! exists {
63- r .rawOutputs [p ] = & bytes.Buffer {}
64- }
65- separator := strings .Repeat ("-" , 80 )
66- r .rawOutputs [p ].WriteString (fmt .Sprintf ("Run %d\n %s\n " , i + 1 , separator ))
67- }
68- jsonFilePath , passed , err := r .runTestPackage (p )
59+ for i := range r .RunCount {
60+ jsonFilePath , passed , err := r .runTestPackage (p , i )
6961 if err != nil {
7062 return nil , fmt .Errorf ("failed to run tests in package %s: %w" , p , err )
7163 }
72- jsonFilePaths = append (jsonFilePaths , jsonFilePath )
64+ r . rawOutputFiles = append (r . rawOutputFiles , jsonFilePath )
7365 if ! passed && r .FailFast {
7466 break
7567 }
7668 }
7769 }
7870
7971 // Parse initial results.
80- results , err := r .parseTestResults (jsonFilePaths , "run" , r .RunCount )
72+ results , err := r .parseTestResults ("run" , r .RunCount )
8173 if err != nil {
8274 return nil , fmt .Errorf ("failed to parse test results: %w" , err )
8375 }
@@ -89,39 +81,31 @@ func (r *Runner) RunTestPackages(packages []string) ([]reports.TestResult, error
8981// that produces the same JSON lines that 'go test -json' would produce on stdout.
9082// It captures those lines in a temp file, then parses them for pass/fail/panic/race data.
9183func (r * Runner ) RunTestCmd (testCmd []string ) ([]reports.TestResult , error ) {
92- var jsonOutputPaths []string
93-
9484 for i := range r .RunCount {
9585 jsonOutputPath , passed , err := r .runCmd (testCmd , i )
9686 if err != nil {
9787 return nil , fmt .Errorf ("failed to run test command: %w" , err )
9888 }
99- jsonOutputPaths = append (jsonOutputPaths , jsonOutputPath )
89+ r . rawOutputFiles = append (r . rawOutputFiles , jsonOutputPath )
10090 if ! passed && r .FailFast {
10191 break
10292 }
10393 }
10494
105- results , err := r .parseTestResults (jsonOutputPaths , "run" , r .RunCount )
95+ results , err := r .parseTestResults ("run" , r .RunCount )
10696 if err != nil {
10797 return nil , fmt .Errorf ("failed to parse test results: %w" , err )
10898 }
10999
110100 return results , nil
111101}
112102
113- // RawOutputs retrieves the raw output from the test runs, if CollectRawOutput enabled.
114- // packageName : raw output
115- func (r * Runner ) RawOutputs () map [string ]* bytes.Buffer {
116- return r .rawOutputs
117- }
118-
119103type exitCoder interface {
120104 ExitCode () int
121105}
122106
123107// runTestPackage runs the tests for a given package and returns the path to the output file.
124- func (r * Runner ) runTestPackage (packageName string ) (string , bool , error ) {
108+ func (r * Runner ) runTestPackage (packageName string , runCount int ) (string , bool , error ) {
125109 args := []string {"test" , packageName , "-json" }
126110 if r .GoTestCountFlag != nil {
127111 args = append (args , fmt .Sprintf ("-count=%d" , * r .GoTestCountFlag ))
@@ -151,25 +135,26 @@ func (r *Runner) runTestPackage(packageName string) (string, bool, error) {
151135 args = append (args , fmt .Sprintf ("-run=^%s$" , selectPattern ))
152136 }
153137
154- if r .Verbose {
155- log .Info ().Str ("command" , fmt .Sprintf ("go %s\n " , strings .Join (args , " " ))).Msg ("Running command" )
138+ err := os .MkdirAll (RawOutputDir , 0o755 )
139+ if err != nil {
140+ return "" , false , fmt .Errorf ("failed to create raw output directory: %w" , err )
156141 }
157-
158142 // Create a temporary file to store the output
159- tmpFile , err := os .CreateTemp ("" , "test-output-*.json" )
143+ saniPackageName := filepath .Base (packageName )
144+ tmpFile , err := os .CreateTemp (RawOutputDir , fmt .Sprintf ("test-output-%s-%d-*.json" , saniPackageName , runCount ))
160145 if err != nil {
161146 return "" , false , fmt .Errorf ("failed to create temp file: %w" , err )
162147 }
163148 defer tmpFile .Close ()
164149
150+ if r .Verbose {
151+ log .Info ().Str ("raw output file" , tmpFile .Name ()).Str ("command" , fmt .Sprintf ("go %s\n " , strings .Join (args , " " ))).Msg ("Running command" )
152+ }
153+
165154 // Run the command with output directed to the file
166155 cmd := exec .Command ("go" , args ... )
167156 cmd .Dir = r .ProjectPath
168- if r .CollectRawOutput {
169- cmd .Stdout = io .MultiWriter (tmpFile , r .rawOutputs [packageName ])
170- } else {
171- cmd .Stdout = tmpFile
172- }
157+ cmd .Stdout = tmpFile
173158
174159 err = cmd .Run ()
175160 if err != nil {
@@ -188,29 +173,21 @@ func (r *Runner) runTestPackage(packageName string) (string, bool, error) {
188173// and returns the temp file path, whether the test passed, and an error if any.
189174func (r * Runner ) runCmd (testCmd []string , runIndex int ) (tempFilePath string , passed bool , err error ) {
190175 // Create temp file for JSON output
191- tmpFile , err := os .CreateTemp ("" , fmt .Sprintf ("test-output-cmd-run%d-*.json" , runIndex + 1 ))
176+ err = os .MkdirAll (RawOutputDir , 0o755 )
177+ if err != nil {
178+ return "" , false , fmt .Errorf ("failed to create raw output directory: %w" , err )
179+ }
180+ tmpFile , err := os .CreateTemp (RawOutputDir , fmt .Sprintf ("test-output-cmd-run%d-*.json" , runIndex + 1 ))
192181 if err != nil {
193182 err = fmt .Errorf ("failed to create temp file: %w" , err )
194- return
183+ return "" , false , err
195184 }
196185 defer tmpFile .Close ()
197186
198187 cmd := exec .Command (testCmd [0 ], testCmd [1 :]... ) //nolint:gosec
199188 cmd .Dir = r .ProjectPath
200189
201- // If collecting raw output, write to both file & buffer
202- if r .CollectRawOutput {
203- if r .rawOutputs == nil {
204- r .rawOutputs = make (map [string ]* bytes.Buffer )
205- }
206- key := fmt .Sprintf ("customCmd-run%d" , runIndex + 1 )
207- if _ , exists := r .rawOutputs [key ]; ! exists {
208- r .rawOutputs [key ] = & bytes.Buffer {}
209- }
210- cmd .Stdout = io .MultiWriter (tmpFile , r .rawOutputs [key ])
211- } else {
212- cmd .Stdout = tmpFile
213- }
190+ cmd .Stdout = tmpFile
214191 cmd .Stderr = os .Stderr
215192
216193 err = cmd .Run ()
@@ -231,12 +208,12 @@ func (r *Runner) runCmd(testCmd []string, runIndex int) (tempFilePath string, pa
231208 // Some other error that doesn't implement ExitCode() => real error
232209 tempFilePath = ""
233210 err = fmt .Errorf ("error running test command: %w" , err )
234- return
211+ return tempFilePath , passed , err
235212 }
236213
237214 // Otherwise, test passed
238215 passed = true
239- return
216+ return tempFilePath , passed , nil
240217}
241218
242219type entry struct {
@@ -262,14 +239,16 @@ func (e entry) String() string {
262239// panics and failures at that point.
263240// Subtests add more complexity, as panics in subtests are only reported in their parent's output,
264241// and cannot be accurately attributed to the subtest that caused them.
265- func (r * Runner ) parseTestResults (jsonOutputPaths []string , runPrefix string , runCount int ) ([]reports.TestResult , error ) {
242+ func (r * Runner ) parseTestResults (runPrefix string , runCount int ) ([]reports.TestResult , error ) {
243+ var parseFilePaths = r .rawOutputFiles
244+
266245 // If the option is enabled, transform each JSON output file before parsing.
267246 if r .IgnoreParentFailuresOnSubtests {
268- transformedPaths , err := r .transformTestOutputFiles (jsonOutputPaths )
247+ err := r .transformTestOutputFiles (r . rawOutputFiles )
269248 if err != nil {
270249 return nil , err
271250 }
272- jsonOutputPaths = transformedPaths
251+ parseFilePaths = r . transformedOutputFiles
273252 }
274253
275254 var (
@@ -286,7 +265,7 @@ func (r *Runner) parseTestResults(jsonOutputPaths []string, runPrefix string, ru
286265
287266 runNumber := 0
288267 // Process each file
289- for _ , filePath := range jsonOutputPaths {
268+ for _ , filePath := range parseFilePaths {
290269 runNumber ++
291270 runID := fmt .Sprintf ("%s%d" , runPrefix , runNumber )
292271 file , err := os .Open (filePath )
@@ -521,13 +500,9 @@ func (r *Runner) parseTestResults(jsonOutputPaths []string, runPrefix string, ru
521500 if err := scanner .Err (); err != nil {
522501 return nil , fmt .Errorf ("reading test output file: %w" , err )
523502 }
524- // Clean up file after parsing
525503 if err = file .Close (); err != nil {
526504 log .Warn ().Err (err ).Str ("file" , filePath ).Msg ("failed to close file" )
527505 }
528- // if err = os.Remove(filePath); err != nil {
529- // log.Warn().Err(err).Str("file", filePath).Msg("failed to delete file")
530- // }
531506 }
532507
533508 var results []reports.TestResult
@@ -591,32 +566,34 @@ func (r *Runner) parseTestResults(jsonOutputPaths []string, runPrefix string, ru
591566
592567// transformTestOutputFiles transforms the test output JSON files to ignore parent failures when only subtests fail.
593568// It returns the paths to the transformed files.
594- func (r * Runner ) transformTestOutputFiles (filePaths []string ) ([]string , error ) {
595- transformedPaths := make ([]string , len (filePaths ))
596- for i , origPath := range filePaths {
569+ func (r * Runner ) transformTestOutputFiles (filePaths []string ) error {
570+ err := os .MkdirAll (RawOutputDir , 0o755 )
571+ if err != nil {
572+ return fmt .Errorf ("failed to create raw output directory: %w" , err )
573+ }
574+ for _ , origPath := range filePaths {
597575 inFile , err := os .Open (origPath )
598576 if err != nil {
599- return nil , fmt .Errorf ("failed to open original file %s: %w" , origPath , err )
577+ return fmt .Errorf ("failed to open original file %s: %w" , origPath , err )
600578 }
601579 // Create a temporary file for the transformed output.
602- outFile , err := os .CreateTemp ("" , "transformed-output-*.json" )
580+ outFile , err := os .CreateTemp (RawOutputTransformedDir , "transformed-raw -output-*.json" )
603581 if err != nil {
604582 inFile .Close ()
605- return nil , fmt .Errorf ("failed to create transformed temp file: %w" , err )
583+ return fmt .Errorf ("failed to create transformed temp file: %w" , err )
606584 }
607585 // Transform the JSON output.
608586 // The transformer option is set to ignore parent failures when only subtests fail.
609587 err = transformer .TransformJSON (inFile , outFile , transformer .NewOptions (true ))
610588 inFile .Close ()
611589 outFile .Close ()
612590 if err != nil {
613- return nil , fmt .Errorf ("failed to transform output file %s: %v" , origPath , err )
591+ return fmt .Errorf ("failed to transform output file %s: %v" , origPath , err )
614592 }
615593 // Use the transformed file path.
616- transformedPaths [i ] = outFile .Name ()
617- os .Remove (origPath )
594+ r .transformedOutputFiles = append (r .transformedOutputFiles , outFile .Name ())
618595 }
619- return transformedPaths , nil
596+ return nil
620597}
621598
622599// attributePanicToTest properly attributes panics to the test that caused them.
@@ -674,8 +651,6 @@ func (r *Runner) RerunFailedTests(failedTests []reports.TestResult, rerunCount i
674651 log .Info ().Msgf ("Rerunning failing tests grouped by package: %v" , failingTestsByPackage )
675652 }
676653
677- var rerunJsonOutputPaths []string
678-
679654 // Rerun each failing test package up to RerunCount times
680655 for i := range rerunCount {
681656 for pkg , tests := range failingTestsByPackage {
@@ -710,15 +685,15 @@ func (r *Runner) RerunFailedTests(failedTests []reports.TestResult, rerunCount i
710685 if err != nil {
711686 return nil , nil , fmt .Errorf ("error on rerunCmd for package %s: %w" , pkg , err )
712687 }
713- rerunJsonOutputPaths = append (rerunJsonOutputPaths , jsonOutputPath )
688+ r . rawOutputFiles = append (r . rawOutputFiles , jsonOutputPath )
714689 }
715690 }
716691
717692 // Parse all rerun results at once with a consistent prefix
718- rerunResults , err := r .parseTestResults (rerunJsonOutputPaths , "rerun" , rerunCount )
693+ rerunResults , err := r .parseTestResults ("rerun" , rerunCount )
719694 if err != nil {
720- return nil , rerunJsonOutputPaths , fmt .Errorf ("failed to parse rerun results: %w" , err )
695+ return nil , r . rawOutputFiles , fmt .Errorf ("failed to parse rerun results: %w" , err )
721696 }
722697
723- return rerunResults , rerunJsonOutputPaths , nil
698+ return rerunResults , r . rawOutputFiles , nil
724699}
0 commit comments