From 9102a3f2705704199a3c085e2e274c3aa37b0646 Mon Sep 17 00:00:00 2001 From: Dan Curran Date: Wed, 28 Feb 2024 05:25:58 -0600 Subject: [PATCH] Adding JUnit support to chart verifier report --- cmd/report_test.go | 11 ++ cmd/verify.go | 9 +- pkg/chartverifier/report/junitConverter.go | 198 +++++++++++++++++++++ pkg/chartverifier/report/report.go | 20 ++- 4 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 pkg/chartverifier/report/junitConverter.go diff --git a/cmd/report_test.go b/cmd/report_test.go index 67580ab1..c788a899 100644 --- a/cmd/report_test.go +++ b/cmd/report_test.go @@ -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{ diff --git a/cmd/verify.go b/cmd/verify.go index d5e8a39a..1405c363 100644 --- a/cmd/verify.go +++ b/cmd/verify.go @@ -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" } } @@ -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") diff --git a/pkg/chartverifier/report/junitConverter.go b/pkg/chartverifier/report/junitConverter.go new file mode 100644 index 00000000..2f0ca4c2 --- /dev/null +++ b/pkg/chartverifier/report/junitConverter.go @@ -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) + e.Encode(m.ToolMetadata) + encodeString(e, "Overrides", m.Overrides) + + // 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) + + // 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())) + } + } + + // Loop through helmchart.Metadata Fields + encodeStringArray(e, "Sources", chartData.Sources) + encodeStringArray(e, "Keywords", chartData.Keywords) + encodeStringMap(e, "Annotations", chartData.Annotations) + + // Maintainers Start + maintainersStartToken := xml.StartElement{Name: xml.Name{"", "Maintainers"}} + e.EncodeToken(maintainersStartToken) + e.Encode(chartData.Maintainers) + e.EncodeToken(xml.EndElement{maintainersStartToken.Name}) + // Maintainers End + + // Dependencies Start + dependenciesStartToken := xml.StartElement{Name: xml.Name{"", "Dependencies"}} + e.EncodeToken(dependenciesStartToken) + e.Encode(chartData.Dependencies) + e.EncodeToken(xml.EndElement{dependenciesStartToken.Name}) + // Dependencies End + + e.EncodeToken(xml.EndElement{chartDataStartToken.Name}) + // ChartData End + + e.EncodeToken(xml.EndElement{start.Name}) + // Metadata End + + e.Flush() +} + +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) + e.EncodeToken(propertiesStart) + e.EncodeToken(propertyStart) + encodeString(e, "ApiVersion", r.Apiversion) + encodeString(e, "Kind", r.Kind) + e.Encode(r.options) + encodeReportMetadata(e, r.Metadata, xml.StartElement{Name: xml.Name{"", "Metadata"}}) + e.EncodeToken(xml.EndElement{propertyStart.Name}) + e.EncodeToken(xml.EndElement{propertiesStart.Name}) + + 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) + + // e.Encode(r.Results) + encodeResults(e, r) + + // flush to ensure tokens are written + e.EncodeToken(xml.EndElement{testSuiteStart.Name}) + e.EncodeToken(xml.EndElement{junitStart.Name}) + + return e.Flush() +} diff --git a/pkg/chartverifier/report/report.go b/pkg/chartverifier/report/report.go index 93dbb0d9..96c39d8c 100644 --- a/pkg/chartverifier/report/report.go +++ b/pkg/chartverifier/report/report.go @@ -2,6 +2,7 @@ package report import ( "encoding/json" + "encoding/xml" "errors" "fmt" "io" @@ -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" ) @@ -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 { @@ -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), "") { + 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 {