Skip to content

Commit 2bdfa29

Browse files
committed
Move assertStringsEqualWithDiff into a module that doesn’t depend on Foundation or XCTest
This allows us to use these functions from a framework-agnostic `SwiftSyntaxMacroTestSupport` module. All changes are in underscored modules and thus don’t have any API impact.
1 parent 3301d33 commit 2bdfa29

File tree

5 files changed

+182
-93
lines changed

5 files changed

+182
-93
lines changed

Package.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,18 @@ let package = Package(
4040

4141
.target(
4242
name: "_SwiftSyntaxTestSupport",
43-
dependencies: ["SwiftBasicFormat", "SwiftSyntax", "SwiftSyntaxBuilder", "SwiftSyntaxMacroExpansion"]
43+
dependencies: [
44+
"_SwiftSyntaxTestSupportFrameworkAgnostic",
45+
"SwiftBasicFormat",
46+
"SwiftSyntax",
47+
"SwiftSyntaxBuilder",
48+
"SwiftSyntaxMacroExpansion",
49+
]
50+
),
51+
52+
.target(
53+
name: "_SwiftSyntaxTestSupportFrameworkAgnostic",
54+
dependencies: []
4455
),
4556

4657
.testTarget(

Sources/_SwiftSyntaxTestSupport/AssertEqualWithDiff.swift

Lines changed: 23 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
#if swift(>=6)
1414
public import Foundation
1515
private import XCTest
16+
private import _SwiftSyntaxTestSupportFrameworkAgnostic
1617
#else
1718
import Foundation
1819
import XCTest
20+
import _SwiftSyntaxTestSupportFrameworkAgnostic
1921
#endif
2022

2123
/// Asserts that the two strings are equal, providing Unix `diff`-style output if they are not.
@@ -37,16 +39,21 @@ public func assertStringsEqualWithDiff(
3739
file: StaticString = #filePath,
3840
line: UInt = #line
3941
) {
40-
if actual == expected {
41-
return
42-
}
43-
failStringsEqualWithDiff(
42+
let location = TestFailureLocation(
43+
fileID: "", // Not used in the failure handler
44+
filePath: file,
45+
line: line,
46+
column: 0 // Not used in the failure handler
47+
)
48+
return _SwiftSyntaxTestSupportFrameworkAgnostic.assertStringsEqualWithDiff(
4449
actual,
4550
expected,
4651
message,
4752
additionalInfo: additionalInfo(),
48-
file: file,
49-
line: line
53+
location: location,
54+
failureHandler: {
55+
XCTFail($0.message, file: $0.location.filePath, line: $0.location.line)
56+
}
5057
)
5158
}
5259

@@ -73,89 +80,19 @@ public func assertDataEqualWithDiff(
7380
return
7481
}
7582

76-
// NOTE: Converting to `Stirng` here looses invalid UTF8 sequence difference,
77-
// but at least we can see something is different.
78-
failStringsEqualWithDiff(
79-
String(decoding: actual, as: UTF8.self),
80-
String(decoding: expected, as: UTF8.self),
83+
let actualString = String(decoding: actual, as: UTF8.self)
84+
let expectedString = String(decoding: expected, as: UTF8.self)
85+
86+
if actualString == expectedString {
87+
XCTFail("Actual differs from expected data but underlying strings are equivalent", file: file, line: line)
88+
}
89+
90+
assertStringsEqualWithDiff(
91+
actualString,
92+
expectedString,
8193
message,
8294
additionalInfo: additionalInfo(),
8395
file: file,
8496
line: line
8597
)
8698
}
87-
88-
/// `XCTFail` with `diff`-style output.
89-
public func failStringsEqualWithDiff(
90-
_ actual: String,
91-
_ expected: String,
92-
_ message: String = "",
93-
additionalInfo: @autoclosure () -> String? = nil,
94-
file: StaticString = #filePath,
95-
line: UInt = #line
96-
) {
97-
let stringComparison: String
98-
99-
// Use `CollectionDifference` on supported platforms to get `diff`-like line-based output. On
100-
// older platforms, fall back to simple string comparison.
101-
if #available(macOS 10.15, *) {
102-
let actualLines = actual.components(separatedBy: .newlines)
103-
let expectedLines = expected.components(separatedBy: .newlines)
104-
105-
let difference = actualLines.difference(from: expectedLines)
106-
107-
var result = ""
108-
109-
var insertions = [Int: String]()
110-
var removals = [Int: String]()
111-
112-
for change in difference {
113-
switch change {
114-
case .insert(let offset, let element, _):
115-
insertions[offset] = element
116-
case .remove(let offset, let element, _):
117-
removals[offset] = element
118-
}
119-
}
120-
121-
var expectedLine = 0
122-
var actualLine = 0
123-
124-
while expectedLine < expectedLines.count || actualLine < actualLines.count {
125-
if let removal = removals[expectedLine] {
126-
result += "\(removal)\n"
127-
expectedLine += 1
128-
} else if let insertion = insertions[actualLine] {
129-
result += "+\(insertion)\n"
130-
actualLine += 1
131-
} else {
132-
result += " \(expectedLines[expectedLine])\n"
133-
expectedLine += 1
134-
actualLine += 1
135-
}
136-
}
137-
138-
stringComparison = result
139-
} else {
140-
// Fall back to simple message on platforms that don't support CollectionDifference.
141-
stringComparison = """
142-
Expected:
143-
\(expected)
144-
145-
Actual:
146-
\(actual)
147-
"""
148-
}
149-
150-
var fullMessage = """
151-
\(message.isEmpty ? "Actual output does not match the expected" : message)
152-
\(stringComparison)
153-
"""
154-
if let additional = additionalInfo() {
155-
fullMessage = """
156-
\(fullMessage)
157-
\(additional)
158-
"""
159-
}
160-
XCTFail(fullMessage, file: file, line: line)
161-
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// Defines the location at which the a test failure should be anchored. This is typically the location where the
14+
/// assertion function is called.
15+
public struct TestFailureLocation {
16+
public let fileID: StaticString
17+
public let filePath: StaticString
18+
public let line: UInt
19+
public let column: UInt
20+
21+
public init(
22+
fileID: StaticString,
23+
filePath: StaticString,
24+
line: UInt,
25+
column: UInt
26+
) {
27+
self.fileID = fileID
28+
self.filePath = filePath
29+
self.line = line
30+
self.column = column
31+
}
32+
}
33+
34+
/// Defines the details of a test failure, consisting of a message and the location at which the l
35+
public struct TestFailureSpec {
36+
public let message: String
37+
public let location: TestFailureLocation
38+
}
39+
40+
/// Asserts that the two strings are equal, providing Unix `diff`-style output if they are not.
41+
///
42+
/// - Parameters:
43+
/// - actual: The actual string.
44+
/// - expected: The expected string.
45+
/// - message: An optional description of the failure.
46+
/// - additionalInfo: Additional information about the failed test case that will be printed after the diff
47+
/// - file: The file in which failure occurred. Defaults to the file name of the test case in
48+
/// which this function was called.
49+
/// - line: The line number on which failure occurred. Defaults to the line number on which this
50+
/// function was called.
51+
public func assertStringsEqualWithDiff(
52+
_ actual: String,
53+
_ expected: String,
54+
_ message: String = "",
55+
additionalInfo: @autoclosure () -> String? = nil,
56+
location: TestFailureLocation,
57+
failureHandler: (TestFailureSpec) -> Void
58+
) {
59+
if actual == expected {
60+
return
61+
}
62+
failStringsEqualWithDiff(
63+
actual,
64+
expected,
65+
message,
66+
additionalInfo: additionalInfo(),
67+
location: location,
68+
failureHandler: failureHandler
69+
)
70+
}
71+
72+
/// `XCTFail` with `diff`-style output.
73+
public func failStringsEqualWithDiff(
74+
_ actual: String,
75+
_ expected: String,
76+
_ message: String = "",
77+
additionalInfo: @autoclosure () -> String? = nil,
78+
location: TestFailureLocation,
79+
failureHandler: (TestFailureSpec) -> Void
80+
) {
81+
let stringComparison: String
82+
83+
// Use `CollectionDifference` on supported platforms to get `diff`-like line-based output. On
84+
// older platforms, fall back to simple string comparison.
85+
let actualLines = actual.split(separator: "\n")
86+
let expectedLines = expected.split(separator: "\n")
87+
88+
let difference = actualLines.difference(from: expectedLines)
89+
90+
var result = ""
91+
92+
var insertions = [Int: Substring]()
93+
var removals = [Int: Substring]()
94+
95+
for change in difference {
96+
switch change {
97+
case .insert(let offset, let element, _):
98+
insertions[offset] = element
99+
case .remove(let offset, let element, _):
100+
removals[offset] = element
101+
}
102+
}
103+
104+
var expectedLine = 0
105+
var actualLine = 0
106+
107+
while expectedLine < expectedLines.count || actualLine < actualLines.count {
108+
if let removal = removals[expectedLine] {
109+
result += "\(removal)\n"
110+
expectedLine += 1
111+
} else if let insertion = insertions[actualLine] {
112+
result += "+\(insertion)\n"
113+
actualLine += 1
114+
} else {
115+
result += " \(expectedLines[expectedLine])\n"
116+
expectedLine += 1
117+
actualLine += 1
118+
}
119+
}
120+
121+
stringComparison = result
122+
123+
var fullMessage = """
124+
\(message.isEmpty ? "Actual output does not match the expected" : message)
125+
\(stringComparison)
126+
"""
127+
if let additional = additionalInfo() {
128+
fullMessage = """
129+
\(fullMessage)
130+
\(additional)
131+
"""
132+
}
133+
failureHandler(TestFailureSpec(message: fullMessage, location: location))
134+
}

Sources/_SwiftSyntaxTestSupport/String+TrimmingTrailingWhitespace.swift renamed to Sources/_SwiftSyntaxTestSupportFrameworkAgnostic/String+TrimmingTrailingWhitespace.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ extension String {
1515
public func trimmingTrailingWhitespace() -> String {
1616
return
1717
self
18-
.replacingOccurrences(of: "[ ]+\\n", with: "\n", options: .regularExpression)
19-
.trimmingCharacters(in: [" "])
18+
.split(separator: "\n", omittingEmptySubsequences: false)
19+
.map { $0.dropLast(while: { $0 == " " }) }
20+
.joined(separator: "\n")
21+
}
22+
}
23+
24+
fileprivate extension Substring {
25+
func dropLast(while predicate: (Character) -> Bool) -> String {
26+
return String(self.reversed().drop(while: predicate).reversed())
2027
}
2128
}

Tests/SwiftParserTest/Assertions.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ private func assertTokens(
9999
}
100100

101101
if actualLexeme.leadingTriviaText != expectedLexeme.leadingTrivia {
102-
failStringsEqualWithDiff(
102+
assertStringsEqualWithDiff(
103103
String(syntaxText: actualLexeme.leadingTriviaText),
104104
String(syntaxText: expectedLexeme.leadingTrivia),
105105
"Leading trivia does not match",
@@ -109,7 +109,7 @@ private func assertTokens(
109109
}
110110

111111
if actualLexeme.tokenText.debugDescription != expectedLexeme.tokenText.debugDescription {
112-
failStringsEqualWithDiff(
112+
assertStringsEqualWithDiff(
113113
actualLexeme.tokenText.debugDescription,
114114
expectedLexeme.tokenText.debugDescription,
115115
"Token text does not match",
@@ -119,7 +119,7 @@ private func assertTokens(
119119
}
120120

121121
if actualLexeme.trailingTriviaText != expectedLexeme.trailingTrivia {
122-
failStringsEqualWithDiff(
122+
assertStringsEqualWithDiff(
123123
String(syntaxText: actualLexeme.trailingTriviaText),
124124
String(syntaxText: expectedLexeme.trailingTrivia),
125125
"Trailing trivia does not match",
@@ -402,7 +402,7 @@ func assertDiagnostic<T: SyntaxProtocol>(
402402
line: spec.line
403403
)
404404
} else if spec.fixIts != diag.fixIts.map(\.message.message) {
405-
failStringsEqualWithDiff(
405+
assertStringsEqualWithDiff(
406406
diag.fixIts.map(\.message.message).joined(separator: "\n"),
407407
spec.fixIts.joined(separator: "\n"),
408408
file: spec.file,

0 commit comments

Comments
 (0)