@@ -10,6 +10,7 @@ import (
1010 "log"
1111 "os"
1212 "os/exec"
13+ "path/filepath"
1314 "regexp"
1415 "strconv"
1516 "strings"
@@ -19,7 +20,8 @@ import (
1920)
2021
2122var (
22- panicRe = regexp .MustCompile (`^panic:` )
23+ startPanicRe = regexp .MustCompile (`^panic:` )
24+ startRaceRe = regexp .MustCompile (`^WARNING: DATA RACE` )
2325)
2426
2527type Runner struct {
@@ -40,6 +42,16 @@ func (r *Runner) RunTests() ([]reports.TestResult, error) {
4042 var jsonFilePaths []string
4143 for _ , p := range r .SelectedTestPackages {
4244 for i := 0 ; i < r .RunCount ; i ++ {
45+ if r .CollectRawOutput { // Collect raw output for debugging
46+ if r .rawOutputs == nil {
47+ r .rawOutputs = make (map [string ]* bytes.Buffer )
48+ }
49+ if _ , exists := r .rawOutputs [p ]; ! exists {
50+ r .rawOutputs [p ] = & bytes.Buffer {}
51+ }
52+ separator := strings .Repeat ("-" , 80 )
53+ r .rawOutputs [p ].WriteString (fmt .Sprintf ("%d%s\n " , i , separator ))
54+ }
4355 jsonFilePath , passed , err := r .runTests (p )
4456 if err != nil {
4557 return nil , fmt .Errorf ("failed to run tests in package %s: %w" , p , err )
@@ -91,10 +103,6 @@ func (r *Runner) runTests(packageName string) (string, bool, error) {
91103 cmd := exec .Command ("go" , args ... )
92104 cmd .Dir = r .ProjectPath
93105 if r .CollectRawOutput {
94- if r .rawOutputs == nil {
95- r .rawOutputs = make (map [string ]* bytes.Buffer )
96- }
97- r .rawOutputs [packageName ] = & bytes.Buffer {}
98106 cmd .Stdout = io .MultiWriter (tmpFile , r .rawOutputs [packageName ])
99107 cmd .Stderr = io .MultiWriter (tmpFile , r .rawOutputs [packageName ])
100108 } else {
@@ -115,9 +123,25 @@ func (r *Runner) runTests(packageName string) (string, bool, error) {
115123 return tmpFile .Name (), true , nil // Test succeeded
116124}
117125
126+ type entry struct {
127+ Action string `json:"Action"`
128+ Test string `json:"Test"`
129+ Package string `json:"Package"`
130+ Output string `json:"Output"`
131+ Elapsed float64 `json:"Elapsed"` // Decimal value in seconds
132+ }
133+
118134// parseTestResults reads the test output files and returns the parsed test results.
119135func parseTestResults (filePaths []string ) ([]reports.TestResult , error ) {
120- testDetails := make (map [string ]* reports.TestResult ) // Holds run, pass counts, and other details for each test
136+ var (
137+ testDetails = make (map [string ]* reports.TestResult ) // Holds run, pass counts, and other details for each test
138+ panickedPackages = map [string ]struct {}{} // Packages with tests that panicked
139+ racePackages = map [string ]struct {}{} // Packages with tests that raced
140+ packageLevelOutputs = map [string ][]string {} // Package-level outputs
141+ panicDetectionMode = false
142+ raceDetectionMode = false
143+ detectedEntries = []entry {} // race or panic entries
144+ )
121145
122146 // Process each file
123147 for _ , filePath := range filePaths {
@@ -139,14 +163,8 @@ func parseTestResults(filePaths []string) ([]reports.TestResult, error) {
139163 precedingLines = precedingLines [1 :]
140164 }
141165
142- var entry struct {
143- Action string `json:"Action"`
144- Test string `json:"Test"`
145- Package string `json:"Package"`
146- Output string `json:"Output"`
147- Elapsed float64 `json:"Elapsed"` // Decimal value in seconds
148- }
149- if err := json .Unmarshal (scanner .Bytes (), & entry ); err != nil {
166+ var entryLine entry
167+ if err := json .Unmarshal (scanner .Bytes (), & entryLine ); err != nil {
150168 // Collect 15 lines after the error for more context
151169 for scanner .Scan () && len (followingLines ) < 15 {
152170 followingLines = append (followingLines , scanner .Text ())
@@ -159,14 +177,14 @@ func parseTestResults(filePaths []string) ([]reports.TestResult, error) {
159177
160178 // Only create TestResult for test-level entries
161179 var result * reports.TestResult
162- if entry .Test != "" {
180+ if entryLine .Test != "" {
163181 // Determine the key
164- key := entry . Package + "/" + entry .Test
182+ key := fmt . Sprintf ( "%s/%s" , entryLine . Package , entryLine .Test )
165183
166184 if _ , exists := testDetails [key ]; ! exists {
167185 testDetails [key ] = & reports.TestResult {
168- TestName : entry .Test ,
169- TestPackage : entry .Package ,
186+ TestName : entryLine .Test ,
187+ TestPackage : entryLine .Package ,
170188 PassRatio : 0 ,
171189 Outputs : []string {},
172190 PackageOutputs : []string {},
@@ -175,79 +193,91 @@ func parseTestResults(filePaths []string) ([]reports.TestResult, error) {
175193 result = testDetails [key ]
176194 }
177195
178- // Collect outputs
179- if entry .Output != "" {
180- if entry .Test != "" {
181- // Test-level output
182- result .Outputs = append (result .Outputs , strings .TrimSpace (entry .Output ))
183- } else {
184- // Package-level output
185- // Append to PackageOutputs of all TestResults in the same package
186- for _ , res := range testDetails {
187- if res .TestPackage == entry .Package {
188- res .PackageOutputs = append (res .PackageOutputs , entry .Output )
196+ // TODO: This is a bit of a logical mess, refactor
197+ if entryLine .Output != "" {
198+ if panicDetectionMode || raceDetectionMode { // currently collecting panic or race output
199+ detectedEntries = append (detectedEntries , entryLine )
200+ if entryLine .Action == "fail" { // End of panic output
201+ if panicDetectionMode {
202+ panicTest , err := attributePanicToTest (entryLine .Package , detectedEntries )
203+ if err != nil {
204+ return nil , err
205+ }
206+ panicTestKey := fmt .Sprintf ("%s/%s" , entryLine .Package , panicTest )
207+ testDetails [panicTestKey ].Panicked = true
208+ testDetails [panicTestKey ].Panics ++
209+ testDetails [panicTestKey ].Runs ++
210+ testDetails [panicTestKey ].Outputs = append (testDetails [panicTestKey ].Outputs , entryLine .Output )
211+ } else if raceDetectionMode {
212+ raceTest , err := attributeRaceToTest (entryLine .Package , detectedEntries )
213+ if err != nil {
214+ return nil , err
215+ }
216+ raceTestKey := fmt .Sprintf ("%s/%s" , entryLine .Package , raceTest )
217+ testDetails [raceTestKey ].Races ++
218+ testDetails [raceTestKey ].Runs ++
219+ testDetails [raceTestKey ].Outputs = append (testDetails [raceTestKey ].Outputs , entryLine .Output )
189220 }
221+
222+ detectedEntries = []entry {}
223+ panicDetectionMode = false
224+ raceDetectionMode = false
190225 }
226+ continue // Don't process this entry further
227+ } else if startPanicRe .MatchString (entryLine .Output ) { // found a panic, start collecting output
228+ panickedPackages [entryLine .Package ] = struct {}{}
229+ detectedEntries = append (detectedEntries , entryLine )
230+ panicDetectionMode = true
231+ continue // Don't process this entry further
232+ } else if startRaceRe .MatchString (entryLine .Output ) {
233+ racePackages [entryLine .Package ] = struct {}{}
234+ detectedEntries = append (detectedEntries , entryLine )
235+ raceDetectionMode = true
236+ continue // Don't process this entry further
237+ } else if entryLine .Test == "" {
238+ if _ , exists := packageLevelOutputs [entryLine .Package ]; ! exists {
239+ packageLevelOutputs [entryLine .Package ] = []string {}
240+ }
241+ packageLevelOutputs [entryLine .Package ] = append (packageLevelOutputs [entryLine .Package ], entryLine .Output )
242+ } else if entryLine .Test != "" {
243+ result .Outputs = append (result .Outputs , entryLine .Output )
191244 }
192245 }
193246
194- switch entry .Action {
195- case "run" :
196- if entry .Test != "" {
197- result .Runs ++
198- }
247+ switch entryLine .Action {
199248 case "pass" :
200- if entry .Test != "" {
201- duration , err := time .ParseDuration (strconv .FormatFloat (entry .Elapsed , 'f' , - 1 , 64 ) + "s" )
249+ if entryLine .Test != "" {
250+ duration , err := time .ParseDuration (strconv .FormatFloat (entryLine .Elapsed , 'f' , - 1 , 64 ) + "s" )
202251 if err != nil {
203252 return nil , fmt .Errorf ("failed to parse duration: %w" , err )
204253 }
205254 result .Durations = append (result .Durations , duration )
206255 result .Successes ++
256+ result .Runs ++
207257 }
208258 case "fail" :
209- if entry .Test != "" {
210- duration , err := time .ParseDuration (strconv .FormatFloat (entry .Elapsed , 'f' , - 1 , 64 ) + "s" )
259+ if entryLine .Test != "" {
260+ duration , err := time .ParseDuration (strconv .FormatFloat (entryLine .Elapsed , 'f' , - 1 , 64 ) + "s" )
211261 if err != nil {
212262 return nil , fmt .Errorf ("failed to parse duration: %w" , err )
213263 }
214264 result .Durations = append (result .Durations , duration )
215265 result .Failures ++
216- }
217- case "output" :
218- // plain output already handled above
219- if panicRe .MatchString (entry .Output ) {
220- if entry .Test != "" {
221- duration , err := time .ParseDuration (strconv .FormatFloat (entry .Elapsed , 'f' , - 1 , 64 ) + "s" )
222- if err != nil {
223- return nil , fmt .Errorf ("failed to parse duration: %w" , err )
224- }
225- result .Durations = append (result .Durations , duration )
226- // Test-level panic
227- result .Panicked = true
228- result .Panics ++
229- } else {
230- // Package-level panic
231- // Mark PackagePanicked for all TestResults in the package
232- for _ , res := range testDetails {
233- if res .TestPackage == entry .Package {
234- res .PackagePanicked = true
235- }
236- }
237- }
266+ result .Runs ++
238267 }
239268 case "skip" :
240- if entry .Test != "" {
241- duration , err := time .ParseDuration (strconv .FormatFloat (entry .Elapsed , 'f' , - 1 , 64 ) + "s" )
269+ if entryLine .Test != "" {
270+ duration , err := time .ParseDuration (strconv .FormatFloat (entryLine .Elapsed , 'f' , - 1 , 64 ) + "s" )
242271 if err != nil {
243272 return nil , fmt .Errorf ("failed to parse duration: %w" , err )
244273 }
245274 result .Durations = append (result .Durations , duration )
246275 result .Skipped = true
247276 result .Skips ++
277+ result .Runs ++
248278 }
249279 }
250- if entry .Test != "" {
280+ if entryLine .Test != "" {
251281 result .PassRatio = float64 (result .Successes ) / float64 (result .Runs )
252282 result .PassRatioPercentage = fmt .Sprintf ("%.0f%%" , result .PassRatio * 100 )
253283 }
@@ -268,7 +298,39 @@ func parseTestResults(filePaths []string) ([]reports.TestResult, error) {
268298 var results []reports.TestResult
269299 for _ , result := range testDetails {
270300 results = append (results , * result )
301+ if _ , panicked := panickedPackages [result .TestPackage ]; panicked {
302+ result .PackagePanicked = true
303+ }
304+ if outputs , exists := packageLevelOutputs [result .TestPackage ]; exists {
305+ result .PackageOutputs = outputs
306+ }
271307 }
272308
273309 return results , nil
274310}
311+
312+ // properly attributes panics to the test that caused them
313+ // Go JSON output gets confused, especially when tests are run in parallel
314+ func attributePanicToTest (panicPackage string , panicEntries []entry ) (string , error ) {
315+ regexSanitizePanicPackage := filepath .Base (panicPackage )
316+ panicAttributionRe := regexp .MustCompile (fmt .Sprintf (`%s\.(Test.*?)\(.*\)` , regexSanitizePanicPackage ))
317+ for _ , entry := range panicEntries {
318+ if matches := panicAttributionRe .FindStringSubmatch (entry .Output ); len (matches ) > 1 {
319+ return matches [1 ], nil
320+ }
321+ }
322+ return "" , fmt .Errorf ("failed to attribute panic to test, using regex: %s" , panicAttributionRe .String ())
323+ }
324+
325+ // properly attributes races to the test that caused them
326+ // Go JSON output gets confused, especially when tests are run in parallel
327+ func attributeRaceToTest (racePackage string , raceEntries []entry ) (string , error ) {
328+ regexSanitizeRacePackage := filepath .Base (racePackage )
329+ raceAttributionRe := regexp .MustCompile (fmt .Sprintf (`%s\.(Test[^\.]+?)\(.*\)` , regexSanitizeRacePackage ))
330+ for _ , entry := range raceEntries {
331+ if matches := raceAttributionRe .FindStringSubmatch (entry .Output ); len (matches ) > 1 {
332+ return matches [1 ], nil
333+ }
334+ }
335+ return "" , fmt .Errorf ("failed to attribute race to test, using regex: %s" , raceAttributionRe .String ())
336+ }
0 commit comments