diff --git a/cmd/vacuum_report.go b/cmd/vacuum_report.go index 40ede5db..4068a2ae 100644 --- a/cmd/vacuum_report.go +++ b/cmd/vacuum_report.go @@ -42,6 +42,8 @@ func GetVacuumReportCommand() *cobra.Command { stdIn, _ := cmd.Flags().GetBool("stdin") stdOut, _ := cmd.Flags().GetBool("stdout") noStyleFlag, _ := cmd.Flags().GetBool("no-style") + baseFlag, _ := cmd.Flags().GetString("base") + junitFlag, _ := cmd.Flags().GetBool("junit") // disable color and styling, for CI/CD use. // https://github.com/daveshanley/vacuum/issues/234 @@ -134,6 +136,7 @@ func GetVacuumReportCommand() *cobra.Command { Spec: specBytes, CustomFunctions: customFunctions, SilenceLogs: true, + Base: baseFlag, }) resultSet := model.NewRuleResultSet(ruleset.Results) @@ -141,6 +144,30 @@ func GetVacuumReportCommand() *cobra.Command { duration := time.Since(start) + // if we want jUnit output, then build the report and be done with it. + if junitFlag { + junitXML := vacuum_report.BuildJUnitReport(resultSet, start) + if stdOut { + fmt.Print(string(junitXML)) + return nil + } else { + + reportOutputName := fmt.Sprintf("%s-%s%s", + reportOutput, time.Now().Format("01-02-06-15_04_05"), ".xml") + + err := os.WriteFile(reportOutputName, junitXML, 0664) + if err != nil { + pterm.Error.Printf("Unable to write junit report file: '%s': %s\n", reportOutputName, err.Error()) + pterm.Println() + return err + } + + pterm.Success.Printf("JUnit Report generated for '%s', written to '%s'\n", args[0], reportOutputName) + pterm.Println() + return nil + } + } + // pre-render resultSet.PrepareForSerialization(ruleset.SpecInfo) @@ -208,6 +235,7 @@ func GetVacuumReportCommand() *cobra.Command { } cmd.Flags().BoolP("stdin", "i", false, "Use stdin as input, instead of a file") cmd.Flags().BoolP("stdout", "o", false, "Use stdout as output, instead of a file") + cmd.Flags().BoolP("junit", "j", false, "Generate report in JUnit format (cannot be compressed)") cmd.Flags().BoolP("compress", "c", false, "Compress results using gzip") cmd.Flags().BoolP("no-pretty", "n", false, "Render JSON with no formatting") cmd.Flags().BoolP("no-style", "q", false, "Disable styling and color output, just plain text (useful for CI/CD)") diff --git a/vacuum-report/junit.go b/vacuum-report/junit.go new file mode 100644 index 00000000..0fd110e1 --- /dev/null +++ b/vacuum-report/junit.go @@ -0,0 +1,105 @@ +// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package vacuum_report + +import ( + "bytes" + "encoding/xml" + "fmt" + "github.com/daveshanley/vacuum/model" + "strings" + "text/template" + "time" +) + +type TestSuites struct { + XMLName xml.Name `xml:"testsuites"` + TestSuites []*TestSuite `xml:"testsuite"` + Tests int `xml:"tests,attr"` + Failures int `xml:"failures,attr"` + Time float64 `xml:"time,attr"` +} + +type TestSuite struct { + XMLName xml.Name `xml:"testsuite"` + Name string `xml:"name,attr"` + Tests int `xml:"tests,attr"` + Failures int `xml:"failures,attr"` + Time float64 `xml:"time,attr"` + TestCases []*TestCase `xml:"testcase"` +} + +type TestCase struct { + Name string `xml:"name,attr"` + ClassName string `xml:"classname,attr"` + Time float64 `xml:"time,attr"` + Failure *Failure `xml:"failure,omitempty"` +} + +type Failure struct { + Message string `xml:"message,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + Contents string `xml:",innerxml"` +} + +func BuildJUnitReport(resultSet *model.RuleResultSet, t time.Time) []byte { + + since := time.Since(t) + + var suites []*TestSuite + + var cats = model.RuleCategoriesOrdered + + tmpl := ` + {{ .Message }} + JSON Path: {{ .Path }} + Rule: {{ .Rule.Id }} + Severity: {{ .Rule.Severity }} + Start Line: {{ .StartNode.Line }} + End Line: {{ .EndNode.Line }}` + + parsedTemplate, _ := template.New("failure").Parse(tmpl) + + // try a category print out. + for _, val := range cats { + categoryResults := resultSet.GetResultsByRuleCategory(val.Id) + + f := 0 + var tc []*TestCase + + for _, r := range categoryResults { + var sb bytes.Buffer + _ = parsedTemplate.Execute(&sb, r) + if r.Rule.Severity == model.SeverityError || r.Rule.Severity == model.SeverityWarn { + f++ + } + tc = append(tc, &TestCase{ + Name: fmt.Sprintf("Category: %s", val.Id), + ClassName: r.Rule.Id, + Time: since.Seconds(), + Failure: &Failure{ + Message: r.Message, + Type: strings.ToUpper(r.Rule.Severity), + Contents: sb.String(), + }, + }) + } + + if len(tc) > 0 { + ts := &TestSuite{ + Name: fmt.Sprintf("Category: %s", val.Id), + Tests: len(categoryResults), + Failures: f, + Time: since.Seconds(), + TestCases: tc, + } + + suites = append(suites, ts) + } + } + + b, _ := xml.MarshalIndent(suites, "", " ") + return b + +} diff --git a/vacuum-report/junit_test.go b/vacuum-report/junit_test.go new file mode 100644 index 00000000..3ead927d --- /dev/null +++ b/vacuum-report/junit_test.go @@ -0,0 +1,20 @@ +// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package vacuum_report + +import ( + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestBuildJUnitReport(t *testing.T) { + j := testhelp_generateReport() + j.ResultSet.Results[0].Message = "testing, 123" + j.ResultSet.Results[0].Path = "$.somewhere.out.there" + j.ResultSet.Results[0].RuleId = "R0001" + f := time.Now().Add(-time.Millisecond * 5) + data := BuildJUnitReport(j.ResultSet, f) + assert.Len(t, data, 295) +}