Skip to content

Commit 8cdfa06

Browse files
committed
Add support for custom test commands in RunTestsCmd
1 parent 5e13d77 commit 8cdfa06

File tree

2 files changed

+117
-10
lines changed

2 files changed

+117
-10
lines changed

tools/flakeguard/cmd/run.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var RunTestsCmd = &cobra.Command{
2121
projectPath, _ := cmd.Flags().GetString("project-path")
2222
testPackagesJson, _ := cmd.Flags().GetString("test-packages-json")
2323
testPackagesArg, _ := cmd.Flags().GetStringSlice("test-packages")
24+
testCmdStrings, _ := cmd.Flags().GetStringArray("test-cmd")
2425
runCount, _ := cmd.Flags().GetInt("run-count")
2526
timeout, _ := cmd.Flags().GetDuration("timeout")
2627
tags, _ := cmd.Flags().GetStringArray("tags")
@@ -40,14 +41,17 @@ var RunTestsCmd = &cobra.Command{
4041

4142
// Determine test packages
4243
var testPackages []string
43-
if testPackagesJson != "" {
44-
if err := json.Unmarshal([]byte(testPackagesJson), &testPackages); err != nil {
45-
log.Fatal().Err(err).Msg("Error decoding test packages JSON")
44+
if len(testCmdStrings) == 0 {
45+
// No custom command -> parse packages
46+
if testPackagesJson != "" {
47+
if err := json.Unmarshal([]byte(testPackagesJson), &testPackages); err != nil {
48+
log.Fatal().Err(err).Msg("Error decoding test packages JSON")
49+
}
50+
} else if len(testPackagesArg) > 0 {
51+
testPackages = testPackagesArg
52+
} else {
53+
log.Fatal().Msg("Error: must specify either --test-packages-json or --test-packages (or use --test-cmd).")
4654
}
47-
} else if len(testPackagesArg) > 0 {
48-
testPackages = testPackagesArg
49-
} else {
50-
log.Fatal().Msg("Error: must specify either --test-packages-json or --test-packages")
5155
}
5256

5357
// Initialize the runner
@@ -67,9 +71,22 @@ var RunTestsCmd = &cobra.Command{
6771
}
6872

6973
// Run the tests
70-
testReport, err := testRunner.RunTests()
71-
if err != nil {
72-
log.Fatal().Err(err).Msg("Error running tests")
74+
var (
75+
testReport *reports.TestReport
76+
err error
77+
)
78+
79+
if len(testCmdStrings) > 0 {
80+
testReport, err = testRunner.RunTestsByCmd(testCmdStrings)
81+
if err != nil {
82+
log.Fatal().Err(err).Msg("Error running custom test command")
83+
}
84+
} else {
85+
// Otherwise, use the normal go test approach
86+
testReport, err = testRunner.RunTests()
87+
if err != nil {
88+
log.Fatal().Err(err).Msg("Error running tests")
89+
}
7390
}
7491

7592
// Save the test results in JSON format
@@ -108,6 +125,9 @@ func init() {
108125
RunTestsCmd.Flags().StringP("project-path", "r", ".", "The path to the Go project. Default is the current directory. Useful for subprojects")
109126
RunTestsCmd.Flags().String("test-packages-json", "", "JSON-encoded string of test packages")
110127
RunTestsCmd.Flags().StringSlice("test-packages", nil, "Comma-separated list of test packages to run")
128+
RunTestsCmd.Flags().StringArray("test-cmd", nil,
129+
"Optional custom test command (e.g. 'go run e2e_test.go -someflag'), which must produce go test -json output.",
130+
)
111131
RunTestsCmd.Flags().Bool("run-all-packages", false, "Run all test packages in the project. This flag overrides --test-packages and --test-packages-json")
112132
RunTestsCmd.Flags().IntP("run-count", "c", 1, "Number of times to run the tests")
113133
RunTestsCmd.Flags().Duration("timeout", 0, "Passed on to the 'go test' command as the -timeout flag")

tools/flakeguard/runner/runner.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,93 @@ func (r *Runner) runTests(packageName string) (string, bool, error) {
160160
return tmpFile.Name(), true, nil // Test succeeded
161161
}
162162

163+
// RunTestsByCmd runs an arbitrary command testCmd (like ["go", "run", "my_test.go", ...])
164+
// that produces the same JSON lines that 'go test -json' would produce on stdout.
165+
// It captures those lines in a temp file, then parses them for pass/fail/panic/race data.
166+
func (r *Runner) RunTestsByCmd(testCmd []string) (*reports.TestReport, error) {
167+
var jsonFilePaths []string
168+
169+
// Run the command r.RunCount times
170+
for i := 0; i < r.RunCount; i++ {
171+
jsonFilePath, passed, err := r.runCmd(testCmd, i)
172+
if err != nil {
173+
return nil, fmt.Errorf("failed to run test command: %w", err)
174+
}
175+
jsonFilePaths = append(jsonFilePaths, jsonFilePath)
176+
if !passed && r.FailFast {
177+
break
178+
}
179+
}
180+
181+
results, err := r.parseTestResults(jsonFilePaths)
182+
if err != nil {
183+
return nil, fmt.Errorf("failed to parse test results: %w", err)
184+
}
185+
186+
// Build a TestReport, same shape as RunTests()
187+
return &reports.TestReport{
188+
GoProject: r.prettyProjectPath,
189+
TestRunCount: r.RunCount,
190+
RaceDetection: r.UseRace,
191+
Results: results,
192+
}, nil
193+
}
194+
195+
// runCmd is a helper that runs the user-supplied command once, captures its JSON output,
196+
// and returns (tempFilePath, passed, error).
197+
func (r *Runner) runCmd(testCmd []string, runIndex int) (string, bool, error) {
198+
// Create temp file for JSON output
199+
tmpFile, err := os.CreateTemp("", fmt.Sprintf("test-output-cmd-run%d-*.json", runIndex+1))
200+
if err != nil {
201+
return "", false, fmt.Errorf("failed to create temp file: %w", err)
202+
}
203+
defer tmpFile.Close()
204+
205+
if r.Verbose {
206+
log.Info().Msgf("Running custom test command (%d/%d): %s", runIndex+1, r.RunCount, strings.Join(testCmd, " "))
207+
}
208+
209+
cmd := exec.Command(testCmd[0], testCmd[1:]...)
210+
cmd.Dir = r.ProjectPath
211+
212+
// If collecting raw output, write to both file & buffer
213+
if r.CollectRawOutput {
214+
if r.rawOutputs == nil {
215+
r.rawOutputs = make(map[string]*bytes.Buffer)
216+
}
217+
key := fmt.Sprintf("customCmd-run%d", runIndex+1)
218+
if _, exists := r.rawOutputs[key]; !exists {
219+
r.rawOutputs[key] = &bytes.Buffer{}
220+
}
221+
cmd.Stdout = io.MultiWriter(tmpFile, r.rawOutputs[key])
222+
} else {
223+
cmd.Stdout = tmpFile
224+
}
225+
// Handle stderr however you like; here we just dump it to console
226+
cmd.Stderr = os.Stderr
227+
228+
// Run it
229+
err = cmd.Run()
230+
231+
// Determine pass/fail from exit code
232+
type exitCoder interface {
233+
ExitCode() int
234+
}
235+
var ec exitCoder
236+
if errors.As(err, &ec) {
237+
// Non-zero exit code => test failure
238+
if ec.ExitCode() != 0 {
239+
return tmpFile.Name(), false, nil
240+
}
241+
} else if err != nil {
242+
// Some other error that doesn't implement ExitCode() => real error
243+
return "", false, fmt.Errorf("error running test command: %w", err)
244+
}
245+
246+
// Otherwise, assume success
247+
return tmpFile.Name(), true, nil
248+
}
249+
163250
type entry struct {
164251
Action string `json:"Action"`
165252
Test string `json:"Test"`

0 commit comments

Comments
 (0)