Skip to content

Commit c63375d

Browse files
committed
Generalize Linkage Testing Infrastructure
Allow for linkage conditions based on the language version and build configuration.
1 parent 65d7d9a commit c63375d

File tree

1 file changed

+180
-35
lines changed

1 file changed

+180
-35
lines changed

Tests/SwiftParserTest/Linkage.swift

Lines changed: 180 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,69 @@ 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: .when(configuration: .release)),
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: .when(configuration: .release)),
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 expectedLinkages = sortedLinkages.makeIterator()
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+
// Then evaluate the assertion against the linkage.
58+
guard let linkage = expectedLinkages.next() else {
59+
XCTFail("Expected linkage was not present: \(assert.linkage)",
60+
file: assert.file, line: assert.line)
61+
continue
62+
}
63+
64+
XCTAssertTrue(assert.matches(linkage),
65+
"Expected linkage to \(assert.linkage), but recieved linkage to \(linkage.linkage); Perhaps linkage assertions are out of order?",
66+
file: assert.file, line: assert.line)
67+
}
68+
69+
while let superfluousLinkages = expectedLinkages.next() {
70+
XCTFail("Found unasserted link-time dependency: \(superfluousLinkages.linkage)")
71+
}
72+
}
73+
74+
private func findEnclosingTestBundle() throws -> URL? {
1275
for i in 0..<_dyld_image_count() {
1376
let name = try XCTUnwrap(_dyld_get_image_name(i))
1477
let path = String(cString: name)
@@ -22,39 +85,12 @@ final class LinkageTest: XCTestCase {
2285
}
2386

2487
if baseURL.pathComponents.isEmpty {
25-
XCTFail("Unable to determine path to enclosing xctest bundle")
26-
return
88+
return nil
2789
}
2890

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-
}
91+
return baseURL.deletingLastPathComponent()
5292
}
53-
}
54-
55-
private enum Linkage: Comparable {
56-
case library(String)
57-
case framework(String)
93+
return nil
5894
}
5995

6096
private func readLoadCommands(in object: URL) throws -> [String] {
@@ -75,14 +111,35 @@ final class LinkageTest: XCTestCase {
75111
return output.components(separatedBy: .newlines)
76112
}
77113

78-
private func assertLinkage(of object: URL, assertion: ([Linkage]) throws -> Void) throws {
114+
private func extractLinkages(from object: URL) throws -> [Linkage] {
79115
var linkages = [Linkage]()
80116
var lines = try self.readLoadCommands(in: object).makeIterator()
81117
while let line = lines.next() {
82118
guard line.starts(with: "Load command") else {
83119
continue
84120
}
85121

122+
// The load commands we're interested in are all autolinking hints of the
123+
// following form:
124+
//
125+
// ```
126+
// Load command <N>
127+
// cmd LC_LINKER_OPTION
128+
// cmdsize <N>
129+
// count 1
130+
// string #1 -l<lib>
131+
// ```
132+
//
133+
// Or
134+
//
135+
// ```
136+
// Load command <N>
137+
// cmd LC_LINKER_OPTION
138+
// cmdsize <N>
139+
// count 2
140+
// string #1 -framework
141+
// string #2 Foundation
142+
// ```
86143
guard
87144
let command = lines.next(),
88145
command.hasSuffix("LC_LINKER_OPTION"),
@@ -96,13 +153,13 @@ final class LinkageTest: XCTestCase {
96153
.suffix(from: count.index(count.startIndex, offsetBy: "count ".count))
97154
guard let count = Int(countString), count == 1 || count == 2 else {
98155
XCTFail("Malformed load command: \(line)")
99-
return
156+
return linkages
100157
}
101158

102159
if count == 1 {
103160
guard let library = lines.next() else {
104161
XCTFail("No load command payload: \(line)")
105-
return
162+
return linkages
106163
}
107164

108165
let linkLibrary = library.trimmingCharacters(in: .whitespaces)
@@ -116,15 +173,103 @@ final class LinkageTest: XCTestCase {
116173
let framework = lines.next()
117174
else {
118175
XCTFail("No load command payload: \(line)")
119-
return
176+
return linkages
120177
}
121178

122179
let linkedFramework = framework.trimmingCharacters(in: .whitespaces)
123180
.suffix(from: framework.index(framework.startIndex, offsetBy: "string #2 ".count))
124181
linkages.append(.framework(String(linkedFramework)))
125182
}
126183
}
127-
return try assertion(linkages.sorted())
184+
return linkages
128185
}
129186
}
130187
#endif
188+
189+
fileprivate enum Linkage: Comparable, Hashable {
190+
case library(String)
191+
case framework(String)
192+
193+
var linkage: String {
194+
switch self {
195+
case .library(let s): return s
196+
case .framework(let s): return s
197+
}
198+
}
199+
200+
func hasPrefix(_ prefix: String) -> Bool {
201+
return self.linkage.hasPrefix(prefix)
202+
}
203+
}
204+
205+
extension Linkage {
206+
fileprivate struct Assertion {
207+
var linkage: Linkage
208+
var condition: Condition?
209+
var file: StaticString
210+
var line: UInt
211+
212+
func matches(_ linkage: Linkage) -> Bool {
213+
return self.linkage == linkage
214+
}
215+
216+
static func library(_ linkage: String, condition: Condition? = nil, file: StaticString = #file, line: UInt = #line) -> Assertion {
217+
return Linkage.Assertion(linkage: .library(linkage), condition: condition, file: file, line: line)
218+
}
219+
220+
static func framework(_ linkage: String, condition: Condition? = nil, file: StaticString = #file, line: UInt = #line) -> Assertion {
221+
return Linkage.Assertion(linkage: .framework(linkage), condition: condition, file: file, line: line)
222+
}
223+
}
224+
}
225+
226+
extension Linkage.Assertion {
227+
fileprivate enum Condition {
228+
case swiftVersionAtLeast(versionBound: SwiftVersion)
229+
case configuration(ProductConfiguration)
230+
// case custom((Linkage) -> Bool)
231+
232+
enum SwiftVersion: Comparable {
233+
case v5_5
234+
case v5_6
235+
case v5_7
236+
// We don't support compiling with <=5.4
237+
}
238+
239+
enum ProductConfiguration: Equatable {
240+
case debug
241+
case release
242+
}
243+
244+
fileprivate static func when(swiftVersionAtLeast version: SwiftVersion) -> Condition {
245+
return .swiftVersionAtLeast(versionBound: version)
246+
}
247+
248+
fileprivate static func when(configuration: ProductConfiguration) -> Condition {
249+
return .configuration(configuration)
250+
}
251+
252+
fileprivate func evaluate() -> Bool {
253+
switch self {
254+
case let .swiftVersionAtLeast(versionBound: bound):
255+
#if swift(>=5.7)
256+
let version: SwiftVersion = .v5_7
257+
#elseif swift(>=5.6)
258+
let version: SwiftVersion = .v5_6
259+
#elseif swift(>=5.5)
260+
let version: SwiftVersion = .v5_5
261+
#else
262+
#error("Swift version is too old!")
263+
#endif
264+
return version >= bound
265+
case let .configuration(expectation):
266+
#if DEBUG
267+
let config: ProductConfiguration = .debug
268+
#else
269+
let config: ProductConfiguration = .release
270+
#endif
271+
return config == expectation
272+
}
273+
}
274+
}
275+
}

0 commit comments

Comments
 (0)