Skip to content

Commit 33a5e76

Browse files
committed
Generalize Linkage Testing Infrastructure
Allow for linkage conditions based on the language version and build configuration.
1 parent 2a03c79 commit 33a5e76

File tree

1 file changed

+202
-35
lines changed

1 file changed

+202
-35
lines changed

Tests/SwiftParserTest/Linkage.swift

Lines changed: 202 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,85 @@ final class LinkageTest: XCTestCase {
99
// a SwiftSyntax maintainer to see if there's a way to avoid adding the
1010
// dependency.
1111
func testLinkage() throws {
12+
guard let baseURL = try self.findEnclosingTestBundle() else {
13+
XCTFail("Unable to determine path to enclosing xctest bundle")
14+
return
15+
}
16+
17+
let swiftSyntaxURL = baseURL.appendingPathComponent("SwiftSyntax.o")
18+
try assertLinkage(of: swiftSyntaxURL, assertions: [
19+
.library("-lobjc"),
20+
.library("-lswiftCompatibility51", condition: .mayBeAbsent("Starting in Xcode 14 this library is not always autolinked")),
21+
.library("-lswiftCompatibilityConcurrency"),
22+
.library("-lswiftCore"),
23+
.library("-lswiftDarwin"),
24+
.library("-lswiftSwiftOnoneSupport", condition: .when(configuration: .debug)),
25+
.library("-lswift_Concurrency"),
26+
.library("-lswift_StringProcessing", condition: .when(swiftVersionAtLeast: .v5_7)),
27+
])
28+
29+
let swiftParserURL = baseURL.appendingPathComponent("SwiftParser.o")
30+
try assertLinkage(of: swiftParserURL, assertions: [
31+
.library("-lobjc"),
32+
.library("-lswiftCompatibility51", condition: .mayBeAbsent("Starting in Xcode 14 this library is not always autolinked")),
33+
.library("-lswiftCompatibilityConcurrency"),
34+
.library("-lswiftCore"),
35+
.library("-lswiftDarwin"),
36+
.library("-lswiftSwiftOnoneSupport", condition: .when(configuration: .debug)),
37+
.library("-lswift_Concurrency"),
38+
.library("-lswift_StringProcessing", condition: .when(swiftVersionAtLeast: .v5_7)),
39+
])
40+
}
41+
42+
private func assertLinkage(
43+
of object: URL,
44+
assertions: [Linkage.Assertion]
45+
) throws {
46+
let linkages = try self.extractLinkages(from: object)
47+
48+
let sortedLinkages = Set(linkages).sorted()
49+
var expectedLinkagesIdx = sortedLinkages.startIndex
50+
var assertions = assertions.makeIterator()
51+
while let assert = assertions.next() {
52+
// Evaluate the condition first, if any.
53+
if let condition = assert.condition, !condition.evaluate() {
54+
continue
55+
}
56+
57+
// Make sure there's a linkage to even assert against. If not, then we've
58+
// got too many assertions and not enough link libraries. This isn't
59+
// always a bad thing, but it's worth calling out so we document this
60+
// with `.mayBeAbsent(...)`.
61+
guard expectedLinkagesIdx < sortedLinkages.endIndex else {
62+
XCTFail("Expected linkage was not present: \(assert.linkage)",
63+
file: assert.file, line: assert.line)
64+
continue
65+
}
66+
67+
let linkage = sortedLinkages[expectedLinkagesIdx]
68+
69+
// Check the linkage assertion. If it doesn't hold, the real fun begins.
70+
guard !assert.matches(linkage) else {
71+
expectedLinkagesIdx += 1
72+
continue
73+
}
74+
75+
// Skip flaky linkages if they're absent.
76+
if case .flaky = assert.condition {
77+
continue
78+
}
79+
80+
XCTFail("Expected linkage to \(assert.linkage), but recieved linkage to \(linkage.linkage); Perhaps linkage assertions are out of order?",
81+
file: assert.file, line: assert.line)
82+
expectedLinkagesIdx += 1
83+
}
84+
85+
for superfluousLinkage in sortedLinkages[expectedLinkagesIdx..<sortedLinkages.endIndex] {
86+
XCTFail("Found unasserted link-time dependency: \(superfluousLinkage.linkage)")
87+
}
88+
}
89+
90+
private func findEnclosingTestBundle() throws -> URL? {
1291
for i in 0..<_dyld_image_count() {
1392
let name = try XCTUnwrap(_dyld_get_image_name(i))
1493
let path = String(cString: name)
@@ -22,39 +101,12 @@ final class LinkageTest: XCTestCase {
22101
}
23102

24103
if baseURL.pathComponents.isEmpty {
25-
XCTFail("Unable to determine path to enclosing xctest bundle")
26-
return
104+
continue
27105
}
28106

29-
baseURL = baseURL.deletingLastPathComponent()
30-
let swiftSyntaxURL = baseURL.appendingPathComponent("SwiftSyntax.o")
31-
try assertLinkage(of: swiftSyntaxURL) { linkages in
32-
XCTAssertEqual(linkages, [
33-
.library("-lobjc"),
34-
.library("-lswiftCompatibilityConcurrency"),
35-
.library("-lswiftCore"),
36-
.library("-lswiftDarwin"),
37-
.library("-lswiftSwiftOnoneSupport"),
38-
.library("-lswift_Concurrency"),
39-
])
40-
}
41-
let swiftParserURL = baseURL.appendingPathComponent("SwiftParser.o")
42-
try assertLinkage(of: swiftParserURL) { linkages in
43-
XCTAssertEqual(linkages, [
44-
.library("-lobjc"),
45-
.library("-lswiftCompatibilityConcurrency"),
46-
.library("-lswiftCore"),
47-
.library("-lswiftDarwin"),
48-
.library("-lswiftSwiftOnoneSupport"),
49-
.library("-lswift_Concurrency"),
50-
])
51-
}
107+
return baseURL.deletingLastPathComponent()
52108
}
53-
}
54-
55-
private enum Linkage: Comparable {
56-
case library(String)
57-
case framework(String)
109+
return nil
58110
}
59111

60112
private func readLoadCommands(in object: URL) throws -> [String] {
@@ -75,14 +127,35 @@ final class LinkageTest: XCTestCase {
75127
return output.components(separatedBy: .newlines)
76128
}
77129

78-
private func assertLinkage(of object: URL, assertion: ([Linkage]) throws -> Void) throws {
130+
private func extractLinkages(from object: URL) throws -> [Linkage] {
79131
var linkages = [Linkage]()
80132
var lines = try self.readLoadCommands(in: object).makeIterator()
81133
while let line = lines.next() {
82134
guard line.starts(with: "Load command") else {
83135
continue
84136
}
85137

138+
// The load commands we're interested in are all autolinking hints of the
139+
// following form:
140+
//
141+
// ```
142+
// Load command <N>
143+
// cmd LC_LINKER_OPTION
144+
// cmdsize <N>
145+
// count 1
146+
// string #1 -l<lib>
147+
// ```
148+
//
149+
// Or
150+
//
151+
// ```
152+
// Load command <N>
153+
// cmd LC_LINKER_OPTION
154+
// cmdsize <N>
155+
// count 2
156+
// string #1 -framework
157+
// string #2 Foundation
158+
// ```
86159
guard
87160
let command = lines.next(),
88161
command.hasSuffix("LC_LINKER_OPTION"),
@@ -96,13 +169,13 @@ final class LinkageTest: XCTestCase {
96169
.suffix(from: count.index(count.startIndex, offsetBy: "count ".count))
97170
guard let count = Int(countString), count == 1 || count == 2 else {
98171
XCTFail("Malformed load command: \(line)")
99-
return
172+
return linkages
100173
}
101174

102175
if count == 1 {
103176
guard let library = lines.next() else {
104177
XCTFail("No load command payload: \(line)")
105-
return
178+
return linkages
106179
}
107180

108181
let linkLibrary = library.trimmingCharacters(in: .whitespaces)
@@ -116,15 +189,109 @@ final class LinkageTest: XCTestCase {
116189
let framework = lines.next()
117190
else {
118191
XCTFail("No load command payload: \(line)")
119-
return
192+
return linkages
120193
}
121194

122195
let linkedFramework = framework.trimmingCharacters(in: .whitespaces)
123196
.suffix(from: framework.index(framework.startIndex, offsetBy: "string #2 ".count))
124197
linkages.append(.framework(String(linkedFramework)))
125198
}
126199
}
127-
return try assertion(linkages.sorted())
200+
return linkages
128201
}
129202
}
130203
#endif
204+
205+
fileprivate enum Linkage: Comparable, Hashable {
206+
case library(String)
207+
case framework(String)
208+
209+
var linkage: String {
210+
switch self {
211+
case .library(let s): return s
212+
case .framework(let s): return s
213+
}
214+
}
215+
216+
func hasPrefix(_ prefix: String) -> Bool {
217+
return self.linkage.hasPrefix(prefix)
218+
}
219+
}
220+
221+
extension Linkage {
222+
fileprivate struct Assertion {
223+
var linkage: Linkage
224+
var condition: Condition?
225+
var file: StaticString
226+
var line: UInt
227+
228+
func matches(_ linkage: Linkage) -> Bool {
229+
return self.linkage == linkage
230+
}
231+
232+
static func library(_ linkage: String, condition: Condition? = nil, file: StaticString = #file, line: UInt = #line) -> Assertion {
233+
return Linkage.Assertion(linkage: .library(linkage), condition: condition, file: file, line: line)
234+
}
235+
236+
static func framework(_ linkage: String, condition: Condition? = nil, file: StaticString = #file, line: UInt = #line) -> Assertion {
237+
return Linkage.Assertion(linkage: .framework(linkage), condition: condition, file: file, line: line)
238+
}
239+
}
240+
}
241+
242+
extension Linkage.Assertion {
243+
fileprivate enum Condition {
244+
case swiftVersionAtLeast(versionBound: SwiftVersion)
245+
case configuration(ProductConfiguration)
246+
case flaky
247+
248+
enum SwiftVersion: Comparable {
249+
case v5_5
250+
case v5_6
251+
case v5_7
252+
// We don't support compiling with <=5.4
253+
}
254+
255+
enum ProductConfiguration: Equatable {
256+
case debug
257+
case release
258+
}
259+
260+
fileprivate static func when(swiftVersionAtLeast version: SwiftVersion) -> Condition {
261+
return .swiftVersionAtLeast(versionBound: version)
262+
}
263+
264+
fileprivate static func when(configuration: ProductConfiguration) -> Condition {
265+
return .configuration(configuration)
266+
}
267+
268+
fileprivate static func mayBeAbsent(_ reason: StaticString) -> Condition {
269+
return .flaky
270+
}
271+
272+
fileprivate func evaluate() -> Bool {
273+
switch self {
274+
case let .swiftVersionAtLeast(versionBound: bound):
275+
#if swift(>=5.7)
276+
let version: SwiftVersion = .v5_7
277+
#elseif swift(>=5.6)
278+
let version: SwiftVersion = .v5_6
279+
#elseif swift(>=5.5)
280+
let version: SwiftVersion = .v5_5
281+
#else
282+
#error("Swift version is too old!")
283+
#endif
284+
return version >= bound
285+
case let .configuration(expectation):
286+
#if DEBUG
287+
let config: ProductConfiguration = .debug
288+
#else
289+
let config: ProductConfiguration = .release
290+
#endif
291+
return config == expectation
292+
case .flaky:
293+
return true
294+
}
295+
}
296+
}
297+
}

0 commit comments

Comments
 (0)