Skip to content

Commit be06798

Browse files
authored
Adds Tests and Fixes Panic Detection for Flakeguard (#1367)
1 parent af81e8a commit be06798

File tree

11 files changed

+1173
-209
lines changed

11 files changed

+1173
-209
lines changed

tools/flakeguard/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
tmp_test_flaky_state
2+
test_results_*.json
3+
debug_outputs/

tools/flakeguard/Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
1+
.PHONY: test_unit
12
test_unit:
2-
go test -timeout 5m -json -cover -covermode=count -coverprofile=unit-test-coverage.out ./... 2>&1 | tee /tmp/gotest.log | gotestloghelper -ci
3+
go list ./... | grep -v 'example_test_package' | xargs go test -timeout 5m -json -cover -covermode=count -coverprofile=unit-test-coverage.out 2>&1 | tee /tmp/gotest.log | gotestloghelper -ci
4+
5+
.PHONY: test
6+
test:
7+
go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
8+
set -euo pipefail
9+
go list ./... | grep -v 'example_test_package' | xargs go test -json -cover -coverprofile unit-test-coverage.out -v 2>&1 | tee /tmp/gotest.log | gotestfmt

tools/flakeguard/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ go install github.com/smartcontractkit/chainlink-testing-framework/tools/flakegu
2020

2121
## Usage
2222

23-
Flakeguard offers two main commands:
23+
Flakeguard offers two main commands:
24+
2425
- `find` identifies test packages affected by recent changes.
2526
- `run` executes tests multiple times to identify flaky tests
2627

tools/flakeguard/cmd/aggregate_results.go

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@ import (
77
"github.com/spf13/cobra"
88
)
99

10+
var (
11+
resultsFolderPath string
12+
outputResultsPath string
13+
outputLogsPath string
14+
maxPassRatio float64
15+
filterFailed bool
16+
)
17+
1018
var AggregateResultsCmd = &cobra.Command{
1119
Use: "aggregate-results",
1220
Short: "Aggregate test results and optionally filter failed tests based on a threshold",
1321
Run: func(cmd *cobra.Command, args []string) {
14-
resultsFolderPath, _ := cmd.Flags().GetString("results-path")
15-
outputResultsPath, _ := cmd.Flags().GetString("output-results")
16-
outputLogsPath, _ := cmd.Flags().GetString("output-logs")
17-
threshold, _ := cmd.Flags().GetFloat64("threshold")
18-
minPassRatio, _ := cmd.Flags().GetFloat64("min-pass-ratio")
19-
filterFailed, _ := cmd.Flags().GetBool("filter-failed")
20-
2122
// Aggregate all test results
2223
allResults, err := reports.AggregateTestResults(resultsFolderPath)
2324
if err != nil {
@@ -27,9 +28,9 @@ var AggregateResultsCmd = &cobra.Command{
2728
var resultsToSave []reports.TestResult
2829

2930
if filterFailed {
30-
// Filter to only include failed tests based on threshold and minPassRatio
31+
// Filter to only include tests that failed below the threshold
3132
for _, result := range allResults {
32-
if result.PassRatio < threshold && result.PassRatio > minPassRatio && !result.Skipped {
33+
if result.PassRatio < maxPassRatio && !result.Skipped {
3334
resultsToSave = append(resultsToSave, result)
3435
}
3536
}
@@ -45,10 +46,9 @@ var AggregateResultsCmd = &cobra.Command{
4546
}
4647

4748
func init() {
48-
AggregateResultsCmd.Flags().String("results-path", "", "Path to the folder containing JSON test result files")
49-
AggregateResultsCmd.Flags().String("output-results", "./results.json", "Path to output the aggregated or filtered test results in JSON format")
50-
AggregateResultsCmd.Flags().String("output-logs", "", "Path to output the filtered test logs in JSON format")
51-
AggregateResultsCmd.Flags().Float64("threshold", 0.8, "Threshold for considering a test as failed (used with --filter-failed)")
52-
AggregateResultsCmd.Flags().Float64("min-pass-ratio", 0.001, "Minimum pass ratio for considering a test as flaky (used with --filter-failed)")
53-
AggregateResultsCmd.Flags().Bool("filter-failed", false, "If true, filter and output only failed tests based on the threshold")
49+
AggregateResultsCmd.Flags().StringVarP(&resultsFolderPath, "results-path", "p", "", "Path to the folder containing JSON test result files")
50+
AggregateResultsCmd.Flags().StringVarP(&outputResultsPath, "output-results", "o", "./results.json", "Path to output the aggregated or filtered test results in JSON format")
51+
AggregateResultsCmd.Flags().StringVarP(&outputLogsPath, "output-logs", "l", "", "Path to output the filtered test logs in JSON format")
52+
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.")
53+
AggregateResultsCmd.Flags().BoolVarP(&filterFailed, "filter-failed", "f", false, "If true, filter and output only failed tests based on the max-pass-ratio threshold")
5454
}

tools/flakeguard/cmd/run.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ var RunTestsCmd = &cobra.Command{
2323
runCount, _ := cmd.Flags().GetInt("run-count")
2424
useRace, _ := cmd.Flags().GetBool("race")
2525
outputPath, _ := cmd.Flags().GetString("output-json")
26-
threshold, _ := cmd.Flags().GetFloat64("threshold")
26+
maxPassRatio, _ := cmd.Flags().GetFloat64("max-pass-ratio")
2727
skipTests, _ := cmd.Flags().GetStringSlice("skip-tests")
2828
printFailedTests, _ := cmd.Flags().GetBool("print-failed-tests")
29-
minPassRatio, _ := cmd.Flags().GetFloat64("min-pass-ratio")
3029

3130
// Check if project dependencies are correctly set up
3231
if err := checkDependencies(projectPath); err != nil {
@@ -59,15 +58,15 @@ var RunTestsCmd = &cobra.Command{
5958
os.Exit(1)
6059
}
6160

62-
passedTests := reports.FilterPassedTests(testResults, threshold)
63-
failedTests := reports.FilterFailedTests(testResults, threshold)
61+
passedTests := reports.FilterPassedTests(testResults, maxPassRatio)
62+
failedTests := reports.FilterFailedTests(testResults, maxPassRatio)
6463
skippedTests := reports.FilterSkippedTests(testResults)
65-
flakyTests := reports.FilterFlakyTests(testResults, minPassRatio, threshold)
64+
flakyTests := reports.FilterFlakyTests(testResults, maxPassRatio)
6665

6766
// Print all failed tests including flaky tests
6867
if len(failedTests) > 0 && printFailedTests {
69-
fmt.Printf("MinPassRatio threshold for flaky tests: %.2f\n", minPassRatio)
70-
fmt.Printf("PassRatio threshold for flaky tests: %.2f\n", threshold)
68+
fmt.Printf("Maximum threshold for flaky tests: %.2f\n", maxPassRatio)
69+
fmt.Printf("PassRatio threshold for flaky tests: %.2f\n", maxPassRatio)
7170
fmt.Printf("%d failed tests:\n", len(failedTests))
7271
reports.PrintTests(failedTests, os.Stdout)
7372
}
@@ -80,7 +79,7 @@ var RunTestsCmd = &cobra.Command{
8079
if err != nil {
8180
log.Fatalf("Error marshaling test results to JSON: %v", err)
8281
}
83-
if err := os.WriteFile(outputPath, jsonData, 0644); err != nil {
82+
if err := os.WriteFile(outputPath, jsonData, 0644); err != nil { //nolint:gosec
8483
log.Fatalf("Error writing test results to file: %v", err)
8584
}
8685
fmt.Printf("All test results saved to %s\n", outputPath)
@@ -104,10 +103,9 @@ func init() {
104103
RunTestsCmd.Flags().Bool("race", false, "Enable the race detector")
105104
RunTestsCmd.Flags().Bool("fail-fast", false, "Stop on the first test failure")
106105
RunTestsCmd.Flags().String("output-json", "", "Path to output the test results in JSON format")
107-
RunTestsCmd.Flags().Float64("threshold", 0.8, "Threshold for considering a test as flaky")
108106
RunTestsCmd.Flags().StringSlice("skip-tests", nil, "Comma-separated list of test names to skip from running")
109107
RunTestsCmd.Flags().Bool("print-failed-tests", true, "Print failed test results to the console")
110-
RunTestsCmd.Flags().Float64("min-pass-ratio", 0.001, "Minimum pass ratio for considering a test as flaky. Used to distinguish between tests that are truly flaky (with inconsistent results) and those that are consistently failing.")
108+
RunTestsCmd.Flags().Float64("max-pass-ratio", 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.")
111109
}
112110

113111
func checkDependencies(projectPath string) error {

tools/flakeguard/git/git_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
)
99

1010
func TestGetChangedGoPackagesFromDiff(t *testing.T) {
11+
t.Parallel()
12+
1113
tests := []struct {
1214
name string
1315
out string

tools/flakeguard/reports/reports.go

Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,49 +9,56 @@ import (
99
"path/filepath"
1010
"sort"
1111
"strings"
12+
"time"
1213
)
1314

15+
// TestResult represents the result of a single test being run through flakeguard.
1416
type TestResult struct {
1517
TestName string
1618
TestPackage string
17-
Panicked bool // Indicates a test-level panic
18-
PackagePanicked bool // Indicates a package-level panic
19-
PassRatio float64 // Pass ratio in decimal format like 0.5
20-
PassRatioPercentage string // Pass ratio in percentage format like "50%"
21-
Skipped bool // Indicates if the test was skipped
22-
Runs int
23-
Outputs []string // Stores outputs for a test
24-
Durations []float64 // Stores elapsed time in seconds for each run of the test
25-
PackageOutputs []string // Stores package-level outputs
19+
Panicked bool // Indicates a test-level panic
20+
Skipped bool // Indicates if the test was skipped
21+
PackagePanicked bool // Indicates a package-level panic
22+
PassRatio float64 // Pass ratio in decimal format like 0.5
23+
PassRatioPercentage string // Pass ratio in percentage format like "50%"
24+
Runs int // Count of how many times the test was run
25+
Failures int // Count of how many times the test failed
26+
Successes int // Count of how many times the test passed
27+
Panics int // Count of how many times the test panicked
28+
Races int // Count of how many times the test encountered a data race
29+
Skips int // Count of how many times the test was skipped
30+
Outputs []string `json:"outputs,omitempty"` // Stores outputs for a test
31+
Durations []time.Duration // Stores elapsed time for each run of the test
32+
PackageOutputs []string `json:"package_outputs,omitempty"` // Stores package-level outputs
2633
}
2734

2835
// FilterFailedTests returns a slice of TestResult where the pass ratio is below the specified threshold.
29-
func FilterFailedTests(results []TestResult, threshold float64) []TestResult {
36+
func FilterFailedTests(results []TestResult, maxPassRatio float64) []TestResult {
3037
var failedTests []TestResult
3138
for _, result := range results {
32-
if !result.Skipped && result.PassRatio < threshold {
39+
if !result.Skipped && result.PassRatio < maxPassRatio {
3340
failedTests = append(failedTests, result)
3441
}
3542
}
3643
return failedTests
3744
}
3845

3946
// FilterFlakyTests returns a slice of TestResult where the pass ratio is between the min pass ratio and the threshold.
40-
func FilterFlakyTests(testResults []TestResult, minPassRatio, threshold float64) []TestResult {
47+
func FilterFlakyTests(testResults []TestResult, maxPassRatio float64) []TestResult {
4148
var flakyTests []TestResult
4249
for _, test := range testResults {
43-
if test.PassRatio >= minPassRatio && test.PassRatio < threshold && !test.Skipped {
50+
if test.PassRatio < maxPassRatio && !test.Skipped {
4451
flakyTests = append(flakyTests, test)
4552
}
4653
}
4754
return flakyTests
4855
}
4956

5057
// FilterPassedTests returns a slice of TestResult where the tests passed and were not skipped.
51-
func FilterPassedTests(results []TestResult, threshold float64) []TestResult {
58+
func FilterPassedTests(results []TestResult, maxPassRatio float64) []TestResult {
5259
var passedTests []TestResult
5360
for _, result := range results {
54-
if !result.Skipped && result.PassRatio >= threshold {
61+
if !result.Skipped && result.PassRatio >= maxPassRatio {
5562
passedTests = append(passedTests, result)
5663
}
5764
}
@@ -143,9 +150,14 @@ func PrintTests(tests []TestResult, w io.Writer) {
143150
fmt.Fprintf(w, "PassRatio: %.2f\n", test.PassRatio)
144151
fmt.Fprintf(w, "Skipped: %v\n", test.Skipped)
145152
fmt.Fprintf(w, "Runs: %d\n", test.Runs)
153+
fmt.Fprintf(w, "Successes: %d\n", test.Successes)
154+
fmt.Fprintf(w, "Failures: %d\n", test.Failures)
155+
fmt.Fprintf(w, "Panics: %d\n", test.Panics)
156+
fmt.Fprintf(w, "Races: %d\n", test.Races)
157+
fmt.Fprintf(w, "Skips: %d\n", test.Skips)
146158
durationsStr := make([]string, len(test.Durations))
147159
for i, duration := range test.Durations {
148-
durationsStr[i] = fmt.Sprintf("%.2fs", duration)
160+
durationsStr[i] = duration.String()
149161
}
150162
fmt.Fprintf(w, "Durations: %s\n", strings.Join(durationsStr, ", "))
151163
fmt.Fprintf(w, "Outputs:\n%s\n", strings.Join(test.Outputs, ""))
@@ -171,41 +183,21 @@ func SaveFilteredResultsAndLogs(outputResultsPath, outputLogsPath string, failed
171183
}
172184
}
173185

174-
// Helper function to save results to JSON file
186+
// Helper function to save results to JSON file without outputs
175187
func saveResults(filePath string, results []TestResult) error {
176-
// Define a struct type without Outputs and PackageOutputs
177-
type filteredTestResult struct {
178-
TestName string
179-
TestPackage string
180-
Panicked bool
181-
PackagePanicked bool
182-
PassRatio float64
183-
PassRatioPercentage string
184-
Skipped bool
185-
Runs int
186-
Durations []float64
187-
}
188-
189-
var filteredResults []filteredTestResult
188+
var filteredResults []TestResult
190189
for _, r := range results {
191-
filteredResults = append(filteredResults, filteredTestResult{
192-
TestName: r.TestName,
193-
TestPackage: r.TestPackage,
194-
Panicked: r.Panicked,
195-
PackagePanicked: r.PackagePanicked,
196-
PassRatio: r.PassRatio,
197-
PassRatioPercentage: r.PassRatioPercentage,
198-
Skipped: r.Skipped,
199-
Runs: r.Runs,
200-
Durations: r.Durations,
201-
})
190+
filteredResult := r
191+
filteredResult.Outputs = nil
192+
filteredResult.PackageOutputs = nil
193+
filteredResults = append(filteredResults, filteredResult)
202194
}
203195

204196
data, err := json.MarshalIndent(filteredResults, "", " ")
205197
if err != nil {
206198
return fmt.Errorf("error marshaling results: %v", err)
207199
}
208-
return os.WriteFile(filePath, data, 0644)
200+
return os.WriteFile(filePath, data, 0644) //nolint:gosec
209201
}
210202

211203
// Helper function to save test names, packages, and outputs to JSON file
@@ -233,5 +225,5 @@ func saveTestOutputs(filePath string, results []TestResult) error {
233225
if err != nil {
234226
return fmt.Errorf("error marshaling outputs: %v", err)
235227
}
236-
return os.WriteFile(filePath, data, 0644)
228+
return os.WriteFile(filePath, data, 0644) //nolint:gosec
237229
}

0 commit comments

Comments
 (0)