Skip to content

Commit d2c1488

Browse files
committed
Add infrastructure to write tests for Swift macros
Add a default package manifest that defines two targets: - A macro target named `MyMacro` - And executable target named `MyMacroClient` It builds the macro using the swift-syntax that was already built as part of the SourceKit-LSP build. Re-using the SwiftSyntax modules that are already built is significantly faster than building swift-syntax in each test case run and does not require internet access.
1 parent 48df054 commit d2c1488

File tree

5 files changed

+156
-12
lines changed

5 files changed

+156
-12
lines changed

Package.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,10 @@ let package = Package(
381381
.product(name: "SwiftParser", package: "swift-syntax"),
382382
.product(name: "SwiftSyntax", package: "swift-syntax"),
383383
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
384+
// Depend on `SwiftCompilerPlugin` and `SwiftSyntaxMacros` so the modules are built before running tests and can
385+
// be used by test cases that test macros (see `SwiftPMTestProject.macroPackageManifest`).
386+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
387+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
384388
]
385389
),
386390
]

Sources/SKTestSupport/SwiftPMTestProject.swift

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,87 @@ public class SwiftPMTestProject: MultiFileTestProject {
3333
)
3434
"""
3535

36+
/// A manifest that defines two targets:
37+
/// - A macro target named `MyMacro`
38+
/// - And executable target named `MyMacroClient`
39+
///
40+
/// It builds the macro using the swift-syntax that was already built as part of the SourceKit-LSP build.
41+
/// Re-using the SwiftSyntax modules that are already built is significantly faster than building swift-syntax in
42+
/// each test case run and does not require internet access.
43+
public static var macroPackageManifest: String {
44+
get async throws {
45+
// build directory is `.build/debug/arm64-apple-macosx` or the platform-specific equivalent
46+
let buildDirectory = testBundle.bundleURL.deletingLastPathComponent()
47+
48+
let swiftSyntaxModulesToLink = [
49+
"SwiftBasicFormat",
50+
"SwiftCompilerPlugin",
51+
"SwiftCompilerPluginMessageHandling",
52+
"SwiftDiagnostics",
53+
"SwiftOperators",
54+
"SwiftParser",
55+
"SwiftParserDiagnostics",
56+
"SwiftSyntax",
57+
"SwiftSyntaxBuilder",
58+
"SwiftSyntaxMacroExpansion",
59+
"SwiftSyntaxMacros",
60+
]
61+
62+
var objectFiles: [String] = []
63+
for moduleName in swiftSyntaxModulesToLink {
64+
let dir = buildDirectory.appendingPathComponent("\(moduleName).build")
65+
let enumerator = FileManager.default.enumerator(at: dir, includingPropertiesForKeys: nil)
66+
while let file = enumerator?.nextObject() as? URL {
67+
if file.pathExtension == "o" {
68+
objectFiles.append(file.path)
69+
}
70+
}
71+
}
72+
73+
let linkerFlags = objectFiles.map {
74+
"""
75+
"-l", "\($0)",
76+
"""
77+
}.joined(separator: "\n")
78+
79+
let moduleSearchPath: String
80+
if let toolchainVersion = try await ToolchainRegistry.forTesting.default?.swiftVersion,
81+
toolchainVersion < SwiftVersion(6, 0)
82+
{
83+
moduleSearchPath = buildDirectory.path
84+
} else {
85+
moduleSearchPath = "\(buildDirectory.path)/Modules"
86+
}
87+
88+
return """
89+
// swift-tools-version: 5.10
90+
91+
import PackageDescription
92+
import CompilerPluginSupport
93+
94+
let package = Package(
95+
name: "MyMacro",
96+
platforms: [.macOS(.v10_15)],
97+
targets: [
98+
.macro(
99+
name: "MyMacros",
100+
swiftSettings: [.unsafeFlags([
101+
"-I", "\(moduleSearchPath)",
102+
"-Xcc", "-fmodule-map-file=\(buildDirectory.path)/../../checkouts/swift-syntax/Sources/_SwiftSyntaxCShims/include/module.modulemap"
103+
])],
104+
linkerSettings: [
105+
.unsafeFlags([
106+
\(linkerFlags)
107+
])
108+
]
109+
),
110+
.executableTarget(name: "MyMacroClient", dependencies: ["MyMacros"]),
111+
]
112+
)
113+
"""
114+
}
115+
}
116+
36117
/// Create a new SwiftPM package with the given files.
37118
///
38119
/// If `index` is `true`, then the package will be built, indexing all modules within the package.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
15+
/// The bundle of the currently executing test.
16+
public let testBundle: Bundle = {
17+
#if os(macOS)
18+
if let bundle = Bundle.allBundles.first(where: { $0.bundlePath.hasSuffix(".xctest") }) {
19+
return bundle
20+
}
21+
fatalError("couldn't find the test bundle")
22+
#else
23+
return Bundle.main
24+
#endif
25+
}()

Tests/SKCoreTests/BuildServerBuildSystemTests.swift

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,6 @@ import SKTestSupport
2020
import TSCBasic
2121
import XCTest
2222

23-
/// The bundle of the currently executing test.
24-
private let testBundle: Bundle = {
25-
#if os(macOS)
26-
if let bundle = Bundle.allBundles.first(where: { $0.bundlePath.hasSuffix(".xctest") }) {
27-
return bundle
28-
}
29-
fatalError("couldn't find the test bundle")
30-
#else
31-
return Bundle.main
32-
#endif
33-
}()
34-
3523
/// The path to the built products directory.
3624
private let productsDirectory: URL = {
3725
#if os(macOS)

Tests/SKSwiftPMWorkspaceTests/SwiftPMBuildSystemTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,52 @@ final class SwiftPMBuildSystemTests: XCTestCase {
772772
assertArgumentsContain(aswift.pathString, arguments: arguments)
773773
}
774774
}
775+
776+
func testBuildMacro() async throws {
777+
// This test is just a dummy to show how to create a `SwiftPMTestProject` that builds a macro using the SwiftSyntax
778+
// modules that were already built during the build of SourceKit-LSP.
779+
// It should be removed once we have a real test that tests macros (like macro expansion).
780+
_ = try await SwiftPMTestProject(
781+
files: [
782+
"MyMacros/MyMacros.swift": #"""
783+
import SwiftCompilerPlugin
784+
import SwiftSyntax
785+
import SwiftSyntaxBuilder
786+
import SwiftSyntaxMacros
787+
788+
public struct StringifyMacro: ExpressionMacro {
789+
public static func expansion(
790+
of node: some FreestandingMacroExpansionSyntax,
791+
in context: some MacroExpansionContext
792+
) -> ExprSyntax {
793+
guard let argument = node.argumentList.first?.expression else {
794+
fatalError("compiler bug: the macro does not have any arguments")
795+
}
796+
797+
return "(\(argument), \(literal: argument.description))"
798+
}
799+
}
800+
801+
@main
802+
struct MyMacroPlugin: CompilerPlugin {
803+
let providingMacros: [Macro.Type] = [
804+
StringifyMacro.self,
805+
]
806+
}
807+
"""#,
808+
"MyMacroClient/MyMacroClient.swift": """
809+
@freestanding(expression)
810+
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacros", type: "StringifyMacro")
811+
812+
func test() {
813+
#stringify(1 + 2)
814+
}
815+
""",
816+
],
817+
manifest: SwiftPMTestProject.macroPackageManifest,
818+
build: true
819+
)
820+
}
775821
}
776822

777823
private func assertArgumentsDoNotContain(

0 commit comments

Comments
 (0)