Skip to content

Commit abf31a8

Browse files
committed
Don’t include files from package dependencies in the syntactic test index
`SwiftPMBuildSystem.testFiles()` returned all source files in the package, including files of package dependencies. This caused us to index those files for tests in the syntactic test index, which we should not. Make `SwiftPMBuildSystem.testFiles` only return files from the root package. Also add test infrastructure to be able to test cross-package functionality. rdar://126965614
1 parent ae215f9 commit abf31a8

File tree

5 files changed

+173
-11
lines changed

5 files changed

+173
-11
lines changed

Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ extension SwiftPMBuildSystem {
240240
self.fileToTarget = [AbsolutePath: SwiftBuildTarget](
241241
modulesGraph.allTargets.flatMap { target in
242242
return target.sources.paths.compactMap {
243-
guard let buildTarget = buildDescription.getBuildTarget(for: target) else {
243+
guard let buildTarget = buildDescription.getBuildTarget(for: target, in: modulesGraph) else {
244244
return nil
245245
}
246246
return (key: $0, value: buildTarget)
@@ -254,7 +254,7 @@ extension SwiftPMBuildSystem {
254254

255255
self.sourceDirToTarget = [AbsolutePath: SwiftBuildTarget](
256256
modulesGraph.allTargets.compactMap { (target) -> (AbsolutePath, SwiftBuildTarget)? in
257-
guard let buildTarget = buildDescription.getBuildTarget(for: target) else {
257+
guard let buildTarget = buildDescription.getBuildTarget(for: target, in: modulesGraph) else {
258258
return nil
259259
}
260260
return (key: target.sources.root, value: buildTarget)
@@ -388,8 +388,13 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
388388
}
389389

390390
public func testFiles() -> [DocumentURI] {
391-
// We should only include source files from test targets (https://github.com/apple/sourcekit-lsp/issues/1174).
392-
return fileToTarget.map { DocumentURI($0.key.asURL) }
391+
return fileToTarget.compactMap { (path, target) -> DocumentURI? in
392+
guard target.isPartOfRootPackage else {
393+
// Don't consider files from package dependencies as possible test files.
394+
return nil
395+
}
396+
return DocumentURI(path.asURL)
397+
}
393398
}
394399

395400
public func addTestFilesDidChangeCallback(_ callback: @Sendable @escaping () async -> Void) async {

Sources/SKTestSupport/MultiFileTestProject.swift

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ public struct RelativeFileLocation: Hashable, ExpressibleByStringLiteral {
3131
let components = value.components(separatedBy: "/")
3232
self.init(directories: components.dropLast(), components.last!)
3333
}
34+
35+
public func url(relativeTo: URL) -> URL {
36+
var url = relativeTo
37+
for directory in directories {
38+
url = url.appendingPathComponent(directory)
39+
}
40+
url = url.appendingPathComponent(fileName)
41+
return url
42+
}
3443
}
3544

3645
/// A test project that writes multiple files to disk and opens a `TestSourceKitLSPClient` client with a workspace
@@ -69,7 +78,7 @@ public class MultiFileTestProject {
6978
/// File contents can also contain `$TEST_DIR`, which gets replaced by the temporary directory.
7079
public init(
7180
files: [RelativeFileLocation: String],
72-
workspaces: (URL) -> [WorkspaceFolder] = { [WorkspaceFolder(uri: DocumentURI($0))] },
81+
workspaces: (URL) async throws -> [WorkspaceFolder] = { [WorkspaceFolder(uri: DocumentURI($0))] },
7382
usePullDiagnostics: Bool = true,
7483
testName: String = #function
7584
) async throws {
@@ -79,11 +88,7 @@ public class MultiFileTestProject {
7988
var fileData: [String: FileData] = [:]
8089
for (fileLocation, markedText) in files {
8190
let markedText = markedText.replacingOccurrences(of: "$TEST_DIR", with: scratchDirectory.path)
82-
var fileURL = scratchDirectory
83-
for directory in fileLocation.directories {
84-
fileURL = fileURL.appendingPathComponent(directory)
85-
}
86-
fileURL = fileURL.appendingPathComponent(fileLocation.fileName)
91+
let fileURL = fileLocation.url(relativeTo: scratchDirectory)
8792
try FileManager.default.createDirectory(
8893
at: fileURL.deletingLastPathComponent(),
8994
withIntermediateDirectories: true
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
import ISDBTibs
15+
import XCTest
16+
17+
import struct TSCBasic.AbsolutePath
18+
import class TSCBasic.Process
19+
import enum TSCBasic.ProcessEnv
20+
21+
/// A SwiftPM package that gets written to disk and for which a Git repository is initialized with a commit tagged
22+
/// `1.0.0`. This repository can then be used as a dependency for another package, usually a `SwiftPMTestProject`.
23+
public class SwiftPMDependencyProject {
24+
/// The directory in which the repository lives.
25+
public let packageDirectory: URL
26+
27+
private func runCommand(_ toolName: String, _ arguments: [String], workingDirectory: URL) async throws {
28+
struct CannotFindToolError: Error {}
29+
guard let toolUrl = findTool(name: toolName) else {
30+
if ProcessEnv.block["SWIFTCI_USE_LOCAL_DEPS"] == nil {
31+
// Never skip the test in CI, similar to what SkipUnless does.
32+
throw XCTSkip("\(toolName) cannot be found")
33+
}
34+
throw CannotFindToolError()
35+
}
36+
let process = TSCBasic.Process(
37+
arguments: [toolUrl.path] + arguments,
38+
workingDirectory: try AbsolutePath(validating: workingDirectory.path)
39+
)
40+
try process.launch()
41+
try await process.waitUntilExit()
42+
}
43+
44+
public static let defaultPackageManifest: String = """
45+
// swift-tools-version: 5.7
46+
47+
import PackageDescription
48+
49+
let package = Package(
50+
name: "MyDependency",
51+
products: [.library(name: "MyDependency", targets: ["MyDependency"])],
52+
targets: [.target(name: "MyDependency")]
53+
)
54+
"""
55+
56+
public init(
57+
files: [RelativeFileLocation: String],
58+
manifest: String = defaultPackageManifest,
59+
testName: String = #function
60+
) async throws {
61+
packageDirectory = try testScratchDir(testName: testName).appendingPathComponent("MyDependency")
62+
63+
var files = files
64+
files["Package.swift"] = manifest
65+
66+
for (fileLocation, contents) in files {
67+
let fileURL = fileLocation.url(relativeTo: packageDirectory)
68+
try FileManager.default.createDirectory(
69+
at: fileURL.deletingLastPathComponent(),
70+
withIntermediateDirectories: true
71+
)
72+
try contents.write(to: fileURL, atomically: true, encoding: .utf8)
73+
}
74+
75+
try await runCommand("git", ["init"], workingDirectory: packageDirectory)
76+
try await runCommand(
77+
"git",
78+
["add"] + files.keys.map { $0.url(relativeTo: packageDirectory).path },
79+
workingDirectory: packageDirectory
80+
)
81+
try await runCommand("git", ["commit", "-m", "Initial commit"], workingDirectory: packageDirectory)
82+
try await runCommand("git", ["tag", "1.0.0"], workingDirectory: packageDirectory)
83+
}
84+
85+
deinit {
86+
if cleanScratchDirectories {
87+
try? FileManager.default.removeItem(at: packageDirectory)
88+
}
89+
}
90+
91+
/// Function that makes sure the project stays alive until this is called. Otherwise, the `SwiftPMDependencyProject`
92+
/// might get deinitialized, which deletes the package on disk.
93+
public func keepAlive() {
94+
withExtendedLifetime(self) { _ in }
95+
}
96+
}

Sources/SKTestSupport/SwiftPMTestProject.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public class SwiftPMTestProject: MultiFileTestProject {
3838
public init(
3939
files: [RelativeFileLocation: String],
4040
manifest: String = SwiftPMTestProject.defaultPackageManifest,
41-
workspaces: (URL) -> [WorkspaceFolder] = { [WorkspaceFolder(uri: DocumentURI($0))] },
41+
workspaces: (URL) async throws -> [WorkspaceFolder] = { [WorkspaceFolder(uri: DocumentURI($0))] },
4242
build: Bool = false,
4343
allowBuildFailure: Bool = false,
4444
usePullDiagnostics: Bool = true,
@@ -59,6 +59,7 @@ public class SwiftPMTestProject: MultiFileTestProject {
5959
filesByPath[RelativeFileLocation(directories: directories, fileLocation.fileName)] = contents
6060
}
6161
filesByPath["Package.swift"] = manifest
62+
6263
try await super.init(
6364
files: filesByPath,
6465
workspaces: workspaces,
@@ -96,4 +97,18 @@ public class SwiftPMTestProject: MultiFileTestProject {
9697
environment["SWIFTPM_ENABLE_CLANG_INDEX_STORE"] = "1"
9798
try await Process.checkNonZeroExit(arguments: arguments, environmentBlock: environment)
9899
}
100+
101+
/// Resolve package dependencies for the package at `path`.
102+
public static func resolvePackageDependencies(at path: URL) async throws {
103+
guard let swift = await ToolchainRegistry.forTesting.default?.swift?.asURL else {
104+
throw Error.swiftNotFound
105+
}
106+
let arguments = [
107+
swift.path,
108+
"package",
109+
"resolve",
110+
"--package-path", path.path,
111+
]
112+
try await Process.checkNonZeroExit(arguments: arguments)
113+
}
99114
}

Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,4 +807,45 @@ final class WorkspaceTestDiscoveryTests: XCTestCase {
807807
let testsAfterFileReAdded = try await project.testClient.send(WorkspaceTestsRequest())
808808
XCTAssertEqual(testsAfterFileReAdded, expectedTests)
809809
}
810+
811+
func testDontIncludeTestsFromDependentPackageInSyntacticIndex() async throws {
812+
let dependencyProject = try await SwiftPMDependencyProject(files: [
813+
"Sources/MyDependency/MyDependency.swift": """
814+
class MySuperclass {}
815+
class LooksALittleLikeTests: MySuperclass {
816+
func testSomething() {}
817+
}
818+
"""
819+
])
820+
defer { dependencyProject.keepAlive() }
821+
822+
let project = try await SwiftPMTestProject(
823+
files: [
824+
"Test.swift": ""
825+
],
826+
manifest: """
827+
// swift-tools-version: 5.7
828+
829+
import PackageDescription
830+
831+
let package = Package(
832+
name: "MyLibrary",
833+
dependencies: [.package(url: "\(dependencyProject.packageDirectory)", from: "1.0.0")],
834+
targets: [
835+
.target(
836+
name: "MyLibrary",
837+
dependencies: [.product(name: "MyDependency", package: "MyDependency")]
838+
)
839+
]
840+
)
841+
""",
842+
workspaces: { scratchDirectory in
843+
try await SwiftPMTestProject.resolvePackageDependencies(at: scratchDirectory)
844+
return [WorkspaceFolder(uri: DocumentURI(scratchDirectory))]
845+
}
846+
)
847+
848+
let tests = try await project.testClient.send(WorkspaceTestsRequest())
849+
XCTAssertEqual(tests, [])
850+
}
810851
}

0 commit comments

Comments
 (0)