Skip to content

Commit b4ae3fd

Browse files
authored
flakeguard: Add support for go projects and add test duration to results (#1283)
* flakeguard: Add support for multi go projects * Add project-path flag to runner * Add test duration to test results
1 parent e23466e commit b4ae3fd

File tree

5 files changed

+59
-22
lines changed

5 files changed

+59
-22
lines changed

tools/flakeguard/cmd/find.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ var FindTestsCmd = &cobra.Command{
3030
// Find all changes in test files and get their package names
3131
var changedTestPkgs []string
3232
if findByTestFilesDiff {
33-
changedTestFiles, err := git.FindChangedFiles(baseRef, "grep '_test\\.go$'", excludes)
33+
changedTestFiles, err := git.FindChangedFiles(projectPath, baseRef, "grep '_test\\.go$'")
3434
if err != nil {
3535
log.Fatalf("Error finding changed test files: %v", err)
3636
}

tools/flakeguard/cmd/run.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ var RunTestsCmd = &cobra.Command{
1515
Use: "run",
1616
Short: "Run tests to check if they are flaky",
1717
Run: func(cmd *cobra.Command, args []string) {
18+
projectPath, _ := cmd.Flags().GetString("project-path")
1819
testPackagesJson, _ := cmd.Flags().GetString("test-packages-json")
1920
testPackagesArg, _ := cmd.Flags().GetStringSlice("test-packages")
2021
runCount, _ := cmd.Flags().GetInt("run-count")
@@ -34,10 +35,11 @@ var RunTestsCmd = &cobra.Command{
3435
}
3536

3637
runner := runner.Runner{
37-
Verbose: true,
38-
RunCount: runCount,
39-
UseRace: useRace,
40-
FailFast: threshold == 1.0, // Fail test on first test run if threshold is 1.0
38+
ProjectPath: projectPath,
39+
Verbose: true,
40+
RunCount: runCount,
41+
UseRace: useRace,
42+
FailFast: threshold == 1.0, // Fail test on first test run if threshold is 1.0
4143
}
4244

4345
testResults, err := runner.RunTests(testPackages)
@@ -82,6 +84,7 @@ var RunTestsCmd = &cobra.Command{
8284
}
8385

8486
func init() {
87+
RunTestsCmd.Flags().StringP("project-path", "r", ".", "The path to the Go project. Default is the current directory. Useful for subprojects.")
8588
RunTestsCmd.Flags().String("test-packages-json", "", "JSON-encoded string of test packages")
8689
RunTestsCmd.Flags().StringSlice("test-packages", nil, "Comma-separated list of test packages to run")
8790
RunTestsCmd.Flags().IntP("run-count", "c", 1, "Number of times to run the tests")

tools/flakeguard/git/git.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package git
33
import (
44
"bytes"
55
"fmt"
6+
"os"
67
"os/exec"
78
"path/filepath"
89
"strings"
@@ -14,15 +15,15 @@ import (
1415
// FindChangedFiles executes a git diff against a specified base reference and pipes the output through a user-defined grep command or sequence.
1516
// The baseRef parameter specifies the base git reference for comparison (e.g., "main", "develop").
1617
// The filterCmd parameter should include the full command to be executed after git diff, such as "grep '_test.go$'" or "grep -v '_test.go$' | sort".
17-
func FindChangedFiles(baseRef, filterCmd string, excludePaths []string) ([]string, error) {
18-
// Constructing the exclusion part of the git command
19-
excludeStr := ""
20-
for _, path := range excludePaths {
21-
excludeStr += fmt.Sprintf("':(exclude)%s' ", path)
18+
func FindChangedFiles(rootGoModPath, baseRef, filterCmd string) ([]string, error) {
19+
// Find directories containing a go.mod file and build an exclusion string
20+
excludeStr, err := buildExcludeStringForGoModDirs(rootGoModPath)
21+
if err != nil {
22+
return nil, fmt.Errorf("error finding go.mod directories: %w", err)
2223
}
2324

2425
// First command to list files changed between the baseRef and HEAD, excluding specified paths
25-
diffCmdStr := fmt.Sprintf("git diff --name-only --diff-filter=AM %s...HEAD %s", baseRef, excludeStr)
26+
diffCmdStr := fmt.Sprintf("git diff --name-only --diff-filter=AM %s...HEAD -- %s %s", baseRef, rootGoModPath, excludeStr)
2627
diffCmd := exec.Command("bash", "-c", diffCmdStr)
2728

2829
// Using a buffer to capture stdout and a separate buffer for stderr
@@ -36,7 +37,7 @@ func FindChangedFiles(baseRef, filterCmd string, excludePaths []string) ([]strin
3637
return nil, fmt.Errorf("error executing git diff command: %s; error: %w; stderr: %s", diffCmdStr, err, errBuf.String())
3738
}
3839

39-
// Check if there are any files listed, if not, return an empty slice
40+
// Check if there are any files listed; if not, return an empty slice
4041
diffOutput := strings.TrimSpace(out.String())
4142
if diffOutput == "" {
4243
return []string{}, nil
@@ -74,6 +75,32 @@ func FindChangedFiles(baseRef, filterCmd string, excludePaths []string) ([]strin
7475
return files, nil
7576
}
7677

78+
// buildExcludeStringForGoModDirs searches the given root directory for subdirectories
79+
// containing a go.mod file and returns a formatted string to exclude those directories
80+
// (except the root directory if it contains a go.mod file) from git diff.
81+
func buildExcludeStringForGoModDirs(rootGoModPath string) (string, error) {
82+
var excludeStr string
83+
84+
err := filepath.Walk(rootGoModPath, func(path string, info os.FileInfo, err error) error {
85+
if err != nil {
86+
return err
87+
}
88+
if info.Name() == "go.mod" {
89+
dir := filepath.Dir(path)
90+
// Skip excluding the root directory if go.mod is found there
91+
if dir != rootGoModPath {
92+
excludeStr += fmt.Sprintf("':(exclude)%s/**' ", dir)
93+
}
94+
}
95+
return nil
96+
})
97+
if err != nil {
98+
return "", err
99+
}
100+
101+
return excludeStr, nil
102+
}
103+
77104
func Diff(baseBranch string) (*utils.CmdOutput, error) {
78105
return utils.ExecuteCmd("git", "diff", "--name-only", baseBranch)
79106
}

tools/flakeguard/reports/reports.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ type TestResult struct {
44
TestName string
55
TestPackage string
66
PassRatio float64
7+
Skipped bool // Indicates if the test was skipped
78
Runs int
8-
Outputs []string // Stores outputs for a test
9-
Skipped bool // Indicates if the test was skipped
9+
Outputs []string // Stores outputs for a test
10+
Durations []float64 // Stores elapsed time in seconds for each run of the test
1011
}
1112

1213
// FilterFailedTests returns a slice of TestResult where the pass ratio is below the specified threshold.

tools/flakeguard/runner/runner.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ import (
1414
)
1515

1616
type Runner struct {
17-
Verbose bool // If true, provides detailed logging.
18-
RunCount int // Number of times to run the tests.
19-
UseRace bool // Enable race detector.
20-
FailFast bool // Stop on first test failure.
17+
ProjectPath string // Path to the Go project directory.
18+
Verbose bool // If true, provides detailed logging.
19+
RunCount int // Number of times to run the tests.
20+
UseRace bool // Enable race detector.
21+
FailFast bool // Stop on first test failure.
2122
}
2223

2324
// RunTests executes the tests for each provided package and aggregates all results.
@@ -58,6 +59,7 @@ func (r *Runner) runTestPackage(testPackage string) ([]byte, bool, error) {
5859
log.Printf("Running command: go %s\n", strings.Join(args, " "))
5960
}
6061
cmd := exec.Command("go", args...)
62+
cmd.Dir = r.ProjectPath
6163

6264
// cmd.Env = append(cmd.Env, "GOFLAGS=-extldflags=-Wl,-ld_classic") // Ensure modules are enabled
6365
var out bytes.Buffer
@@ -89,10 +91,11 @@ func parseTestResults(datas [][]byte) ([]reports.TestResult, error) {
8991
scanner := bufio.NewScanner(bytes.NewReader(data))
9092
for scanner.Scan() {
9193
var entry struct {
92-
Action string `json:"Action"`
93-
Test string `json:"Test"`
94-
Package string `json:"Package"`
95-
Output string `json:"Output"`
94+
Action string `json:"Action"`
95+
Test string `json:"Test"`
96+
Package string `json:"Package"`
97+
Output string `json:"Output"`
98+
Elapsed float64 `json:"Elapsed"`
9699
}
97100
if err := json.Unmarshal(scanner.Bytes(), &entry); err != nil {
98101
return nil, fmt.Errorf("failed to parse json test output: %s, err: %w", scanner.Text(), err)
@@ -120,13 +123,16 @@ func parseTestResults(datas [][]byte) ([]reports.TestResult, error) {
120123
result.Runs++
121124
case "pass":
122125
result.PassRatio = (result.PassRatio*float64(result.Runs-1) + 1) / float64(result.Runs)
126+
result.Durations = append(result.Durations, entry.Elapsed)
123127
case "output":
124128
result.Outputs = append(result.Outputs, entry.Output)
125129
case "fail":
126130
result.PassRatio = (result.PassRatio * float64(result.Runs-1)) / float64(result.Runs)
131+
result.Durations = append(result.Durations, entry.Elapsed)
127132
case "skip":
128133
result.Skipped = true
129134
result.Runs++
135+
result.Durations = append(result.Durations, entry.Elapsed)
130136
}
131137
}
132138

0 commit comments

Comments
 (0)