Skip to content

Commit d12c946

Browse files
authored
Merge pull request #1321 from ahoppen/target-preparation-testing
2 parents 3f9ff29 + 815fea8 commit d12c946

11 files changed

+152
-38
lines changed

Sources/SKTestSupport/MultiFileTestProject.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public class MultiFileTestProject {
8484
serverOptions: SourceKitLSPServer.Options = .testDefault,
8585
usePullDiagnostics: Bool = true,
8686
preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil,
87+
cleanUp: (() -> Void)? = nil,
8788
testName: String = #function
8889
) async throws {
8990
scratchDirectory = try testScratchDir(testName: testName)
@@ -122,6 +123,7 @@ public class MultiFileTestProject {
122123
if cleanScratchDirectories {
123124
try? FileManager.default.removeItem(at: scratchDirectory)
124125
}
126+
cleanUp?()
125127
}
126128
)
127129
}

Sources/SKTestSupport/SwiftPMTestProject.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public class SwiftPMTestProject: MultiFileTestProject {
4747
pollIndex: Bool = true,
4848
preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil,
4949
usePullDiagnostics: Bool = true,
50+
cleanUp: (() -> Void)? = nil,
5051
testName: String = #function
5152
) async throws {
5253
var filesByPath: [RelativeFileLocation: String] = [:]
@@ -72,6 +73,7 @@ public class SwiftPMTestProject: MultiFileTestProject {
7273
serverOptions: serverOptions,
7374
usePullDiagnostics: usePullDiagnostics,
7475
preInitialization: preInitialization,
76+
cleanUp: cleanUp,
7577
testName: testName
7678
)
7779

Sources/SemanticIndex/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ add_library(SemanticIndex STATIC
55
IndexTaskDescription.swift
66
PreparationTaskDescription.swift
77
SemanticIndexManager.swift
8+
TestHooks.swift
89
UpdateIndexStoreTaskDescription.swift
910
)
1011
set_target_properties(SemanticIndex PROPERTIES

Sources/SemanticIndex/PreparationTaskDescription.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,14 @@ public struct PreparationTaskDescription: IndexTaskDescription {
3030
public let id = preparationIDForLogging.fetchAndIncrement()
3131

3232
/// The targets that should be prepared.
33-
private let targetsToPrepare: [ConfiguredTarget]
33+
public let targetsToPrepare: [ConfiguredTarget]
3434

3535
/// The build system manager that is used to get the toolchain and build settings for the files to index.
3636
private let buildSystemManager: BuildSystemManager
3737

38+
/// Test hooks that should be called when the preparation task finishes.
39+
private let testHooks: IndexTestHooks
40+
3841
/// The task is idempotent because preparing the same target twice produces the same result as preparing it once.
3942
public var isIdempotent: Bool { true }
4043

@@ -50,10 +53,12 @@ public struct PreparationTaskDescription: IndexTaskDescription {
5053

5154
init(
5255
targetsToPrepare: [ConfiguredTarget],
53-
buildSystemManager: BuildSystemManager
56+
buildSystemManager: BuildSystemManager,
57+
testHooks: IndexTestHooks
5458
) {
5559
self.targetsToPrepare = targetsToPrepare
5660
self.buildSystemManager = buildSystemManager
61+
self.testHooks = testHooks
5762
}
5863

5964
public func execute() async {
@@ -79,6 +84,7 @@ public struct PreparationTaskDescription: IndexTaskDescription {
7984
"Preparation failed: \(error.forLogging)"
8085
)
8186
}
87+
await testHooks.preparationTaskDidFinish?(self)
8288
logger.log(
8389
"Finished preparation in \(Date().timeIntervalSince(startDate) * 1000, privacy: .public)ms: \(targetsToPrepareDescription)"
8490
)

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,19 @@ private enum IndexStatus<T> {
3838
}
3939
}
4040

41+
/// See `SemanticIndexManager.preparationStatus`
42+
private struct PreparationTaskStatusData {
43+
/// A UUID to track the task. This is used to ensure that status updates from this task don't update
44+
/// `preparationStatus` for targets that are tracked by a different task.
45+
let taskID: UUID
46+
47+
/// The list of targets that are being prepared in a joint preparation operation.
48+
let targets: [ConfiguredTarget]
49+
50+
/// The task that prepares the target
51+
let task: Task<Void, Never>
52+
}
53+
4154
/// Schedules index tasks and keeps track of the index status of files.
4255
public final actor SemanticIndexManager {
4356
/// The underlying index. This is used to check if the index of a file is already up-to-date, in which case it doesn't
@@ -47,20 +60,16 @@ public final actor SemanticIndexManager {
4760
/// The build system manager that is used to get compiler arguments for a file.
4861
private let buildSystemManager: BuildSystemManager
4962

63+
private let testHooks: IndexTestHooks
64+
5065
/// The task to generate the build graph (resolving package dependencies, generating the build description,
5166
/// ...). `nil` if no build graph is currently being generated.
5267
private var generateBuildGraphTask: Task<Void, Never>?
5368

5469
/// The preparation status of the targets that the `SemanticIndexManager` has started preparation for.
5570
///
5671
/// Targets will be removed from this dictionary when they are no longer known to be up-to-date.
57-
///
58-
/// The associated values of the `IndexStatus` are:
59-
/// - A UUID to track the task. This is used to ensure that status updates from this task don't update
60-
/// `preparationStatus` for targets that are tracked by a different task.
61-
/// - The list of targets that are being prepared in a joint preparation operation
62-
/// - The task that prepares the target
63-
private var preparationStatus: [ConfiguredTarget: IndexStatus<(UUID, [ConfiguredTarget], Task<Void, Never>)>] = [:]
72+
private var preparationStatus: [ConfiguredTarget: IndexStatus<PreparationTaskStatusData>] = [:]
6473

6574
/// The index status of the source files that the `SemanticIndexManager` knows about.
6675
///
@@ -114,12 +123,14 @@ public final actor SemanticIndexManager {
114123
public init(
115124
index: UncheckedIndex,
116125
buildSystemManager: BuildSystemManager,
126+
testHooks: IndexTestHooks,
117127
indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>,
118128
indexTasksWereScheduled: @escaping @Sendable (Int) -> Void,
119129
indexTaskDidFinish: @escaping @Sendable () -> Void
120130
) {
121131
self.index = index
122132
self.buildSystemManager = buildSystemManager
133+
self.testHooks = testHooks
123134
self.indexTaskScheduler = indexTaskScheduler
124135
self.indexTasksWereScheduled = indexTasksWereScheduled
125136
self.indexTaskDidFinish = indexTaskDidFinish
@@ -278,16 +289,16 @@ public final actor SemanticIndexManager {
278289
switch preparationStatus[target] {
279290
case .upToDate:
280291
break
281-
case .scheduled((_, let existingTaskTargets, let task)), .executing((_, let existingTaskTargets, let task)):
292+
case .scheduled(let existingTaskData), .executing(let existingTaskData):
282293
// If we already have a task scheduled that prepares fewer targets, await that instead of overriding the
283294
// target's preparation status with a longer-running task. The key benefit here is that when we get many
284295
// preparation requests for the same target (eg. one for every text document request sent to a file), we don't
285296
// re-create new `PreparationTaskDescription`s for every preparation request. Instead, all the preparation
286297
// requests await the same task. At the same time, if we have a multi-file preparation request and then get a
287298
// single-file preparation request, we will override the preparation of that target with the single-file
288299
// preparation task, ensuring that the task gets prepared as quickly as possible.
289-
if existingTaskTargets.count <= targets.count {
290-
preparationTasksToAwait.append(task)
300+
if existingTaskData.targets.count <= targets.count {
301+
preparationTasksToAwait.append(existingTaskData.task)
291302
} else {
292303
targetsToPrepare.append(target)
293304
}
@@ -299,7 +310,8 @@ public final actor SemanticIndexManager {
299310
let taskDescription = AnyIndexTaskDescription(
300311
PreparationTaskDescription(
301312
targetsToPrepare: targetsToPrepare,
302-
buildSystemManager: self.buildSystemManager
313+
buildSystemManager: self.buildSystemManager,
314+
testHooks: testHooks
303315
)
304316
)
305317
if !targetsToPrepare.isEmpty {
@@ -311,20 +323,22 @@ public final actor SemanticIndexManager {
311323
switch newState {
312324
case .executing:
313325
for target in targetsToPrepare {
314-
if case .scheduled((taskID, let targets, let task)) = self.preparationStatus[target] {
315-
self.preparationStatus[target] = .executing((taskID, targets, task))
326+
if case .scheduled(let existingTaskData) = self.preparationStatus[target], existingTaskData.taskID == taskID
327+
{
328+
self.preparationStatus[target] = .executing(existingTaskData)
316329
}
317330
}
318331
case .cancelledToBeRescheduled:
319332
for target in targetsToPrepare {
320-
if case .executing((taskID, let targets, let task)) = self.preparationStatus[target] {
321-
self.preparationStatus[target] = .scheduled((taskID, targets, task))
333+
if case .executing(let existingTaskData) = self.preparationStatus[target], existingTaskData.taskID == taskID
334+
{
335+
self.preparationStatus[target] = .scheduled(existingTaskData)
322336
}
323337
}
324338
case .finished:
325339
for target in targetsToPrepare {
326340
switch self.preparationStatus[target] {
327-
case .executing((taskID, _, _)):
341+
case .executing(let existingTaskData) where existingTaskData.taskID == taskID:
328342
self.preparationStatus[target] = .upToDate
329343
default:
330344
break
@@ -334,14 +348,16 @@ public final actor SemanticIndexManager {
334348
}
335349
}
336350
for target in targetsToPrepare {
337-
preparationStatus[target] = .scheduled((taskID, targetsToPrepare, preparationTask))
351+
preparationStatus[target] = .scheduled(
352+
PreparationTaskStatusData(taskID: taskID, targets: targetsToPrepare, task: preparationTask)
353+
)
338354
}
339355
preparationTasksToAwait.append(preparationTask)
340356
}
341357
await withTaskGroup(of: Void.self) { taskGroup in
342358
for task in preparationTasksToAwait {
343359
taskGroup.addTask {
344-
await task.value
360+
await task.valuePropagatingCancellation
345361
}
346362
}
347363
await taskGroup.waitForAll()
@@ -354,7 +370,8 @@ public final actor SemanticIndexManager {
354370
UpdateIndexStoreTaskDescription(
355371
filesToIndex: filesAndTargets,
356372
buildSystemManager: self.buildSystemManager,
357-
index: index
373+
index: index,
374+
testHooks: testHooks
358375
)
359376
)
360377
let updateIndexStoreTask = await self.indexTaskScheduler.schedule(priority: priority, taskDescription) { newState in

Sources/SemanticIndex/TestHooks.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
/// Callbacks that allow inspection of internal state modifications during testing.
14+
public struct IndexTestHooks: Sendable {
15+
public var preparationTaskDidFinish: (@Sendable (PreparationTaskDescription) async -> Void)?
16+
17+
/// A callback that is called when an index task finishes.
18+
public var updateIndexStoreTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)?
19+
20+
public init(
21+
preparationTaskDidFinish: (@Sendable (PreparationTaskDescription) async -> Void)? = nil,
22+
updateIndexStoreTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)? = nil
23+
) {
24+
self.preparationTaskDidFinish = preparationTaskDidFinish
25+
self.updateIndexStoreTaskDidFinish = updateIndexStoreTaskDidFinish
26+
}
27+
}

Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
9393
/// case we don't need to index it again.
9494
private let index: UncheckedIndex
9595

96+
/// Test hooks that should be called when the index task finishes.
97+
private let testHooks: IndexTestHooks
98+
9699
/// The task is idempotent because indexing the same file twice produces the same result as indexing it once.
97100
public var isIdempotent: Bool { true }
98101

@@ -109,11 +112,13 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
109112
init(
110113
filesToIndex: [FileAndTarget],
111114
buildSystemManager: BuildSystemManager,
112-
index: UncheckedIndex
115+
index: UncheckedIndex,
116+
testHooks: IndexTestHooks
113117
) {
114118
self.filesToIndex = filesToIndex
115119
self.buildSystemManager = buildSystemManager
116120
self.index = index
121+
self.testHooks = testHooks
117122
}
118123

119124
public func execute() async {
@@ -140,6 +145,7 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
140145
for file in filesToIndex {
141146
await updateIndexStore(forSingleFile: file.file, in: file.target)
142147
}
148+
await testHooks.updateIndexStoreTaskDidFinish?(self)
143149
logger.log(
144150
"Finished updating index store in \(Date().timeIntervalSince(startDate) * 1000, privacy: .public)ms: \(filesToIndexDescription)"
145151
)

Sources/SourceKitLSP/SourceKitLSPServer+Options.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Foundation
1414
import LanguageServerProtocol
1515
import SKCore
1616
import SKSupport
17+
import SemanticIndex
1718

1819
import struct TSCBasic.AbsolutePath
1920
import struct TSCBasic.RelativePath
@@ -22,7 +23,6 @@ extension SourceKitLSPServer {
2223

2324
/// Configuration options for the SourceKitServer.
2425
public struct Options: Sendable {
25-
2626
/// Additional compiler flags (e.g. `-Xswiftc` for SwiftPM projects) and other build-related
2727
/// configuration.
2828
public var buildSetup: BuildSetup
@@ -49,10 +49,7 @@ extension SourceKitLSPServer {
4949
/// notification when running unit tests.
5050
public var swiftPublishDiagnosticsDebounceDuration: TimeInterval
5151

52-
/// A callback that is called when an index task finishes.
53-
///
54-
/// Intended for testing purposes.
55-
public var indexTaskDidFinish: (@Sendable () -> Void)?
52+
public var indexTestHooks: IndexTestHooks
5653

5754
public init(
5855
buildSetup: BuildSetup = .default,
@@ -61,7 +58,8 @@ extension SourceKitLSPServer {
6158
indexOptions: IndexOptions = .init(),
6259
completionOptions: SKCompletionOptions = .init(),
6360
generatedInterfacesPath: AbsolutePath = defaultDirectoryForGeneratedInterfaces,
64-
swiftPublishDiagnosticsDebounceDuration: TimeInterval = 2 /* 2s */
61+
swiftPublishDiagnosticsDebounceDuration: TimeInterval = 2, /* 2s */
62+
indexTestHooks: IndexTestHooks = IndexTestHooks()
6563
) {
6664
self.buildSetup = buildSetup
6765
self.clangdOptions = clangdOptions
@@ -70,6 +68,7 @@ extension SourceKitLSPServer {
7068
self.completionOptions = completionOptions
7169
self.generatedInterfacesPath = generatedInterfacesPath
7270
self.swiftPublishDiagnosticsDebounceDuration = swiftPublishDiagnosticsDebounceDuration
71+
self.indexTestHooks = indexTestHooks
7372
}
7473
}
7574
}

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,7 +1210,6 @@ extension SourceKitLSPServer {
12101210
logger.log("Cannot open workspace before server is initialized")
12111211
return nil
12121212
}
1213-
let indexTaskDidFinishCallback = options.indexTaskDidFinish
12141213
var options = self.options
12151214
options.buildSetup = self.options.buildSetup.merging(buildSetup(for: workspaceFolder))
12161215
return try? await Workspace(
@@ -1236,7 +1235,6 @@ extension SourceKitLSPServer {
12361235
},
12371236
indexTaskDidFinish: { [weak self] in
12381237
self?.indexProgressManager.indexStatusDidChange()
1239-
indexTaskDidFinishCallback?()
12401238
}
12411239
)
12421240
}
@@ -1285,7 +1283,6 @@ extension SourceKitLSPServer {
12851283
if self.workspaces.isEmpty {
12861284
logger.error("no workspace found")
12871285

1288-
let indexTaskDidFinishCallback = self.options.indexTaskDidFinish
12891286
let workspace = await Workspace(
12901287
documentManager: self.documentManager,
12911288
rootUri: req.rootURI,
@@ -1301,7 +1298,6 @@ extension SourceKitLSPServer {
13011298
},
13021299
indexTaskDidFinish: { [weak self] in
13031300
self?.indexProgressManager.indexStatusDidChange()
1304-
indexTaskDidFinishCallback?()
13051301
}
13061302
)
13071303

Sources/SourceKitLSP/Workspace.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public final class Workspace: Sendable {
112112
self.semanticIndexManager = SemanticIndexManager(
113113
index: uncheckedIndex,
114114
buildSystemManager: buildSystemManager,
115+
testHooks: options.indexTestHooks,
115116
indexTaskScheduler: indexTaskScheduler,
116117
indexTasksWereScheduled: indexTasksWereScheduled,
117118
indexTaskDidFinish: indexTaskDidFinish

0 commit comments

Comments
 (0)