-
Notifications
You must be signed in to change notification settings - Fork 124
/
results_xunit_parser.go
168 lines (149 loc) · 5.26 KB
/
results_xunit_parser.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package command
import (
"encoding/xml"
"fmt"
"io"
"math"
"strconv"
"strings"
"time"
"github.com/evergreen-ci/evergreen"
"github.com/evergreen-ci/evergreen/agent/internal"
"github.com/evergreen-ci/evergreen/agent/internal/client"
"github.com/evergreen-ci/evergreen/model/testlog"
"github.com/evergreen-ci/evergreen/model/testresult"
"github.com/evergreen-ci/evergreen/util"
"github.com/evergreen-ci/utility"
"github.com/pkg/errors"
)
type customFloat float64
func (cf *customFloat) UnmarshalXMLAttr(attr xml.Attr) error {
s := strings.Replace(attr.Value, ",", "", -1)
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return err
}
*cf = customFloat(f)
return nil
}
type testSuites struct {
Suites []testSuite `xml:"testsuite"`
Errors int `xml:"errors,attr"`
Failures int `xml:"failures,attr"`
Skip int `xml:"skip,attr"`
Name string `xml:"name,attr"`
Time customFloat `xml:"time,attr"`
Tests int `xml:"tests,attr"`
}
type testSuite struct {
Errors int `xml:"errors,attr"`
Failures int `xml:"failures,attr"`
Skip int `xml:"skip,attr"`
Name string `xml:"name,attr"`
Tests int `xml:"tests,attr"`
TestCases []testCase `xml:"testcase"`
Time customFloat `xml:"time,attr"`
Error *failureDetails `xml:"error"`
SysOut string `xml:"system-out"`
SysErr string `xml:"system-err"`
NestedSuites *testSuite `xml:"testsuite"`
}
type testCase struct {
Name string `xml:"name,attr"`
Time customFloat `xml:"time,attr"`
ClassName string `xml:"classname,attr"`
Failure *failureDetails `xml:"failure"`
Error *failureDetails `xml:"error"`
SysOut string `xml:"system-out"`
SysErr string `xml:"system-err"`
Skipped *failureDetails `xml:"skipped"`
}
type failureDetails struct {
Message string `xml:"message,attr"`
Type string `xml:"type,attr"`
Content string `xml:",chardata"`
}
func parseXMLResults(reader io.Reader) ([]testSuite, error) {
results := testSuites{}
fileData, err := io.ReadAll(reader)
if err != nil {
return nil, errors.Wrap(err, "reading results file")
}
// need to try to unmarshal into 2 different structs since the JUnit XML schema
// allows for <testsuite> or <testsuites> to be the root
// https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd
if err = xml.Unmarshal(fileData, &results); err != nil {
return nil, errors.Wrap(err, "unmarshalling XML test suite")
}
if len(results.Suites) == 0 {
if err = xml.Unmarshal(fileData, &results.Suites); err != nil {
return nil, errors.Wrap(err, "unmarshalling XML test suites")
}
}
return results.Suites, nil
}
// toModelTestResultAndLog converts an XUnit test case into a test result and
// test log. Logs are only generated if the test case did not succeed (this is
// part of the XUnit XML file design).
func (tc testCase) toModelTestResultAndLog(conf *internal.TaskConfig, logger client.LoggerProducer) (testresult.TestResult, *testlog.TestLog) {
res := testresult.TestResult{}
var log *testlog.TestLog
if tc.ClassName != "" {
res.TestName = fmt.Sprintf("%v.%v", tc.ClassName, tc.Name)
} else {
res.TestName = tc.Name
}
// Replace spaces, dashes, etc. with underscores.
res.TestName = util.CleanForPath(res.TestName)
if math.IsNaN(float64(tc.Time)) {
logger.Task().Errorf("Test '%s' time was NaN, its calculated duration will be incorrect", res.TestName)
tc.Time = 0
}
// Passing 0 as the sign will check for Inf as well as -Inf.
if math.IsInf(float64(tc.Time), 0) {
logger.Task().Errorf("Test '%s' time was Inf, its calculated duration will be incorrect", res.TestName)
tc.Time = 0
}
res.TestStartTime = time.Now()
res.TestEndTime = res.TestStartTime.Add(time.Duration(float64(tc.Time) * float64(time.Second)))
// The presence of the Failure, Error, or Skipped fields is used to
// indicate an unsuccessful test case. Logs can only be generated in
// in failure cases, because XUnit results only include messages if
// they did *not* succeed.
switch {
case tc.Failure != nil:
res.Status = evergreen.TestFailedStatus
log = tc.Failure.toBasicTestLog("FAILURE")
case tc.Error != nil:
res.Status = evergreen.TestFailedStatus
log = tc.Error.toBasicTestLog("ERROR")
case tc.Skipped != nil:
res.Status = evergreen.TestSkippedStatus
default:
res.Status = evergreen.TestSucceededStatus
}
if systemLogs := constructSystemLogs(tc.SysOut, tc.SysErr); len(systemLogs) > 0 {
if log == nil {
log = &testlog.TestLog{}
}
log.Lines = append(log.Lines, systemLogs...)
}
if log != nil {
// When sending test logs to Cedar we need to use a
// unique string since there may be duplicate file
// names if there are duplicate test names.
log.Name = utility.RandomString()
log.Task = conf.Task.Id
log.TaskExecution = conf.Task.Execution
res.LogInfo = &testresult.TestLogInfo{LogName: log.Name}
}
return res, log
}
func (fd failureDetails) toBasicTestLog(fdType string) *testlog.TestLog {
log := testlog.TestLog{
Lines: []string{fmt.Sprintf("%v: %v (%v)", fdType, fd.Message, fd.Type)},
}
logLines := strings.Split(strings.TrimSpace(fd.Content), "\n")
log.Lines = append(log.Lines, logLines...)
return &log
}