359 lines
12 KiB
Go
359 lines
12 KiB
Go
|
/*
|
||
|
|
||
|
JUnit XML Reporter for Ginkgo
|
||
|
|
||
|
For usage instructions: http://onsi.github.io/ginkgo/#generating_junit_xml_output
|
||
|
|
||
|
The schema used for the generated JUnit xml file was adapted from https://llg.cubic.org/docs/junit/
|
||
|
|
||
|
*/
|
||
|
|
||
|
package reporters
|
||
|
|
||
|
import (
|
||
|
"encoding/xml"
|
||
|
"fmt"
|
||
|
"os"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/onsi/ginkgo/v2/config"
|
||
|
"github.com/onsi/ginkgo/v2/types"
|
||
|
)
|
||
|
|
||
|
type JUnitTestSuites struct {
|
||
|
XMLName xml.Name `xml:"testsuites"`
|
||
|
// Tests maps onto the total number of specs in all test suites (this includes any suite nodes such as BeforeSuite)
|
||
|
Tests int `xml:"tests,attr"`
|
||
|
// Disabled maps onto specs that are pending and/or skipped
|
||
|
Disabled int `xml:"disabled,attr"`
|
||
|
// Errors maps onto specs that panicked or were interrupted
|
||
|
Errors int `xml:"errors,attr"`
|
||
|
// Failures maps onto specs that failed
|
||
|
Failures int `xml:"failures,attr"`
|
||
|
// Time is the time in seconds to execute all test suites
|
||
|
Time float64 `xml:"time,attr"`
|
||
|
|
||
|
//The set of all test suites
|
||
|
TestSuites []JUnitTestSuite `xml:"testsuite"`
|
||
|
}
|
||
|
|
||
|
type JUnitTestSuite struct {
|
||
|
// Name maps onto the description of the test suite - maps onto Report.SuiteDescription
|
||
|
Name string `xml:"name,attr"`
|
||
|
// Package maps onto the absolute path to the test suite - maps onto Report.SuitePath
|
||
|
Package string `xml:"package,attr"`
|
||
|
// Tests maps onto the total number of specs in the test suite (this includes any suite nodes such as BeforeSuite)
|
||
|
Tests int `xml:"tests,attr"`
|
||
|
// Disabled maps onto specs that are pending
|
||
|
Disabled int `xml:"disabled,attr"`
|
||
|
// Skiped maps onto specs that are skipped
|
||
|
Skipped int `xml:"skipped,attr"`
|
||
|
// Errors maps onto specs that panicked or were interrupted
|
||
|
Errors int `xml:"errors,attr"`
|
||
|
// Failures maps onto specs that failed
|
||
|
Failures int `xml:"failures,attr"`
|
||
|
// Time is the time in seconds to execute all the test suite - maps onto Report.RunTime
|
||
|
Time float64 `xml:"time,attr"`
|
||
|
// Timestamp is the ISO 8601 formatted start-time of the suite - maps onto Report.StartTime
|
||
|
Timestamp string `xml:"timestamp,attr"`
|
||
|
|
||
|
//Properties captures the information stored in the rest of the Report type (including SuiteConfig) as key-value pairs
|
||
|
Properties JUnitProperties `xml:"properties"`
|
||
|
|
||
|
//TestCases capture the individual specs
|
||
|
TestCases []JUnitTestCase `xml:"testcase"`
|
||
|
}
|
||
|
|
||
|
type JUnitProperties struct {
|
||
|
Properties []JUnitProperty `xml:"property"`
|
||
|
}
|
||
|
|
||
|
func (jup JUnitProperties) WithName(name string) string {
|
||
|
for _, property := range jup.Properties {
|
||
|
if property.Name == name {
|
||
|
return property.Value
|
||
|
}
|
||
|
}
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
type JUnitProperty struct {
|
||
|
Name string `xml:"name,attr"`
|
||
|
Value string `xml:"value,attr"`
|
||
|
}
|
||
|
|
||
|
type JUnitTestCase struct {
|
||
|
// Name maps onto the full text of the spec - equivalent to "[SpecReport.LeafNodeType] SpecReport.FullText()"
|
||
|
Name string `xml:"name,attr"`
|
||
|
// Classname maps onto the name of the test suite - equivalent to Report.SuiteDescription
|
||
|
Classname string `xml:"classname,attr"`
|
||
|
// Status maps onto the string representation of SpecReport.State
|
||
|
Status string `xml:"status,attr"`
|
||
|
// Time is the time in seconds to execute the spec - maps onto SpecReport.RunTime
|
||
|
Time float64 `xml:"time,attr"`
|
||
|
//Skipped is populated with a message if the test was skipped or pending
|
||
|
Skipped *JUnitSkipped `xml:"skipped,omitempty"`
|
||
|
//Error is populated if the test panicked or was interrupted
|
||
|
Error *JUnitError `xml:"error,omitempty"`
|
||
|
//Failure is populated if the test failed
|
||
|
Failure *JUnitFailure `xml:"failure,omitempty"`
|
||
|
//SystemOut maps onto any captured stdout/stderr output - maps onto SpecReport.CapturedStdOutErr
|
||
|
SystemOut string `xml:"system-out,omitempty"`
|
||
|
//SystemOut maps onto any captured GinkgoWriter output - maps onto SpecReport.CapturedGinkgoWriterOutput
|
||
|
SystemErr string `xml:"system-err,omitempty"`
|
||
|
}
|
||
|
|
||
|
type JUnitSkipped struct {
|
||
|
// Message maps onto "pending" if the test was marked pending, "skipped" if the test was marked skipped, and "skipped - REASON" if the user called Skip(REASON)
|
||
|
Message string `xml:"message,attr"`
|
||
|
}
|
||
|
|
||
|
type JUnitError struct {
|
||
|
//Message maps onto the panic/exception thrown - equivalent to SpecReport.Failure.ForwardedPanic - or to "interrupted"
|
||
|
Message string `xml:"message,attr"`
|
||
|
//Type is one of "panicked" or "interrupted"
|
||
|
Type string `xml:"type,attr"`
|
||
|
//Description maps onto the captured stack trace for a panic, or the failure message for an interrupt which will include the dump of running goroutines
|
||
|
Description string `xml:",chardata"`
|
||
|
}
|
||
|
|
||
|
type JUnitFailure struct {
|
||
|
//Message maps onto the failure message - equivalent to SpecReport.Failure.Message
|
||
|
Message string `xml:"message,attr"`
|
||
|
//Type is "failed"
|
||
|
Type string `xml:"type,attr"`
|
||
|
//Description maps onto the location and stack trace of the failure
|
||
|
Description string `xml:",chardata"`
|
||
|
}
|
||
|
|
||
|
func GenerateJUnitReport(report types.Report, dst string) error {
|
||
|
suite := JUnitTestSuite{
|
||
|
Name: report.SuiteDescription,
|
||
|
Package: report.SuitePath,
|
||
|
Time: report.RunTime.Seconds(),
|
||
|
Timestamp: report.StartTime.Format("2006-01-02T15:04:05"),
|
||
|
Properties: JUnitProperties{
|
||
|
Properties: []JUnitProperty{
|
||
|
{"SuiteSucceeded", fmt.Sprintf("%t", report.SuiteSucceeded)},
|
||
|
{"SuiteHasProgrammaticFocus", fmt.Sprintf("%t", report.SuiteHasProgrammaticFocus)},
|
||
|
{"SpecialSuiteFailureReason", strings.Join(report.SpecialSuiteFailureReasons, ",")},
|
||
|
{"SuiteLabels", fmt.Sprintf("[%s]", strings.Join(report.SuiteLabels, ","))},
|
||
|
{"RandomSeed", fmt.Sprintf("%d", report.SuiteConfig.RandomSeed)},
|
||
|
{"RandomizeAllSpecs", fmt.Sprintf("%t", report.SuiteConfig.RandomizeAllSpecs)},
|
||
|
{"LabelFilter", report.SuiteConfig.LabelFilter},
|
||
|
{"FocusStrings", strings.Join(report.SuiteConfig.FocusStrings, ",")},
|
||
|
{"SkipStrings", strings.Join(report.SuiteConfig.SkipStrings, ",")},
|
||
|
{"FocusFiles", strings.Join(report.SuiteConfig.FocusFiles, ";")},
|
||
|
{"SkipFiles", strings.Join(report.SuiteConfig.SkipFiles, ";")},
|
||
|
{"FailOnPending", fmt.Sprintf("%t", report.SuiteConfig.FailOnPending)},
|
||
|
{"FailFast", fmt.Sprintf("%t", report.SuiteConfig.FailFast)},
|
||
|
{"FlakeAttempts", fmt.Sprintf("%d", report.SuiteConfig.FlakeAttempts)},
|
||
|
{"EmitSpecProgress", fmt.Sprintf("%t", report.SuiteConfig.EmitSpecProgress)},
|
||
|
{"DryRun", fmt.Sprintf("%t", report.SuiteConfig.DryRun)},
|
||
|
{"ParallelTotal", fmt.Sprintf("%d", report.SuiteConfig.ParallelTotal)},
|
||
|
{"OutputInterceptorMode", report.SuiteConfig.OutputInterceptorMode},
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
for _, spec := range report.SpecReports {
|
||
|
name := fmt.Sprintf("[%s]", spec.LeafNodeType)
|
||
|
if spec.FullText() != "" {
|
||
|
name = name + " " + spec.FullText()
|
||
|
}
|
||
|
labels := spec.Labels()
|
||
|
if len(labels) > 0 {
|
||
|
name = name + " [" + strings.Join(labels, ", ") + "]"
|
||
|
}
|
||
|
|
||
|
test := JUnitTestCase{
|
||
|
Name: name,
|
||
|
Classname: report.SuiteDescription,
|
||
|
Status: spec.State.String(),
|
||
|
Time: spec.RunTime.Seconds(),
|
||
|
SystemOut: systemOutForUnstructuredReporters(spec),
|
||
|
SystemErr: systemErrForUnstructuredReporters(spec),
|
||
|
}
|
||
|
suite.Tests += 1
|
||
|
|
||
|
switch spec.State {
|
||
|
case types.SpecStateSkipped:
|
||
|
message := "skipped"
|
||
|
if spec.Failure.Message != "" {
|
||
|
message += " - " + spec.Failure.Message
|
||
|
}
|
||
|
test.Skipped = &JUnitSkipped{Message: message}
|
||
|
suite.Skipped += 1
|
||
|
case types.SpecStatePending:
|
||
|
test.Skipped = &JUnitSkipped{Message: "pending"}
|
||
|
suite.Disabled += 1
|
||
|
case types.SpecStateFailed:
|
||
|
test.Failure = &JUnitFailure{
|
||
|
Message: spec.Failure.Message,
|
||
|
Type: "failed",
|
||
|
Description: failureDescriptionForUnstructuredReporters(spec),
|
||
|
}
|
||
|
suite.Failures += 1
|
||
|
case types.SpecStateTimedout:
|
||
|
test.Failure = &JUnitFailure{
|
||
|
Message: spec.Failure.Message,
|
||
|
Type: "timedout",
|
||
|
Description: failureDescriptionForUnstructuredReporters(spec),
|
||
|
}
|
||
|
suite.Failures += 1
|
||
|
case types.SpecStateInterrupted:
|
||
|
test.Error = &JUnitError{
|
||
|
Message: spec.Failure.Message,
|
||
|
Type: "interrupted",
|
||
|
Description: failureDescriptionForUnstructuredReporters(spec),
|
||
|
}
|
||
|
suite.Errors += 1
|
||
|
case types.SpecStateAborted:
|
||
|
test.Failure = &JUnitFailure{
|
||
|
Message: spec.Failure.Message,
|
||
|
Type: "aborted",
|
||
|
Description: failureDescriptionForUnstructuredReporters(spec),
|
||
|
}
|
||
|
suite.Errors += 1
|
||
|
case types.SpecStatePanicked:
|
||
|
test.Error = &JUnitError{
|
||
|
Message: spec.Failure.ForwardedPanic,
|
||
|
Type: "panicked",
|
||
|
Description: failureDescriptionForUnstructuredReporters(spec),
|
||
|
}
|
||
|
suite.Errors += 1
|
||
|
}
|
||
|
|
||
|
suite.TestCases = append(suite.TestCases, test)
|
||
|
}
|
||
|
|
||
|
junitReport := JUnitTestSuites{
|
||
|
Tests: suite.Tests,
|
||
|
Disabled: suite.Disabled + suite.Skipped,
|
||
|
Errors: suite.Errors,
|
||
|
Failures: suite.Failures,
|
||
|
Time: suite.Time,
|
||
|
TestSuites: []JUnitTestSuite{suite},
|
||
|
}
|
||
|
|
||
|
f, err := os.Create(dst)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
f.WriteString(xml.Header)
|
||
|
encoder := xml.NewEncoder(f)
|
||
|
encoder.Indent(" ", " ")
|
||
|
encoder.Encode(junitReport)
|
||
|
|
||
|
return f.Close()
|
||
|
}
|
||
|
|
||
|
func MergeAndCleanupJUnitReports(sources []string, dst string) ([]string, error) {
|
||
|
messages := []string{}
|
||
|
mergedReport := JUnitTestSuites{}
|
||
|
for _, source := range sources {
|
||
|
report := JUnitTestSuites{}
|
||
|
f, err := os.Open(source)
|
||
|
if err != nil {
|
||
|
messages = append(messages, fmt.Sprintf("Could not open %s:\n%s", source, err.Error()))
|
||
|
continue
|
||
|
}
|
||
|
err = xml.NewDecoder(f).Decode(&report)
|
||
|
if err != nil {
|
||
|
messages = append(messages, fmt.Sprintf("Could not decode %s:\n%s", source, err.Error()))
|
||
|
continue
|
||
|
}
|
||
|
os.Remove(source)
|
||
|
|
||
|
mergedReport.Tests += report.Tests
|
||
|
mergedReport.Disabled += report.Disabled
|
||
|
mergedReport.Errors += report.Errors
|
||
|
mergedReport.Failures += report.Failures
|
||
|
mergedReport.Time += report.Time
|
||
|
mergedReport.TestSuites = append(mergedReport.TestSuites, report.TestSuites...)
|
||
|
}
|
||
|
|
||
|
f, err := os.Create(dst)
|
||
|
if err != nil {
|
||
|
return messages, err
|
||
|
}
|
||
|
f.WriteString(xml.Header)
|
||
|
encoder := xml.NewEncoder(f)
|
||
|
encoder.Indent(" ", " ")
|
||
|
encoder.Encode(mergedReport)
|
||
|
|
||
|
return messages, f.Close()
|
||
|
}
|
||
|
|
||
|
func failureDescriptionForUnstructuredReporters(spec types.SpecReport) string {
|
||
|
out := &strings.Builder{}
|
||
|
out.WriteString(spec.Failure.Location.String() + "\n")
|
||
|
out.WriteString(spec.Failure.Location.FullStackTrace)
|
||
|
if !spec.Failure.ProgressReport.IsZero() {
|
||
|
out.WriteString("\n")
|
||
|
NewDefaultReporter(types.ReporterConfig{NoColor: true}, out).EmitProgressReport(spec.Failure.ProgressReport)
|
||
|
}
|
||
|
if len(spec.AdditionalFailures) > 0 {
|
||
|
out.WriteString("\nThere were additional failures detected after the initial failure:\n")
|
||
|
for i, additionalFailure := range spec.AdditionalFailures {
|
||
|
NewDefaultReporter(types.ReporterConfig{NoColor: true}, out).EmitFailure(0, additionalFailure.State, additionalFailure.Failure, true)
|
||
|
if i < len(spec.AdditionalFailures)-1 {
|
||
|
out.WriteString("----------\n")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return out.String()
|
||
|
}
|
||
|
|
||
|
func systemErrForUnstructuredReporters(spec types.SpecReport) string {
|
||
|
out := &strings.Builder{}
|
||
|
gw := spec.CapturedGinkgoWriterOutput
|
||
|
cursor := 0
|
||
|
for _, pr := range spec.ProgressReports {
|
||
|
if cursor < pr.GinkgoWriterOffset {
|
||
|
if pr.GinkgoWriterOffset < len(gw) {
|
||
|
out.WriteString(gw[cursor:pr.GinkgoWriterOffset])
|
||
|
cursor = pr.GinkgoWriterOffset
|
||
|
} else if cursor < len(gw) {
|
||
|
out.WriteString(gw[cursor:])
|
||
|
cursor = len(gw)
|
||
|
}
|
||
|
}
|
||
|
NewDefaultReporter(types.ReporterConfig{NoColor: true}, out).EmitProgressReport(pr)
|
||
|
}
|
||
|
|
||
|
if cursor < len(gw) {
|
||
|
out.WriteString(gw[cursor:])
|
||
|
}
|
||
|
|
||
|
return out.String()
|
||
|
}
|
||
|
|
||
|
func systemOutForUnstructuredReporters(spec types.SpecReport) string {
|
||
|
systemOut := spec.CapturedStdOutErr
|
||
|
if len(spec.ReportEntries) > 0 {
|
||
|
systemOut += "\nReport Entries:\n"
|
||
|
for i, entry := range spec.ReportEntries {
|
||
|
systemOut += fmt.Sprintf("%s\n%s\n%s\n", entry.Name, entry.Location, entry.Time.Format(time.RFC3339Nano))
|
||
|
if representation := entry.StringRepresentation(); representation != "" {
|
||
|
systemOut += representation + "\n"
|
||
|
}
|
||
|
if i+1 < len(spec.ReportEntries) {
|
||
|
systemOut += "--\n"
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return systemOut
|
||
|
}
|
||
|
|
||
|
// Deprecated JUnitReporter (so folks can still compile their suites)
|
||
|
type JUnitReporter struct{}
|
||
|
|
||
|
func NewJUnitReporter(_ string) *JUnitReporter { return &JUnitReporter{} }
|
||
|
func (reporter *JUnitReporter) SuiteWillBegin(_ config.GinkgoConfigType, _ *types.SuiteSummary) {}
|
||
|
func (reporter *JUnitReporter) BeforeSuiteDidRun(_ *types.SetupSummary) {}
|
||
|
func (reporter *JUnitReporter) SpecWillRun(_ *types.SpecSummary) {}
|
||
|
func (reporter *JUnitReporter) SpecDidComplete(_ *types.SpecSummary) {}
|
||
|
func (reporter *JUnitReporter) AfterSuiteDidRun(_ *types.SetupSummary) {}
|
||
|
func (reporter *JUnitReporter) SuiteDidEnd(_ *types.SuiteSummary) {}
|