Skip to content

Commit ec371c0

Browse files
committed
test summary
1 parent 419d061 commit ec371c0

File tree

3 files changed

+130
-1
lines changed

3 files changed

+130
-1
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: 'Test Summary'
2+
description: 'Summarise test results and coverage'
3+
inputs:
4+
junit:
5+
description: 'path to junit xml file'
6+
required: true
7+
coverage:
8+
description: 'path to lcov.json file'
9+
required: false
10+
11+
runs:
12+
using: "composite"
13+
steps:
14+
- name: 'Summarise'
15+
run: ./.github/actions/test-summary/make-summary.swift ${{ inputs.junit }} ${{ inputs.coverage }} >> $GITHUB_STEP_SUMMARY
16+
shell: bash
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/swift
2+
3+
import Foundation
4+
5+
let junit = CommandLine.arguments.count > 1 ? URL(filePath: CommandLine.arguments[1]) : nil
6+
let coverage = CommandLine.arguments.count > 2 ? URL(filePath: CommandLine.arguments[2]) : nil
7+
8+
guard let junit else {
9+
print("usage: ./failed-tests <junit.xml>")
10+
exit(70)
11+
}
12+
13+
let document = try XMLDocument(contentsOf: junit)
14+
let testCases = try document.nodes(forXPath: "//testcase")
15+
let failures = try document.nodes(forXPath: "//failure/..")
16+
17+
let table = makeTable(
18+
total: testCases.count,
19+
failed: failures.count,
20+
passed: testCases.count - failures.count
21+
)
22+
print(table)
23+
24+
for node in failures {
25+
guard let element = node as? XMLElement,
26+
let testClass = element.attribute(forName: "classname")?.stringValue,
27+
let testName = element.attribute(forName: "name")?.stringValue else {
28+
continue
29+
}
30+
31+
print("::warning ::Failed Test: \(testClass).\(testName)()")
32+
let messages = element
33+
.elements(forName: "failure")
34+
.compactMap { $0.attribute(forName: "message")?.stringValue }
35+
36+
print(messages.joined(separator: ". "))
37+
}
38+
39+
if let coverage {
40+
let table = try makeTable(
41+
name: coverage.lastPathComponent,
42+
coverage: JSONDecoder().decode(Coverage.self, from: Data(contentsOf: coverage))
43+
)
44+
print(table)
45+
}
46+
47+
func makeTable(
48+
total: Int,
49+
failed: Int,
50+
passed: Int,
51+
skipped: Int = 0
52+
) -> String {
53+
"""
54+
<table>
55+
<tr><td></td><td><b>Tests</b></td><td><b>Passed</b> ✅</td><td><b>Skipped</b> ⏭️</td><td><b>Failed</b> ❌</td></tr>
56+
<tr><td>\(junit.lastPathComponent)</td><td>\(total) ran</td><td>\(passed) passed</td><td>\(skipped) skipped</td><td>\(failed) failed</td></tr>
57+
</table>
58+
"""
59+
}
60+
61+
func makeTable(
62+
name: String,
63+
coverage: Coverage
64+
) -> String {
65+
"""
66+
<table>
67+
<tr><td></td><td><b>Covered</b></td><td><b>Total</b></td><td><b>Coverage</b></td></tr>
68+
<tr><td>\(name)</td><td>\(coverage.covered)</td><td>\(coverage.count)</td><td>\(coverage.percentString)</td></tr>
69+
</table>
70+
"""
71+
}
72+
73+
struct Coverage: Decodable {
74+
var count: Int
75+
var covered: Int
76+
var percent: Double
77+
78+
init(from decoder: any Decoder) throws {
79+
var unkeyed = try decoder
80+
.container(keyedBy: CodingKeys.self)
81+
.nestedUnkeyedContainer(forKey: .data)
82+
let container = try unkeyed
83+
.nestedContainer(keyedBy: CodingKeys.self)
84+
.nestedContainer(keyedBy: CodingKeys.self, forKey: .totals)
85+
.nestedContainer(keyedBy: CodingKeys.self, forKey: .lines)
86+
87+
self.count = try container.decode(Int.self, forKey: .count)
88+
self.covered = try container.decode(Int.self, forKey: .covered)
89+
self.percent = try container.decode(Double.self, forKey: .percent)
90+
}
91+
92+
enum CodingKeys: String, CodingKey {
93+
case data
94+
case totals
95+
case lines
96+
case count
97+
case covered
98+
case percent
99+
}
100+
101+
var percentString: String {
102+
let formatter = NumberFormatter()
103+
formatter.numberStyle = .percent
104+
formatter.maximumFractionDigits = 2
105+
return formatter.string(from: (percent / 100) as NSNumber) ?? "asdf"
106+
}
107+
}

.github/workflows/build.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,20 @@ jobs:
2020
- name: Build
2121
run: swift test --enable-code-coverage --filter do_not_test
2222
- name: Test
23-
run: swift test --enable-code-coverage --skip-build
23+
run: swift test --enable-code-coverage --skip-build --xunit-output result.xml
24+
timeout-minutes: 1
2425
- name: Gather code coverage
2526
run: xcrun llvm-cov export -format="lcov" .build/debug/FlyingFoxPackageTests.xctest/Contents/MacOS/FlyingFoxPackageTests -instr-profile .build/debug/codecov/default.profdata > coverage_report.lcov
2627
- name: Upload Coverage
2728
uses: codecov/codecov-action@v4
2829
with:
2930
token: ${{ secrets.CODECOV_TOKEN }}
3031
files: ./coverage_report.lcov
32+
- name: 📄 Summary
33+
uses: ./.github/actions/test-summary
34+
with:
35+
junit: result-swift-testing.xml
36+
coverage: .build/debug/codecov/FlyingFox.json
3137

3238
xcode_15_4:
3339
runs-on: macos-14

0 commit comments

Comments
 (0)