Files
sourcekit-lsp/Tests/BuildServerIntegrationTests/SwiftPMBuildServerTests.swift

1277 lines
43 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#if !NO_SWIFTPM_DEPENDENCY
@_spi(SourceKitLSP) import BuildServerProtocol
@_spi(Testing) import BuildServerIntegration
@_spi(SourceKitLSP) import LanguageServerProtocol
@_spi(SourceKitLSP) import LanguageServerProtocolExtensions
@_spi(SourceKitLSP) import LanguageServerProtocolTransport
import PackageModel
import SKLogging
import SKOptions
import SKTestSupport
import SourceKitLSP
@preconcurrency import SPMBuildCore
import SwiftExtensions
import TSCBasic
import TSCExtensions
import ToolchainRegistry
import Foundation
import Testing
import struct Basics.AbsolutePath
import struct Basics.Triple
private var hostTriple: Triple {
get async throws {
let toolchain = try #require(
await ToolchainRegistry.forTesting.preferredToolchain(containing: [
\.clang, \.clangd, \.sourcekitd, \.swift, \.swiftc,
])
)
let destinationToolchainBinDir = try #require(toolchain.swiftc?.deletingLastPathComponent())
let hostSDK = try SwiftSDK.hostSwiftSDK(Basics.AbsolutePath(validating: destinationToolchainBinDir.filePath))
let hostSwiftPMToolchain = try UserToolchain(swiftSDK: hostSDK)
return hostSwiftPMToolchain.targetTriple
}
}
fileprivate extension SourceKitLSPOptions {
static var forTestingExperimentalSwiftPMBuildServer: Self {
SourceKitLSPOptions(swiftPM: SwiftPMOptions(buildSystem: .swiftbuild))
}
}
@Suite(.serialized, .configureLogging)
struct SwiftPMBuildServerTests {
@Test
func testNoPackage() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": ""
]
)
let packageRoot = tempDir.appending(component: "pkg")
let buildServerSpec = SwiftPMBuildServer.searchForConfig(in: packageRoot, options: try await .testDefault())
#expect(buildServerSpec == nil)
}
}
@Test
func testNoToolchain() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
await expectThrowsError(
try await SwiftPMBuildServer(
projectRoot: packageRoot,
toolchainRegistry: ToolchainRegistry(toolchains: []),
options: SourceKitLSPOptions(),
connectionToSourceKitLSP: LocalConnection(receiverName: "dummy"),
testHooks: SwiftPMTestHooks()
)
)
}
}
@Test
func testRelativeScratchPath() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let options = SourceKitLSPOptions(
swiftPM: .init(
scratchPath: "non_default_relative_build_path"
),
backgroundIndexing: false
)
let swiftpmBuildServer = try await SwiftPMBuildServer(
projectRoot: packageRoot,
toolchainRegistry: .forTesting,
options: options,
connectionToSourceKitLSP: LocalConnection(receiverName: "dummy"),
testHooks: SwiftPMTestHooks()
)
let dataPath = await swiftpmBuildServer.destinationBuildParameters.dataPath
let expectedScratchPath = packageRoot.appending(component: try #require(options.swiftPMOrDefault.scratchPath))
#expect(dataPath.asURL.isDescendant(of: expectedScratchPath))
}
}
@Test(
arguments: Platform.current == .windows
? [SourceKitLSPOptions()] : [SourceKitLSPOptions(), .forTestingExperimentalSwiftPMBuildServer]
)
func testBasicSwiftArgs(options: SourceKitLSPOptions) async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: options,
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "lib", "a.swift")
let build = try await buildPath(root: packageRoot, platform: hostTriple.platformBuildPathComponent)
_ = try #require(await buildServerManager.initializationData?.indexDatabasePath)
_ = try #require(await buildServerManager.initializationData?.indexStorePath)
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain("-module-name", "lib", arguments: arguments)
expectArgumentsContain("-parse-as-library", arguments: arguments)
expectArgumentsContain("-target", arguments: arguments) // Only one!
#if os(macOS)
let versionString = PackageModel.Platform.macOS.oldestSupportedVersion.versionString
if options.swiftPMOrDefault.buildSystem == .swiftbuild {
expectArgumentsContain(
"-target",
// Account for differences in macOS naming canonicalization
try await hostTriple.tripleString(forPlatformVersion: versionString).replacing("macosx", with: "macos"),
arguments: arguments
)
} else {
expectArgumentsContain(
"-target",
try await hostTriple.tripleString(forPlatformVersion: versionString),
arguments: arguments
)
}
expectArgumentsContain(
"-sdk",
arguments: arguments,
allowMultiple: options.swiftPMOrDefault.buildSystem == .swiftbuild
)
expectArgumentsContain("-F", arguments: arguments, allowMultiple: true)
#else
expectArgumentsContain("-target", try await hostTriple.tripleString, arguments: arguments)
#endif
if options.swiftPMOrDefault.buildSystem != .swiftbuild {
// Swift Build and the native build system setup search paths differently. We deliberately avoid testing implementation details of Swift Build here.
expectArgumentsContain("-I", try build.appending(component: "Modules").filePath, arguments: arguments)
}
expectArgumentsContain(try aswift.filePath, arguments: arguments)
}
}
@Test
func testCompilerArgumentsForFileThatContainsPlusCharacterURLEncoded() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Sources/lib/a+something.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aPlusSomething =
packageRoot
.appending(components: "Sources", "lib", "a+something.swift")
_ = try #require(await buildServerManager.initializationData?.indexStorePath)
let pathWithPlusEscaped = "\(try aPlusSomething.filePath.replacing("+", with: "%2B"))"
#if os(Windows)
let urlWithPlusEscaped = try #require(URL(string: "file:///\(pathWithPlusEscaped)"))
#else
let urlWithPlusEscaped = try #require(URL(string: "file://\(pathWithPlusEscaped)"))
#endif
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(urlWithPlusEscaped),
language: .swift,
fallbackAfterTimeout: false
)
)
.compilerArguments
// Check that we have both source files in the compiler arguments, which means that we didn't compute the compiler
// arguments for a+something.swift using substitute arguments from a.swift.
#expect(
try arguments.contains(aPlusSomething.filePath),
"Compiler arguments do not contain a+something.swift: \(arguments)"
)
#expect(
try arguments.contains(
packageRoot.appending(components: "Sources", "lib", "a.swift")
.filePath
),
"Compiler arguments do not contain a.swift: \(arguments)"
)
}
}
@Test
func testBuildSetup() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let options = SourceKitLSPOptions.SwiftPMOptions(
configuration: .release,
scratchPath: try packageRoot.appending(component: "non_default_build_path").filePath,
cCompilerFlags: ["-m32"],
swiftCompilerFlags: ["-typecheck"]
)
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(swiftPM: options),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "lib", "a.swift")
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain("-typecheck", arguments: arguments)
expectArgumentsContain("-Xcc", "-m32", arguments: arguments)
expectArgumentsContain("-O", arguments: arguments)
}
}
@Test
func testManifestArgs() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let source = try packageRoot.appending(component: "Package.swift").realpath
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(source),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain("-swift-version", "4.2", arguments: arguments)
expectArgumentsContain(try source.filePath, arguments: arguments)
}
}
@Test
func testMultiFileSwift() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Sources/lib/b.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "lib", "a.swift")
let bswift =
packageRoot
.appending(components: "Sources", "lib", "b.swift")
let argumentsA = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain(try aswift.filePath, arguments: argumentsA)
expectArgumentsContain(try bswift.filePath, arguments: argumentsA)
let argumentsB = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain(try aswift.filePath, arguments: argumentsB)
expectArgumentsContain(try bswift.filePath, arguments: argumentsB)
}
}
@Test
func testMultiTargetSwift() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/libA/a.swift": "",
"pkg/Sources/libB/b.swift": "",
"pkg/Sources/libC/include/libC.h": "",
"pkg/Sources/libC/libC.c": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [
.target(name: "libA", dependencies: ["libB", "libC"]),
.target(name: "libB"),
.target(name: "libC"),
]
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "libA", "a.swift")
let bswift =
packageRoot
.appending(components: "Sources", "libB", "b.swift")
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain(try aswift.filePath, arguments: arguments)
expectArgumentsDoNotContain(try bswift.filePath, arguments: arguments)
expectArgumentsContain(
"-Xcc",
"-I",
"-Xcc",
try packageRoot
.appending(components: "Sources", "libC", "include")
.filePath,
arguments: arguments
)
let argumentsB = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(bswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain(try bswift.filePath, arguments: argumentsB)
expectArgumentsDoNotContain(try aswift.filePath, arguments: argumentsB)
expectArgumentsDoNotContain(
"-I",
try packageRoot
.appending(components: "Sources", "libC", "include")
.filePath,
arguments: argumentsB
)
}
}
@Test
func testUnknownFile() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/libA/a.swift": "",
"pkg/Sources/libB/b.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "libA")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "libA", "a.swift")
let bswift =
packageRoot
.appending(components: "Sources", "libB", "b.swift")
_ = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
)
#expect(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(bswift),
language: .swift,
fallbackAfterTimeout: false
)?.isFallback == true
)
#expect(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(URL(string: "https://www.apple.com")!),
language: .swift,
fallbackAfterTimeout: false
)?.isFallback == true
)
}
}
@Test
func testBasicCXXArgs() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.cpp": "",
"pkg/Sources/lib/b.cpp": "",
"pkg/Sources/lib/include/a.h": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")],
cxxLanguageStandard: .cxx14
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let acxx =
packageRoot
.appending(components: "Sources", "lib", "a.cpp")
let bcxx =
packageRoot
.appending(components: "Sources", "lib", "b.cpp")
let header =
packageRoot
.appending(components: "Sources", "lib", "include", "a.h")
let build = buildPath(root: packageRoot, platform: try await hostTriple.platformBuildPathComponent)
_ = try #require(await buildServerManager.initializationData?.indexStorePath)
for file in [acxx, header] {
let args = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(file),
language: .cpp,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain("-std=c++14", arguments: args)
expectArgumentsDoNotContain("-arch", arguments: args)
expectArgumentsContain("-target", arguments: args) // Only one!
#if os(macOS)
let versionString = PackageModel.Platform.macOS.oldestSupportedVersion.versionString
expectArgumentsContain(
"-target",
try await hostTriple.tripleString(forPlatformVersion: versionString),
arguments: args
)
expectArgumentsContain("-isysroot", arguments: args)
expectArgumentsContain("-F", arguments: args, allowMultiple: true)
#else
expectArgumentsContain("-target", try await hostTriple.tripleString, arguments: args)
#endif
expectArgumentsContain(
"-I",
try packageRoot
.appending(components: "Sources", "lib", "include")
.filePath,
arguments: args
)
expectArgumentsDoNotContain("-I", try build.filePath, arguments: args)
expectArgumentsDoNotContain(try bcxx.filePath, arguments: args)
URL(fileURLWithPath: try build.appending(components: "lib.build", "a.cpp.o").filePath)
.withUnsafeFileSystemRepresentation {
expectArgumentsContain("-o", String(cString: $0!), arguments: args)
}
}
}
}
@Test
func testDeploymentTargetSwift() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:5.0
import PackageDescription
let package = Package(name: "a",
platforms: [.macOS(.v10_13)],
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "lib", "a.swift")
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain("-target", arguments: arguments) // Only one!
#if os(macOS)
try await expectArgumentsContain(
"-target",
hostTriple.tripleString(forPlatformVersion: "10.13"),
arguments: arguments
)
#else
expectArgumentsContain("-target", try await hostTriple.tripleString, arguments: arguments)
#endif
}
}
@Test
func testSymlinkInWorkspaceSwift() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg_real/Sources/lib/a.swift": "",
"pkg_real/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
try FileManager.default.createSymbolicLink(
at: URL(fileURLWithPath: packageRoot.filePath),
withDestinationURL: URL(fileURLWithPath: tempDir.appending(component: "pkg_real").filePath)
)
let buildServerSpec = try #require(
SwiftPMBuildServer.searchForConfig(in: packageRoot, options: await .testDefault())
)
let buildServerManager = await BuildServerManager(
buildServerSpec: buildServerSpec,
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswiftSymlink =
packageRoot
.appending(components: "Sources", "lib", "a.swift")
let aswiftReal = try aswiftSymlink.realpath
let manifest = packageRoot.appending(component: "Package.swift")
let argumentsFromSymlink = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswiftSymlink),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
// We opened the project from a symlink. The realpath isn't part of the project and we should thus not receive
// build settings for it.
#expect(
try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswiftReal),
language: .swift,
fallbackAfterTimeout: false
)
).isFallback
)
expectArgumentsContain(try aswiftSymlink.filePath, arguments: argumentsFromSymlink)
expectArgumentsDoNotContain(try aswiftReal.filePath, arguments: argumentsFromSymlink)
let argsManifest = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(manifest),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain(try manifest.filePath, arguments: argsManifest)
expectArgumentsDoNotContain(try manifest.realpath.filePath, arguments: argsManifest)
}
}
@Test
func testSymlinkInWorkspaceCXX() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg_real/Sources/lib/a.cpp": "",
"pkg_real/Sources/lib/b.cpp": "",
"pkg_real/Sources/lib/include/a.h": "",
"pkg_real/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")],
cxxLanguageStandard: .cxx14
)
""",
]
)
let acpp = ["Sources", "lib", "a.cpp"]
let ah = ["Sources", "lib", "include", "a.h"]
let realRoot = tempDir.appending(component: "pkg_real")
let symlinkRoot = tempDir.appending(component: "pkg")
try FileManager.default.createSymbolicLink(
at: URL(fileURLWithPath: symlinkRoot.filePath),
withDestinationURL: URL(fileURLWithPath: tempDir.appending(component: "pkg_real").filePath)
)
let buildServerSpec = try #require(
SwiftPMBuildServer.searchForConfig(in: symlinkRoot, options: await .testDefault())
)
let buildServerManager = await BuildServerManager(
buildServerSpec: buildServerSpec,
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
for file in [acpp, ah] {
let args = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(symlinkRoot.appending(components: file)),
language: .cpp,
fallbackAfterTimeout: false
)?
.compilerArguments
)
expectArgumentsDoNotContain(try realRoot.appending(components: file).filePath, arguments: args)
expectArgumentsContain(try symlinkRoot.appending(components: file).filePath, arguments: args)
}
}
}
@Test
func testSwiftDerivedSources() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Sources/lib/a.txt": "",
"pkg/Package.swift": """
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib", resources: [.copy("a.txt")])]
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "lib", "a.swift")
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
)
.compilerArguments
expectArgumentsContain(try aswift.filePath, arguments: arguments)
_ = try #require(
arguments.firstIndex(where: {
$0.hasSuffix(".swift") && $0.contains("DerivedSources")
}),
"missing resource_bundle_accessor.swift from \(arguments)"
)
}
}
@Test
func testNestedInvalidPackageSwift() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/Package.swift": "// not a valid package",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let workspaceRoot =
tempDir
.appending(components: "pkg", "Sources", "lib")
let buildServerSpec = SwiftPMBuildServer.searchForConfig(in: workspaceRoot, options: try await .testDefault())
#expect(buildServerSpec == nil)
}
}
@Test
func testPluginArgs() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Plugins/MyPlugin/a.swift": "",
"pkg/Sources/lib/lib.swift": "",
"pkg/Package.swift": """
// swift-tools-version:5.7
import PackageDescription
let package = Package(
name: "a",
targets: [
.target(name: "lib"),
.plugin(name: "MyPlugin", capability: .buildTool)
]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Plugins", "MyPlugin", "a.swift")
_ = try #require(await buildServerManager.initializationData?.indexStorePath)
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
// Plugins get compiled with the same compiler arguments as the package manifest
expectArgumentsContain("-package-description-version", "5.7.0", arguments: arguments)
expectArgumentsContain(try aswift.filePath, arguments: arguments)
}
}
@Test
func testPackageWithDependencyWithoutResolving() async throws {
// This package has a dependency but we haven't run `swift package resolve`. We don't want to resolve packages from
// SourceKit-LSP because it has side-effects to the build directory.
// But even without the dependency checked out, we should be able to create a SwiftPMBuildServer and retrieve the
// existing source files.
let project = try await SwiftPMTestProject(
files: [
"Tests/PackageTests/PackageTests.swift": """
import Testing
1⃣@Test func topLevelTestPassing() {}2
"""
],
manifest: """
let package = Package(
name: "MyLibrary",
dependencies: [.package(url: "https://github.com/swiftlang/swift-testing.git", branch: "main")],
targets: [
.testTarget(name: "PackageTests", dependencies: [.product(name: "Testing", package: "swift-testing")]),
]
)
"""
)
let tests = try await project.testClient.send(WorkspaceTestsRequest())
#expect(
tests == [
TestItem(
id: "PackageTests.topLevelTestPassing()",
label: "topLevelTestPassing()",
disabled: false,
style: "swift-testing",
location: try project.location(from: "1", to: "2", in: "PackageTests.swift"),
children: [],
tags: []
)
]
)
}
@Test
func testPackageLoadingWorkDoneProgress() async throws {
let didReceiveWorkDoneProgressNotification = WrappedSemaphore(name: "work done progress received")
let project = try await SwiftPMTestProject(
files: [
"MyLibrary/Test.swift": ""
],
capabilities: ClientCapabilities(window: WindowClientCapabilities(workDoneProgress: true)),
hooks: Hooks(
buildServerHooks: BuildServerHooks(
swiftPMTestHooks: SwiftPMTestHooks(reloadPackageDidStart: {
didReceiveWorkDoneProgressNotification.waitOrXCTFail()
})
)
),
pollIndex: false,
preInitialization: { testClient in
testClient.handleMultipleRequests { (request: CreateWorkDoneProgressRequest) in
return VoidResponse()
}
}
)
let begin = try await project.testClient.nextNotification(ofType: WorkDoneProgress.self)
#expect(begin.value == .begin(WorkDoneProgressBegin(title: "SourceKit-LSP: Reloading Package")))
didReceiveWorkDoneProgressNotification.signal()
let end = try await project.testClient.nextNotification(ofType: WorkDoneProgress.self)
#expect(end.token == begin.token)
#expect(end.value == .end(WorkDoneProgressEnd()))
}
@Test
func testBuildSettingsForVersionSpecificPackageManifest() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
"pkg/Package@swift-5.8.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let versionSpecificManifestURL = packageRoot.appending(component: "Package@swift-5.8.swift")
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let settings = await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(versionSpecificManifestURL),
language: .swift,
fallbackAfterTimeout: false
)
let compilerArgs = try #require(settings?.compilerArguments)
#expect(compilerArgs.contains("-package-description-version"))
#expect(compilerArgs.contains(try versionSpecificManifestURL.filePath))
}
}
@Test
func testBuildSettingsForInvalidManifest() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:5.1
import PackageDescription
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let manifestURL = packageRoot.appending(component: "Package.swift")
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let settings = await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(manifestURL),
language: .swift,
fallbackAfterTimeout: false
)
let compilerArgs = try #require(settings?.compilerArguments)
expectArgumentsContain("-package-description-version", "5.1.0", arguments: compilerArgs)
#expect(compilerArgs.contains(try manifestURL.filePath))
}
}
@Test(
.enabled(if: Platform.current != .windows, "Toolsets are not working on Windows, see swift-package-manager#9438.")
)
func testToolsets() async throws {
let project = try await SwiftPMTestProject(
files: [
"Foo/foo.swift": """
import Bar
func foo() {
bar()
}
""",
"Bar/bar.swift": """
#if BAR
public func bar() {}
#endif
""",
"/toolset.json": """
{
"schemaVersion": "1.0",
"swiftCompiler": {
"extraCLIOptions": [
"-DBAR"
]
}
}
""",
"/.sourcekit-lsp/config.json": """
{
"swiftPM": {
"toolsets": ["toolset.json"]
}
}
""",
],
manifest: """
let package = Package(
name: "MyLibrary",
targets: [
.target(name: "Foo", dependencies: ["Bar"]),
.target(name: "Bar"),
]
)
""",
options: .testDefault(experimentalFeatures: [.sourceKitOptionsRequest]),
enableBackgroundIndexing: true,
)
let (uri, _) = try project.openDocument("foo.swift")
let options = try await project.testClient.send(
SourceKitOptionsRequest(
textDocument: TextDocumentIdentifier(uri),
prepareTarget: false,
allowFallbackSettings: false
)
)
#expect(options.compilerArguments.contains("-DBAR"))
let diagnostics = try #require(
await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
).fullReport?.items
)
#expect(diagnostics.isEmpty)
}
}
private func expectArgumentsDoNotContain(
_ pattern: String...,
arguments: [String],
sourceLocation: SourceLocation = #_sourceLocation
) {
if let index = arguments.firstRange(of: pattern)?.startIndex {
Issue.record(
"not-pattern \(pattern) unexpectedly found at \(index) in arguments \(arguments)",
sourceLocation: sourceLocation
)
return
}
}
private func expectArgumentsContain(
_ pattern: String...,
arguments: [String],
allowMultiple: Bool = false,
sourceLocation: SourceLocation = #_sourceLocation
) {
guard let index = arguments.firstRange(of: pattern)?.startIndex else {
Issue.record("pattern \(pattern) not found in arguments \(arguments)", sourceLocation: sourceLocation)
return
}
if !allowMultiple, let index2 = arguments[(index + 1)...].firstRange(of: pattern)?.startIndex {
Issue.record(
"pattern \(pattern) found twice (\(index), \(index2)) in \(arguments)",
sourceLocation: sourceLocation
)
}
}
private func buildPath(
root: URL,
options: SourceKitLSPOptions.SwiftPMOptions = SourceKitLSPOptions.SwiftPMOptions(),
platform: String
) -> URL {
let buildPath =
if let scratchPath = options.scratchPath {
URL(fileURLWithPath: scratchPath)
} else {
root.appending(components: ".build", "index-build")
}
return buildPath.appending(components: platform, "\(options.configuration ?? .debug)")
}
fileprivate extension URL {
func appending(components: [String]) -> URL {
var result = self
for component in components {
result.appendPathComponent(component)
}
return result
}
}
fileprivate extension BuildServerSpec {
static func swiftpmSpec(for packageRoot: URL) -> BuildServerSpec {
return BuildServerSpec(
kind: .swiftPM,
projectRoot: packageRoot,
configPath: packageRoot.appending(component: "Package.swift")
)
}
}
#endif