report.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. /*Package junitxml creates a JUnit XML report from a testjson.Execution.
  2. */
  3. package junitxml
  4. import (
  5. "encoding/xml"
  6. "fmt"
  7. "io"
  8. "os"
  9. "os/exec"
  10. "strings"
  11. "time"
  12. "github.com/pkg/errors"
  13. "github.com/sirupsen/logrus"
  14. "gotest.tools/gotestsum/testjson"
  15. )
  16. // JUnitTestSuites is a collection of JUnit test suites.
  17. type JUnitTestSuites struct {
  18. XMLName xml.Name `xml:"testsuites"`
  19. Suites []JUnitTestSuite
  20. }
  21. // JUnitTestSuite is a single JUnit test suite which may contain many
  22. // testcases.
  23. type JUnitTestSuite struct {
  24. XMLName xml.Name `xml:"testsuite"`
  25. Tests int `xml:"tests,attr"`
  26. Failures int `xml:"failures,attr"`
  27. Time string `xml:"time,attr"`
  28. Name string `xml:"name,attr"`
  29. Properties []JUnitProperty `xml:"properties>property,omitempty"`
  30. TestCases []JUnitTestCase
  31. }
  32. // JUnitTestCase is a single test case with its result.
  33. type JUnitTestCase struct {
  34. XMLName xml.Name `xml:"testcase"`
  35. Classname string `xml:"classname,attr"`
  36. Name string `xml:"name,attr"`
  37. Time string `xml:"time,attr"`
  38. SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"`
  39. Failure *JUnitFailure `xml:"failure,omitempty"`
  40. }
  41. // JUnitSkipMessage contains the reason why a testcase was skipped.
  42. type JUnitSkipMessage struct {
  43. Message string `xml:"message,attr"`
  44. }
  45. // JUnitProperty represents a key/value pair used to define properties.
  46. type JUnitProperty struct {
  47. Name string `xml:"name,attr"`
  48. Value string `xml:"value,attr"`
  49. }
  50. // JUnitFailure contains data related to a failed test.
  51. type JUnitFailure struct {
  52. Message string `xml:"message,attr"`
  53. Type string `xml:"type,attr"`
  54. Contents string `xml:",chardata"`
  55. }
  56. // Write creates an XML document and writes it to out.
  57. func Write(out io.Writer, exec *testjson.Execution) error {
  58. return errors.Wrap(write(out, generate(exec)), "failed to write JUnit XML")
  59. }
  60. func generate(exec *testjson.Execution) JUnitTestSuites {
  61. version := goVersion()
  62. suites := JUnitTestSuites{}
  63. for _, pkgname := range exec.Packages() {
  64. pkg := exec.Package(pkgname)
  65. junitpkg := JUnitTestSuite{
  66. Name: pkgname,
  67. Tests: pkg.Total,
  68. Time: formatDurationAsSeconds(pkg.Elapsed()),
  69. Properties: packageProperties(version),
  70. TestCases: packageTestCases(pkg),
  71. Failures: len(pkg.Failed),
  72. }
  73. suites.Suites = append(suites.Suites, junitpkg)
  74. }
  75. return suites
  76. }
  77. func formatDurationAsSeconds(d time.Duration) string {
  78. return fmt.Sprintf("%f", d.Seconds())
  79. }
  80. func packageProperties(goVersion string) []JUnitProperty {
  81. return []JUnitProperty{
  82. {Name: "go.version", Value: goVersion},
  83. }
  84. }
  85. // goVersion returns the version as reported by the go binary in PATH. This
  86. // version will not be the same as runtime.Version, which is always the version
  87. // of go used to build the gotestsum binary.
  88. //
  89. // To skip the os/exec call set the GOVERSION environment variable to the
  90. // desired value.
  91. func goVersion() string {
  92. if version, ok := os.LookupEnv("GOVERSION"); ok {
  93. return version
  94. }
  95. logrus.Debugf("exec: go version")
  96. cmd := exec.Command("go", "version")
  97. out, err := cmd.Output()
  98. if err != nil {
  99. logrus.WithError(err).Warn("failed to lookup go version for junit xml")
  100. return "unknown"
  101. }
  102. return strings.TrimPrefix(strings.TrimSpace(string(out)), "go version ")
  103. }
  104. func packageTestCases(pkg *testjson.Package) []JUnitTestCase {
  105. cases := []JUnitTestCase{}
  106. if pkg.TestMainFailed() {
  107. jtc := newJUnitTestCase(testjson.TestCase{
  108. Test: "TestMain",
  109. })
  110. jtc.Failure = &JUnitFailure{
  111. Message: "Failed",
  112. Contents: pkg.Output(""),
  113. }
  114. cases = append(cases, jtc)
  115. }
  116. for _, tc := range pkg.Failed {
  117. jtc := newJUnitTestCase(tc)
  118. jtc.Failure = &JUnitFailure{
  119. Message: "Failed",
  120. Contents: pkg.Output(tc.Test),
  121. }
  122. cases = append(cases, jtc)
  123. }
  124. for _, tc := range pkg.Skipped {
  125. jtc := newJUnitTestCase(tc)
  126. jtc.SkipMessage = &JUnitSkipMessage{Message: pkg.Output(tc.Test)}
  127. cases = append(cases, jtc)
  128. }
  129. for _, tc := range pkg.Passed {
  130. jtc := newJUnitTestCase(tc)
  131. cases = append(cases, jtc)
  132. }
  133. return cases
  134. }
  135. func newJUnitTestCase(tc testjson.TestCase) JUnitTestCase {
  136. return JUnitTestCase{
  137. Classname: tc.Package,
  138. Name: tc.Test,
  139. Time: formatDurationAsSeconds(tc.Elapsed),
  140. }
  141. }
  142. func write(out io.Writer, suites JUnitTestSuites) error {
  143. doc, err := xml.MarshalIndent(suites, "", "\t")
  144. if err != nil {
  145. return err
  146. }
  147. _, err = out.Write([]byte(xml.Header))
  148. if err != nil {
  149. return err
  150. }
  151. _, err = out.Write(doc)
  152. return err
  153. }