@@ -9,6 +9,85 @@ final class LinkageTest: XCTestCase {
9
9
// a SwiftSyntax maintainer to see if there's a way to avoid adding the
10
10
// dependency.
11
11
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 ? {
12
91
for i in 0 ..< _dyld_image_count ( ) {
13
92
let name = try XCTUnwrap ( _dyld_get_image_name ( i) )
14
93
let path = String ( cString: name)
@@ -22,39 +101,12 @@ final class LinkageTest: XCTestCase {
22
101
}
23
102
24
103
if baseURL. pathComponents. isEmpty {
25
- XCTFail ( " Unable to determine path to enclosing xctest bundle " )
26
- return
104
+ return nil
27
105
}
28
106
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 ( )
52
108
}
53
- }
54
-
55
- private enum Linkage : Comparable {
56
- case library( String )
57
- case framework( String )
109
+ return nil
58
110
}
59
111
60
112
private func readLoadCommands( in object: URL ) throws -> [ String ] {
@@ -75,14 +127,35 @@ final class LinkageTest: XCTestCase {
75
127
return output. components ( separatedBy: . newlines)
76
128
}
77
129
78
- private func assertLinkage ( of object: URL , assertion : ( [ Linkage ] ) throws -> Void ) throws {
130
+ private func extractLinkages ( from object: URL ) throws -> [ Linkage ] {
79
131
var linkages = [ Linkage] ( )
80
132
var lines = try self . readLoadCommands ( in: object) . makeIterator ( )
81
133
while let line = lines. next ( ) {
82
134
guard line. starts ( with: " Load command " ) else {
83
135
continue
84
136
}
85
137
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
+ // ```
86
159
guard
87
160
let command = lines. next ( ) ,
88
161
command. hasSuffix ( " LC_LINKER_OPTION " ) ,
@@ -96,13 +169,13 @@ final class LinkageTest: XCTestCase {
96
169
. suffix ( from: count. index ( count. startIndex, offsetBy: " count " . count) )
97
170
guard let count = Int ( countString) , count == 1 || count == 2 else {
98
171
XCTFail ( " Malformed load command: \( line) " )
99
- return
172
+ return linkages
100
173
}
101
174
102
175
if count == 1 {
103
176
guard let library = lines. next ( ) else {
104
177
XCTFail ( " No load command payload: \( line) " )
105
- return
178
+ return linkages
106
179
}
107
180
108
181
let linkLibrary = library. trimmingCharacters ( in: . whitespaces)
@@ -116,15 +189,109 @@ final class LinkageTest: XCTestCase {
116
189
let framework = lines. next ( )
117
190
else {
118
191
XCTFail ( " No load command payload: \( line) " )
119
- return
192
+ return linkages
120
193
}
121
194
122
195
let linkedFramework = framework. trimmingCharacters ( in: . whitespaces)
123
196
. suffix ( from: framework. index ( framework. startIndex, offsetBy: " string #2 " . count) )
124
197
linkages. append ( . framework( String ( linkedFramework) ) )
125
198
}
126
199
}
127
- return try assertion ( linkages. sorted ( ) )
200
+ return linkages
201
+ }
202
+ }
203
+ #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
+ }
128
239
}
129
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!")
130
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