-
-
Notifications
You must be signed in to change notification settings - Fork 75
/
Copy pathJunitReporter.swift
282 lines (245 loc) · 11.1 KB
/
JunitReporter.swift
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
// Information about the JUNIT Schema/specification used in this file can be found here:
// * https://stackoverflow.com/a/9410271
// * https://github.com/bazelbuild/bazel/blob/45092bb122b840e3410845522df9fe89c59db465/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/AntXmlResultWriter.java#L29
// * http://windyroad.com.au/dl/Open%20Source/JUnit.xsd
#if compiler(>=6.0)
package import Foundation
#else
import Foundation
#endif
import XMLCoder
package final class JunitReporter {
private var components: [JunitComponent] = []
// Parallel output does not guarantee order - so it is _very_ hard
// to match to the parent suite. We can still capture test success/failure
// and output a generic result file.
private var parallelComponents: [JunitComponent] = []
package init() { }
package func add(line: String) {
// Remove any preceding or excessive spaces
let line = line.trimmingCharacters(in: .whitespacesAndNewlines)
if FailingTestCaptureGroup.regex.match(string: line) {
guard let testCase = generateFailingTest(line: line) else { return }
components.append(.failingTest(testCase))
} else if RestartingTestCaptureGroup.regex.match(string: line) {
guard let testCase = generateRestartingTest(line: line) else { return }
components.append(.failingTest(testCase))
} else if TestCasePassedCaptureGroup.regex.match(string: line) {
guard let testCase = generatePassingTest(line: line) else { return }
components.append(.testCasePassed(testCase))
} else if TestCaseSkippedCaptureGroup.regex.match(string: line) {
guard let testCase = generateSkippedTest(line: line) else { return }
components.append(.skippedTest(testCase))
} else if TestSuiteStartCaptureGroup.regex.match(string: line) {
guard let testStart = generateSuiteStart(line: line) else { return }
components.append(.testSuiteStart(testStart))
} else if ParallelTestCaseFailedCaptureGroup.regex.match(string: line) {
guard let testCase = generateParallelFailingTest(line: line) else { return }
parallelComponents.append(.failingTest(testCase))
} else if ParallelTestCasePassedCaptureGroup.regex.match(string: line) {
guard let testCase = generatePassingParallelTest(line: line) else { return }
parallelComponents.append(.testCasePassed(testCase))
} else if ParallelTestCaseSkippedCaptureGroup.regex.match(string: line) {
guard let testCase = generateSkippedParallelTest(line: line) else { return }
parallelComponents.append(.testCasePassed(testCase))
} else {
// Not needed for generating a junit report
return
}
}
private func generateFailingTest(line: String) -> TestCase? {
let groups = FailingTestCaptureGroup.regex.captureGroups(for: line)
guard let group = FailingTestCaptureGroup(groups: groups) else { return nil }
return TestCase(classname: group.testSuite, name: group.testCase, time: nil, failure: .init(message: "\(group.file) - \(group.reason)"))
}
private func generateRestartingTest(line: String) -> TestCase? {
let groups = RestartingTestCaptureGroup.regex.captureGroups(for: line)
guard let group = RestartingTestCaptureGroup(groups: groups) else { return nil }
return TestCase(classname: group.testSuite, name: group.testCase, time: nil, failure: .init(message: line))
}
private func generateParallelFailingTest(line: String) -> TestCase? {
// Parallel tests do not provide meaningful failure messages
let groups = ParallelTestCaseFailedCaptureGroup.regex.captureGroups(for: line)
guard let group = ParallelTestCaseFailedCaptureGroup(groups: groups) else { return nil }
return TestCase(classname: group.suite, name: group.testCase, time: nil, failure: .init(message: "Parallel test failed"))
}
private func generatePassingTest(line: String) -> TestCase? {
let groups = TestCasePassedCaptureGroup.regex.captureGroups(for: line)
guard let group = TestCasePassedCaptureGroup(groups: groups) else { return nil }
return TestCase(classname: group.suite, name: group.testCase, time: group.time)
}
private func generateSkippedTest(line: String) -> TestCase? {
let groups = TestCaseSkippedCaptureGroup.regex.captureGroups(for: line)
guard let group = TestCaseSkippedCaptureGroup(groups: groups) else { return nil }
return TestCase(classname: group.suite, name: group.testCase, time: group.time, skipped: .init(message: nil))
}
private func generatePassingParallelTest(line: String) -> TestCase? {
let groups = ParallelTestCasePassedCaptureGroup.regex.captureGroups(for: line)
guard let group = ParallelTestCasePassedCaptureGroup(groups: groups) else { return nil }
return TestCase(classname: group.suite, name: group.testCase, time: group.time)
}
private func generateSkippedParallelTest(line: String) -> TestCase? {
let groups = ParallelTestCaseSkippedCaptureGroup.regex.captureGroups(for: line)
guard let group = ParallelTestCaseSkippedCaptureGroup(groups: groups) else { return nil }
return TestCase(classname: group.suite, name: group.testCase, time: group.time, skipped: .init(message: nil))
}
private func generateSuiteStart(line: String) -> String? {
let groups = TestSuiteStartCaptureGroup.regex.captureGroups(for: line)
guard let group = TestSuiteStartCaptureGroup(groups: groups) else { return nil }
return group.testSuiteName
}
package func generateReport() throws -> Data {
let parser = JunitComponentParser()
for item in components {
parser.parse(component: item)
}
// Prefix a fake test suite start for the parallel tests.
parallelComponents.insert(.testSuiteStart("PARALLEL_TESTS"), at: 0)
for parallelComponent in parallelComponents {
parser.parse(component: parallelComponent)
}
let encoder = XMLEncoder()
encoder.keyEncodingStrategy = .lowercased
encoder.outputFormatting = [.prettyPrinted]
let result = parser.result()
return try encoder.encode(result)
}
}
private final class JunitComponentParser {
private var mainTestSuiteName: String?
private var testCases: [TestCase] = []
func parse(component: JunitComponent) {
switch component {
case let .testSuiteStart(suiteName):
guard mainTestSuiteName == nil else {
break
}
mainTestSuiteName = suiteName
case let .failingTest(testCase),
let .testCasePassed(testCase),
let .skippedTest(testCase):
testCases.append(testCase)
}
}
func result() -> Testsuites {
var testSuites: [Testsuite] = []
for testCase in testCases {
let index: Int
if let existingTestSuiteIndex = testSuites.firstIndex(where: { $0.name == testCase.classname }) {
index = existingTestSuiteIndex
} else {
let newTestSuite = Testsuite(name: testCase.classname, testcases: [])
testSuites.append(newTestSuite)
index = testSuites.count - 1
}
var testSuite = testSuites[index]
testSuite.testcases.append(testCase)
testSuites[index] = testSuite
}
let container = Testsuites(name: mainTestSuiteName, testsuites: testSuites)
return container
}
}
private enum JunitComponent {
case testSuiteStart(String)
case failingTest(TestCase)
case testCasePassed(TestCase)
case skippedTest(TestCase)
}
private struct Testsuites: Encodable, DynamicNodeEncoding {
var name: String?
var testsuites: [Testsuite] = []
enum CodingKeys: String, CodingKey {
case name
case tests
case failures
case testsuites = "testsuite"
}
static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
let key = CodingKeys(stringValue: key.stringValue)!
switch key {
case .name, .tests, .failures:
return .attribute
case .testsuites:
return .element
}
}
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(testsuites.reduce(into: 0) { $0 += $1.testcases.count }, forKey: .tests)
try container.encode(testsuites.reduce(into: 0) { $0 += $1.testcases.filter { $0.failure != nil }.count }, forKey: .failures)
try container.encode(testsuites, forKey: .testsuites)
}
}
private struct Testsuite: Encodable, DynamicNodeEncoding {
let name: String
var testcases: [TestCase]
enum CodingKeys: String, CodingKey {
case name
case tests
case failures
case testcases = "testcase"
}
static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
let key = CodingKeys(stringValue: key.stringValue)!
switch key {
case .name, .tests, .failures:
return .attribute
case .testcases:
return .element
}
}
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(testcases.count, forKey: .tests)
try container.encode(testcases.filter { $0.failure != nil }.count, forKey: .failures)
try container.encode(testcases, forKey: .testcases)
}
}
private struct TestCase: Codable, DynamicNodeEncoding {
let classname: String
let name: String
let time: String?
let failure: Failure?
let skipped: Skipped?
init(classname: String, name: String, time: String?, failure: Failure? = nil, skipped: Skipped? = nil) {
self.classname = classname
self.name = name
self.time = time
self.failure = failure
self.skipped = skipped
}
static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
let key = CodingKeys(stringValue: key.stringValue)!
switch key {
case .classname, .name, .time:
return .attribute
case .failure, .skipped:
return .element
}
}
}
private extension TestCase {
struct Failure: Codable, DynamicNodeEncoding {
let message: String
static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
let key = CodingKeys(stringValue: key.stringValue)!
switch key {
case .message:
return .attribute
}
}
}
struct Skipped: Codable, DynamicNodeEncoding {
let message: String?
static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
let key = CodingKeys(stringValue: key.stringValue)!
switch key {
case .message:
return .attribute
}
}
}
}