Skip to content

Commit

Permalink
Add support for JUnit reports (#91)
Browse files Browse the repository at this point in the history
* initial commit

* junit report test

* tests updated

* tests

* fix html entities encoding, split checkPropertyValidity into several functions

---------

Co-authored-by: Vladimir Siman <[email protected]>
  • Loading branch information
onlineque and Vladimir Siman authored Dec 4, 2023
1 parent d57862b commit 336d2c5
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 6 deletions.
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")
}
}

0 comments on commit 336d2c5

Please sign in to comment.