gotest.tools/gotestsum@v1.11.0/internal/junitxml/report.go (about) 1 /*Package junitxml creates a JUnit XML report from a testjson.Execution. 2 */ 3 package junitxml 4 5 import ( 6 "bytes" 7 "encoding/xml" 8 "fmt" 9 "io" 10 "os" 11 "os/exec" 12 "strings" 13 "time" 14 15 "gotest.tools/gotestsum/internal/log" 16 "gotest.tools/gotestsum/testjson" 17 ) 18 19 // JUnitTestSuites is a collection of JUnit test suites. 20 type JUnitTestSuites struct { 21 XMLName xml.Name `xml:"testsuites"` 22 Name string `xml:"name,attr,omitempty"` 23 Tests int `xml:"tests,attr"` 24 Failures int `xml:"failures,attr"` 25 Errors int `xml:"errors,attr"` 26 Time string `xml:"time,attr"` 27 Suites []JUnitTestSuite 28 } 29 30 // JUnitTestSuite is a single JUnit test suite which may contain many 31 // testcases. 32 type JUnitTestSuite struct { 33 XMLName xml.Name `xml:"testsuite"` 34 Tests int `xml:"tests,attr"` 35 Failures int `xml:"failures,attr"` 36 Time string `xml:"time,attr"` 37 Name string `xml:"name,attr"` 38 Properties []JUnitProperty `xml:"properties>property,omitempty"` 39 TestCases []JUnitTestCase 40 Timestamp string `xml:"timestamp,attr"` 41 } 42 43 // JUnitTestCase is a single test case with its result. 44 type JUnitTestCase struct { 45 XMLName xml.Name `xml:"testcase"` 46 Classname string `xml:"classname,attr"` 47 Name string `xml:"name,attr"` 48 Time string `xml:"time,attr"` 49 SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"` 50 Failure *JUnitFailure `xml:"failure,omitempty"` 51 } 52 53 // JUnitSkipMessage contains the reason why a testcase was skipped. 54 type JUnitSkipMessage struct { 55 Message string `xml:"message,attr"` 56 } 57 58 // JUnitProperty represents a key/value pair used to define properties. 59 type JUnitProperty struct { 60 Name string `xml:"name,attr"` 61 Value string `xml:"value,attr"` 62 } 63 64 // JUnitFailure contains data related to a failed test. 65 type JUnitFailure struct { 66 Message string `xml:"message,attr"` 67 Type string `xml:"type,attr"` 68 Contents string `xml:",chardata"` 69 } 70 71 // Config used to write a junit XML document. 72 type Config struct { 73 ProjectName string 74 FormatTestSuiteName FormatFunc 75 FormatTestCaseClassname FormatFunc 76 HideEmptyPackages bool 77 // This is used for tests to have a consistent timestamp 78 customTimestamp string 79 customElapsed string 80 } 81 82 // FormatFunc converts a string from one format into another. 83 type FormatFunc func(string) string 84 85 // Write creates an XML document and writes it to out. 86 func Write(out io.Writer, exec *testjson.Execution, cfg Config) error { 87 if err := write(out, generate(exec, cfg)); err != nil { 88 return fmt.Errorf("failed to write JUnit XML: %v", err) 89 } 90 return nil 91 } 92 93 func generate(exec *testjson.Execution, cfg Config) JUnitTestSuites { 94 cfg = configWithDefaults(cfg) 95 version := goVersion() 96 suites := JUnitTestSuites{ 97 Name: cfg.ProjectName, 98 Tests: exec.Total(), 99 Failures: len(exec.Failed()), 100 Errors: len(exec.Errors()), 101 Time: formatDurationAsSeconds(time.Since(exec.Started())), 102 } 103 104 if cfg.customElapsed != "" { 105 suites.Time = cfg.customElapsed 106 } 107 for _, pkgname := range exec.Packages() { 108 pkg := exec.Package(pkgname) 109 if cfg.HideEmptyPackages && pkg.IsEmpty() { 110 continue 111 } 112 junitpkg := JUnitTestSuite{ 113 Name: cfg.FormatTestSuiteName(pkgname), 114 Tests: pkg.Total, 115 Time: formatDurationAsSeconds(pkg.Elapsed()), 116 Properties: packageProperties(version), 117 TestCases: packageTestCases(pkg, cfg.FormatTestCaseClassname), 118 Failures: len(pkg.Failed), 119 Timestamp: cfg.customTimestamp, 120 } 121 if cfg.customTimestamp == "" { 122 junitpkg.Timestamp = exec.Started().Format(time.RFC3339) 123 } 124 suites.Suites = append(suites.Suites, junitpkg) 125 } 126 return suites 127 } 128 129 func configWithDefaults(cfg Config) Config { 130 noop := func(v string) string { 131 return v 132 } 133 if cfg.FormatTestSuiteName == nil { 134 cfg.FormatTestSuiteName = noop 135 } 136 if cfg.FormatTestCaseClassname == nil { 137 cfg.FormatTestCaseClassname = noop 138 } 139 return cfg 140 } 141 142 func formatDurationAsSeconds(d time.Duration) string { 143 return fmt.Sprintf("%f", d.Seconds()) 144 } 145 146 func packageProperties(goVersion string) []JUnitProperty { 147 return []JUnitProperty{ 148 {Name: "go.version", Value: goVersion}, 149 } 150 } 151 152 // goVersion returns the version as reported by the go binary in PATH. This 153 // version will not be the same as runtime.Version, which is always the version 154 // of go used to build the gotestsum binary. 155 // 156 // To skip the os/exec call set the GOVERSION environment variable to the 157 // desired value. 158 func goVersion() string { 159 if version, ok := os.LookupEnv("GOVERSION"); ok { 160 return version 161 } 162 log.Debugf("exec: go version") 163 cmd := exec.Command("go", "version") 164 out, err := cmd.Output() 165 if err != nil { 166 log.Warnf("Failed to lookup go version for junit xml: %v", err) 167 return "unknown" 168 } 169 return strings.TrimPrefix(strings.TrimSpace(string(out)), "go version ") 170 } 171 172 func packageTestCases(pkg *testjson.Package, formatClassname FormatFunc) []JUnitTestCase { 173 cases := []JUnitTestCase{} 174 175 if pkg.TestMainFailed() { 176 var buf bytes.Buffer 177 pkg.WriteOutputTo(&buf, 0) //nolint:errcheck 178 jtc := newJUnitTestCase(testjson.TestCase{Test: "TestMain"}, formatClassname) 179 jtc.Failure = &JUnitFailure{ 180 Message: "Failed", 181 Contents: buf.String(), 182 } 183 cases = append(cases, jtc) 184 } 185 186 for _, tc := range pkg.Failed { 187 jtc := newJUnitTestCase(tc, formatClassname) 188 jtc.Failure = &JUnitFailure{ 189 Message: "Failed", 190 Contents: strings.Join(pkg.OutputLines(tc), ""), 191 } 192 cases = append(cases, jtc) 193 } 194 195 for _, tc := range pkg.Skipped { 196 jtc := newJUnitTestCase(tc, formatClassname) 197 jtc.SkipMessage = &JUnitSkipMessage{ 198 Message: strings.Join(pkg.OutputLines(tc), ""), 199 } 200 cases = append(cases, jtc) 201 } 202 203 for _, tc := range pkg.Passed { 204 jtc := newJUnitTestCase(tc, formatClassname) 205 cases = append(cases, jtc) 206 } 207 return cases 208 } 209 210 func newJUnitTestCase(tc testjson.TestCase, formatClassname FormatFunc) JUnitTestCase { 211 return JUnitTestCase{ 212 Classname: formatClassname(tc.Package), 213 Name: tc.Test.Name(), 214 Time: formatDurationAsSeconds(tc.Elapsed), 215 } 216 } 217 218 func write(out io.Writer, suites JUnitTestSuites) error { 219 doc, err := xml.MarshalIndent(suites, "", "\t") 220 if err != nil { 221 return err 222 } 223 _, err = out.Write([]byte(xml.Header)) 224 if err != nil { 225 return err 226 } 227 _, err = out.Write(doc) 228 return err 229 }