Skip to content

Commit

Permalink
Adding JUnit support to chart verifier report
Browse files Browse the repository at this point in the history
  • Loading branch information
Dan Curran committed Apr 15, 2024
1 parent 784ac07 commit 9102a3f
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 6 deletions.
11 changes: 11 additions & 0 deletions cmd/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,17 @@ func TestReport(t *testing.T) {
},
wantErr: false,
},
{
name: "Should pass writing output to junit file",
args: []string{
"-w",
"-o",
"junit",
string(apireportsummary.AnnotationsSummary),
"test/report.xml",
},
wantErr: false,
},
{
name: "Should pass for skip digest check",
args: []string{
Expand Down
9 changes: 6 additions & 3 deletions cmd/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,17 @@ func NewVerifyCmd(config *viper.Viper) *cobra.Command {
reportFormat := apireport.YamlReport
if outputFormatFlag == "json" {
reportFormat = apireport.JSONReport
} else if outputFormatFlag == "junit" {
reportFormat = apireport.JUnitReport
}

reportName := ""
if reportToFile {
reportName = "report.yaml"
if outputFormatFlag == "json" {
reportName = "report.json"
} else {
reportName = "report.yaml"
} else if outputFormatFlag == "junit" {
reportName = "report.xml"
}
}

Expand Down Expand Up @@ -244,7 +247,7 @@ func NewVerifyCmd(config *viper.Viper) *cobra.Command {

cmd.Flags().StringSliceVarP(&disabledChecksFlag, "disable", "x", nil, "all checks will be enabled except the informed ones")

cmd.Flags().StringVarP(&outputFormatFlag, "output", "o", "", "the output format: default, json or yaml")
cmd.Flags().StringVarP(&outputFormatFlag, "output", "o", "", "the output format: default, json, junit or yaml")

cmd.Flags().StringSliceVarP(&verifyOpts.Values, "set", "s", []string{}, "overrides a configuration, e.g: dummy.ok=false")

Expand Down
198 changes: 198 additions & 0 deletions pkg/chartverifier/report/junitConverter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package report

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

func encodeTokenArray(e *xml.Encoder, tokens []xml.Token) error {
for _, t := range tokens {
err := e.EncodeToken(t)
if err != nil {
return err
}
}
return e.Flush()
}

func encodeString(e *xml.Encoder, name string, value string) error {
start := xml.StartElement{Name: xml.Name{"", name}}
tokens := []xml.Token{start}
tokens = append(tokens, xml.CharData(value), xml.EndElement{start.Name})

return encodeTokenArray(e, tokens)
}

func encodeStringArray(e *xml.Encoder, name string, values []string) error {
start := xml.StartElement{Name: xml.Name{"", name}}
tokens := []xml.Token{start}

for _, value := range values {
t := xml.StartElement{Name: xml.Name{"", "value"}}
tokens = append(tokens, t, xml.CharData(value), xml.EndElement{t.Name})
}

tokens = append(tokens, xml.EndElement{start.Name})

return encodeTokenArray(e, tokens)
}

func encodeStringMap(e *xml.Encoder, name string, values map[string]string) error {
start := xml.StartElement{Name: xml.Name{"", name}}
tokens := []xml.Token{start}

for key, value := range values {
t := xml.StartElement{Name: xml.Name{"", key}}
tokens = append(tokens, t, xml.CharData(value), xml.EndElement{t.Name})
}

tokens = append(tokens, xml.EndElement{start.Name})

return encodeTokenArray(e, tokens)
}

func encodeReportMetadata(e *xml.Encoder, m ReportMetadata, start xml.StartElement) {
// Start ReportMetadata and encode what can be done automatically
e.EncodeToken(start)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
e.Encode(m.ToolMetadata)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
encodeString(e, "Overrides", m.Overrides)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.

// Work through parts of the ChartData which is a problem due to Annotations field
chartData := m.ChartData
chartDataStartToken := xml.StartElement{Name: xml.Name{"", "ChartData"}}
e.EncodeToken(chartDataStartToken)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.

// Loop through helmchart.Metadata Fields and encode strings/bools
v := reflect.ValueOf(*chartData)
typeOfS := v.Type()
for i := 0; i < v.NumField(); i++ {
fieldType := fmt.Sprintf("%T", v.Field(i).Interface())

if fieldType == "string" || fieldType == "bool" {
encodeString(e,
typeOfS.Field(i).Name,
fmt.Sprintf("%v", v.Field(i).Interface()))

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
}
}

// Loop through helmchart.Metadata Fields
encodeStringArray(e, "Sources", chartData.Sources)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
encodeStringArray(e, "Keywords", chartData.Keywords)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
encodeStringMap(e, "Annotations", chartData.Annotations)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.

// Maintainers Start
maintainersStartToken := xml.StartElement{Name: xml.Name{"", "Maintainers"}}
e.EncodeToken(maintainersStartToken)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
e.Encode(chartData.Maintainers)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
e.EncodeToken(xml.EndElement{maintainersStartToken.Name})

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
// Maintainers End

// Dependencies Start
dependenciesStartToken := xml.StartElement{Name: xml.Name{"", "Dependencies"}}
e.EncodeToken(dependenciesStartToken)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
e.Encode(chartData.Dependencies)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
e.EncodeToken(xml.EndElement{dependenciesStartToken.Name})

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
// Dependencies End

e.EncodeToken(xml.EndElement{chartDataStartToken.Name})

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
// ChartData End

e.EncodeToken(xml.EndElement{start.Name})

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
// Metadata End

e.Flush()

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
}

func encodeResults(e *xml.Encoder, r Report) error {
tokens := []xml.Token{}

for _, testcase := range r.Results {

// Put Check, Type, and Outcome into XML testcase element
t := xml.StartElement{Name: xml.Name{"", "testcase"},
Attr: []xml.Attr{
{xml.Name{"", "name"}, string(testcase.Check)},
{xml.Name{"", "classname"}, string(testcase.Reason)},
{xml.Name{"", "assertions"}, "1"},
},
}

//Create an element for Reason
reasonToken := xml.StartElement{Name: xml.Name{"", "system-out"}}

tokens = append(tokens, t,
reasonToken, xml.CharData(testcase.Reason), xml.EndElement{reasonToken.Name},
xml.EndElement{t.Name})

}

return encodeTokenArray(e, tokens)
}

func (r Report) MarshalXML(e *xml.Encoder, start xml.StartElement) error {

numTests := len(r.Results)
numFailures := 0
numSkipped := 0
numPassed := 0
timestamp := time.Now().Format(time.RFC3339)

for _, element := range r.Results {
switch element.Outcome {
case "FAIL":
numFailures += 1
case "SKIPPED":
numSkipped += 1
case "PASS":
numPassed += 1
}
}

junitStart := xml.StartElement{Name: xml.Name{"", "testsuites"},
Attr: []xml.Attr{
{xml.Name{"", "name"}, "chart-verifier test run"},
{xml.Name{"", "tests"}, fmt.Sprint(numTests)},
{xml.Name{"", "failures"}, fmt.Sprint(numFailures)},
{xml.Name{"", "skipped"}, fmt.Sprint(numSkipped)},
{xml.Name{"", "timestamp"}, timestamp},
},
}
propertiesStart := xml.StartElement{Name: xml.Name{"", "properties"}}
propertyStart := xml.StartElement{Name: xml.Name{"", "property"},
Attr: []xml.Attr{
{xml.Name{"", "name"}, "config"},
},
}

e.EncodeToken(junitStart)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
e.EncodeToken(propertiesStart)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
e.EncodeToken(propertyStart)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
encodeString(e, "ApiVersion", r.Apiversion)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
encodeString(e, "Kind", r.Kind)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
e.Encode(r.options)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
encodeReportMetadata(e, r.Metadata, xml.StartElement{Name: xml.Name{"", "Metadata"}})
e.EncodeToken(xml.EndElement{propertyStart.Name})

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
e.EncodeToken(xml.EndElement{propertiesStart.Name})

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.

testSuiteStart := xml.StartElement{Name: xml.Name{"", "testsuite"},
Attr: []xml.Attr{
{xml.Name{"", "name"}, "chart-verifier test run"},
{xml.Name{"", "tests"}, fmt.Sprint(numTests)},
{xml.Name{"", "failures"}, fmt.Sprint(numFailures)},
{xml.Name{"", "skipped"}, fmt.Sprint(numSkipped)},
{xml.Name{"", "timestamp"}, timestamp},
},
}
e.EncodeToken(testSuiteStart)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.

// e.Encode(r.Results)
encodeResults(e, r)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.

// flush to ensure tokens are written
e.EncodeToken(xml.EndElement{testSuiteStart.Name})

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
e.EncodeToken(xml.EndElement{junitStart.Name})

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.

return e.Flush()
}
20 changes: 17 additions & 3 deletions pkg/chartverifier/report/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package report

import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
Expand All @@ -20,8 +21,9 @@ const (
SkippedOutcomeType OutcomeType = "SKIPPED"
UnknownOutcomeType OutcomeType = "UNKNOWN"

JSONReport ReportFormat = "json"
YamlReport ReportFormat = "yaml"
JSONReport ReportFormat = "json"
JUnitReport ReportFormat = "junit"
YamlReport ReportFormat = "yaml"

ReportShaVersion string = "v1.9.0"
)
Expand Down Expand Up @@ -56,9 +58,16 @@ func (r *Report) GetContent(format ReportFormat) (string, error) {
if format == JSONReport {
b, marshalErr := json.Marshal(report)
if marshalErr != nil {
return "", fmt.Errorf("report json marshal failed : %v", marshalErr)
return "", fmt.Errorf("report xml marshal failed : %v", marshalErr)
}
reportContent = string(b)
} else if format == JUnitReport {
// convert report to JUnit
out, marshalErr := xml.MarshalIndent(r, " ", " ")
if marshalErr != nil {
return "", fmt.Errorf("report JUnit marshal failed: %v", marshalErr)
}
reportContent = xml.Header + string(out)
} else {
b, marshalErr := yaml.Marshal(report)
if marshalErr != nil {
Expand Down Expand Up @@ -113,6 +122,11 @@ func (r *Report) loadReport() error {
if unMarshalErr != nil {
return fmt.Errorf("report json ummarshal failed : %v", unMarshalErr)
}
} else if strings.HasPrefix(strings.TrimSpace(reportString), "<?xml version=\"1.0\" encoding=\"UTF-8\"?>") {
unMarshalErr := xml.Unmarshal([]byte(reportString), r)
if unMarshalErr != nil {
return fmt.Errorf("report junit ummarshal failed : %v", unMarshalErr)
}
} else {
unMarshalErr := yaml.Unmarshal([]byte(reportString), r)
if unMarshalErr != nil {
Expand Down

0 comments on commit 9102a3f

Please sign in to comment.