mirror of
https://github.com/jstemmer/go-junit-report.git
synced 2025-04-05 05:00:15 -05:00

The junit package shouldn't need to know anything about benchmarks and gtr.Benchmark will be removed in a future commit. Instead, it will be the responsibility of the gotest parser to represent benchmarks using gtr.Test.
240 lines
6.3 KiB
Go
240 lines
6.3 KiB
Go
// Package junit defines a JUnit XML report and includes convenience methods
|
|
// for working with these reports.
|
|
package junit
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jstemmer/go-junit-report/v2/gtr"
|
|
)
|
|
|
|
// Testsuites is a collection of JUnit testsuites.
|
|
type Testsuites struct {
|
|
XMLName xml.Name `xml:"testsuites"`
|
|
|
|
Name string `xml:"name,attr,omitempty"`
|
|
Time string `xml:"time,attr,omitempty"` // total duration in seconds
|
|
Tests int `xml:"tests,attr,omitempty"`
|
|
Errors int `xml:"errors,attr,omitempty"`
|
|
Failures int `xml:"failures,attr,omitempty"`
|
|
Skipped int `xml:"skipped,attr,omitempty"`
|
|
Disabled int `xml:"disabled,attr,omitempty"`
|
|
|
|
Suites []Testsuite `xml:"testsuite,omitempty"`
|
|
}
|
|
|
|
// AddSuite adds a Testsuite and updates this testssuites' totals.
|
|
func (t *Testsuites) AddSuite(ts Testsuite) {
|
|
t.Suites = append(t.Suites, ts)
|
|
t.Tests += ts.Tests
|
|
t.Errors += ts.Errors
|
|
t.Failures += ts.Failures
|
|
t.Skipped += ts.Skipped
|
|
t.Disabled += ts.Disabled
|
|
}
|
|
|
|
// Testsuite is a single JUnit testsuite containing testcases.
|
|
type Testsuite struct {
|
|
// required attributes
|
|
Name string `xml:"name,attr"`
|
|
Tests int `xml:"tests,attr"`
|
|
Failures int `xml:"failures,attr"`
|
|
Errors int `xml:"errors,attr"`
|
|
ID int `xml:"id,attr"`
|
|
|
|
// optional attributes
|
|
Disabled int `xml:"disabled,attr,omitempty"`
|
|
Hostname string `xml:"hostname,attr,omitempty"`
|
|
Package string `xml:"package,attr,omitempty"`
|
|
Skipped int `xml:"skipped,attr,omitempty"`
|
|
Time string `xml:"time,attr"` // duration in seconds
|
|
Timestamp string `xml:"timestamp,attr,omitempty"` // date and time in ISO8601
|
|
|
|
Properties *[]Property `xml:"properties>property,omitempty"`
|
|
Testcases []Testcase `xml:"testcase,omitempty"`
|
|
SystemOut *Output `xml:"system-out,omitempty"`
|
|
SystemErr *Output `xml:"system-err,omitempty"`
|
|
}
|
|
|
|
// AddProperty adds a property with the given name and value to this Testsuite.
|
|
func (t *Testsuite) AddProperty(name, value string) {
|
|
prop := Property{Name: name, Value: value}
|
|
if t.Properties == nil {
|
|
t.Properties = &[]Property{prop}
|
|
return
|
|
}
|
|
props := append(*t.Properties, prop)
|
|
t.Properties = &props
|
|
}
|
|
|
|
// AddTestcase adds Testcase tc to this Testsuite.
|
|
func (t *Testsuite) AddTestcase(tc Testcase) {
|
|
t.Testcases = append(t.Testcases, tc)
|
|
t.Tests += 1
|
|
|
|
if tc.Error != nil {
|
|
t.Errors += 1
|
|
}
|
|
|
|
if tc.Failure != nil {
|
|
t.Failures += 1
|
|
}
|
|
|
|
if tc.Skipped != nil {
|
|
t.Skipped += 1
|
|
}
|
|
}
|
|
|
|
// SetTimestamp sets the timestamp in this Testsuite.
|
|
func (ts *Testsuite) SetTimestamp(t time.Time) {
|
|
ts.Timestamp = t.Format(time.RFC3339)
|
|
}
|
|
|
|
// Testcase represents a single test with its results.
|
|
type Testcase struct {
|
|
// required attributes
|
|
Name string `xml:"name,attr"`
|
|
Classname string `xml:"classname,attr"`
|
|
|
|
// optional attributes
|
|
Time string `xml:"time,attr,omitempty"` // duration in seconds
|
|
Status string `xml:"status,attr,omitempty"`
|
|
|
|
Skipped *Result `xml:"skipped,omitempty"`
|
|
Error *Result `xml:"error,omitempty"`
|
|
Failure *Result `xml:"failure,omitempty"`
|
|
SystemOut *Output `xml:"system-out,omitempty"`
|
|
SystemErr *Output `xml:"system-err,omitempty"`
|
|
}
|
|
|
|
// Property represents a key/value pair.
|
|
type Property struct {
|
|
Name string `xml:"name,attr"`
|
|
Value string `xml:"value,attr"`
|
|
}
|
|
|
|
// Result represents the result of a single test.
|
|
type Result struct {
|
|
Message string `xml:"message,attr"`
|
|
Type string `xml:"type,attr,omitempty"`
|
|
Data string `xml:",cdata"`
|
|
}
|
|
|
|
// Output represents output written to stdout or sderr.
|
|
type Output struct {
|
|
Data string `xml:",cdata"`
|
|
}
|
|
|
|
// CreateFromReport creates a JUnit representation of the given gtr.Report.
|
|
func CreateFromReport(report gtr.Report, hostname string) Testsuites {
|
|
var suites Testsuites
|
|
for _, pkg := range report.Packages {
|
|
var duration time.Duration
|
|
suite := Testsuite{
|
|
Name: pkg.Name,
|
|
Hostname: hostname,
|
|
ID: len(suites.Suites),
|
|
}
|
|
|
|
if !pkg.Timestamp.IsZero() {
|
|
suite.SetTimestamp(pkg.Timestamp)
|
|
}
|
|
|
|
for k, v := range pkg.Properties {
|
|
suite.AddProperty(k, v)
|
|
}
|
|
|
|
if len(pkg.Output) > 0 {
|
|
suite.SystemOut = &Output{Data: formatOutput(pkg.Output, 0)}
|
|
}
|
|
|
|
if pkg.Coverage > 0 {
|
|
suite.AddProperty("coverage.statements.pct", fmt.Sprintf("%.2f", pkg.Coverage))
|
|
}
|
|
|
|
for _, test := range pkg.Tests {
|
|
duration += test.Duration
|
|
suite.AddTestcase(createTestcaseForTest(pkg.Name, test))
|
|
}
|
|
|
|
// JUnit doesn't have a good way of dealing with build or runtime
|
|
// errors that happen before a test has started, so we create a single
|
|
// failing test that contains the build error details.
|
|
if pkg.BuildError.Name != "" {
|
|
tc := Testcase{
|
|
Classname: pkg.BuildError.Name,
|
|
Name: pkg.BuildError.Cause,
|
|
Time: formatDuration(0),
|
|
Error: &Result{
|
|
Message: "Build error",
|
|
Data: strings.Join(pkg.BuildError.Output, "\n"),
|
|
},
|
|
}
|
|
suite.AddTestcase(tc)
|
|
}
|
|
|
|
if pkg.RunError.Name != "" {
|
|
tc := Testcase{
|
|
Classname: pkg.RunError.Name,
|
|
Name: "Failure",
|
|
Time: formatDuration(0),
|
|
Error: &Result{
|
|
Message: "Runtime error",
|
|
Data: strings.Join(pkg.RunError.Output, "\n"),
|
|
},
|
|
}
|
|
suite.AddTestcase(tc)
|
|
}
|
|
|
|
if (pkg.Duration) == 0 {
|
|
suite.Time = formatDuration(duration)
|
|
} else {
|
|
suite.Time = formatDuration(pkg.Duration)
|
|
}
|
|
suites.AddSuite(suite)
|
|
}
|
|
return suites
|
|
}
|
|
|
|
func createTestcaseForTest(pkgName string, test gtr.Test) Testcase {
|
|
tc := Testcase{
|
|
Classname: pkgName,
|
|
Name: test.Name,
|
|
Time: formatDuration(test.Duration),
|
|
}
|
|
|
|
if test.Result == gtr.Fail {
|
|
tc.Failure = &Result{
|
|
Message: "Failed",
|
|
Data: formatOutput(test.Output, test.Level),
|
|
}
|
|
} else if test.Result == gtr.Skip {
|
|
tc.Skipped = &Result{
|
|
Message: "Skipped",
|
|
Data: formatOutput(test.Output, test.Level),
|
|
}
|
|
} else if test.Result == gtr.Unknown {
|
|
tc.Error = &Result{
|
|
Message: "No test result found",
|
|
Data: formatOutput(test.Output, test.Level),
|
|
}
|
|
} else if len(test.Output) > 0 {
|
|
tc.SystemOut = &Output{Data: formatOutput(test.Output, test.Level)}
|
|
}
|
|
return tc
|
|
}
|
|
|
|
// formatDuration returns the JUnit string representation of the given
|
|
// duration.
|
|
func formatDuration(d time.Duration) string {
|
|
return fmt.Sprintf("%.3f", d.Seconds())
|
|
}
|
|
|
|
// formatOutput combines the lines from the given output into a single string.
|
|
func formatOutput(output []string, indent int) string {
|
|
return strings.Join(output, "\n")
|
|
}
|