Skip to content

Commit e0487f8

Browse files
committed
Add integration tests for the runner package in Flakeguard
1 parent 074843c commit e0487f8

File tree

1 file changed

+361
-0
lines changed

1 file changed

+361
-0
lines changed
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
//go:build integration_tests
2+
// +build integration_tests
3+
4+
// Integration tests for the runner package, executing real tests.
5+
package runner_test
6+
7+
import (
8+
"encoding/json"
9+
"fmt"
10+
"os"
11+
"path/filepath"
12+
"strings"
13+
"testing"
14+
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
18+
"github.com/smartcontractkit/chainlink-testing-framework/tools/flakeguard/reports"
19+
"github.com/smartcontractkit/chainlink-testing-framework/tools/flakeguard/runner"
20+
// We will use the actual runner, executor, and parser implementations
21+
)
22+
23+
var (
24+
// Use relative path from runner directory for example tests
25+
flakyTestPackagePath = "./example_test_package"
26+
// Use a subdirectory within runner for debug output
27+
debugDir = "_debug_outputs_integration"
28+
)
29+
30+
// expectedTestResult mirrors the structure from the old tests
31+
type expectedTestResult struct {
32+
allSuccesses bool
33+
someSuccesses bool
34+
allFailures bool
35+
someFailures bool
36+
allSkips bool
37+
testPanic bool
38+
packagePanic bool
39+
race bool
40+
maximumRuns int
41+
42+
exactRuns *int
43+
minimumRuns *int
44+
exactPassRate *float64
45+
minimumPassRate *float64
46+
maximumPassRate *float64
47+
48+
seen bool
49+
}
50+
51+
// TestRunIntegration adapts the original TestRun to use the refactored runner.
52+
func TestRunIntegration(t *testing.T) {
53+
var (
54+
zeroRuns = 0
55+
oneCount = 1
56+
defaultRunCount = 3 // Use a smaller count for faster integration tests
57+
successPassRate = 1.0
58+
failPassRate = 0.0
59+
)
60+
testCases := []struct {
61+
name string
62+
cfg runnerConfig // Use a helper struct for clarity
63+
expectedTests map[string]*expectedTestResult
64+
expectBuildErr bool
65+
}{
66+
{
67+
name: "default (integration)",
68+
cfg: runnerConfig{
69+
ProjectPath: "../",
70+
RunCount: defaultRunCount,
71+
SkipTests: []string{"TestPanic", "TestFlakyPanic", "TestSubTestsSomePanic", "TestTimeout"},
72+
GoTestCountFlag: &oneCount, // Force count=1 internally for predictability
73+
OmitOutputs: true,
74+
IgnoreSubtestErr: false,
75+
},
76+
expectedTests: map[string]*expectedTestResult{
77+
"TestFlaky": {exactRuns: &defaultRunCount, someSuccesses: true, someFailures: true},
78+
"TestFail": {exactRuns: &defaultRunCount, allFailures: true, exactPassRate: &failPassRate},
79+
"TestFailLargeOutput": {exactRuns: &defaultRunCount, allFailures: true, exactPassRate: &failPassRate},
80+
"TestPass": {exactRuns: &defaultRunCount, allSuccesses: true, exactPassRate: &successPassRate},
81+
"TestSkipped": {exactRuns: &zeroRuns, allSkips: true, exactPassRate: &successPassRate},
82+
"TestRace": {exactRuns: &defaultRunCount, allSuccesses: true, exactPassRate: &successPassRate},
83+
"TestSubTestsAllPass": {exactRuns: &defaultRunCount, allSuccesses: true},
84+
"TestSubTestsAllPass/Pass1": {exactRuns: &defaultRunCount, allSuccesses: true},
85+
"TestSubTestsAllPass/Pass2": {exactRuns: &defaultRunCount, allSuccesses: true},
86+
"TestFailInParentAfterSubTests": {exactRuns: &defaultRunCount, allFailures: true},
87+
"TestFailInParentAfterSubTests/Pass1": {exactRuns: &defaultRunCount, allSuccesses: true},
88+
"TestFailInParentAfterSubTests/Pass2": {exactRuns: &defaultRunCount, allSuccesses: true},
89+
"TestFailInParentBeforeSubTests": {exactRuns: &defaultRunCount, allFailures: true},
90+
"TestSubTestsAllFail": {exactRuns: &defaultRunCount, allFailures: true},
91+
"TestSubTestsAllFail/Fail1": {exactRuns: &defaultRunCount, allFailures: true},
92+
"TestSubTestsAllFail/Fail2": {exactRuns: &defaultRunCount, allFailures: true},
93+
"TestSubTestsSomeFail": {exactRuns: &defaultRunCount, allFailures: true},
94+
"TestSubTestsSomeFail/Pass": {exactRuns: &defaultRunCount, allSuccesses: true},
95+
"TestSubTestsSomeFail/Fail": {exactRuns: &defaultRunCount, allFailures: true},
96+
},
97+
},
98+
{
99+
name: "race (integration)",
100+
cfg: runnerConfig{
101+
ProjectPath: "../", // Set ProjectPath relative to runner dir -> flakeguard dir
102+
RunCount: defaultRunCount,
103+
SelectTests: []string{"TestRace"},
104+
GoTestRaceFlag: true,
105+
OmitOutputs: true,
106+
IgnoreSubtestErr: false,
107+
},
108+
expectedTests: map[string]*expectedTestResult{
109+
"TestRace": {race: true, maximumRuns: defaultRunCount, allFailures: true}, // Races cause failures
110+
},
111+
},
112+
// Add other cases like panic, subtest panic, failfast if needed
113+
{
114+
name: "always panic (integration)",
115+
cfg: runnerConfig{
116+
ProjectPath: "../",
117+
RunCount: defaultRunCount,
118+
SelectTests: []string{"TestPanic"},
119+
GoTestCountFlag: &oneCount, // Force count=1 for predictability
120+
OmitOutputs: true,
121+
},
122+
expectedTests: map[string]*expectedTestResult{
123+
"TestPanic": {packagePanic: true, testPanic: true, maximumRuns: defaultRunCount, allFailures: true},
124+
},
125+
},
126+
{
127+
name: "flaky panic (integration)",
128+
cfg: runnerConfig{
129+
ProjectPath: "../",
130+
RunCount: defaultRunCount,
131+
SelectTests: []string{"TestFlakyPanic"},
132+
GoTestCountFlag: &oneCount,
133+
OmitOutputs: true,
134+
},
135+
expectedTests: map[string]*expectedTestResult{
136+
// This test panics on first run, passes on second. We run 3 times.
137+
// Expect PackagePanic=true, TestPanic=true (as it panicked at least once)
138+
// Expect some failures (at least 1), some successes (at least 1).
139+
// Exact runs should be defaultRunCount.
140+
"TestFlakyPanic": {exactRuns: &defaultRunCount, packagePanic: true, testPanic: true, someSuccesses: true, someFailures: true},
141+
},
142+
},
143+
{
144+
name: "subtest panic (integration)",
145+
cfg: runnerConfig{
146+
ProjectPath: "../",
147+
RunCount: defaultRunCount,
148+
SelectTests: []string{"TestSubTestsSomePanic"},
149+
GoTestCountFlag: &oneCount,
150+
OmitOutputs: true,
151+
},
152+
expectedTests: map[string]*expectedTestResult{
153+
"TestSubTestsSomePanic": {exactRuns: &defaultRunCount, packagePanic: true, testPanic: true, allFailures: true}, // Parent fails due to subtest panic
154+
"TestSubTestsSomePanic/Pass": {exactRuns: &defaultRunCount, packagePanic: true, testPanic: true, allFailures: true}, // Inherits panic, successes become failures
155+
"TestSubTestsSomePanic/Panic": {exactRuns: &defaultRunCount, packagePanic: true, testPanic: true, allFailures: true}, // Panics directly
156+
},
157+
},
158+
{
159+
name: "failfast (integration)",
160+
cfg: runnerConfig{
161+
ProjectPath: "../",
162+
RunCount: defaultRunCount, // Will try 3 times, but fail-fast stops early
163+
SelectTests: []string{"TestFail", "TestPass"},
164+
GoTestCountFlag: &oneCount,
165+
FailFast: true,
166+
OmitOutputs: true,
167+
},
168+
expectedTests: map[string]*expectedTestResult{
169+
// Only one execution attempt happens because FailFast=true and TestFail fails.
170+
"TestFail": {exactRuns: &oneCount, allFailures: true},
171+
"TestPass": {exactRuns: &oneCount, allSuccesses: true},
172+
},
173+
},
174+
}
175+
176+
for _, tc := range testCases {
177+
tc := tc // Capture range variable
178+
t.Run(tc.name, func(t *testing.T) {
179+
// Integration tests cannot run in parallel as they modify shared state (files)
180+
// t.Parallel()
181+
182+
// Adjust project path relative to this test file's location
183+
absProjectPath, err := filepath.Abs(tc.cfg.ProjectPath)
184+
require.NoError(t, err)
185+
186+
// Initialize runner using the constructor
187+
testRunner := runner.NewRunner(
188+
absProjectPath,
189+
false, // Verbose off for integration tests unless debugging
190+
tc.cfg.RunCount,
191+
tc.cfg.GoTestCountFlag,
192+
tc.cfg.GoTestRaceFlag,
193+
tc.cfg.GoTestTimeoutFlag,
194+
tc.cfg.Tags,
195+
tc.cfg.UseShuffle,
196+
tc.cfg.ShuffleSeed,
197+
tc.cfg.FailFast,
198+
tc.cfg.SkipTests,
199+
tc.cfg.SelectTests,
200+
tc.cfg.IgnoreSubtestErr,
201+
tc.cfg.OmitOutputs,
202+
nil, // Use default executor
203+
nil, // Use default parser
204+
)
205+
206+
// Use package path relative to the ProjectPath (flakeguard dir)
207+
testResults, err := testRunner.RunTestPackages([]string{"./runner/example_test_package"})
208+
209+
if tc.expectBuildErr {
210+
require.Error(t, err)
211+
// Assuming ErrBuild is exported from parser and accessible via runner
212+
// require.ErrorIs(t, err, runner.ErrBuild) // Need to check how to access this error
213+
return
214+
}
215+
require.NoError(t, err)
216+
217+
t.Cleanup(func() {
218+
if !t.Failed() {
219+
return
220+
}
221+
if err := os.MkdirAll(debugDir, 0755); err != nil {
222+
t.Logf("error creating directory: %v", err)
223+
return
224+
}
225+
saniTName := strings.ReplaceAll(t.Name(), "/", "_")
226+
resultsFileName := filepath.Join(debugDir, fmt.Sprintf("test_results_%s.json", saniTName))
227+
jsonResults, err := json.MarshalIndent(testResults, "", " ")
228+
if err != nil {
229+
t.Logf("error marshalling test report: %v", err)
230+
return
231+
}
232+
err = os.WriteFile(resultsFileName, jsonResults, 0644) //nolint:gosec
233+
if err != nil {
234+
t.Logf("error writing test results: %v", err)
235+
return
236+
}
237+
t.Logf("Saved failing test results to %s", resultsFileName)
238+
})
239+
240+
// Assertions
241+
checkTestResults(t, tc.expectedTests, testResults)
242+
})
243+
}
244+
}
245+
246+
// Helper function to check results against expectations
247+
func checkTestResults(t *testing.T, expectedTests map[string]*expectedTestResult, actualResults []reports.TestResult) {
248+
t.Helper()
249+
assert.Equal(t, len(expectedTests), len(actualResults), "unexpected number of test results recorded")
250+
251+
for _, result := range actualResults {
252+
t.Run(fmt.Sprintf("checking results of %s", result.TestName), func(t *testing.T) {
253+
require.NotNil(t, result, "test result was nil")
254+
expected, ok := expectedTests[result.TestName]
255+
require.True(t, ok, "unexpected test name found in results: %s", result.TestName)
256+
require.False(t, expected.seen, "test '%s' was seen multiple times", result.TestName)
257+
expected.seen = true
258+
259+
// Assertions adapted from original test
260+
if !expected.testPanic { // Panics end up wrecking durations
261+
// Can't reliably assert duration length == runs if some runs panicked and didn't report duration
262+
// assert.Len(t, result.Durations, result.Runs, "test '%s' has mismatch of runs %d and duration counts %d", result.TestName, result.Runs, len(result.Durations))
263+
assert.False(t, result.Panic, "test '%s' should not have panicked", result.TestName)
264+
}
265+
// Runs count is now calculated differently (based on processed terminal actions)
266+
// The assertion result.Runs == result.Successes + result.Failures is no longer always true if skips occurred.
267+
// We rely on the specific run count assertions below.
268+
269+
if expected.minimumRuns != nil {
270+
assert.GreaterOrEqual(t, result.Runs, *expected.minimumRuns, "test '%s' had fewer runs (%d) than expected minimum (%d)", result.TestName, result.Runs, *expected.minimumRuns)
271+
}
272+
if expected.exactRuns != nil {
273+
assert.Equal(t, *expected.exactRuns, result.Runs, "test '%s' had an unexpected number of runs", result.TestName)
274+
} else {
275+
assert.LessOrEqual(t, result.Runs, expected.maximumRuns, "test '%s' had more runs (%d) than expected maximum (%d)", result.TestName, result.Runs, expected.maximumRuns)
276+
}
277+
if expected.exactPassRate != nil {
278+
assert.InDelta(t, *expected.exactPassRate, result.PassRatio, 0.001, "test '%s' had an unexpected pass ratio", result.TestName)
279+
}
280+
if expected.minimumPassRate != nil {
281+
assert.Greater(t, result.PassRatio, *expected.minimumPassRate, "test '%s' had a pass ratio below the minimum", result.TestName)
282+
}
283+
if expected.maximumPassRate != nil {
284+
assert.Less(t, result.PassRatio, *expected.maximumPassRate, "test '%s' had a pass ratio above the maximum", result.TestName)
285+
}
286+
if expected.allSuccesses {
287+
assert.Equal(t, result.Runs, result.Successes, "test '%s' has %d runs and should have passed all, only passed %d", result.TestName, result.Runs, result.Successes)
288+
assert.Zero(t, result.Failures, "test '%s' has %d runs and should have passed all, but failed %d", result.TestName, result.Runs, result.Failures)
289+
assert.False(t, result.Panic, "test '%s' should not have panicked", result.TestName)
290+
assert.False(t, result.Race, "test '%s' should not have raced", result.TestName)
291+
}
292+
if expected.someSuccesses {
293+
assert.Greater(t, result.Successes, 0, "test '%s' has %d runs and should have passed some runs, passed none", result.TestName, result.Runs)
294+
}
295+
if expected.allFailures {
296+
assert.Equal(t, result.Runs, result.Failures, "test '%s' has %d runs and should have failed all, only failed %d", result.TestName, result.Runs, result.Failures)
297+
assert.Zero(t, result.Successes, "test '%s' has %d runs and should have failed all, but succeeded %d", result.TestName, result.Runs, result.Successes)
298+
// Do not assert Race == false here, a test could fail for other reasons even if race detector was on
299+
}
300+
if expected.packagePanic {
301+
assert.True(t, result.PackagePanic, "test '%s' should have package panicked", result.TestName)
302+
}
303+
if expected.testPanic {
304+
assert.True(t, result.Panic, "test '%s' should have panicked", result.TestName)
305+
assert.True(t, result.PackagePanic, "test '%s' should have package panicked", result.TestName)
306+
expected.someFailures = true // Panic implies failure
307+
}
308+
if expected.someFailures {
309+
assert.Greater(t, result.Failures, 0, "test '%s' has %d runs and should have failed some runs, failed none", result.TestName, result.Runs)
310+
}
311+
if expected.allSkips {
312+
assert.Equal(t, 0, result.Runs, "test '%s' has %d runs and should have skipped all of them, no runs expected", result.TestName, result.Runs)
313+
assert.True(t, result.Skipped, "test '%s' should be marked skipped", result.TestName)
314+
assert.Zero(t, result.Successes, "test '%s' should have skipped all runs, but succeeded some", result.TestName)
315+
assert.Zero(t, result.Failures, "test '%s' should have skipped all runs, but failed some", result.TestName)
316+
assert.False(t, result.Panic, "test '%s' should not have panicked", result.TestName)
317+
assert.False(t, result.Race, "test '%s' should not have raced", result.TestName)
318+
}
319+
if expected.race {
320+
assert.True(t, result.Race, "test '%s' should have a data race", result.TestName)
321+
// A race condition implies a failure in Go's test output
322+
assert.GreaterOrEqual(t, result.Failures, 1, "test '%s' should have failed due to race", result.TestName)
323+
}
324+
})
325+
}
326+
327+
// Final check to ensure all expected tests were seen
328+
allTestsRun := []string{}
329+
for testName, expected := range expectedTests {
330+
if expected.seen {
331+
allTestsRun = append(allTestsRun, testName)
332+
}
333+
}
334+
for testName, expected := range expectedTests {
335+
require.True(t, expected.seen, "expected test '%s' not found in test runs\nAll tests run: %s", testName, strings.Join(allTestsRun, ", "))
336+
}
337+
}
338+
339+
// resultsString helper (copied from old test)
340+
func resultsString(result reports.TestResult) string {
341+
resultCounts := result.Successes + result.Failures + result.Skips
342+
return fmt.Sprintf("Runs: %d\nPanicked: %t\nRace: %t\nSuccesses: %d\nFailures: %d\nSkips: %d\nTotal Results: %d",
343+
result.Runs, result.Panic, result.Race, result.Successes, result.Failures, result.Skips, resultCounts)
344+
}
345+
346+
// runnerConfig helper struct for test cases
347+
type runnerConfig struct {
348+
ProjectPath string
349+
RunCount int
350+
GoTestCountFlag *int
351+
GoTestRaceFlag bool
352+
GoTestTimeoutFlag string
353+
Tags []string
354+
UseShuffle bool
355+
ShuffleSeed string
356+
FailFast bool
357+
SkipTests []string
358+
SelectTests []string
359+
OmitOutputs bool
360+
IgnoreSubtestErr bool
361+
}

0 commit comments

Comments
 (0)