Skip to content

Commit 652943f

Browse files
authored
Merge pull request #796 from CodaFi/unchained-melody
2 parents 6bec0f9 + 217100f commit 652943f

File tree

1 file changed

+319
-0
lines changed

1 file changed

+319
-0
lines changed

Tests/SwiftParserTest/Linkage.swift

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
#if canImport(Darwin)
2+
import Darwin
3+
import XCTest
4+
5+
final class LinkageTest: XCTestCase {
6+
// Assert that SwiftSyntax and SwiftParser do not introduce more link-time
7+
// dependencies than are strictly necessary. We want to minimize our link-time
8+
// dependencies. If this set changes - in particular, if it grows - consult
9+
// a SwiftSyntax maintainer to see if there's a way to avoid adding the
10+
// dependency.
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+
try assertLinkage(of: "SwiftSyntax", in: baseURL, assertions: [
18+
.library("-lobjc"),
19+
.library("-lswiftCompatibility51", condition: .mayBeAbsent("Starting in Xcode 14 this library is not always autolinked")),
20+
.library("-lswiftCompatibility56", 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+
try assertLinkage(of: "SwiftParser", in: baseURL, assertions: [
30+
.library("-lobjc"),
31+
.library("-lswiftCompatibility51", condition: .mayBeAbsent("Starting in Xcode 14 this library is not always autolinked")),
32+
.library("-lswiftCompatibility56", 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+
43+
extension LinkageTest {
44+
public struct MachHeader {
45+
var magic: UInt32
46+
var cputype: UInt32
47+
var cpusubtype: UInt32
48+
var filetype: UInt32
49+
var ncmds: UInt32
50+
var sizeofcmds: UInt32
51+
var flags: UInt32
52+
var reserved: UInt32
53+
54+
public struct LoadCommand: OptionSet {
55+
public var rawValue: UInt32
56+
57+
public init(rawValue: UInt32) {
58+
self.rawValue = rawValue
59+
}
60+
61+
/// A load command that defines a list of linker options strings embedded
62+
/// directly into this file.
63+
public static let linkerOption = Self(rawValue: 0x2D)
64+
}
65+
}
66+
67+
struct LoadCommand {
68+
var cmd: MachHeader.LoadCommand
69+
var cmdsize: UInt32
70+
}
71+
72+
struct LinkerOptionCommand {
73+
var cmd: MachHeader.LoadCommand
74+
var cmdsize: UInt32
75+
var count: UInt32
76+
/* concatenation of zero terminated UTF8 strings.
77+
Zero filled at end to align */
78+
}
79+
}
80+
81+
extension LinkageTest {
82+
private func assertLinkage(
83+
of library: String,
84+
in bundle: EnclosingTestBundle,
85+
assertions: [Linkage.Assertion]
86+
) throws {
87+
let linkages = try bundle.objectFiles(for: library).reduce(into: []) { acc, next in
88+
acc += try self.extractAutolinkingHints(in: next)
89+
}
90+
91+
let sortedLinkages = Set(linkages).sorted()
92+
var expectedLinkagesIdx = sortedLinkages.startIndex
93+
var assertions = assertions.makeIterator()
94+
while let assert = assertions.next() {
95+
// Evaluate the condition first, if any.
96+
if let condition = assert.condition, !condition.evaluate() {
97+
continue
98+
}
99+
100+
// Make sure there's a linkage to even assert against. If not, then we've
101+
// got too many assertions and not enough link libraries. This isn't
102+
// always a bad thing, but it's worth calling out so we document this
103+
// with `.mayBeAbsent(...)`.
104+
guard expectedLinkagesIdx < sortedLinkages.endIndex else {
105+
XCTFail("Expected linkage was not present: \(assert.linkage)",
106+
file: assert.file, line: assert.line)
107+
continue
108+
}
109+
110+
let linkage = sortedLinkages[expectedLinkagesIdx]
111+
112+
// Check the linkage assertion. If it doesn't hold, the real fun begins.
113+
guard !assert.matches(linkage) else {
114+
expectedLinkagesIdx += 1
115+
continue
116+
}
117+
118+
// Skip flaky linkages if they're absent.
119+
if case .flaky = assert.condition {
120+
continue
121+
}
122+
123+
XCTFail("Expected linkage to \(assert.linkage), but recieved linkage to \(linkage.linkage); Perhaps linkage assertions are out of order?",
124+
file: assert.file, line: assert.line)
125+
expectedLinkagesIdx += 1
126+
}
127+
128+
for superfluousLinkage in sortedLinkages[expectedLinkagesIdx..<sortedLinkages.endIndex] {
129+
XCTFail("Found unasserted link-time dependency: \(superfluousLinkage.linkage)")
130+
}
131+
}
132+
133+
private enum EnclosingTestBundle {
134+
case incremental(URL)
135+
case unified(URL)
136+
137+
func objectFiles(for library: String) throws -> [URL] {
138+
switch self {
139+
case .incremental(let baseURL):
140+
return [baseURL.appendingPathComponent(library + ".o")]
141+
case .unified(let baseURL):
142+
return try FileManager.default
143+
.contentsOfDirectory(at: baseURL.appendingPathComponent(library + ".build"),
144+
includingPropertiesForKeys: nil)
145+
.filter({ $0.pathExtension == "o" })
146+
}
147+
}
148+
}
149+
150+
private func findEnclosingTestBundle() throws -> EnclosingTestBundle? {
151+
for i in 0..<_dyld_image_count() {
152+
let name = try XCTUnwrap(_dyld_get_image_name(i))
153+
let path = String(cString: name)
154+
// We can wind up in SwiftParserTest.xctest when built via the IDE or
155+
// in SwiftSyntaxPackageTests.xctest when built at the command line
156+
// via the package manager.
157+
guard path.hasSuffix("SwiftParserTest") || path.hasSuffix("SwiftSyntaxPackageTests") else {
158+
continue
159+
}
160+
161+
var baseURL = URL(fileURLWithPath: path)
162+
while !baseURL.pathComponents.isEmpty, baseURL.pathExtension != "xctest" {
163+
baseURL = baseURL.deletingLastPathComponent()
164+
}
165+
166+
if baseURL.pathComponents.isEmpty {
167+
continue
168+
}
169+
170+
let url = baseURL.deletingLastPathComponent()
171+
if path.hasSuffix("SwiftParserTest") {
172+
return .incremental(url)
173+
} else {
174+
return .unified(url)
175+
}
176+
}
177+
return nil
178+
}
179+
180+
private func extractAutolinkingHints(in object: URL) throws -> [Linkage] {
181+
let data = try Data(contentsOf: object, options: .mappedIfSafe)
182+
assert(data.starts(with: [0xcf,0xfa,0xed,0xfe]), "Not a mach object file?")
183+
return data.withUnsafeBytes { (buf: UnsafeRawBufferPointer) -> [Linkage] in
184+
var result = [Linkage]()
185+
guard let base = buf.baseAddress else {
186+
return []
187+
}
188+
189+
let hdr = base.bindMemory(to: MachHeader.self, capacity: 1)
190+
191+
var commandStart = base + MemoryLayout<MachHeader>.size
192+
for _ in 0..<Int(hdr.pointee.ncmds) {
193+
let command = commandStart.load(as: LoadCommand.self)
194+
defer {
195+
commandStart = commandStart.advanced(by: Int(command.cmdsize))
196+
}
197+
198+
switch command.cmd {
199+
case .linkerOption:
200+
let (namePtr, cmdCount) = commandStart.withMemoryRebound(to: LinkerOptionCommand.self, capacity: 1, { cmd in
201+
return cmd.withMemoryRebound(to: CChar.self, capacity: 1) { p in
202+
return (p.advanced(by: MemoryLayout<LinkerOptionCommand>.size), Int(cmd.pointee.count))
203+
}
204+
})
205+
if cmdCount == 1 {
206+
result.append(.library(String(cString: namePtr)))
207+
} else if cmdCount == 2 {
208+
guard String(cString: namePtr) == "-framework" else {
209+
continue
210+
}
211+
212+
let frameworkName = String(cString: namePtr.advanced(by: "-framework".utf8.count + 1))
213+
result.append(.framework(frameworkName))
214+
} else {
215+
XCTFail("Unexpected number of linker options: \(cmdCount)")
216+
}
217+
default:
218+
continue
219+
}
220+
}
221+
return result
222+
}
223+
}
224+
}
225+
#endif
226+
227+
fileprivate enum Linkage: Comparable, Hashable {
228+
case library(String)
229+
case framework(String)
230+
231+
var linkage: String {
232+
switch self {
233+
case .library(let s): return s
234+
case .framework(let s): return s
235+
}
236+
}
237+
238+
func hasPrefix(_ prefix: String) -> Bool {
239+
return self.linkage.hasPrefix(prefix)
240+
}
241+
}
242+
243+
extension Linkage {
244+
fileprivate struct Assertion {
245+
var linkage: Linkage
246+
var condition: Condition?
247+
var file: StaticString
248+
var line: UInt
249+
250+
func matches(_ linkage: Linkage) -> Bool {
251+
return self.linkage == linkage
252+
}
253+
254+
static func library(_ linkage: String, condition: Condition? = nil, file: StaticString = #file, line: UInt = #line) -> Assertion {
255+
return Linkage.Assertion(linkage: .library(linkage), condition: condition, file: file, line: line)
256+
}
257+
258+
static func framework(_ linkage: String, condition: Condition? = nil, file: StaticString = #file, line: UInt = #line) -> Assertion {
259+
return Linkage.Assertion(linkage: .framework(linkage), condition: condition, file: file, line: line)
260+
}
261+
}
262+
}
263+
264+
extension Linkage.Assertion {
265+
fileprivate enum Condition {
266+
case swiftVersionAtLeast(versionBound: SwiftVersion)
267+
case configuration(ProductConfiguration)
268+
case flaky
269+
270+
enum SwiftVersion: Comparable {
271+
case v5_5
272+
case v5_6
273+
case v5_7
274+
// We don't support compiling with <=5.4
275+
}
276+
277+
enum ProductConfiguration: Equatable {
278+
case debug
279+
case release
280+
}
281+
282+
fileprivate static func when(swiftVersionAtLeast version: SwiftVersion) -> Condition {
283+
return .swiftVersionAtLeast(versionBound: version)
284+
}
285+
286+
fileprivate static func when(configuration: ProductConfiguration) -> Condition {
287+
return .configuration(configuration)
288+
}
289+
290+
fileprivate static func mayBeAbsent(_ reason: StaticString) -> Condition {
291+
return .flaky
292+
}
293+
294+
fileprivate func evaluate() -> Bool {
295+
switch self {
296+
case let .swiftVersionAtLeast(versionBound: bound):
297+
#if swift(>=5.7)
298+
let version: SwiftVersion = .v5_7
299+
#elseif swift(>=5.6)
300+
let version: SwiftVersion = .v5_6
301+
#elseif swift(>=5.5)
302+
let version: SwiftVersion = .v5_5
303+
#else
304+
#error("Swift version is too old!")
305+
#endif
306+
return version >= bound
307+
case let .configuration(expectation):
308+
#if DEBUG
309+
let config: ProductConfiguration = .debug
310+
#else
311+
let config: ProductConfiguration = .release
312+
#endif
313+
return config == expectation
314+
case .flaky:
315+
return true
316+
}
317+
}
318+
}
319+
}

0 commit comments

Comments
 (0)