174 lines
4.5 KiB
Go
174 lines
4.5 KiB
Go
/*Package junitxml creates a JUnit XML report from a testjson.Execution.
|
|
*/
|
|
package junitxml
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"gotest.tools/gotestsum/testjson"
|
|
)
|
|
|
|
// JUnitTestSuites is a collection of JUnit test suites.
|
|
type JUnitTestSuites struct {
|
|
XMLName xml.Name `xml:"testsuites"`
|
|
Suites []JUnitTestSuite
|
|
}
|
|
|
|
// JUnitTestSuite is a single JUnit test suite which may contain many
|
|
// testcases.
|
|
type JUnitTestSuite struct {
|
|
XMLName xml.Name `xml:"testsuite"`
|
|
Tests int `xml:"tests,attr"`
|
|
Failures int `xml:"failures,attr"`
|
|
Time string `xml:"time,attr"`
|
|
Name string `xml:"name,attr"`
|
|
Properties []JUnitProperty `xml:"properties>property,omitempty"`
|
|
TestCases []JUnitTestCase
|
|
}
|
|
|
|
// JUnitTestCase is a single test case with its result.
|
|
type JUnitTestCase struct {
|
|
XMLName xml.Name `xml:"testcase"`
|
|
Classname string `xml:"classname,attr"`
|
|
Name string `xml:"name,attr"`
|
|
Time string `xml:"time,attr"`
|
|
SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"`
|
|
Failure *JUnitFailure `xml:"failure,omitempty"`
|
|
}
|
|
|
|
// JUnitSkipMessage contains the reason why a testcase was skipped.
|
|
type JUnitSkipMessage struct {
|
|
Message string `xml:"message,attr"`
|
|
}
|
|
|
|
// JUnitProperty represents a key/value pair used to define properties.
|
|
type JUnitProperty struct {
|
|
Name string `xml:"name,attr"`
|
|
Value string `xml:"value,attr"`
|
|
}
|
|
|
|
// JUnitFailure contains data related to a failed test.
|
|
type JUnitFailure struct {
|
|
Message string `xml:"message,attr"`
|
|
Type string `xml:"type,attr"`
|
|
Contents string `xml:",chardata"`
|
|
}
|
|
|
|
// Write creates an XML document and writes it to out.
|
|
func Write(out io.Writer, exec *testjson.Execution) error {
|
|
return errors.Wrap(write(out, generate(exec)), "failed to write JUnit XML")
|
|
}
|
|
|
|
func generate(exec *testjson.Execution) JUnitTestSuites {
|
|
version := goVersion()
|
|
suites := JUnitTestSuites{}
|
|
for _, pkgname := range exec.Packages() {
|
|
pkg := exec.Package(pkgname)
|
|
junitpkg := JUnitTestSuite{
|
|
Name: pkgname,
|
|
Tests: pkg.Total,
|
|
Time: formatDurationAsSeconds(pkg.Elapsed()),
|
|
Properties: packageProperties(version),
|
|
TestCases: packageTestCases(pkg),
|
|
Failures: len(pkg.Failed),
|
|
}
|
|
suites.Suites = append(suites.Suites, junitpkg)
|
|
}
|
|
return suites
|
|
}
|
|
|
|
func formatDurationAsSeconds(d time.Duration) string {
|
|
return fmt.Sprintf("%f", d.Seconds())
|
|
}
|
|
|
|
func packageProperties(goVersion string) []JUnitProperty {
|
|
return []JUnitProperty{
|
|
{Name: "go.version", Value: goVersion},
|
|
}
|
|
}
|
|
|
|
// goVersion returns the version as reported by the go binary in PATH. This
|
|
// version will not be the same as runtime.Version, which is always the version
|
|
// of go used to build the gotestsum binary.
|
|
//
|
|
// To skip the os/exec call set the GOVERSION environment variable to the
|
|
// desired value.
|
|
func goVersion() string {
|
|
if version, ok := os.LookupEnv("GOVERSION"); ok {
|
|
return version
|
|
}
|
|
logrus.Debugf("exec: go version")
|
|
cmd := exec.Command("go", "version")
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
logrus.WithError(err).Warn("failed to lookup go version for junit xml")
|
|
return "unknown"
|
|
}
|
|
return strings.TrimPrefix(strings.TrimSpace(string(out)), "go version ")
|
|
}
|
|
|
|
func packageTestCases(pkg *testjson.Package) []JUnitTestCase {
|
|
cases := []JUnitTestCase{}
|
|
|
|
if pkg.TestMainFailed() {
|
|
jtc := newJUnitTestCase(testjson.TestCase{
|
|
Test: "TestMain",
|
|
})
|
|
jtc.Failure = &JUnitFailure{
|
|
Message: "Failed",
|
|
Contents: pkg.Output(""),
|
|
}
|
|
cases = append(cases, jtc)
|
|
}
|
|
|
|
for _, tc := range pkg.Failed {
|
|
jtc := newJUnitTestCase(tc)
|
|
jtc.Failure = &JUnitFailure{
|
|
Message: "Failed",
|
|
Contents: pkg.Output(tc.Test),
|
|
}
|
|
cases = append(cases, jtc)
|
|
}
|
|
|
|
for _, tc := range pkg.Skipped {
|
|
jtc := newJUnitTestCase(tc)
|
|
jtc.SkipMessage = &JUnitSkipMessage{Message: pkg.Output(tc.Test)}
|
|
cases = append(cases, jtc)
|
|
}
|
|
|
|
for _, tc := range pkg.Passed {
|
|
jtc := newJUnitTestCase(tc)
|
|
cases = append(cases, jtc)
|
|
}
|
|
return cases
|
|
}
|
|
|
|
func newJUnitTestCase(tc testjson.TestCase) JUnitTestCase {
|
|
return JUnitTestCase{
|
|
Classname: tc.Package,
|
|
Name: tc.Test,
|
|
Time: formatDurationAsSeconds(tc.Elapsed),
|
|
}
|
|
}
|
|
|
|
func write(out io.Writer, suites JUnitTestSuites) error {
|
|
doc, err := xml.MarshalIndent(suites, "", "\t")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = out.Write([]byte(xml.Header))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = out.Write(doc)
|
|
return err
|
|
}
|