@@ -9,6 +9,69 @@ 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: . 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 ? {
12
75
for i in 0 ..< _dyld_image_count ( ) {
13
76
let name = try XCTUnwrap ( _dyld_get_image_name ( i) )
14
77
let path = String ( cString: name)
@@ -22,39 +85,12 @@ final class LinkageTest: XCTestCase {
22
85
}
23
86
24
87
if baseURL. pathComponents. isEmpty {
25
- XCTFail ( " Unable to determine path to enclosing xctest bundle " )
26
- return
88
+ return nil
27
89
}
28
90
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 ( )
52
92
}
53
- }
54
-
55
- private enum Linkage : Comparable {
56
- case library( String )
57
- case framework( String )
93
+ return nil
58
94
}
59
95
60
96
private func readLoadCommands( in object: URL ) throws -> [ String ] {
@@ -75,14 +111,35 @@ final class LinkageTest: XCTestCase {
75
111
return output. components ( separatedBy: . newlines)
76
112
}
77
113
78
- private func assertLinkage ( of object: URL , assertion : ( [ Linkage ] ) throws -> Void ) throws {
114
+ private func extractLinkages ( from object: URL ) throws -> [ Linkage ] {
79
115
var linkages = [ Linkage] ( )
80
116
var lines = try self . readLoadCommands ( in: object) . makeIterator ( )
81
117
while let line = lines. next ( ) {
82
118
guard line. starts ( with: " Load command " ) else {
83
119
continue
84
120
}
85
121
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
+ // ```
86
143
guard
87
144
let command = lines. next ( ) ,
88
145
command. hasSuffix ( " LC_LINKER_OPTION " ) ,
@@ -96,13 +153,13 @@ final class LinkageTest: XCTestCase {
96
153
. suffix ( from: count. index ( count. startIndex, offsetBy: " count " . count) )
97
154
guard let count = Int ( countString) , count == 1 || count == 2 else {
98
155
XCTFail ( " Malformed load command: \( line) " )
99
- return
156
+ return linkages
100
157
}
101
158
102
159
if count == 1 {
103
160
guard let library = lines. next ( ) else {
104
161
XCTFail ( " No load command payload: \( line) " )
105
- return
162
+ return linkages
106
163
}
107
164
108
165
let linkLibrary = library. trimmingCharacters ( in: . whitespaces)
@@ -116,15 +173,103 @@ final class LinkageTest: XCTestCase {
116
173
let framework = lines. next ( )
117
174
else {
118
175
XCTFail ( " No load command payload: \( line) " )
119
- return
176
+ return linkages
120
177
}
121
178
122
179
let linkedFramework = framework. trimmingCharacters ( in: . whitespaces)
123
180
. suffix ( from: framework. index ( framework. startIndex, offsetBy: " string #2 " . count) )
124
181
linkages. append ( . framework( String ( linkedFramework) ) )
125
182
}
126
183
}
127
- return try assertion ( linkages. sorted ( ) )
184
+ return linkages
128
185
}
129
186
}
130
187
#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