Skip to content

Commit c362fc4

Browse files
authored
flakeguard: Map test in results to code owners (#1410)
* Fix ld warnings and other errors in runner test results * Add codeowners to aggregated results * Update owner name * Fix * Revert some changes
1 parent ef81a77 commit c362fc4

File tree

7 files changed

+213
-2
lines changed

7 files changed

+213
-2
lines changed

tools/flakeguard/cmd/aggregate_results.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ var (
1111
resultsFolderPath string
1212
outputResultsPath string
1313
outputLogsPath string
14+
codeOwnersPath string
15+
projectPath string
1416
maxPassRatio float64
1517
filterFailed bool
1618
)
@@ -24,6 +26,20 @@ var AggregateResultsCmd = &cobra.Command{
2426
log.Fatalf("Error aggregating results: %v", err)
2527
}
2628

29+
// Map test results to paths
30+
err = reports.MapTestResultsToPaths(allReport, projectPath)
31+
if err != nil {
32+
log.Fatalf("Error mapping test results to paths: %v", err)
33+
}
34+
35+
// Map test results to owners if CODEOWNERS path is provided
36+
if codeOwnersPath != "" {
37+
err = reports.MapTestResultsToOwners(allReport, codeOwnersPath)
38+
if err != nil {
39+
log.Fatalf("Error mapping test results to owners: %v", err)
40+
}
41+
}
42+
2743
var resultsToSave []reports.TestResult
2844

2945
if filterFailed {
@@ -52,4 +68,7 @@ func init() {
5268
AggregateResultsCmd.Flags().StringVarP(&outputLogsPath, "output-logs", "l", "", "Path to output the filtered test logs in JSON format")
5369
AggregateResultsCmd.Flags().Float64VarP(&maxPassRatio, "max-pass-ratio", "m", 1.0, "The maximum (non-inclusive) pass ratio threshold for a test to be considered a failure. Any tests below this pass rate will be considered flaky.")
5470
AggregateResultsCmd.Flags().BoolVarP(&filterFailed, "filter-failed", "f", false, "If true, filter and output only failed tests based on the max-pass-ratio threshold")
71+
AggregateResultsCmd.Flags().StringVarP(&codeOwnersPath, "codeowners-path", "c", "", "Path to the CODEOWNERS file")
72+
AggregateResultsCmd.Flags().StringVarP(&projectPath, "project-path", "r", ".", "The path to the Go project. Default is the current directory. Useful for subprojects")
73+
5574
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package codeowners
2+
3+
import (
4+
"bufio"
5+
"os"
6+
"path/filepath"
7+
"regexp"
8+
"strings"
9+
)
10+
11+
// PatternOwner maps a file pattern to its owners
12+
type PatternOwner struct {
13+
Pattern string
14+
Owners []string
15+
}
16+
17+
// Parse reads the CODEOWNERS file and returns a list of PatternOwner
18+
func Parse(filePath string) ([]PatternOwner, error) {
19+
file, err := os.Open(filePath)
20+
if err != nil {
21+
return nil, err
22+
}
23+
defer file.Close()
24+
25+
var patterns []PatternOwner
26+
scanner := bufio.NewScanner(file)
27+
commentPattern := regexp.MustCompile(`^\s*#`)
28+
29+
for scanner.Scan() {
30+
line := strings.TrimSpace(scanner.Text())
31+
if line == "" || commentPattern.MatchString(line) {
32+
continue
33+
}
34+
35+
fields := strings.Fields(line)
36+
if len(fields) > 1 {
37+
patterns = append(patterns, PatternOwner{
38+
Pattern: fields[0],
39+
Owners: fields[1:],
40+
})
41+
}
42+
}
43+
return patterns, scanner.Err()
44+
}
45+
46+
// FindOwners determines the owners for a given file path based on patterns
47+
func FindOwners(filePath string, patterns []PatternOwner) []string {
48+
// Convert filePath to Unix-style for matching
49+
relFilePath := filepath.ToSlash(filePath)
50+
51+
var matchedOwners []string
52+
for _, pattern := range patterns {
53+
// Ensure the pattern is also converted to Unix-style
54+
patternPath := strings.TrimPrefix(pattern.Pattern, "/")
55+
patternPath = strings.TrimSuffix(patternPath, "/")
56+
57+
// Match if the file is in the directory or is an exact match
58+
if strings.HasPrefix(relFilePath, patternPath) || relFilePath == patternPath {
59+
matchedOwners = pattern.Owners
60+
}
61+
}
62+
return matchedOwners
63+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package reports
2+
3+
import (
4+
"go/ast"
5+
"go/parser"
6+
"go/token"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
)
11+
12+
// TestFileMap maps test function names to their file paths.
13+
type TestFileMap map[string]string
14+
15+
// ScanTestFiles scans the codebase for test functions and maps them to file paths.
16+
func ScanTestFiles(rootDir string) (TestFileMap, error) {
17+
testFileMap := make(TestFileMap)
18+
19+
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
20+
if err != nil {
21+
return err
22+
}
23+
if !strings.HasSuffix(path, "_test.go") {
24+
return nil
25+
}
26+
27+
// Parse the file
28+
fset := token.NewFileSet()
29+
node, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
30+
if err != nil {
31+
return err
32+
}
33+
34+
// Traverse the AST to find test functions
35+
ast.Inspect(node, func(n ast.Node) bool {
36+
funcDecl, ok := n.(*ast.FuncDecl)
37+
if !ok {
38+
return true
39+
}
40+
if strings.HasPrefix(funcDecl.Name.Name, "Test") {
41+
// Add both the package and test function to the map
42+
testFileMap[funcDecl.Name.Name] = path
43+
}
44+
return true
45+
})
46+
return nil
47+
})
48+
49+
return testFileMap, err
50+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package reports
2+
3+
import "github.com/smartcontractkit/chainlink-testing-framework/tools/flakeguard/codeowners"
4+
5+
// MapTestResultsToOwners maps test results to their code owners based on the TestPath and CODEOWNERS file.
6+
func MapTestResultsToOwners(report *TestReport, codeOwnersPath string) error {
7+
// Parse the CODEOWNERS file
8+
codeOwnerPatterns, err := codeowners.Parse(codeOwnersPath)
9+
if err != nil {
10+
return err
11+
}
12+
13+
// Assign owners to each test result
14+
for i, result := range report.Results {
15+
if result.TestPath != "NOT FOUND" {
16+
report.Results[i].CodeOwners = codeowners.FindOwners(result.TestPath, codeOwnerPatterns)
17+
} else {
18+
// Mark owners as unknown for unmapped tests
19+
report.Results[i].CodeOwners = []string{"UNKNOWN"}
20+
}
21+
}
22+
23+
return nil
24+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package reports
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
"strings"
7+
)
8+
9+
// MapTestResultsToPaths maps test results to their corresponding file paths.
10+
func MapTestResultsToPaths(report *TestReport, rootDir string) error {
11+
// Scan the codebase for test functions
12+
testFileMap, err := ScanTestFiles(rootDir)
13+
if err != nil {
14+
return err
15+
}
16+
17+
// Assign file paths to each test result
18+
for i, result := range report.Results {
19+
testName := result.TestName
20+
var filePath string
21+
22+
// Handle subtests
23+
if strings.Contains(testName, "/") {
24+
parentTestName := strings.SplitN(testName, "/", 2)[0] // Extract parent test
25+
if path, exists := testFileMap[parentTestName]; exists {
26+
filePath = path
27+
}
28+
} else if path, exists := testFileMap[testName]; exists {
29+
// Handle normal tests
30+
filePath = path
31+
}
32+
33+
// Normalize filePath to be relative to the project root
34+
if filePath != "" {
35+
relFilePath, err := filepath.Rel(rootDir, filePath)
36+
if err != nil {
37+
return fmt.Errorf("error getting relative path: %v", err)
38+
}
39+
report.Results[i].TestPath = relFilePath
40+
} else {
41+
// Log or mark tests not found in the codebase
42+
report.Results[i].TestPath = "NOT FOUND"
43+
}
44+
}
45+
46+
return nil
47+
}

tools/flakeguard/reports/reports.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ type TestResult struct {
4343
Outputs []string `json:"outputs,omitempty"` // Stores outputs for a test
4444
Durations []time.Duration // Stores elapsed time for each run of the test
4545
PackageOutputs []string `json:"package_outputs,omitempty"` // Stores package-level outputs
46+
TestPath string // Path to the test file
47+
CodeOwners []string // Owners of the test
4648
}
4749

4850
// FilterFailedTests returns a slice of TestResult where the pass ratio is below the specified threshold.
@@ -194,6 +196,7 @@ func PrintTests(
194196
) (runs, passes, fails, skips, panickedTests, racedTests, flakyTests int) {
195197
p := message.NewPrinter(language.English) // For formatting numbers
196198
sortTestResults(tests)
199+
197200
headers := []string{
198201
"**Name**",
199202
"**Pass Ratio**",
@@ -207,12 +210,18 @@ func PrintTests(
207210
"**Package**",
208211
"**Package Panicked?**",
209212
"**Avg Duration**",
213+
"**Code Owners**",
210214
}
211215

212216
// Build test rows and summary data
213217
rows := [][]string{}
214218
for _, test := range tests {
215219
if test.PassRatio < maxPassRatio {
220+
owners := "Unknown"
221+
if len(test.CodeOwners) > 0 {
222+
owners = strings.Join(test.CodeOwners, ", ")
223+
}
224+
216225
rows = append(rows, []string{
217226
test.TestName,
218227
fmt.Sprintf("%.2f%%", test.PassRatio*100),
@@ -226,6 +235,7 @@ func PrintTests(
226235
test.TestPackage,
227236
fmt.Sprintf("%t", test.PackagePanic),
228237
avgDuration(test.Durations).String(),
238+
owners,
229239
})
230240
}
231241

tools/flakeguard/runner/runner.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,8 @@ func (r *Runner) runTests(packageName string) (string, bool, error) {
143143
cmd.Dir = r.ProjectPath
144144
if r.CollectRawOutput {
145145
cmd.Stdout = io.MultiWriter(tmpFile, r.rawOutputs[packageName])
146-
cmd.Stderr = io.MultiWriter(tmpFile, r.rawOutputs[packageName])
147146
} else {
148147
cmd.Stdout = tmpFile
149-
cmd.Stderr = tmpFile
150148
}
151149

152150
err = cmd.Run()

0 commit comments

Comments
 (0)