Skip to content

Commit

Permalink
feat: JUnit format now available for report #233
Browse files Browse the repository at this point in the history
Using the new `-j` flag, JUnit output is available for CI/CD as requested in #233
  • Loading branch information
daveshanley committed Mar 27, 2023
1 parent ecd7552 commit 3c19183
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 0 deletions.
28 changes: 28 additions & 0 deletions cmd/vacuum_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -134,13 +136,38 @@ func GetVacuumReportCommand() *cobra.Command {
Spec: specBytes,
CustomFunctions: customFunctions,
SilenceLogs: true,
Base: baseFlag,
})

resultSet := model.NewRuleResultSet(ruleset.Results)
resultSet.SortResultsByLineNumber()

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)

Expand Down Expand Up @@ -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)")
Expand Down
105 changes: 105 additions & 0 deletions vacuum-report/junit.go
Original file line number Diff line number Diff line change
@@ -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

}
20 changes: 20 additions & 0 deletions vacuum-report/junit_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 3c19183

Please sign in to comment.