From 541e54895637bff6cf8373253a006137ddfdb2ec Mon Sep 17 00:00:00 2001 From: Mladen Todorovic Date: Tue, 22 Oct 2024 11:19:01 +0200 Subject: [PATCH] Prefactor junit2jira --- .github/workflows/go.yml | 4 +- .gitignore | 2 +- README.md | 2 +- .../junit2jira/htmlOutput.html.tpl | 0 main.go => cmd/junit2jira/main.go | 162 ++++++------------ main_test.go => cmd/junit2jira/main_test.go | 28 +-- slack_test.go => cmd/junit2jira/slack_test.go | 2 +- .../jira/TEST-DefaultPoliciesTest.xml | 0 .../testdata}/jira/expected-html-output.html | 0 .../testdata}/jira/kuttl-report.xml | 0 .../junit2jira/testdata}/jira/report.xml | 0 .../junit2jira/testdata}/jira/report1.xml | 0 .../junit2jira/testdata}/jira/timeout.xml | 0 .../testdata}/slack/combined-expected.json | 0 .../testdata}/slack/combined-sample.xml | 0 .../testdata}/slack/message-expected.json | 0 .../testdata}/slack/message-sample.xml | 0 .../testdata}/slack/value-expected.json | 0 .../testdata}/slack/value-sample.xml | 0 go.mod | 4 +- pkg/testcase/testcase.go | 105 ++++++++++++ 21 files changed, 182 insertions(+), 127 deletions(-) rename htmlOutput.html.tpl => cmd/junit2jira/htmlOutput.html.tpl (100%) rename main.go => cmd/junit2jira/main.go (84%) rename main_test.go => cmd/junit2jira/main_test.go (98%) rename slack_test.go => cmd/junit2jira/slack_test.go (97%) rename {testdata => cmd/junit2jira/testdata}/jira/TEST-DefaultPoliciesTest.xml (100%) rename {testdata => cmd/junit2jira/testdata}/jira/expected-html-output.html (100%) rename {testdata => cmd/junit2jira/testdata}/jira/kuttl-report.xml (100%) rename {testdata => cmd/junit2jira/testdata}/jira/report.xml (100%) rename {testdata => cmd/junit2jira/testdata}/jira/report1.xml (100%) rename {testdata => cmd/junit2jira/testdata}/jira/timeout.xml (100%) rename {testdata => cmd/junit2jira/testdata}/slack/combined-expected.json (100%) rename {testdata => cmd/junit2jira/testdata}/slack/combined-sample.xml (100%) rename {testdata => cmd/junit2jira/testdata}/slack/message-expected.json (100%) rename {testdata => cmd/junit2jira/testdata}/slack/message-sample.xml (100%) rename {testdata => cmd/junit2jira/testdata}/slack/value-expected.json (100%) rename {testdata => cmd/junit2jira/testdata}/slack/value-sample.xml (100%) create mode 100644 pkg/testcase/testcase.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8b0c469..c81d15b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -18,13 +18,13 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.21 - name: golangci-lint uses: golangci/golangci-lint-action@v3 - name: Build - run: CGO_ENABLED=0 go build -a -tags netgo -ldflags '-s -w' -v ./... + run: CGO_ENABLED=0 go build -a -tags netgo -ldflags '-s -w' -v -o . ./... - name: Compress binaries uses: svenstaro/upx-action@v2 diff --git a/.gitignore b/.gitignore index f16f4cb..2e05c6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -junit2jira +/junit2jira .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/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 6499ace..ba7001d 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" @@ -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,7 +119,7 @@ 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") } @@ -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 0 < j.threshold && j.threshold < len(failedTests) { + 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 { @@ -277,7 +300,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) @@ -479,34 +502,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 @@ -521,42 +517,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, "/") -} + tc := newJ2jTestCase( + testcase.NewTestCase( + junit.Test{ + Message: msg, + Classname: suite, + }), j.params) -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)) -} - -// 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 ( @@ -591,13 +560,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 @@ -626,13 +597,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, @@ -640,18 +612,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 @@ -659,24 +626,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 @@ -774,7 +724,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..17ff431 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module github.com/janisz/junit2jira +module github.com/stackrox/junit2jira -go 1.19 +go 1.21 require ( github.com/andygrunwald/go-jira v1.16.0 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 +}