Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for JUnit reports #91

Merged
merged 5 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ cmd/validator/validator

# Dependency directories (remove the comment below to include it)
# vendor/
.vscode
.vscode
.idea
10 changes: 6 additions & 4 deletions cmd/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func validatorUsage() {
func getFlags() (validatorConfig, error) {
flag.Usage = validatorUsage
excludeDirsPtr := flag.String("exclude-dirs", "", "Subdirectories to exclude when searching for configuration files")
reportTypePtr := flag.String("reporter", "standard", "Format of the printed report. Options are standard and json")
reportTypePtr := flag.String("reporter", "standard", "Format of the printed report. Options are standard, json and junit")
excludeFileTypesPtr := flag.String("exclude-file-types", "", "A comma separated list of file types to ignore")
depthPtr := flag.Int("depth", 0, "Depth of recursion for the provided search paths. Set depth to 0 to disable recursive path traversal")
versionPtr := flag.Bool("version", false, "Version prints the release version of validator")
Expand All @@ -84,10 +84,10 @@ func getFlags() (validatorConfig, error) {
searchPaths = append(searchPaths, flag.Args()...)
}

if *reportTypePtr != "standard" && *reportTypePtr != "json" {
fmt.Println("Wrong parameter value for reporter, only supports standard or json")
if *reportTypePtr != "standard" && *reportTypePtr != "json" && *reportTypePtr != "junit" {
fmt.Println("Wrong parameter value for reporter, only supports standard, json or junit")
flag.Usage()
return validatorConfig{}, errors.New("Wrong parameter value for reporter, only supports standard or json")
return validatorConfig{}, errors.New("Wrong parameter value for reporter, only supports standard, json or junit")
}

if depthPtr != nil && isFlagSet("depth") && *depthPtr < 0 {
Expand Down Expand Up @@ -125,6 +125,8 @@ func isFlagSet(flagName string) bool {
// reportType string
func getReporter(reportType *string) reporter.Reporter {
switch *reportType {
case "junit":
return reporter.JunitReporter{}
case "json":
return reporter.JsonReporter{}
default:
Expand Down
5 changes: 4 additions & 1 deletion cmd/validator/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ func Test_flags(t *testing.T) {
ExpectedExit int
}{
{"blank", []string{}, 0},
{"negative depth set", []string{"-depth=-1", "."}, 1},
{"depth set", []string{"-depth=1", "."}, 0},
{"flags set, wrong reporter", []string{"--exclude-dirs=subdir", "--reporter=wrong", "."}, 1},
{"flags set, json reporter", []string{"--exclude-dirs=subdir", "--reporter=json", "."}, 0},
{"flags set, junit reported", []string{"--exclude-dirs=subdir", "--reporter=junit", "."}, 1},
{"flags set, junit reported", []string{"--exclude-dirs=subdir", "--reporter=junit", "."}, 0},
{"bad path", []string{"/path/does/not/exit"}, 1},
{"exclude file types set", []string{"--exclude-file-types=json", "."}, 0},
{"multiple paths", []string{"../../test/fixtures/subdir/good.json", "../../test/fixtures/good.json"}, 0},
Expand Down
192 changes: 192 additions & 0 deletions pkg/reporter/junit_reporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package reporter

import (
"encoding/xml"
"fmt"
"strings"
"time"
)

type JunitReporter struct{}

const (
Header = `<?xml version="1.0" encoding="UTF-8"?>` + "\n"
)

type Message struct {
InnerXML string `xml:",innerxml"`
}

// https://github.com/testmoapp/junitxml#basic-junit-xml-structure
type Testsuites struct {
XMLName xml.Name `xml:"testsuites"`
Name string `xml:"name,attr,omitempty"`
Tests int `xml:"tests,attr,omitempty"`
Failures int `xml:"failures,attr,omitempty"`
Errors int `xml:"errors,attr,omitempty"`
Skipped int `xml:"skipped,attr,omitempty"`
Assertions int `xml:"assertions,attr,omitempty"`
Time float32 `xml:"time,attr,omitempty"`
Timestamp *time.Time `xml:"timestamp,attr,omitempty"`
Testsuites []Testsuite `xml:"testsuite"`
}

type Testsuite struct {
XMLName xml.Name `xml:"testsuite"`
Name string `xml:"name,attr"`
Tests int `xml:"tests,attr,omitempty"`
Failures int `xml:"failures,attr,omitempty"`
Errors int `xml:"errors,attr,omitempty"`
Skipped int `xml:"skipped,attr,omitempty"`
Assertions int `xml:"assertions,attr,omitempty"`
Time float32 `xml:"time,attr,omitempty"`
Timestamp *time.Time `xml:"timestamp,attr,omitempty"`
File string `xml:"file,attr,omitempty"`
Testcases *[]Testcase `xml:"testcase,omitempty"`
Properties *[]Property `xml:"properties>property,omitempty"`
SystemOut *SystemOut `xml:"system-out,omitempty"`
SystemErr *SystemErr `xml:"system-err,omitempty"`
}

type Testcase struct {
XMLName xml.Name `xml:"testcase"`
Name string `xml:"name,attr"`
ClassName string `xml:"classname,attr"`
Assertions int `xml:"assertions,attr,omitempty"`
Time float32 `xml:"time,attr,omitempty"`
File string `xml:"file,attr,omitempty"`
Line int `xml:"line,attr,omitempty"`
Skipped *Skipped `xml:"skipped,omitempty,omitempty"`
Properties *[]Property `xml:"properties>property,omitempty"`
TestcaseError *TestcaseError `xml:"error,omitempty"`
TestcaseFailure *TestcaseFailure `xml:"failure,omitempty"`
}

type Skipped struct {
XMLName xml.Name `xml:"skipped"`
Message string `xml:"message,attr"`
}

type TestcaseError struct {
XMLName xml.Name `xml:"error"`
Message Message
Type string `xml:"type,omitempty"`
TextValue string `xml:",chardata"`
}

type TestcaseFailure struct {
XMLName xml.Name `xml:"failure"`
// Message string `xml:"message,omitempty"`
Message Message
Type string `xml:"type,omitempty"`
TextValue string `xml:",chardata"`
}

type SystemOut struct {
XMLName xml.Name `xml:"system-out"`
TextValue string `xml:",chardata"`
}

type SystemErr struct {
XMLName xml.Name `xml:"system-err"`
TextValue string `xml:",chardata"`
}

type Property struct {
XMLName xml.Name `xml:"property"`
TextValue string `xml:",chardata"`
Name string `xml:"name,attr"`
Value string `xml:"value,attr,omitempty"`
}

func checkProperty(property Property, xmlElementName string, name string) error {
if property.Value != "" && property.TextValue != "" {
return fmt.Errorf("property %s in %s %s should contain value or a text value, not both",
property.Name, xmlElementName, name)
}
return nil
}

func checkTestCase(testcase Testcase) (err error) {
if testcase.Properties != nil {
for propidx := range *testcase.Properties {
property := (*testcase.Properties)[propidx]
if err = checkProperty(property, "testcase", testcase.Name); err != nil {
return err
}
}
}
return nil
}

func checkTestSuite(testsuite Testsuite) (err error) {
if testsuite.Properties != nil {
for pridx := range *testsuite.Properties {
property := (*testsuite.Properties)[pridx]
if err = checkProperty(property, "testsuite", testsuite.Name); err != nil {
return err
}
}
}

if testsuite.Testcases != nil {
for tcidx := range *testsuite.Testcases {
testcase := (*testsuite.Testcases)[tcidx]
if err = checkTestCase(testcase); err != nil {
return err
}
}
}
return nil
}

func (ts Testsuites) checkPropertyValidity() (err error) {
for tsidx := range ts.Testsuites {
testsuite := ts.Testsuites[tsidx]
if err = checkTestSuite(testsuite); err != nil {
return err
}
}
return nil
}

func (ts Testsuites) getReport() ([]byte, error) {
err := ts.checkPropertyValidity()
if err != nil {
return []byte{}, err
}

data, err := xml.MarshalIndent(ts, " ", " ")
if err != nil {
return []byte{}, err
}

return data, nil
}

func (jr JunitReporter) Print(reports []Report) error {
testcases := []Testcase{}
testErrors := 0

for _, r := range reports {
if strings.Contains(r.FilePath, "\\") {
r.FilePath = strings.ReplaceAll(r.FilePath, "\\", "/")
}
tc := Testcase{Name: fmt.Sprintf("%s validation", r.FilePath), File: r.FilePath, ClassName: "config-file-validator"}
if !r.IsValid {
testErrors++
tc.TestcaseFailure = &TestcaseFailure{Message: Message{InnerXML: r.ValidationError.Error()}}
}
testcases = append(testcases, tc)
}
testsuite := Testsuite{Name: "config-file-validator", Testcases: &testcases, Errors: testErrors}
testsuiteBatch := []Testsuite{testsuite}
ts := Testsuites{Name: "config-file-validator", Tests: len(reports), Testsuites: testsuiteBatch}

data, err := ts.getReport()
if err != nil {
return err
}
fmt.Println(Header + string(data))
return nil
}
64 changes: 64 additions & 0 deletions pkg/reporter/reporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,67 @@ func Test_jsonReport(t *testing.T) {
t.Errorf("Reporting failed")
}
}

func Test_junitReport(t *testing.T) {
prop1 := Property{Name: "property1", Value: "value", TextValue: "text value"}
properties := []Property{prop1}
testsuite := Testsuite{Name: "config-file-validator", Errors: 0, Properties: &properties}
testsuiteBatch := []Testsuite{testsuite}
ts := Testsuites{Name: "config-file-validator", Tests: 1, Testsuites: testsuiteBatch}

_, err := ts.getReport()
if err == nil {
t.Errorf("Reporting failed on getReport")
}

prop2 := Property{Name: "property2", Value: "value"}
properties2 := []Property{prop2}
testsuite = Testsuite{Name: "config-file-validator", Errors: 0, Properties: &properties2}
testsuiteBatch = []Testsuite{testsuite}
ts = Testsuites{Name: "config-file-validator", Tests: 1, Testsuites: testsuiteBatch}

_, err = ts.getReport()
if err != nil {
t.Errorf("Reporting failed on getReport")
}

tc1 := Testcase{Name: "testcase2", ClassName: "config-file-validator", Properties: &properties}
testCasesBatch := []Testcase{tc1}
testsuite = Testsuite{Name: "config-file-validator", Errors: 0, Testcases: &testCasesBatch}
testsuiteBatch = []Testsuite{testsuite}
ts3 := Testsuites{Name: "config-file-validator", Tests: 1, Testsuites: testsuiteBatch}

_, err = ts3.getReport()
if err == nil {
t.Errorf("Reporting failed on getReport")
}

reportNoValidationError := Report{
"good.xml",
"/fake/path/good.xml",
true,
nil,
}

reportWithBackslashPath := Report{
"good.xml",
"\\fake\\path\\good.xml",
true,
nil,
}

reportWithValidationError := Report{
"bad.xml",
"/fake/path/bad.xml",
false,
errors.New("Unable to parse bad.xml file"),
}

reports := []Report{reportNoValidationError, reportWithBackslashPath, reportWithValidationError}

junitReporter := JunitReporter{}
err = junitReporter.Print(reports)
if err != nil {
t.Errorf("Reporting failed")
}
}