@@ -13,6 +13,35 @@ import ArgumentParser
13
13
import ArgumentParserToolInfo
14
14
import XCTest
15
15
16
+ @available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
17
+ extension CollectionDifference . Change {
18
+ var offset : Int {
19
+ switch self {
20
+ case . insert( let offset, _, _) :
21
+ return offset
22
+ case . remove( let offset, _, _) :
23
+ return offset
24
+ }
25
+ }
26
+ }
27
+
28
+ @available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
29
+ extension CollectionDifference . Change : Comparable where ChangeElement: Equatable {
30
+ public static func < ( lhs: Self , rhs: Self ) -> Bool {
31
+ guard lhs. offset == rhs. offset else {
32
+ return lhs. offset < rhs. offset
33
+ }
34
+ switch ( lhs, rhs) {
35
+ case ( . remove, . insert) :
36
+ return true
37
+ case ( . insert, . remove) :
38
+ return false
39
+ default :
40
+ return true
41
+ }
42
+ }
43
+ }
44
+
16
45
// extensions to the ParsableArguments protocol to facilitate XCTestExpectation support
17
46
public protocol TestableParsableArguments : ParsableArguments {
18
47
var didValidateExpectation : XCTestExpectation { get }
@@ -52,7 +81,7 @@ public func AssertResultFailure<T, U: Error>(
52
81
switch expression ( ) {
53
82
case . success:
54
83
let msg = message ( )
55
- XCTFail ( msg. isEmpty ? " Incorrectly succeeded " : msg, file: ( file) , line: line)
84
+ XCTFail ( msg. isEmpty ? " Incorrectly succeeded " : msg, file: file, line: line)
56
85
case . failure:
57
86
break
58
87
}
@@ -61,10 +90,10 @@ public func AssertResultFailure<T, U: Error>(
61
90
public func AssertErrorMessage< A> ( _ type: A . Type , _ arguments: [ String ] , _ errorMessage: String , file: StaticString = #file, line: UInt = #line) where A: ParsableArguments {
62
91
do {
63
92
_ = try A . parse ( arguments)
64
- XCTFail ( " Parsing should have failed. " , file: ( file) , line: line)
93
+ XCTFail ( " Parsing should have failed. " , file: file, line: line)
65
94
} catch {
66
95
// We expect to hit this path, i.e. getting an error:
67
- XCTAssertEqual ( A . message ( for: error) , errorMessage, file: ( file) , line: line)
96
+ XCTAssertEqual ( A . message ( for: error) , errorMessage, file: file, line: line)
68
97
}
69
98
}
70
99
@@ -98,17 +127,58 @@ public func AssertParseCommand<A: ParsableCommand>(_ rootCommand: ParsableComman
98
127
try closure ( aCommand)
99
128
} catch {
100
129
let message = rootCommand. message ( for: error)
101
- XCTFail ( " \" \( message) \" — \( error) " , file: ( file) , line: line)
130
+ XCTFail ( " \" \( message) \" — \( error) " , file: file, line: line)
102
131
}
103
132
}
104
133
105
- public func AssertEqualStringsIgnoringTrailingWhitespace( _ string1: String , _ string2: String , file: StaticString = #file, line: UInt = #line) {
106
- let lines1 = string1. split ( separator: " \n " , omittingEmptySubsequences: false )
107
- let lines2 = string2. split ( separator: " \n " , omittingEmptySubsequences: false )
108
-
109
- XCTAssertEqual ( lines1. count, lines2. count, " Strings have different numbers of lines. " , file: ( file) , line: line)
110
- for (line1, line2) in zip ( lines1, lines2) {
111
- XCTAssertEqual ( line1. trimmed ( ) , line2. trimmed ( ) , file: ( file) , line: line)
134
+ public func AssertEqualStrings( actual: String , expected: String , file: StaticString = #file, line: UInt = #line) {
135
+ // If the input strings are not equal, create a simple diff for debugging...
136
+ guard actual != expected else {
137
+ // Otherwise they are equal, early exit.
138
+ return
139
+ }
140
+
141
+ // Split in the inputs into lines.
142
+ let actualLines = actual. split ( separator: " \n " , omittingEmptySubsequences: false )
143
+ let expectedLines = expected. split ( separator: " \n " , omittingEmptySubsequences: false )
144
+
145
+ // If collectionDifference is available, use it to make a nicer error message.
146
+ if #available( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * ) {
147
+ // Compute the changes between the two strings.
148
+ let changes = actualLines. difference ( from: expectedLines) . sorted ( )
149
+
150
+ // Render the changes into a diff style string.
151
+ var diff = " "
152
+ var expectedLines = expectedLines [ ... ]
153
+ for change in changes {
154
+ if expectedLines. startIndex < change. offset {
155
+ for line in expectedLines [ ..< change. offset] {
156
+ diff += " \( line) \n "
157
+ }
158
+ expectedLines = expectedLines [ change. offset... ] . dropFirst ( )
159
+ }
160
+
161
+ switch change {
162
+ case . insert( _, let line, _) :
163
+ diff += " - \( line) \n "
164
+ case . remove( _, let line, _) :
165
+ diff += " + \( line) \n "
166
+ }
167
+ }
168
+ for line in expectedLines {
169
+ diff += " \( line) \n "
170
+ }
171
+ XCTFail ( " Strings are not equal. \n \( diff) " , file: file, line: line)
172
+ } else {
173
+ XCTAssertEqual (
174
+ actualLines. count,
175
+ expectedLines. count,
176
+ " Strings have different numbers of lines. " ,
177
+ file: file,
178
+ line: line)
179
+ for (actualLine, expectedLine) in zip ( actualLines, expectedLines) {
180
+ XCTAssertEqual ( actualLine, expectedLine, file: file, line: line)
181
+ }
112
182
}
113
183
}
114
184
@@ -142,13 +212,11 @@ public func AssertHelp<T: ParsableArguments>(
142
212
XCTFail ( file: file, line: line)
143
213
} catch {
144
214
let helpString = T . fullMessage ( for: error)
145
- AssertEqualStringsIgnoringTrailingWhitespace (
146
- helpString, expected, file: file, line: line)
215
+ AssertEqualStrings ( actual: helpString, expected: expected, file: file, line: line)
147
216
}
148
217
149
218
let helpString = T . helpMessage ( includeHidden: includeHidden, columns: nil )
150
- AssertEqualStringsIgnoringTrailingWhitespace (
151
- helpString, expected, file: file, line: line)
219
+ AssertEqualStrings ( actual: helpString, expected: expected, file: file, line: line)
152
220
}
153
221
154
222
public func AssertHelp< T: ParsableCommand , U: ParsableCommand > (
@@ -176,8 +244,7 @@ public func AssertHelp<T: ParsableCommand, U: ParsableCommand>(
176
244
177
245
let helpString = U . helpMessage (
178
246
for: T . self, includeHidden: includeHidden, columns: nil )
179
- AssertEqualStringsIgnoringTrailingWhitespace (
180
- helpString, expected, file: file, line: line)
247
+ AssertEqualStrings ( actual: helpString, expected: expected, file: file, line: line)
181
248
}
182
249
183
250
public func AssertDump< T: ParsableArguments > (
@@ -186,7 +253,7 @@ public func AssertDump<T: ParsableArguments>(
186
253
) throws {
187
254
do {
188
255
_ = try T . parse ( [ " --experimental-dump-help " ] )
189
- XCTFail ( file: ( file) , line: line)
256
+ XCTFail ( file: file, line: line)
190
257
} catch {
191
258
let dumpString = T . fullMessage ( for: error)
192
259
try AssertJSONEqualFromString ( actual: dumpString, expected: expected, for: ToolInfoV0 . self)
@@ -241,7 +308,7 @@ extension XCTest {
241
308
let commandURL = debugURL. appendingPathComponent ( commandName)
242
309
guard ( try ? commandURL. checkResourceIsReachable ( ) ) ?? false else {
243
310
XCTFail ( " No executable at ' \( commandURL. standardizedFileURL. path) '. " ,
244
- file: ( file) , line: line)
311
+ file: file, line: line)
245
312
return
246
313
}
247
314
@@ -276,7 +343,11 @@ extension XCTest {
276
343
let errorActual = String ( data: errorData, encoding: . utf8) !. trimmingCharacters ( in: . whitespacesAndNewlines)
277
344
278
345
if let expected = expected {
279
- AssertEqualStringsIgnoringTrailingWhitespace ( expected, errorActual + outputActual, file: file, line: line)
346
+ AssertEqualStrings (
347
+ actual: errorActual + outputActual,
348
+ expected: expected,
349
+ file: file,
350
+ line: line)
280
351
}
281
352
282
353
XCTAssertEqual ( process. terminationStatus, exitCode. rawValue, file: file, line: line)
@@ -301,7 +372,7 @@ extension XCTest {
301
372
let commandURL = debugURL. appendingPathComponent ( commandName)
302
373
guard ( try ? commandURL. checkResourceIsReachable ( ) ) ?? false else {
303
374
XCTFail ( " No executable at ' \( commandURL. standardizedFileURL. path) '. " ,
304
- file: ( file) , line: line)
375
+ file: file, line: line)
305
376
return
306
377
}
307
378
@@ -321,7 +392,7 @@ extension XCTest {
321
392
322
393
if #available( macOS 10 . 13 , * ) {
323
394
guard ( try ? process. run ( ) ) != nil else {
324
- XCTFail ( " Couldn't run command process. " , file: ( file) , line: line)
395
+ XCTFail ( " Couldn't run command process. " , file: file, line: line)
325
396
return
326
397
}
327
398
} else {
0 commit comments