package output // References: // https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd // https://llg.cubic.org/docs/junit/ // https://github.com/jstemmer/go-junit-report/blob/master/formatter/formatter.go // https://github.com/junit-team/junit5/blob/main/platform-tests/src/test/resources/jenkins-junit.xsd import ( "bufio" "encoding/xml" "fmt" "io" "time" "github.com/yannh/kubeconform/pkg/validator" ) type TestSuiteCollection struct { XMLName xml.Name `xml:"testsuites"` Name string `xml:"name,attr"` Time float64 `xml:"time,attr"` Tests int `xml:"tests,attr"` Failures int `xml:"failures,attr"` Disabled int `xml:"disabled,attr"` Errors int `xml:"errors,attr"` Suites []TestSuite `xml:"testsuite"` } type Property struct { Name string `xml:"name,attr"` Value string `xml:"value,attr"` } type TestSuite struct { XMLName xml.Name `xml:"testsuite"` Cases []TestCase `xml:"testcase"` Name string `xml:"name,attr"` Id int `xml:"id,attr"` Tests int `xml:"tests,attr"` Failures int `xml:"failures,attr"` Errors int `xml:"errors,attr"` Disabled int `xml:"disabled,attr"` Skipped int `xml:"skipped,attr"` } type TestCase struct { XMLName xml.Name `xml:"testcase"` Name string `xml:"name,attr"` ClassName string `xml:"classname,attr"` Time int `xml:"time,attr"` // Optional, but for Buildkite support https://github.com/yannh/kubeconform/issues/127 Skipped *TestCaseSkipped `xml:"skipped,omitempty"` Error *TestCaseError `xml:"error,omitempty"` Failure []TestCaseError `xml:"failure,omitempty"` } type TestCaseSkipped struct { Message string `xml:"message,attr"` } type TestCaseError struct { Message string `xml:"message,attr"` Type string `xml:"type,attr"` Content string `xml:",chardata"` } type junito struct { id int w io.Writer withSummary bool verbose bool suites map[string]*TestSuite // map filename to corresponding suite nValid, nInvalid, nErrors, nSkipped int startTime time.Time } func junitOutput(w io.Writer, withSummary bool, isStdin, verbose bool) Output { return &junito{ id: 0, w: w, withSummary: withSummary, verbose: verbose, suites: make(map[string]*TestSuite), nValid: 0, nInvalid: 0, nErrors: 0, nSkipped: 0, startTime: time.Now(), } } // Write adds a result to the report. func (o *junito) Write(result validator.Result) error { var suite *TestSuite suite, found := o.suites[result.Resource.Path] if !found { o.id++ suite = &TestSuite{ Name: result.Resource.Path, Id: o.id, Tests: 0, Failures: 0, Errors: 0, Disabled: 0, Skipped: 0, Cases: make([]TestCase, 0), } o.suites[result.Resource.Path] = suite } sig, _ := result.Resource.Signature() var objectName string if len(sig.Namespace) > 0 { objectName = fmt.Sprintf("%s/%s", sig.Namespace, sig.Name) } else { objectName = sig.Name } typeName := fmt.Sprintf("%s@%s", sig.Kind, sig.Version) testCase := TestCase{ClassName: typeName, Name: objectName} switch result.Status { case validator.Valid: o.nValid++ case validator.Invalid: o.nInvalid++ failure := TestCaseError{Message: result.Err.Error()} testCase.Failure = append(testCase.Failure, failure) case validator.Error: o.nErrors++ testCase.Error = &TestCaseError{Message: result.Err.Error()} case validator.Skipped: testCase.Skipped = &TestCaseSkipped{} o.nSkipped++ case validator.Empty: return nil } suite.Tests++ suite.Cases = append(suite.Cases, testCase) return nil } // Flush outputs the results as XML func (o *junito) Flush() error { runtime := time.Now().Sub(o.startTime) var suites = make([]TestSuite, 0) for _, suite := range o.suites { suites = append(suites, *suite) } root := TestSuiteCollection{ Name: "kubeconform", Time: runtime.Seconds(), Tests: o.nValid + o.nInvalid + o.nErrors + o.nSkipped, Failures: o.nInvalid, Errors: o.nErrors, Disabled: o.nSkipped, Suites: suites, } // 2-space indentation content, err := xml.MarshalIndent(root, "", " ") if err != nil { return err } writer := bufio.NewWriter(o.w) writer.Write(content) writer.WriteByte('\n') writer.Flush() return nil }