diff --git a/.gitignore b/.gitignore index f16f4cb..e065e0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -junit2jira +/junit2jira +/flakechecker .idea # Binaries for programs and plugins *.exe diff --git a/README.md b/README.md index e27e745..352bbd7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Convert test failures to jira issues ### Build ```shell -go build ./... +go build -o . ./... ``` ### Test diff --git a/cmd/flakechecker/bq_client.go b/cmd/flakechecker/bq_client.go new file mode 100644 index 0000000..1e25d19 --- /dev/null +++ b/cmd/flakechecker/bq_client.go @@ -0,0 +1,71 @@ +package main + +import ( + "cloud.google.com/go/bigquery" + "context" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "google.golang.org/api/iterator" + "regexp" + "time" +) + +const projectID = "acs-san-stackroxci" +const queryTimeout = 1 * time.Minute + +type biqQueryClient interface { + GetRatioForTest(flakeTestConfig *flakeCheckerRecord, testName string) (int, int, error) +} + +type biqQueryClientImpl struct { + client *bigquery.Client +} + +func getNewBigQueryClient() (biqQueryClient, error) { + ctx := context.Background() + + client, err := bigquery.NewClient(ctx, projectID) + if err != nil { + return nil, errors.Wrap(err, "creating BigQuery client") + } + + return &biqQueryClientImpl{client: client}, nil +} + +func getFilteredTestName(name string) string { + // Apply the same filtering used in DB to generate test names not influenced with version Z number. + regFilterTestName := regexp.MustCompile("(.*)((-v|: )\\d{3}\\.\\d+\\.)(\\d+)(.*)") + return regFilterTestName.ReplaceAllString(name, `${1}${2}z${5}`) +} + +func (c *biqQueryClientImpl) GetRatioForTest(flakeTestConfig *flakeCheckerRecord, testName string) (int, int, error) { + query := c.client.Query( + `SELECT JobName, FilteredName, Classname, TotalAll, FailRatio + FROM acs-san-stackroxci.ci_metrics.stackrox_tests_recent_flaky_tests + WHERE JobName = @jobName AND FilteredName = @filteredName AND Classname = @classname +`) + + query.Parameters = []bigquery.QueryParameter{ + {Name: "jobName", Value: flakeTestConfig.RatioJobName}, + {Name: "filteredName", Value: getFilteredTestName(testName)}, + {Name: "classname", Value: flakeTestConfig.Classname}, + } + + ctx, _ := context.WithTimeout(context.Background(), queryTimeout) + resIter, err := query.Read(ctx) + if err != nil { + return 0, 0, errors.Wrap(err, "query data from BigQuery") + } + + // We need only first record. No need to loop over iterator. + var record recentFlakyTestsRecord + if errNext := resIter.Next(&record); errNext != nil { + return 0, 0, errors.Wrap(errNext, "read BigQuery record") + } + + if errNext := resIter.Next(&record); !errors.Is(errNext, iterator.Done) { + log.Warnf("Expected to find one row in DB, but got more for query params: %s", query.Parameters) + } + + return record.TotalAll, record.FailRatio, nil +} diff --git a/cmd/flakechecker/flake_config.go b/cmd/flakechecker/flake_config.go new file mode 100644 index 0000000..908471b --- /dev/null +++ b/cmd/flakechecker/flake_config.go @@ -0,0 +1,101 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/pkg/errors" + "io" + "os" + "regexp" +) + +// flakeCheckerRecord represents configuration record used by flakechecker to evaluate failed tests +// +// It contains the following fields: +// match_job_name - name of the job that should be evaluated by flakechecker. i.e. (branch should be evaluated, but main not) +// ratio_job_name - job name that should be used for ratio calculation. i.e. we take main branch test runs as base for evaluation of flake ratio +// test_name_regex - regex used to match test names. Some test names contain detailed information (i.e. version 4.4.4), but we want to use ratio for all tests in that group (i.e. 4.4.z). Using regex allow us to group tests differently. +// classname - class name of the test that should be isolated. With this option we can isolate single flake test from suite and isolate only that one from the rest. +// ratio_threshold - failure percentage that is allowed for this test. This information is usually fetched from historical executions and data collected in DB. +// +// This record also contains helper fields where we keep compiled regex. +type flakeCheckerRecord struct { + MatchJobName string `json:"match_job_name"` + RatioJobName string `json:"ratio_job_name"` + TestNameRegex string `json:"test_name_regex"` + Classname string `json:"classname"` + RatioThreshold int `json:"ratio_threshold"` + + regexMatchJobName *regexp.Regexp + regexTestNameRegex *regexp.Regexp +} + +func (r *flakeCheckerRecord) updateRegex() error { + validRegex, err := regexp.Compile(fmt.Sprintf("^%s$", r.MatchJobName)) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid flake config match job regex: %v", r)) + } + r.regexMatchJobName = validRegex + + validRegex, err = regexp.Compile(fmt.Sprintf("^%s$", r.TestNameRegex)) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid flake config test name regex: %v", r)) + } + r.regexTestNameRegex = validRegex + + return nil +} + +func (r *flakeCheckerRecord) matchJobName(jobName string) (bool, error) { + if r.regexMatchJobName == nil { + err := r.updateRegex() + if err != nil { + return false, err + } + } + + return r.regexMatchJobName.MatchString(jobName), nil +} + +func (r *flakeCheckerRecord) matchTestName(testName string) (bool, error) { + if r.regexTestNameRegex == nil { + err := r.updateRegex() + if err != nil { + return false, err + } + } + + return r.regexTestNameRegex.MatchString(testName), nil +} + +func (r *flakeCheckerRecord) matchClassname(classname string) (bool, error) { + return classname == r.Classname, nil +} + +func loadFlakeConfigFile(fileName string) ([]*flakeCheckerRecord, error) { + jsonConfigFile, err := os.Open(fileName) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("open flake config file: %s", fileName)) + } + defer jsonConfigFile.Close() + + jsonConfigFileData, err := io.ReadAll(jsonConfigFile) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("read flake config file: %s", fileName)) + } + + flakeConfigs := make([]*flakeCheckerRecord, 0) + err = json.Unmarshal(jsonConfigFileData, &flakeConfigs) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("parse flake config file: %s", fileName)) + } + + // Validate regex-es in config and prepare them. + for _, flakeConfig := range flakeConfigs { + if err := flakeConfig.updateRegex(); err != nil { + return nil, err + } + } + + return flakeConfigs, nil +} diff --git a/cmd/flakechecker/flake_config_test.go b/cmd/flakechecker/flake_config_test.go new file mode 100644 index 0000000..422b2d9 --- /dev/null +++ b/cmd/flakechecker/flake_config_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "github.com/stretchr/testify/assert" + "regexp" + "testing" +) + +func TestLoadFlakeConfigFile(t *testing.T) { + samples := []struct { + name string + fileName string + + expectError bool + expectConfig []*flakeCheckerRecord + }{ + { + name: "no config file", + fileName: "no_config.json", + expectError: true, + expectConfig: nil, + }, + { + name: "valid config file", + fileName: "testdata/flake-config.json", + expectError: false, + expectConfig: []*flakeCheckerRecord{ + { + MatchJobName: "pr-.*", + RatioJobName: "main-branch-tests", + TestNameRegex: "TestLoadFlakeConf.*", + Classname: "TestLoadFlakeConfigFile", + RatioThreshold: 5, + regexMatchJobName: regexp.MustCompile("^pr-.*$"), + regexTestNameRegex: regexp.MustCompile("^TestLoadFlakeConf.*$"), + }, + { + MatchJobName: "pull-request-tests", + RatioJobName: "main-branch-tests", + TestNameRegex: "TestLoadFlakeConfigFile", + Classname: "TestLoadFlakeConfigFile", + RatioThreshold: 10, + regexMatchJobName: regexp.MustCompile("^pull-request-tests$"), + regexTestNameRegex: regexp.MustCompile("^TestLoadFlakeConfigFile$"), + }, + }, + }, + } + + for _, sample := range samples { + t.Run(sample.name, func(tt *testing.T) { + config, err := loadFlakeConfigFile(sample.fileName) + + assert.Equal(tt, sample.expectError, err != nil) + assert.Equal(tt, sample.expectConfig, config) + }) + } +} diff --git a/cmd/flakechecker/main.go b/cmd/flakechecker/main.go new file mode 100644 index 0000000..05afe48 --- /dev/null +++ b/cmd/flakechecker/main.go @@ -0,0 +1,154 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "github.com/carlmjohnson/versioninfo" + junit "github.com/joshdk/go-junit" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/stackrox/junit2jira/pkg/testcase" + "os" +) + +const totalRunsLimit = 30 + +type flakeCheckerParams struct { + junitReportsDir string + configFile string + + jobName string + orchestrator string + + dryRun bool +} + +func main() { + var debug bool + var err error + + p := flakeCheckerParams{} + flag.StringVar(&p.junitReportsDir, "junit-reports-dir", os.Getenv("ARTIFACT_DIR"), "Dir that contains jUnit reports XML files") + flag.StringVar(&p.configFile, "config-file", "", "Config file with defined failure ratios") + + flag.StringVar(&p.jobName, "job-name", "", "Name of CI job.") + flag.StringVar(&p.orchestrator, "orchestrator", "", "orchestrator name (such as GKE or OpenShift), if any.") + + flag.BoolVar(&p.dryRun, "dry-run", false, "When set to true issues will NOT be created.") + flag.BoolVar(&debug, "debug", false, "Enable debug log level") + versioninfo.AddFlag(flag.CommandLine) + flag.Parse() + + if debug { + log.SetLevel(log.DebugLevel) + } + + err = p.run() + if err != nil { + log.Fatal(err) + } +} + +type recentFlakyTestsRecord struct { + JobName string + FilteredName string + Classname string + TotalAll int + FailRatio int +} + +func (p *flakeCheckerParams) checkFailedTests(bqClient biqQueryClient, failedTests []testcase.TestCase, flakeConfigs []*flakeCheckerRecord) error { + for _, failedTest := range failedTests { + found := false + log.Infof("Checking failed test: %q / %q / %q", p.jobName, failedTest.Name, failedTest.Classname) + for _, flakeConfig := range flakeConfigs { + match, err := flakeConfig.matchJobName(p.jobName) + if err != nil { + return err + } + + if !match { + continue + } + + match, err = flakeConfig.matchTestName(failedTest.Name) + if err != nil { + return err + } + + if !match { + continue + } + + match, err = flakeConfig.matchClassname(failedTest.Classname) + if err != nil { + return err + } + + if !match { + continue + } + + found = true + log.Infof("Match found: %q / %q / %q", flakeConfig.MatchJobName, flakeConfig.TestNameRegex, flakeConfig.Classname) + totalRuns, failRatio, err := bqClient.GetRatioForTest(flakeConfig, failedTest.Name) + if err != nil { + return errors.New(fmt.Sprintf("Get ratio for test failed: %s", err)) + } + + if totalRuns < totalRunsLimit { + return errors.New(fmt.Sprintf("Total runs for test is under history count threshold: %d", totalRuns)) + } + + if failRatio > flakeConfig.RatioThreshold { + return errors.New(fmt.Sprintf("Allowed flake ratio for test is above threshold: (%d > %d)", failRatio, flakeConfig.RatioThreshold)) + } + + log.Infof("Ratio is below threshold: (%d <= %d)", failRatio, flakeConfig.RatioThreshold) + } + + if !found { + return errors.New(fmt.Sprintf("There is no match in allowed flake tests for: %s", failedTest.Name)) + } + } + + return nil +} + +func (p *flakeCheckerParams) run() error { + testSuites, err := junit.IngestDir(p.junitReportsDir) + if err != nil { + log.Fatalf("could not read files: %s", err) + } + + failedTests, err := testcase.GetFailedTests(testSuites) + if err != nil { + return errors.Wrap(err, "could not find failed tests") + } + + if len(failedTests) == 0 { + log.Info("No failed tests to process") + return nil + } + + log.Infof("Found %d failed tests", len(failedTests)) + + flakeConfigs, err := loadFlakeConfigFile(p.configFile) + if err != nil { + log.Fatalf("unable to load config file (%s): %s", p.configFile, err) + } + + bqClient, err := getNewBigQueryClient() + if err != nil { + log.Fatalf("unable to create BigQuery client: %s", err) + } + + if err = p.checkFailedTests(bqClient, failedTests, flakeConfigs); err != nil { + log.Fatal(err) + } + + log.Info("All failed tests are within allowed flake thresholds") + + return nil +} diff --git a/cmd/flakechecker/main_test.go b/cmd/flakechecker/main_test.go new file mode 100644 index 0000000..fa06ac8 --- /dev/null +++ b/cmd/flakechecker/main_test.go @@ -0,0 +1,159 @@ +package main + +import ( + "github.com/pkg/errors" + "github.com/stackrox/junit2jira/pkg/testcase" + "github.com/stretchr/testify/assert" + "testing" +) + +type biqQueryClientMock struct { + getRatioForTest func(flakeTestConfig *flakeCheckerRecord, testName string) (int, int, error) +} + +func (c *biqQueryClientMock) GetRatioForTest(flakeTestConfig *flakeCheckerRecord, testName string) (int, int, error) { + if c.getRatioForTest != nil { + return c.getRatioForTest(flakeTestConfig, testName) + } + + // By default, fail. In most cases, we will not reach this part in tests. + return 0, 0, errors.New("fail") +} + +func getRatioForTestNoFailures(_ *flakeCheckerRecord, _ string) (int, int, error) { + return totalRunsLimit, 0, nil +} + +func getRatioForTestAllFailures(_ *flakeCheckerRecord, _ string) (int, int, error) { + return totalRunsLimit, 50, nil +} + +func TestCheckFailedTests(t *testing.T) { + p := flakeCheckerParams{jobName: "test-job"} + + samples := map[string]struct { + bqClient biqQueryClient + failedTests []testcase.TestCase + flakeConfigs []*flakeCheckerRecord + expectError bool + }{ + "no failed tests": { + bqClient: &biqQueryClientMock{}, + failedTests: []testcase.TestCase{}, + flakeConfigs: []*flakeCheckerRecord{ + {MatchJobName: "test-job1-", TestNameRegex: "test name"}, + }, + expectError: false, + }, + "no config match - job name": { + bqClient: &biqQueryClientMock{ + getRatioForTest: getRatioForTestNoFailures, + }, + failedTests: []testcase.TestCase{{Name: "test", Classname: "class"}}, + flakeConfigs: []*flakeCheckerRecord{ + {MatchJobName: "test-job-1", TestNameRegex: "test", Classname: "class"}, + }, + expectError: true, + }, + "no config match - test name": { + bqClient: &biqQueryClientMock{ + getRatioForTest: getRatioForTestNoFailures, + }, + failedTests: []testcase.TestCase{{Name: "test", Classname: "class"}}, + flakeConfigs: []*flakeCheckerRecord{ + {MatchJobName: "test-job", TestNameRegex: "wrong-test", Classname: "class"}, + }, + expectError: true, + }, + "no config match - classname": { + bqClient: &biqQueryClientMock{ + getRatioForTest: getRatioForTestNoFailures, + }, + failedTests: []testcase.TestCase{{Name: "test", Classname: "class"}}, + flakeConfigs: []*flakeCheckerRecord{ + {MatchJobName: "test-job", TestNameRegex: "test", Classname: "wrong-class"}, + }, + expectError: true, + }, + "unable to fetch ratio": { + bqClient: &biqQueryClientMock{ + getRatioForTest: func(_ *flakeCheckerRecord, _ string) (int, int, error) { + return 0, 0, errors.New("fail") + }, + }, + failedTests: []testcase.TestCase{{Name: "test", Classname: "class"}}, + flakeConfigs: []*flakeCheckerRecord{ + {MatchJobName: "test-job", TestNameRegex: "test", Classname: "class", RatioThreshold: 1}, + }, + expectError: true, + }, + "total runs below limit": { + bqClient: &biqQueryClientMock{ + getRatioForTest: func(_ *flakeCheckerRecord, _ string) (int, int, error) { + return totalRunsLimit - 1, 0, nil + }, + }, + failedTests: []testcase.TestCase{{Name: "test", Classname: "class"}}, + flakeConfigs: []*flakeCheckerRecord{ + {MatchJobName: "test-job", TestNameRegex: "test", Classname: "class", RatioThreshold: 1}, + }, + expectError: true, + }, + "fail ratio below threshold": { + bqClient: &biqQueryClientMock{ + getRatioForTest: getRatioForTestNoFailures, + }, + failedTests: []testcase.TestCase{{Name: "test", Classname: "class"}}, + flakeConfigs: []*flakeCheckerRecord{ + {MatchJobName: "test-job", TestNameRegex: "test", Classname: "class", RatioThreshold: 1}, + }, + expectError: false, + }, + "fail ratio above threshold": { + bqClient: &biqQueryClientMock{ + getRatioForTest: getRatioForTestAllFailures, + }, + failedTests: []testcase.TestCase{{Name: "test", Classname: "class"}}, + flakeConfigs: []*flakeCheckerRecord{ + {MatchJobName: "test-job", TestNameRegex: "test", Classname: "class", RatioThreshold: 1}, + }, + expectError: true, + }, + "fail ratio below threshold - multiple tests": { + bqClient: &biqQueryClientMock{ + getRatioForTest: getRatioForTestNoFailures, + }, + failedTests: []testcase.TestCase{ + {Name: "test", Classname: "class"}, + {Name: "test-1", Classname: "class-1"}, + }, + flakeConfigs: []*flakeCheckerRecord{ + {MatchJobName: "test-job", TestNameRegex: "test", Classname: "class", RatioThreshold: 1}, + {MatchJobName: "test-job", TestNameRegex: "test-1", Classname: "class-1", RatioThreshold: 1}, + }, + expectError: false, + }, + "fail ratio above threshold - multiple tests": { + bqClient: &biqQueryClientMock{ + getRatioForTest: getRatioForTestAllFailures, + }, + failedTests: []testcase.TestCase{ + {Name: "test-ratio-below", Classname: "class"}, + {Name: "test-ratio-above", Classname: "class"}, + }, + flakeConfigs: []*flakeCheckerRecord{ + {MatchJobName: "test-job", TestNameRegex: "test-ratio-below", Classname: "class", RatioThreshold: 90}, + {MatchJobName: "test-job", TestNameRegex: "test-ratio-above", Classname: "class", RatioThreshold: 10}, + }, + expectError: true, + }, + } + + for sampleName, sample := range samples { + t.Run(sampleName, func(tt *testing.T) { + err := p.checkFailedTests(sample.bqClient, sample.failedTests, sample.flakeConfigs) + + assert.Equal(tt, sample.expectError, err != nil) + }) + } +} diff --git a/cmd/flakechecker/testdata/flake-config.json b/cmd/flakechecker/testdata/flake-config.json new file mode 100644 index 0000000..14f814e --- /dev/null +++ b/cmd/flakechecker/testdata/flake-config.json @@ -0,0 +1,18 @@ +[ + { + "_comment": "Config with regex", + "match_job_name": "pr-.*", + "ratio_job_name": "main-branch-tests", + "test_name_regex": "TestLoadFlakeConf.*", + "classname": "TestLoadFlakeConfigFile", + "ratio_threshold": 5 + }, + { + "_comment": "Config with flat values", + "match_job_name": "pull-request-tests", + "ratio_job_name": "main-branch-tests", + "test_name_regex": "TestLoadFlakeConfigFile", + "classname": "TestLoadFlakeConfigFile", + "ratio_threshold": 10 + } +] diff --git a/htmlOutput.html.tpl b/cmd/junit2jira/htmlOutput.html.tpl similarity index 100% rename from htmlOutput.html.tpl rename to cmd/junit2jira/htmlOutput.html.tpl diff --git a/main.go b/cmd/junit2jira/main.go similarity index 84% rename from main.go rename to cmd/junit2jira/main.go index 00ca526..241e72c 100644 --- a/main.go +++ b/cmd/junit2jira/main.go @@ -7,6 +7,7 @@ import ( "encoding/json" "flag" "fmt" + "github.com/stackrox/junit2jira/pkg/testcase" "html/template" "io" "net/http" @@ -57,7 +58,7 @@ func main() { flag.StringVar(&p.BuildLink, "build-link", "", "Link to build job.") flag.StringVar(&p.BuildTag, "build-tag", "", "Built tag or revision.") flag.StringVar(&p.JobName, "job-name", "", "Name of CI job.") - flag.StringVar(&p.Orchestrator, "orchestrator", "", "Orchestrator name (such as GKE or OpenShift), if any.") + flag.StringVar(&p.Orchestrator, "orchestrator", "", "orchestrator name (such as GKE or OpenShift), if any.") flag.BoolVar(&debug, "debug", false, "Enable debug log level") versioninfo.AddFlag(flag.CommandLine) flag.Parse() @@ -87,7 +88,7 @@ type junit2jira struct { type testIssue struct { issue *jira.Issue newJIRA bool - testCase testCase + testCase j2jTestCase } func run(p params) error { @@ -118,9 +119,9 @@ func run(p params) error { log.Fatalf("could not create CSV: %s", err) } - failedTests, err := j.findFailedTests(testSuites) + failedTests, err := j.getMergedFailedTests(testSuites) if err != nil { - return errors.Wrap(err, "could not find failed tests") + return errors.Wrap(err, "could not create issues or comments") } issues, err := j.createIssuesOrComments(failedTests) @@ -151,6 +152,28 @@ func run(p params) error { return errors.Wrap(j.createHtml(jiraIssues), "could not create HTML report") } +func (j junit2jira) getMergedFailedTests(testSuites []junit.Suite) ([]j2jTestCase, error) { + failedTests, err := testcase.GetFailedTests(testSuites) + if err != nil { + return nil, errors.Wrap(err, "could not get failed tests") + } + log.Infof("Found %d failed tests", len(failedTests)) + + failedJ2jTests := make([]j2jTestCase, 0, len(failedTests)) + for _, failedTest := range failedTests { + failedJ2jTests = append(failedJ2jTests, newJ2jTestCase(failedTest, j.params)) + } + + if len(failedTests) > j.threshold && j.threshold > 0 { + failedJ2jTests, err = j.mergeFailedTests(failedJ2jTests) + if err != nil { + return nil, errors.Wrap(err, "could not merge failed tests") + } + } + + return failedJ2jTests, nil +} + //go:embed htmlOutput.html.tpl var htmlOutputTemplate string @@ -235,7 +258,7 @@ func (j junit2jira) createCsv(testSuites []junit.Suite) error { return junit2csv(testSuites, j.params, out) } -func (j junit2jira) createIssuesOrComments(failedTests []testCase) ([]*testIssue, error) { +func (j junit2jira) createIssuesOrComments(failedTests []j2jTestCase) ([]*testIssue, error) { var result error issues := make([]*testIssue, 0, len(failedTests)) for _, tc := range failedTests { @@ -271,7 +294,7 @@ func (j junit2jira) linkIssues(issues []*jira.Issue) error { return result } -func (j junit2jira) createIssueOrComment(tc testCase) (*testIssue, error) { +func (j junit2jira) createIssueOrComment(tc j2jTestCase) (*testIssue, error) { summary, err := tc.summary() if err != nil { return nil, fmt.Errorf("could not get summary: %w", err) @@ -473,34 +496,7 @@ func testSuiteToCSV(ts junit.Suite, p params, w *csv.Writer) error { return nil } -func (j junit2jira) findFailedTests(testSuites []junit.Suite) ([]testCase, error) { - failedTests := make([]testCase, 0) - for _, ts := range testSuites { - failedTests = j.addFailedTests(ts, failedTests) - } - log.Infof("Found %d failed tests", len(failedTests)) - - if len(failedTests) > j.threshold && j.threshold > 0 { - return j.mergeFailedTests(failedTests) - } - - return failedTests, nil -} - -func (j junit2jira) addFailedTests(ts junit.Suite, failedTests []testCase) []testCase { - for _, suite := range ts.Suites { - failedTests = j.addFailedTests(suite, failedTests) - } - for _, tc := range ts.Tests { - if tc.Error == nil { - continue - } - failedTests = j.addTest(failedTests, tc) - } - return failedTests -} - -func (j junit2jira) mergeFailedTests(failedTests []testCase) ([]testCase, error) { +func (j junit2jira) mergeFailedTests(failedTests []j2jTestCase) ([]j2jTestCase, error) { log.Warning("Too many failed tests, reporting them as a one failure.") msg := "" suite := failedTests[0].Suite @@ -515,42 +511,15 @@ func (j junit2jira) mergeFailedTests(failedTests []testCase) ([]testCase, error) } msg += summary + "\n" } - tc := NewTestCase(junit.Test{ - Message: msg, - Classname: suite, - }, j.params) - return []testCase{tc}, nil -} - -func (j junit2jira) addTest(failedTests []testCase, tc junit.Test) []testCase { - if !isSubTest(tc) { - return append(failedTests, NewTestCase(tc, j.params)) - } - return j.addSubTestToFailedTest(tc, failedTests) -} -func isSubTest(tc junit.Test) bool { - return strings.Contains(tc.Name, "/") -} - -func (j junit2jira) addSubTestToFailedTest(subTest junit.Test, failedTests []testCase) []testCase { - // As long as the separator is not empty, split will always return a slice of length 1. - name := strings.Split(subTest.Name, "/")[0] - for i, failedTest := range failedTests { - // Only consider a failed test a "parent" of the test if the name matches _and_ the class name is the same. - if isGoTest(subTest.Classname) && failedTest.Name == name && failedTest.Suite == subTest.Classname { - failedTest.addSubTest(subTest) - failedTests[i] = failedTest - return failedTests - } - } - // In case we found no matches, we will default to add the subtest plain. - return append(failedTests, NewTestCase(subTest, j.params)) -} + tc := newJ2jTestCase( + testcase.NewTestCase( + junit.Test{ + Message: msg, + Classname: suite, + }), j.params) -// isGoTest will verify that the corresponding classname refers to a go package by expecting the go module name as prefix. -func isGoTest(className string) bool { - return strings.HasPrefix(className, "github.com/stackrox/rox") + return []j2jTestCase{tc}, nil } const ( @@ -585,13 +554,15 @@ const ( summaryTpl = `{{ (print .Suite " / " .Name) | truncateSummary }} FAILED` ) -type testCase struct { - Name string - Suite string - Message string - Stdout string - Stderr string - Error string +type j2jTestCase struct { + Name string + Suite string + Message string + Stdout string + Stderr string + Error string + + // Additional fields for junit2jira BuildId string JobName string Orchestrator string @@ -620,13 +591,14 @@ type params struct { summaryOutput string } -func NewTestCase(tc junit.Test, p params) testCase { - c := testCase{ - Name: tc.Name, - Message: tc.Message, - Stdout: tc.SystemOut, - Stderr: tc.SystemErr, - Suite: tc.Classname, +func newJ2jTestCase(testCase testcase.TestCase, p params) j2jTestCase { + return j2jTestCase{ + Name: testCase.Name, + Suite: testCase.Suite, + Message: testCase.Message, + Stdout: testCase.Stdout, + Stderr: testCase.Stderr, + Error: testCase.Error, BuildId: p.BuildId, JobName: p.JobName, Orchestrator: p.Orchestrator, @@ -634,18 +606,13 @@ func NewTestCase(tc junit.Test, p params) testCase { BaseLink: p.BaseLink, BuildLink: p.BuildLink, } - - if tc.Error != nil { - c.Error = tc.Error.Error() - } - return c } -func (tc *testCase) description() (string, error) { +func (tc *j2jTestCase) description() (string, error) { return render(*tc, desc) } -func (tc testCase) summary() (string, error) { +func (tc j2jTestCase) summary() (string, error) { s, err := render(tc, summaryTpl) if err != nil { return "", err @@ -653,24 +620,7 @@ func (tc testCase) summary() (string, error) { return clearString(s), nil } -const subTestFormat = "\nSub test %s: %s" - -func (tc *testCase) addSubTest(subTest junit.Test) { - if subTest.Message != "" { - tc.Message += fmt.Sprintf(subTestFormat, subTest.Name, subTest.Message) - } - if subTest.SystemOut != "" { - tc.Stdout += fmt.Sprintf(subTestFormat, subTest.Name, subTest.SystemOut) - } - if subTest.SystemErr != "" { - tc.Stderr += fmt.Sprintf(subTestFormat, subTest.Name, subTest.SystemErr) - } - if subTest.Error != nil { - tc.Error += fmt.Sprintf(subTestFormat, subTest.Name, subTest.Error.Error()) - } -} - -func render(tc testCase, text string) (string, error) { +func render(tc j2jTestCase, text string) (string, error) { tmpl, err := template.New("test").Funcs(map[string]any{"truncate": truncate, "truncateSummary": truncateSummary}).Parse(text) if err != nil { return "", err @@ -768,7 +718,7 @@ func convertJunitToSlack(issues ...*testIssue) []slack.Attachment { return attachments } -func failureToAttachment(title string, tc testCase) (slack.Attachment, error) { +func failureToAttachment(title string, tc j2jTestCase) (slack.Attachment, error) { failureMessage := tc.Message failureValue := tc.Error diff --git a/main_test.go b/cmd/junit2jira/main_test.go similarity index 98% rename from main_test.go rename to cmd/junit2jira/main_test.go index bfb6c06..52da7ae 100644 --- a/main_test.go +++ b/cmd/junit2jira/main_test.go @@ -24,9 +24,9 @@ func TestParseJunitReport(t *testing.T) { } testsSuites, err := junit.IngestDir(j.junitReportsDir) assert.NoError(t, err) - tests, err := j.findFailedTests(testsSuites) + tests, err := j.getMergedFailedTests(testsSuites) assert.NoError(t, err) - assert.Equal(t, []testCase{ + assert.Equal(t, []j2jTestCase{ { Name: "TestDifferentBaseTypes", Suite: "github.com/stackrox/rox/pkg/booleanpolicy/evaluator", @@ -49,9 +49,9 @@ func TestParseJunitReport(t *testing.T) { } testsSuites, err := junit.IngestDir(j.junitReportsDir) assert.NoError(t, err) - tests, err := j.findFailedTests(testsSuites) + tests, err := j.getMergedFailedTests(testsSuites) assert.NoError(t, err) - assert.Equal(t, []testCase{ + assert.Equal(t, []j2jTestCase{ { Message: `github.com/stackrox/rox/pkg/booleanpolicy/evaluator / TestDifferentBaseTypes FAILED github.com/stackrox/rox/sensor/kubernetes/localscanner / TestLocalScannerTLSIssuerIntegrationTests FAILED @@ -67,12 +67,12 @@ github.com/stackrox/rox/sensor/kubernetes/localscanner / TestLocalScannerTLSIssu } testsSuites, err := junit.IngestDir(j.junitReportsDir) assert.NoError(t, err) - tests, err := j.findFailedTests(testsSuites) + tests, err := j.getMergedFailedTests(testsSuites) assert.NoError(t, err) assert.ElementsMatch( t, - []testCase{ + []j2jTestCase{ { Message: `DefaultPoliciesTest / Verify policy Apache Struts CVE-2017-5638 is triggered FAILED central-basic / step 90-activate-scanner-v4 FAILED @@ -95,12 +95,12 @@ command-line-arguments / TestTimeout FAILED } testsSuites, err := junit.IngestDir(j.junitReportsDir) assert.NoError(t, err) - tests, err := j.findFailedTests(testsSuites) + tests, err := j.getMergedFailedTests(testsSuites) assert.NoError(t, err) assert.ElementsMatch( t, - []testCase{ + []j2jTestCase{ { Name: "Verify policy Apache Struts: CVE-2017-5638 is triggered", Message: "Condition not satisfied:\n" + @@ -180,12 +180,12 @@ command-line-arguments / TestTimeout FAILED } testsSuites, err := junit.IngestDir(j.junitReportsDir) assert.NoError(t, err) - tests, err := j.findFailedTests(testsSuites) + tests, err := j.getMergedFailedTests(testsSuites) assert.NoError(t, err) assert.Equal( t, - []testCase{{ + []j2jTestCase{{ Name: "Verify policy Apache Struts: CVE-2017-5638 is triggered", Message: "Condition not satisfied:\n" + "\n" + @@ -226,7 +226,7 @@ command-line-arguments / TestTimeout FAILED } func TestDescription(t *testing.T) { - tc := testCase{ + tc := j2jTestCase{ Name: "Verify policy Apache Struts: CVE-2017-5638 is triggered", Message: "Condition not satisfied:\n" + "\n" + @@ -430,17 +430,17 @@ func TestSummaryNoFailures(t *testing.T) { { issue: &jira.Issue{Key: "ROX-1"}, newJIRA: false, - testCase: testCase{}, + testCase: j2jTestCase{}, }, { issue: &jira.Issue{Key: "ROX-2"}, newJIRA: true, - testCase: testCase{}, + testCase: j2jTestCase{}, }, { issue: &jira.Issue{Key: "ROX-3"}, newJIRA: true, - testCase: testCase{}, + testCase: j2jTestCase{}, }, } diff --git a/slack_test.go b/cmd/junit2jira/slack_test.go similarity index 97% rename from slack_test.go rename to cmd/junit2jira/slack_test.go index 1836457..54600c2 100644 --- a/slack_test.go +++ b/cmd/junit2jira/slack_test.go @@ -39,7 +39,7 @@ func TestConstructSlackMessage(t *testing.T) { testsSuites, err := junit.Ingest(sample) assert.NoError(t, err) - suites, err := j.findFailedTests(testsSuites) + suites, err := j.getMergedFailedTests(testsSuites) assert.NoError(t, err, "If this fails, it probably indicates a problem with the sample junit report rather than the code") assert.NotNil(t, suites, "If this fails, it probably indicates a problem with the sample junit report rather than the code") diff --git a/testdata/jira/TEST-DefaultPoliciesTest.xml b/cmd/junit2jira/testdata/jira/TEST-DefaultPoliciesTest.xml similarity index 100% rename from testdata/jira/TEST-DefaultPoliciesTest.xml rename to cmd/junit2jira/testdata/jira/TEST-DefaultPoliciesTest.xml diff --git a/testdata/jira/expected-html-output.html b/cmd/junit2jira/testdata/jira/expected-html-output.html similarity index 100% rename from testdata/jira/expected-html-output.html rename to cmd/junit2jira/testdata/jira/expected-html-output.html diff --git a/testdata/jira/kuttl-report.xml b/cmd/junit2jira/testdata/jira/kuttl-report.xml similarity index 100% rename from testdata/jira/kuttl-report.xml rename to cmd/junit2jira/testdata/jira/kuttl-report.xml diff --git a/testdata/jira/report.xml b/cmd/junit2jira/testdata/jira/report.xml similarity index 100% rename from testdata/jira/report.xml rename to cmd/junit2jira/testdata/jira/report.xml diff --git a/testdata/jira/report1.xml b/cmd/junit2jira/testdata/jira/report1.xml similarity index 100% rename from testdata/jira/report1.xml rename to cmd/junit2jira/testdata/jira/report1.xml diff --git a/testdata/jira/timeout.xml b/cmd/junit2jira/testdata/jira/timeout.xml similarity index 100% rename from testdata/jira/timeout.xml rename to cmd/junit2jira/testdata/jira/timeout.xml diff --git a/testdata/slack/combined-expected.json b/cmd/junit2jira/testdata/slack/combined-expected.json similarity index 100% rename from testdata/slack/combined-expected.json rename to cmd/junit2jira/testdata/slack/combined-expected.json diff --git a/testdata/slack/combined-sample.xml b/cmd/junit2jira/testdata/slack/combined-sample.xml similarity index 100% rename from testdata/slack/combined-sample.xml rename to cmd/junit2jira/testdata/slack/combined-sample.xml diff --git a/testdata/slack/message-expected.json b/cmd/junit2jira/testdata/slack/message-expected.json similarity index 100% rename from testdata/slack/message-expected.json rename to cmd/junit2jira/testdata/slack/message-expected.json diff --git a/testdata/slack/message-sample.xml b/cmd/junit2jira/testdata/slack/message-sample.xml similarity index 100% rename from testdata/slack/message-sample.xml rename to cmd/junit2jira/testdata/slack/message-sample.xml diff --git a/testdata/slack/value-expected.json b/cmd/junit2jira/testdata/slack/value-expected.json similarity index 100% rename from testdata/slack/value-expected.json rename to cmd/junit2jira/testdata/slack/value-expected.json diff --git a/testdata/slack/value-sample.xml b/cmd/junit2jira/testdata/slack/value-sample.xml similarity index 100% rename from testdata/slack/value-sample.xml rename to cmd/junit2jira/testdata/slack/value-sample.xml diff --git a/go.mod b/go.mod index ef4745d..fb44815 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ -module github.com/janisz/junit2jira +module github.com/stackrox/junit2jira -go 1.19 +go 1.21 require ( + cloud.google.com/go/bigquery v1.63.1 github.com/andygrunwald/go-jira v1.16.0 github.com/carlmjohnson/versioninfo v0.22.4 github.com/hashicorp/go-multierror v1.1.1 @@ -10,20 +11,62 @@ require ( github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.2 github.com/slack-go/slack v0.11.3 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.9.0 + google.golang.org/api v0.197.0 ) require ( + cloud.google.com/go v0.115.1 // indirect + cloud.google.com/go/auth v0.9.3 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect + cloud.google.com/go/compute/metadata v0.5.0 // indirect + cloud.google.com/go/iam v1.2.1 // indirect + github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/structs v1.1.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/golang-jwt/jwt/v4 v4.4.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/flatbuffers v23.5.26+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/kr/pretty v0.2.0 // indirect + github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/trivago/tgo v1.0.7 // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/time v0.6.0 // indirect + golang.org/x/tools v0.24.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.66.2 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3fae229..d66094e 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,96 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ= +cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= +cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U= +cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= +cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= +cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= +cloud.google.com/go/bigquery v1.63.1 h1:/6syiWrSpardKNxdvldS5CUTRJX1iIkSPXCjLjiGL+g= +cloud.google.com/go/bigquery v1.63.1/go.mod h1:ufaITfroCk17WTqBhMpi8CRjsfHjMX07pDrQaRKKX2o= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/datacatalog v1.22.1 h1:i0DyKb/o7j+0vgaFtimcRFjYsD6wFw1jpnODYUyiYRs= +cloud.google.com/go/datacatalog v1.22.1/go.mod h1:MscnJl9B2lpYlFoxRjicw19kFTwEke8ReKL5Y/6TWg8= +cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= +cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= +cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= +cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= +cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= +cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/andygrunwald/go-jira v1.16.0 h1:PU7C7Fkk5L96JvPc6vDVIrd99vdPnYudHu4ju2c2ikQ= github.com/andygrunwald/go-jira v1.16.0/go.mod h1:UQH4IBVxIYWbgagc0LF/k9FRs9xjIiQ8hIcC6HfLwFU= +github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= +github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/carlmjohnson/versioninfo v0.22.4 h1:AucUHDSKmk6j7Yx3dECGUxaowGHOAN0Zx5/EBtsXn4Y= github.com/carlmjohnson/versioninfo v0.22.4/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= +github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= @@ -25,35 +99,146 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/joshdk/go-junit v0.0.0-20210226021600-6145f504ca0d h1:lcSbmPJf3b19MTZtGDLI6Y2Jnk3VBDT8UG/8IVCEMxA= github.com/joshdk/go-junit v0.0.0-20210226021600-6145f504ca0d/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= +github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slack-go/slack v0.11.3 h1:GN7revxEMax4amCc3El9a+9SGnjmBvSUobs0QnO6ZO8= github.com/slack-go/slack v0.11.3/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM= github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= +gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= +google.golang.org/api v0.197.0 h1:x6CwqQLsFiA5JKAiGyGBjc2bNtHtLddhJCE2IKuhhcQ= +google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU= +google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= +google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -61,3 +246,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/testcase/testcase.go b/pkg/testcase/testcase.go new file mode 100644 index 0000000..007bb33 --- /dev/null +++ b/pkg/testcase/testcase.go @@ -0,0 +1,105 @@ +package testcase + +import ( + "fmt" + "github.com/joshdk/go-junit" + "strings" +) + +const subTestFormat = "\nSub test %s: %s" + +type TestCase struct { + Name string + Classname string + Suite string + Message string + Stdout string + Stderr string + Error string + IsSubtest bool +} + +func (tc *TestCase) addSubTest(subTest junit.Test) { + if subTest.Message != "" { + tc.Message += fmt.Sprintf(subTestFormat, subTest.Name, subTest.Message) + } + if subTest.SystemOut != "" { + tc.Stdout += fmt.Sprintf(subTestFormat, subTest.Name, subTest.SystemOut) + } + if subTest.SystemErr != "" { + tc.Stderr += fmt.Sprintf(subTestFormat, subTest.Name, subTest.SystemErr) + } + if subTest.Error != nil { + tc.Error += fmt.Sprintf(subTestFormat, subTest.Name, subTest.Error.Error()) + } +} + +func NewTestCase(tc junit.Test) TestCase { + c := TestCase{ + Name: tc.Name, + Classname: tc.Classname, + Message: tc.Message, + Stdout: tc.SystemOut, + Stderr: tc.SystemErr, + Suite: tc.Classname, + } + + if tc.Error != nil { + c.Error = tc.Error.Error() + } + + return c +} + +func isSubTest(tc junit.Test) bool { + return strings.Contains(tc.Name, "/") +} + +// isGoTest will verify that the corresponding classname refers to a go package by expecting the go module name as prefix. +func isGoTest(className string) bool { + return strings.HasPrefix(className, "github.com/stackrox/rox") +} + +func addSubTestToFailedTest(subTest junit.Test, failedTests []TestCase) []TestCase { + // As long as the separator is not empty, split will always return a slice of length 1. + name := strings.Split(subTest.Name, "/")[0] + for i, failedTest := range failedTests { + // Only consider a failed test a "parent" of the test if the name matches _and_ the class name is the same. + if isGoTest(subTest.Classname) && failedTest.Name == name && failedTest.Suite == subTest.Classname { + failedTest.addSubTest(subTest) + failedTests[i] = failedTest + return failedTests + } + } + // In case we found no matches, we will default to add the subtest plain. + return append(failedTests, NewTestCase(subTest)) +} + +func addTest(failedTests []TestCase, tc junit.Test) []TestCase { + if !isSubTest(tc) { + return append(failedTests, NewTestCase(tc)) + } + return addSubTestToFailedTest(tc, failedTests) +} + +func addFailedTests(ts junit.Suite, failedTests []TestCase) []TestCase { + for _, suite := range ts.Suites { + failedTests = addFailedTests(suite, failedTests) + } + for _, tc := range ts.Tests { + if tc.Error == nil { + continue + } + failedTests = addTest(failedTests, tc) + } + return failedTests +} + +func GetFailedTests(testSuites []junit.Suite) ([]TestCase, error) { + failedTests := make([]TestCase, 0) + for _, ts := range testSuites { + failedTests = addFailedTests(ts, failedTests) + } + + return failedTests, nil +}