44import Foundation
55import RegexBuilder
66
7- protocol InteractiveWriter {
8- func write( _ string: String )
9- }
10-
11- protocol TestsParser {
12- /// Parse the output of a test process, format it, then output in the `InteractiveWriter`.
13- func onLine( _ line: String , _ terminal: InteractiveWriter )
14- func finalize( _ terminal: InteractiveWriter )
15- }
16-
177extension String . StringInterpolation {
188 /// Display `value` with the specified ANSI-escaped `color` values, then apply the reset.
199 fileprivate mutating func appendInterpolation< T> ( _ value: T , color: String ... ) {
2010 appendInterpolation ( " \( color. map { " \u{001B} \( $0) " } . joined ( ) ) \( value) \u{001B} [0m " )
2111 }
2212}
2313
24- class FancyTestsParser : TestsParser {
25- init ( ) { }
14+ class FancyTestsParser {
15+ let write : ( String ) -> Void
2616
27- enum Status : Equatable {
17+ init ( write: @escaping ( String ) -> Void ) {
18+ self . write = write
19+ }
20+
21+ private enum Status : Equatable {
2822 case passed, failed, skipped
2923 case unknown( String . SubSequence ? )
3024
@@ -45,7 +39,7 @@ class FancyTestsParser: TestsParser {
4539 }
4640 }
4741
48- struct Suite {
42+ private struct Suite {
4943 let name : String . SubSequence
5044 var status : Status = . unknown( nil )
5145
@@ -76,19 +70,19 @@ class FancyTestsParser: TestsParser {
7670 }
7771 }
7872
79- var suites = [ Suite] ( )
73+ private var suites = [ Suite] ( )
8074
81- let swiftIdentifier = #/[_\p{L}\p{Nl}][_\p{L}\p{Nl}\p{Mn}\p{Nd}\p{Pc}]*/#
82- let timestamp = #/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}/#
83- lazy var suiteStarted = Regex {
75+ private let swiftIdentifier = #/[_\p{L}\p{Nl}][_\p{L}\p{Nl}\p{Mn}\p{Nd}\p{Pc}]*/#
76+ private let timestamp = #/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}/#
77+ private lazy var suiteStarted = Regex {
8478 " Test Suite ' "
8579 Capture {
8680 OneOrMore ( CharacterClass . anyOf ( " ' " ) . inverted)
8781 }
8882 " ' started at "
8983 Capture { self . timestamp }
9084 }
91- lazy var suiteStatus = Regex {
85+ private lazy var suiteStatus = Regex {
9286 " Test Suite ' "
9387 Capture { OneOrMore ( CharacterClass . anyOf ( " ' " ) . inverted) }
9488 " ' "
@@ -101,14 +95,14 @@ class FancyTestsParser: TestsParser {
10195 " at "
10296 Capture { self . timestamp }
10397 }
104- lazy var testCaseStarted = Regex {
98+ private lazy var testCaseStarted = Regex {
10599 " Test Case ' "
106100 Capture { self . swiftIdentifier }
107101 " . "
108102 Capture { self . swiftIdentifier }
109103 " ' started "
110104 }
111- lazy var testCaseStatus = Regex {
105+ private lazy var testCaseStatus = Regex {
112106 " Test Case ' "
113107 Capture { self . swiftIdentifier }
114108 " . "
@@ -130,10 +124,10 @@ class FancyTestsParser: TestsParser {
130124 " seconds) "
131125 }
132126
133- let testSummary =
127+ private let testSummary =
134128 #/Executed \d+ (test|tests), with (?:\d+ (?:test|tests) skipped and )?\d+ (failure|failures) \((?<unexpected>\d+) unexpected\) in (?<duration>\d+\.\d+) \(\d+\.\d+\) seconds/#
135129
136- func onLine( _ line: String , _ terminal : InteractiveWriter ) {
130+ func onLine( _ line: String ) {
137131 if let match = line. firstMatch (
138132 of: suiteStarted
139133 ) {
@@ -145,7 +139,7 @@ class FancyTestsParser: TestsParser {
145139 let ( _, suite, status, _) = match. output
146140 if let suiteIdx = suites. firstIndex ( where: { $0. name == suite } ) {
147141 suites [ suiteIdx] . status = Status ( rawValue: status)
148- flushSingleSuite ( suites [ suiteIdx] , terminal )
142+ flushSingleSuite ( suites [ suiteIdx] )
149143 }
150144 } else if let match = line. firstMatch (
151145 of: testCaseStarted
@@ -172,86 +166,87 @@ class FancyTestsParser: TestsParser {
172166 // do nothing
173167 } else {
174168 if !line. isEmpty {
175- terminal . write ( line + " \n " )
169+ write ( line + " \n " )
176170 }
177171 }
178172 }
179173
180- func finalize( _ terminal: InteractiveWriter ) {
181- terminal. write ( " \n " )
182- flushSummary ( of: suites, terminal)
183- }
184-
185- private func flushSingleSuite( _ suite: Suite , _ terminal: InteractiveWriter ) {
186- terminal. write ( suite. statusLabel)
187- terminal. write ( " \( suite. name) \n " )
174+ private func flushSingleSuite( _ suite: Suite ) {
175+ write ( suite. statusLabel)
176+ write ( " \( suite. name) \n " )
188177 for testCase in suite. cases {
189- terminal . write ( " \( testCase. statusMark) " )
178+ write ( " \( testCase. statusMark) " )
190179 if let duration = testCase. duration {
191- terminal
192- . write (
180+ write (
193181 " \( testCase. name) \( " ( \( Int ( Double ( duration) ! * 1000 ) ) ms) " , color: " [90m " ) \n "
194182 ) // gray
195183 }
196184 }
197185 }
198186
199- private func flushSummary( of suites: [ Suite ] , _ terminal: InteractiveWriter ) {
200- let suitesWithCases = suites. filter { $0. cases. count > 0 }
201-
202- terminal. write ( " Test Suites: " )
203- let suitesPassed = suitesWithCases. filter { $0. status == . passed } . count
204- if suitesPassed > 0 {
205- terminal. write ( " \( " \( suitesPassed) passed " , color: " [32m " ) , " )
206- }
207- let suitesSkipped = suitesWithCases. filter { $0. status == . skipped } . count
208- if suitesSkipped > 0 {
209- terminal. write ( " \( " \( suitesSkipped) skipped " , color: " [97m " ) , " )
210- }
211- let suitesFailed = suitesWithCases. filter { $0. status == . failed } . count
212- if suitesFailed > 0 {
213- terminal. write ( " \( " \( suitesFailed) failed " , color: " [31m " ) , " )
214- }
215- let suitesUnknown = suitesWithCases. filter { $0. status == . unknown( nil ) } . count
216- if suitesUnknown > 0 {
217- terminal. write ( " \( " \( suitesUnknown) unknown " , color: " [31m " ) , " )
187+ func finalize( ) {
188+ write ( " \n " )
189+
190+ func formatCategory(
191+ label: String , statuses: [ Status ]
192+ ) -> String {
193+ var passed = 0
194+ var skipped = 0
195+ var failed = 0
196+ var unknown = 0
197+ for status in statuses {
198+ switch status {
199+ case . passed: passed += 1
200+ case . skipped: skipped += 1
201+ case . failed: failed += 1
202+ case . unknown: unknown += 1
203+ }
204+ }
205+ var result = " \( label) "
206+ if passed > 0 {
207+ result += " \u{001B} [32m \( passed) passed \u{001B} [0m, "
208+ }
209+ if skipped > 0 {
210+ result += " \u{001B} [97m \( skipped) skipped \u{001B} [0m, "
211+ }
212+ if failed > 0 {
213+ result += " \u{001B} [31m \( failed) failed \u{001B} [0m, "
214+ }
215+ if unknown > 0 {
216+ result += " \u{001B} [31m \( unknown) unknown \u{001B} [0m, "
217+ }
218+ result += " \u{001B} [0m \( statuses. count) total \n "
219+ return result
218220 }
219- terminal. write ( " \( suitesWithCases. count) total \n " )
220221
221- terminal. write ( " Tests: " )
222- let allTests = suitesWithCases. map ( \. cases) . reduce ( [ ] , + )
223- let testsPassed = allTests. filter { $0. status == . passed } . count
224- if testsPassed > 0 {
225- terminal. write ( " \( " \( testsPassed) passed " , color: " [32m " ) , " )
226- }
227- let testsSkipped = allTests. filter { $0. status == . skipped } . count
228- if testsSkipped > 0 {
229- terminal. write ( " \( " \( testsSkipped) skipped " , color: " [97m " ) , " )
230- }
231- let testsFailed = allTests. filter { $0. status == . failed } . count
232- if testsFailed > 0 {
233- terminal. write ( " \( " \( testsFailed) failed " , color: " [31m " ) , " )
234- }
235- let testsUnknown = allTests. filter { $0. status == . unknown( nil ) } . count
236- if testsUnknown > 0 {
237- terminal. write ( " \( " \( testsUnknown) unknown " , color: " [31m " ) , " )
222+ let suitesWithCases = suites. filter { $0. cases. count > 0 }
223+ write (
224+ formatCategory (
225+ label: " Test Suites: " , statuses: suitesWithCases. map ( \. status)
226+ )
227+ )
228+ let allCaseStatuses = suitesWithCases. flatMap {
229+ $0. cases. map { $0. status }
238230 }
239- terminal. write ( " \( allTests. count) total \n " )
231+ write (
232+ formatCategory (
233+ label: " Tests: " , statuses: allCaseStatuses
234+ )
235+ )
240236
241237 if suites. contains ( where: { $0. name == " All tests " } ) {
242- terminal . write ( " \( " Ran all test suites. " , color: " [90m " ) \n " ) // gray
238+ write ( " \( " Ran all test suites. " , color: " [90m " ) \n " ) // gray
243239 }
244240
245241 if suites. contains ( where: { $0. status. isNegative } ) {
246- print ( suites. filter ( { $0. status. isNegative } ) )
247- terminal. write ( " \n \( " Failed test cases: " , color: " [31m " ) \n " )
242+ write ( " \n \( " Failed test cases: " , color: " [31m " ) \n " )
248243 for suite in suites. filter ( { $0. status. isNegative } ) {
249244 for testCase in suite. cases. filter ( { $0. status. isNegative } ) {
250- terminal . write ( " \( testCase. statusMark) \( suite. name) . \( testCase. name) \n " )
245+ write ( " \( testCase. statusMark) \( suite. name) . \( testCase. name) \n " )
251246 }
252247 }
253248
254- terminal . write (
249+ write (
255250 " \n \( " Some tests failed. Use --verbose for raw test output. " , color: " [33m " ) \n "
256251 )
257252 }
0 commit comments