Skip to content

Commit de11cba

Browse files
authored
TT-1995 Prevent double-counting of test failures in Flakeguard (#1665)
1 parent 74b656e commit de11cba

File tree

8 files changed

+1379
-32
lines changed

8 files changed

+1379
-32
lines changed

tools/flakeguard/cmd/run.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ var RunTestsCmd = &cobra.Command{
4141
useShuffle, _ := cmd.Flags().GetBool("shuffle")
4242
shuffleSeed, _ := cmd.Flags().GetString("shuffle-seed")
4343
omitOutputsOnSuccess, _ := cmd.Flags().GetBool("omit-test-outputs-on-success")
44+
ignoreParentFailuresOnSubtests, _ := cmd.Flags().GetBool("ignore-parent-failures-on-subtests")
4445

4546
outputDir := filepath.Dir(outputPath)
4647
initialDirSize, err := getDirSize(outputDir)
@@ -63,7 +64,6 @@ var RunTestsCmd = &cobra.Command{
6364
// Determine test packages
6465
var testPackages []string
6566
if len(testCmdStrings) == 0 {
66-
// No custom command -> parse packages
6767
if testPackagesJson != "" {
6868
if err := json.Unmarshal([]byte(testPackagesJson), &testPackages); err != nil {
6969
log.Error().Err(err).Msg("Error decoding test packages JSON")
@@ -79,18 +79,19 @@ var RunTestsCmd = &cobra.Command{
7979

8080
// Initialize the runner
8181
testRunner := runner.Runner{
82-
ProjectPath: projectPath,
83-
Verbose: true,
84-
RunCount: runCount,
85-
Timeout: timeout,
86-
Tags: tags,
87-
UseRace: useRace,
88-
SkipTests: skipTests,
89-
SelectTests: selectTests,
90-
UseShuffle: useShuffle,
91-
ShuffleSeed: shuffleSeed,
92-
OmitOutputsOnSuccess: omitOutputsOnSuccess,
93-
MaxPassRatio: maxPassRatio,
82+
ProjectPath: projectPath,
83+
Verbose: true,
84+
RunCount: runCount,
85+
Timeout: timeout,
86+
Tags: tags,
87+
UseRace: useRace,
88+
SkipTests: skipTests,
89+
SelectTests: selectTests,
90+
UseShuffle: useShuffle,
91+
ShuffleSeed: shuffleSeed,
92+
OmitOutputsOnSuccess: omitOutputsOnSuccess,
93+
MaxPassRatio: maxPassRatio,
94+
IgnoreParentFailuresOnSubtests: ignoreParentFailuresOnSubtests,
9495
}
9596

9697
// Run the tests
@@ -105,7 +106,6 @@ var RunTestsCmd = &cobra.Command{
105106
os.Exit(ErrorExitCode)
106107
}
107108
} else {
108-
// Otherwise, use the normal go test approach
109109
testReport, err = testRunner.RunTestPackages(testPackages)
110110
if err != nil {
111111
log.Fatal().Err(err).Msg("Error running test packages")
@@ -180,6 +180,7 @@ func init() {
180180
RunTestsCmd.Flags().StringSlice("select-tests", nil, "Comma-separated list of test names to specifically run")
181181
RunTestsCmd.Flags().Float64("max-pass-ratio", 1.0, "The maximum pass ratio threshold for a test to be considered flaky. Any tests below this pass rate will be considered flaky.")
182182
RunTestsCmd.Flags().Bool("omit-test-outputs-on-success", true, "Omit test outputs and package outputs for tests that pass")
183+
RunTestsCmd.Flags().Bool("ignore-parent-failures-on-subtests", false, "Ignore failures in parent tests when only subtests fail")
183184
}
184185

185186
func checkDependencies(projectPath string) error {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Go Test Transform
2+
3+
This utility transforms the output of Go's test JSON format to handle subtest failures more intelligently. It prevents parent tests from failing when only their subtests fail, while preserving the original test structure and output format.
4+
5+
## Features
6+
7+
- Transforms Go test JSON output to modify how test failures propagate
8+
- Parent tests won't fail if they only have failing subtests but no direct failures
9+
- Maintains the original JSON format for compatibility with other tools
10+
11+
## Key Behavior
12+
13+
- **Subtest Failure Handling**: When a subtest fails, the parent test will not be marked as failed unless the parent itself has a direct failure
14+
- **Important Note**: If a parent test has both log messages (`t.Log()`) and failing subtests, it will still be marked as failed, as seen in the `TestLogMessagesNotDirectFailures` test
15+
16+
## Usage
17+
18+
```bash
19+
go test -json ./... | go-test-transform -ignore-all
20+
```
21+
22+
This will transform the test output to prevent parent tests from failing when only their subtests fail.
23+
24+
## Options
25+
26+
- `-ignore-all`: Ignore all subtest failures
27+
- `-input`: Input file (default: stdin)
28+
- `-output`: Output file (default: stdout)
29+
30+
## Example
31+
32+
For a test structure like:
33+
34+
```go
35+
func TestParent(t *testing.T) {
36+
t.Run("Subtest1", func(t *testing.T) {
37+
t.Error("Subtest 1 failed")
38+
})
39+
}
40+
```
41+
42+
The transformed output will be:
43+
44+
```json
45+
{"Time":"2023-05-10T15:04:05.123Z","Action":"run","Package":"example/pkg","Test":"TestParent"}
46+
{"Time":"2023-05-10T15:04:05.124Z","Action":"run","Package":"example/pkg","Test":"TestParent/Subtest1"}
47+
{"Time":"2023-05-10T15:04:05.125Z","Action":"output","Package":"example/pkg","Test":"TestParent/Subtest1","Output":" subtest1_test.go:12: Subtest 1 failed\n"}
48+
{"Time":"2023-05-10T15:04:05.126Z","Action":"fail","Package":"example/pkg","Test":"TestParent/Subtest1","Elapsed":0.001}
49+
{"Time":"2023-05-10T15:04:05.127Z","Action":"pass","Package":"example/pkg","Test":"TestParent","Elapsed":0.004}
50+
{"Time":"2023-05-10T15:04:05.128Z","Action":"output","Package":"example/pkg","Output":"FAIL\texample/pkg\t0.004s\n"}
51+
{"Time":"2023-05-10T15:04:05.129Z","Action":"fail","Package":"example/pkg","Elapsed":0.005}
52+
```
53+
54+
Note that in the original output, both `TestParent/Subtest1` and `TestParent` would be marked as failed. After transformation, `TestParent/Subtest1` remains failed, but `TestParent` is changed to pass since it doesn't have a direct failure.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"io"
7+
"os"
8+
9+
"github.com/smartcontractkit/chainlink-testing-framework/tools/flakeguard/go-test-transform/pkg/transformer"
10+
)
11+
12+
func main() {
13+
// Parse command-line flags
14+
var (
15+
ignoreAll bool
16+
inputFile string
17+
outputFile string
18+
)
19+
20+
flag.BoolVar(&ignoreAll, "ignore-all", false, "Ignore all subtest failures")
21+
flag.StringVar(&inputFile, "input", "", "Input JSON file (if not provided, reads from stdin)")
22+
flag.StringVar(&outputFile, "output", "", "File to write the report to (default: stdout)")
23+
flag.Parse()
24+
25+
// Set up options
26+
opts := transformer.NewOptions(ignoreAll)
27+
28+
// Determine input source
29+
var input io.Reader
30+
if inputFile != "" {
31+
// Read from file
32+
file, err := os.Open(inputFile)
33+
if err != nil {
34+
fmt.Fprintf(os.Stderr, "Error opening input file: %v\n", err)
35+
os.Exit(1)
36+
}
37+
defer file.Close()
38+
input = file
39+
} else {
40+
// Read from stdin
41+
input = os.Stdin
42+
}
43+
44+
// Determine output destination
45+
var output io.Writer
46+
if outputFile != "" {
47+
// Write to file
48+
file, err := os.Create(outputFile)
49+
if err != nil {
50+
fmt.Fprintf(os.Stderr, "Error creating output file: %v\n", err)
51+
os.Exit(1)
52+
}
53+
defer file.Close()
54+
output = file
55+
} else {
56+
// Write to stdout
57+
output = os.Stdout
58+
}
59+
60+
// Transform the output
61+
err := transformer.TransformJSON(input, output, opts)
62+
if err != nil {
63+
fmt.Fprintf(os.Stderr, "Error transforming JSON: %v\n", err)
64+
os.Exit(1)
65+
}
66+
67+
// Exit with the appropriate code
68+
os.Exit(0)
69+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package transformer
2+
3+
// Options defines configuration options for the test transformer
4+
type Options struct {
5+
// IgnoreAllSubtestFailures determines if all subtest failures should be ignored
6+
IgnoreAllSubtestFailures bool
7+
}
8+
9+
// DefaultOptions returns a new Options with default values
10+
func DefaultOptions() *Options {
11+
return &Options{
12+
IgnoreAllSubtestFailures: false,
13+
}
14+
}
15+
16+
// NewOptions creates a new Options with the specified parameters
17+
func NewOptions(ignoreAll bool) *Options {
18+
return &Options{
19+
IgnoreAllSubtestFailures: ignoreAll,
20+
}
21+
}

0 commit comments

Comments
 (0)