Skip to content

Commit 92bda89

Browse files
committed
Adds tests to runner implementation
1 parent 8b02ed1 commit 92bda89

File tree

9 files changed

+223
-25
lines changed

9 files changed

+223
-25
lines changed

tools/flakeguard/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tmp_test_flaky_state
2+
flaky_test_results.json

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 'flaky_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 'flaky_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/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ var RunTestsCmd = &cobra.Command{
8080
if err != nil {
8181
log.Fatalf("Error marshaling test results to JSON: %v", err)
8282
}
83-
if err := os.WriteFile(outputPath, jsonData, 0644); err != nil {
83+
if err := os.WriteFile(outputPath, jsonData, 0644); err != nil { //nolint:gosec
8484
log.Fatalf("Error writing test results to file: %v", err)
8585
}
8686
fmt.Printf("All test results saved to %s\n", outputPath)

tools/flakeguard/reports/reports.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@ import (
1111
"strings"
1212
)
1313

14+
// TestResult represents the result of a single test being run through flakeguard.
1415
type TestResult struct {
1516
TestName string
1617
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
18+
Panicked bool // Indicates a test-level panic
19+
PackagePanicked bool // Indicates a package-level panic
20+
PassRatio float64 // Pass ratio in decimal format like 0.5
21+
PassRatioPercentage string // Pass ratio in percentage format like "50%"
22+
Skipped bool // Indicates if the test was skipped
23+
Runs int // Count of how many times the test was run
24+
Failures int // Count of how many times the test failed
25+
Successes int // Count of how many times the test passed
26+
Panics int // Count of how many times the test panicked
27+
Skips int // Count of how many times the test was skipped
2328
Outputs []string // Stores outputs for a test
2429
Durations []float64 // Stores elapsed time in seconds for each run of the test
2530
PackageOutputs []string // Stores package-level outputs
@@ -205,7 +210,7 @@ func saveResults(filePath string, results []TestResult) error {
205210
if err != nil {
206211
return fmt.Errorf("error marshaling results: %v", err)
207212
}
208-
return os.WriteFile(filePath, data, 0644)
213+
return os.WriteFile(filePath, data, 0644) //nolint:gosec
209214
}
210215

211216
// Helper function to save test names, packages, and outputs to JSON file
@@ -233,5 +238,5 @@ func saveTestOutputs(filePath string, results []TestResult) error {
233238
if err != nil {
234239
return fmt.Errorf("error marshaling outputs: %v", err)
235240
}
236-
return os.WriteFile(filePath, data, 0644)
241+
return os.WriteFile(filePath, data, 0644) //nolint:gosec
237242
}

tools/flakeguard/reports/reports_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func writeTempJSONFile(t *testing.T, dir string, filename string, data interface
132132
if err != nil {
133133
t.Fatalf("Failed to marshal JSON: %v", err)
134134
}
135-
if err := os.WriteFile(filePath, fileData, 0644); err != nil {
135+
if err := os.WriteFile(filePath, fileData, 0644); err != nil { //nolint:gosec
136136
t.Fatalf("Failed to write JSON file: %v", err)
137137
}
138138
return filePath
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package flakytestpackage
2+
3+
import (
4+
"os"
5+
"testing"
6+
)
7+
8+
func TestPass(t *testing.T) {
9+
t.Parallel()
10+
t.Log("This test always passes")
11+
}
12+
13+
func TestFail(t *testing.T) {
14+
t.Parallel()
15+
t.Fatal("This test always fails")
16+
}
17+
18+
// This test should have a 50% pass ratio
19+
func TestFlaky(t *testing.T) {
20+
t.Parallel()
21+
22+
// Track if the test has run before
23+
stateFile := "tmp_test_flaky_state"
24+
25+
// If the state file does not exist, create it and fail the test
26+
if _, err := os.Stat(stateFile); os.IsNotExist(err) {
27+
if err := os.WriteFile(stateFile, []byte("run once"), 0644); err != nil { //nolint:gosec
28+
t.Fatalf("THIS IS UNEXPECTED: failed to create state file: %v", err)
29+
}
30+
t.Fatalf("This is a designed flaky test working as intended")
31+
} else {
32+
t.Cleanup(func() {
33+
err := os.Remove(stateFile)
34+
if err != nil {
35+
t.Fatalf("THIS IS UNEXPECTED: failed to remove state file: %v", err)
36+
}
37+
})
38+
}
39+
40+
t.Log("This test passes after the first run")
41+
}
42+
43+
func TestSkipped(t *testing.T) {
44+
t.Parallel()
45+
t.Skip("This test is intentionally skipped")
46+
}
47+
48+
// func TestPanic(t *testing.T) {
49+
// t.Parallel()
50+
// panic("This test intentionally panics")
51+
// }

tools/flakeguard/runner/runner.go

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,14 @@ type exitCoder interface {
5454

5555
// runTests runs the tests for a given package and returns the path to the output file.
5656
func (r *Runner) runTests(packageName string) (string, bool, error) {
57-
args := []string{"test", packageName, "-json", "-count=1"} // Enable JSON output
57+
args := []string{"test", packageName, "-json", "-count=1"}
5858
if r.UseRace {
5959
args = append(args, "-race")
6060
}
6161
if len(r.SkipTests) > 0 {
6262
skipPattern := strings.Join(r.SkipTests, "|")
6363
args = append(args, fmt.Sprintf("-skip=%s", skipPattern))
6464
}
65-
// The last arg redirects stderr to null
66-
args = append(args, "2>/dev/null")
6765

6866
if r.Verbose {
6967
log.Printf("Running command: go %s\n", strings.Join(args, " "))
@@ -105,8 +103,6 @@ func parseTestResults(filePaths []string) ([]reports.TestResult, error) {
105103
if err != nil {
106104
return nil, fmt.Errorf("failed to open test output file: %w", err)
107105
}
108-
defer os.Remove(filePath) // Clean up file after parsing
109-
defer file.Close()
110106

111107
scanner := bufio.NewScanner(file)
112108
var precedingLines []string // Store preceding lines for context
@@ -149,7 +145,6 @@ func parseTestResults(filePaths []string) ([]reports.TestResult, error) {
149145
testDetails[key] = &reports.TestResult{
150146
TestName: entry.Test,
151147
TestPackage: entry.Package,
152-
Runs: 0,
153148
PassRatio: 0,
154149
Outputs: []string{},
155150
PackageOutputs: []string{},
@@ -181,25 +176,22 @@ func parseTestResults(filePaths []string) ([]reports.TestResult, error) {
181176
}
182177
case "pass":
183178
if entry.Test != "" {
184-
result.PassRatio = (result.PassRatio*float64(result.Runs-1) + 1) / float64(result.Runs)
185-
result.PassRatioPercentage = fmt.Sprintf("%.0f%%", result.PassRatio*100)
186179
result.Durations = append(result.Durations, entry.Elapsed)
180+
result.Successes++
187181
}
188182
case "fail":
189183
if entry.Test != "" {
190-
result.PassRatio = (result.PassRatio * float64(result.Runs-1)) / float64(result.Runs)
191-
result.PassRatioPercentage = fmt.Sprintf("%.0f%%", result.PassRatio*100)
192184
result.Durations = append(result.Durations, entry.Elapsed)
185+
result.Failures++
193186
}
194187
case "output":
195188
// Output already handled above
196189
if panicRe.MatchString(entry.Output) {
197190
if entry.Test != "" {
198191
// Test-level panic
199192
result.Panicked = true
200-
result.PassRatio = (result.PassRatio * float64(result.Runs-1)) / float64(result.Runs)
201-
result.PassRatioPercentage = fmt.Sprintf("%.0f%%", result.PassRatio*100)
202193
result.Durations = append(result.Durations, entry.Elapsed)
194+
result.Panics++
203195
} else {
204196
// Package-level panic
205197
// Mark PackagePanicked for all TestResults in the package
@@ -213,15 +205,26 @@ func parseTestResults(filePaths []string) ([]reports.TestResult, error) {
213205
case "skip":
214206
if entry.Test != "" {
215207
result.Skipped = true
216-
result.Runs++
208+
result.Skips++
217209
result.Durations = append(result.Durations, entry.Elapsed)
218210
}
219211
}
212+
if entry.Test != "" {
213+
result.PassRatio = float64(result.Successes) / float64(result.Runs)
214+
result.PassRatioPercentage = fmt.Sprintf("%.0f%%", result.PassRatio*100)
215+
}
220216
}
221217

222218
if err := scanner.Err(); err != nil {
223219
return nil, fmt.Errorf("reading test output file: %w", err)
224220
}
221+
// Clean up file after parsing
222+
if err = file.Close(); err != nil {
223+
log.Printf("WARN: failed to close file: %v", err)
224+
}
225+
if err = os.Remove(filePath); err != nil {
226+
log.Printf("WARN: failed to delete file: %v", err)
227+
}
225228
}
226229

227230
var results []reports.TestResult
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package runner
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"testing"
8+
9+
"github.com/smartcontractkit/chainlink-testing-framework/tools/flakeguard/reports"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
type MockGoRunner struct {
15+
RunFunc func(dir string, args []string) (string, bool, error)
16+
}
17+
18+
func (m MockGoRunner) RunCommand(dir string, args []string) (string, bool, error) {
19+
return m.RunFunc(dir, args)
20+
}
21+
22+
func TestRun(t *testing.T) {
23+
t.Parallel()
24+
25+
runs := 5
26+
27+
runner := Runner{
28+
ProjectPath: "./",
29+
Verbose: true,
30+
RunCount: runs,
31+
UseRace: false,
32+
SkipTests: []string{},
33+
FailFast: false,
34+
SelectedTestPackages: []string{"./flaky_test_package"},
35+
}
36+
37+
expectedResults := map[string]*struct {
38+
*reports.TestResult
39+
seen bool
40+
}{
41+
"TestFlaky": {
42+
TestResult: &reports.TestResult{
43+
TestName: "TestFlaky",
44+
Panicked: false,
45+
Skipped: false,
46+
},
47+
},
48+
"TestFail": {
49+
TestResult: &reports.TestResult{
50+
TestName: "TestFail",
51+
Panicked: false,
52+
Skipped: false,
53+
PassRatio: 0,
54+
Failures: runs,
55+
},
56+
},
57+
"TestPass": {
58+
TestResult: &reports.TestResult{
59+
TestName: "TestPass",
60+
Panicked: false,
61+
Skipped: false,
62+
PassRatio: 1,
63+
Successes: runs,
64+
},
65+
},
66+
// "TestPanic": {
67+
// TestResult: &reports.TestResult{
68+
// TestName: "TestPanic",
69+
// Panicked: true,
70+
// Skipped: false,
71+
// PassRatio: 0,
72+
// },
73+
// },
74+
"TestSkipped": {
75+
TestResult: &reports.TestResult{
76+
TestName: "TestSkipped",
77+
Panicked: false,
78+
Skipped: true,
79+
PassRatio: 0,
80+
},
81+
},
82+
}
83+
84+
testResults, err := runner.RunTests()
85+
require.NoError(t, err)
86+
t.Cleanup(func() {
87+
if t.Failed() {
88+
t.Log("Writing test results to flaky_test_results.json")
89+
jsonResults, err := json.Marshal(testResults)
90+
require.NoError(t, err)
91+
err = os.WriteFile("flaky_test_results.json", jsonResults, 0644) //nolint:gosec
92+
require.NoError(t, err)
93+
}
94+
})
95+
for _, result := range testResults {
96+
t.Run(fmt.Sprintf("checking results of %s", result.TestName), func(t *testing.T) {
97+
expected, ok := expectedResults[result.TestName]
98+
// Sanity checks
99+
require.True(t, ok, "unexpected test result: %s", result.TestName)
100+
require.False(t, expected.seen, "test '%s' was seen multiple times", result.TestName)
101+
expected.seen = true
102+
103+
assert.Equal(t, runs, result.Runs, "test '%s' had an unexpected number of runs", result.TestName)
104+
assert.Len(t, result.Durations, runs, "test '%s' had an unexpected number of durations as it was run %d times", result.TestName, runs)
105+
if result.TestName == "TestSlow" {
106+
for _, duration := range result.Durations {
107+
assert.GreaterOrEqual(t, duration, float64(1), "slow test '%s' should have a duration of at least 2s", result.TestName)
108+
}
109+
}
110+
assert.Equal(t, expected.TestResult.Panicked, result.Panicked, "test '%s' had an unexpected panic result", result.TestName)
111+
assert.Equal(t, expected.TestResult.Skipped, result.Skipped, "test '%s' had an unexpected skipped result", result.TestName)
112+
113+
if result.TestName == "TestFlaky" {
114+
assert.Greater(t, result.Successes, 0, "flaky test '%s' should have passed some", result.TestName)
115+
assert.Greater(t, result.Failures, 0, "flaky test '%s' should have failed some", result.TestName)
116+
assert.Greater(t, result.PassRatio, float64(0), "flaky test '%s' should have a flaky pass ratio", result.TestName)
117+
assert.Less(t, result.PassRatio, float64(1), "flaky test '%s' should have a flaky pass ratio", result.TestName)
118+
} else {
119+
assert.Equal(t, expected.TestResult.PassRatio, result.PassRatio, "test '%s' had an unexpected pass ratio", result.TestName)
120+
assert.Equal(t, expected.TestResult.Successes, result.Successes, "test '%s' had an unexpected number of successes", result.TestName)
121+
assert.Equal(t, expected.TestResult.Failures, result.Failures, "test '%s' had an unexpected number of failures", result.TestName)
122+
}
123+
})
124+
}
125+
126+
for _, expected := range expectedResults {
127+
assert.True(t, expected.seen, "expected test '%s' not found in test runs", expected.TestResult.TestName)
128+
}
129+
}

0 commit comments

Comments
 (0)