Skip to content

Commit bc29743

Browse files
authored
Update tests with easier to read diff output (#529)
- Renames AssertEqualStringsIgnoringTrailingWhitespace to AssertEqualStrings and updates the implementation to require matching trailing whitespace. - Updates AssertEqualStrings to include much easier to read diff output when CollectionDifference is available. This should add developers when tests fail by providing more clear errors.
1 parent e7f312e commit bc29743

File tree

3 files changed

+101
-30
lines changed

3 files changed

+101
-30
lines changed

Sources/ArgumentParserTestHelpers/TestHelpers.swift

Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,35 @@ import ArgumentParser
1313
import ArgumentParserToolInfo
1414
import XCTest
1515

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+
1645
// extensions to the ParsableArguments protocol to facilitate XCTestExpectation support
1746
public protocol TestableParsableArguments: ParsableArguments {
1847
var didValidateExpectation: XCTestExpectation { get }
@@ -52,7 +81,7 @@ public func AssertResultFailure<T, U: Error>(
5281
switch expression() {
5382
case .success:
5483
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)
5685
case .failure:
5786
break
5887
}
@@ -61,10 +90,10 @@ public func AssertResultFailure<T, U: Error>(
6190
public func AssertErrorMessage<A>(_ type: A.Type, _ arguments: [String], _ errorMessage: String, file: StaticString = #file, line: UInt = #line) where A: ParsableArguments {
6291
do {
6392
_ = try A.parse(arguments)
64-
XCTFail("Parsing should have failed.", file: (file), line: line)
93+
XCTFail("Parsing should have failed.", file: file, line: line)
6594
} catch {
6695
// 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)
6897
}
6998
}
7099

@@ -98,17 +127,58 @@ public func AssertParseCommand<A: ParsableCommand>(_ rootCommand: ParsableComman
98127
try closure(aCommand)
99128
} catch {
100129
let message = rootCommand.message(for: error)
101-
XCTFail("\"\(message)\"\(error)", file: (file), line: line)
130+
XCTFail("\"\(message)\"\(error)", file: file, line: line)
102131
}
103132
}
104133

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+
}
112182
}
113183
}
114184

@@ -142,13 +212,11 @@ public func AssertHelp<T: ParsableArguments>(
142212
XCTFail(file: file, line: line)
143213
} catch {
144214
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)
147216
}
148217

149218
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)
152220
}
153221

154222
public func AssertHelp<T: ParsableCommand, U: ParsableCommand>(
@@ -176,8 +244,7 @@ public func AssertHelp<T: ParsableCommand, U: ParsableCommand>(
176244

177245
let helpString = U.helpMessage(
178246
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)
181248
}
182249

183250
public func AssertDump<T: ParsableArguments>(
@@ -186,7 +253,7 @@ public func AssertDump<T: ParsableArguments>(
186253
) throws {
187254
do {
188255
_ = try T.parse(["--experimental-dump-help"])
189-
XCTFail(file: (file), line: line)
256+
XCTFail(file: file, line: line)
190257
} catch {
191258
let dumpString = T.fullMessage(for: error)
192259
try AssertJSONEqualFromString(actual: dumpString, expected: expected, for: ToolInfoV0.self)
@@ -241,7 +308,7 @@ extension XCTest {
241308
let commandURL = debugURL.appendingPathComponent(commandName)
242309
guard (try? commandURL.checkResourceIsReachable()) ?? false else {
243310
XCTFail("No executable at '\(commandURL.standardizedFileURL.path)'.",
244-
file: (file), line: line)
311+
file: file, line: line)
245312
return
246313
}
247314

@@ -276,7 +343,11 @@ extension XCTest {
276343
let errorActual = String(data: errorData, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines)
277344

278345
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)
280351
}
281352

282353
XCTAssertEqual(process.terminationStatus, exitCode.rawValue, file: file, line: line)
@@ -301,7 +372,7 @@ extension XCTest {
301372
let commandURL = debugURL.appendingPathComponent(commandName)
302373
guard (try? commandURL.checkResourceIsReachable()) ?? false else {
303374
XCTFail("No executable at '\(commandURL.standardizedFileURL.path)'.",
304-
file: (file), line: line)
375+
file: file, line: line)
305376
return
306377
}
307378

@@ -321,7 +392,7 @@ extension XCTest {
321392

322393
if #available(macOS 10.13, *) {
323394
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)
325396
return
326397
}
327398
} else {

Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ extension SubcommandEndToEndTests {
7070
let helpA = Foo.message(for: CleanExit.helpRequest(CommandA.self))
7171
let helpB = Foo.message(for: CleanExit.helpRequest(CommandB.self))
7272

73-
AssertEqualStringsIgnoringTrailingWhitespace("""
73+
AssertEqualStrings(actual: helpFoo, expected: """
7474
USAGE: foo --name <name> <subcommand>
7575
7676
OPTIONS:
@@ -82,25 +82,25 @@ extension SubcommandEndToEndTests {
8282
b
8383
8484
See 'foo help <subcommand>' for detailed help.
85-
""", helpFoo)
86-
AssertEqualStringsIgnoringTrailingWhitespace("""
85+
""")
86+
AssertEqualStrings(actual: helpA, expected: """
8787
USAGE: foo a --name <name> --bar <bar>
8888
8989
OPTIONS:
9090
--name <name>
9191
--bar <bar>
9292
-h, --help Show help information.
9393
94-
""", helpA)
95-
AssertEqualStringsIgnoringTrailingWhitespace("""
94+
""")
95+
AssertEqualStrings(actual: helpB, expected: """
9696
USAGE: foo b --name <name> --baz <baz>
9797
9898
OPTIONS:
9999
--name <name>
100100
--baz <baz>
101101
-h, --help Show help information.
102102
103-
""", helpB)
103+
""")
104104
}
105105

106106

Tests/ArgumentParserUnitTests/HelpGenerationTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,8 @@ extension HelpGenerationTests {
363363
func testOverviewButNoAbstractSpacing() {
364364
let renderedHelp = HelpGenerator(J.self, visibility: .default)
365365
.rendered()
366-
AssertEqualStringsIgnoringTrailingWhitespace(renderedHelp, """
367-
OVERVIEW:
366+
AssertEqualStrings(actual: renderedHelp, expected: """
367+
OVERVIEW: \n\
368368
test
369369
370370
USAGE: j

0 commit comments

Comments
 (0)