Files
sourcekit-lsp/Tests/SourceKitLSPTests/ExpectedIndexTaskTracker.swift
Alex Hoppen 3788a45def Add log messages to track target preparation
These should help us figure out the root cause of rdar://129698768 if we hit it again.
2024-06-12 14:40:41 -07:00

229 lines
7.9 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 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
//
//===----------------------------------------------------------------------===//
import LSPLogging
import LanguageServerProtocol
import SKCore
import SemanticIndex
import XCTest
struct ExpectedPreparation {
let targetID: String
let runDestinationID: String
/// A closure that will be executed when a preparation task starts.
/// This allows the artificial delay of a preparation task to force two preparation task to race.
let didStart: (() -> Void)?
/// A closure that will be executed when a preparation task finishes.
/// This allows the artificial delay of a preparation task to force two preparation task to race.
let didFinish: (() -> Void)?
internal init(
targetID: String,
runDestinationID: String,
didStart: (() -> Void)? = nil,
didFinish: (() -> Void)? = nil
) {
self.targetID = targetID
self.runDestinationID = runDestinationID
self.didStart = didStart
self.didFinish = didFinish
}
var configuredTarget: ConfiguredTarget {
return ConfiguredTarget(targetID: targetID, runDestinationID: runDestinationID)
}
}
struct ExpectedIndexStoreUpdate {
let sourceFileName: String
/// A closure that will be executed when a preparation task starts.
/// This allows the artificial delay of a preparation task to force two preparation task to race.
let didStart: (() -> Void)?
/// A closure that will be executed when a preparation task finishes.
/// This allows the artificial delay of a preparation task to force two preparation task to race.
let didFinish: (() -> Void)?
internal init(
sourceFileName: String,
didStart: (() -> Void)? = nil,
didFinish: (() -> Void)? = nil
) {
self.sourceFileName = sourceFileName
self.didStart = didStart
self.didFinish = didFinish
}
}
actor ExpectedIndexTaskTracker {
/// The targets we expect to be prepared. For targets within the same set, we don't care about the exact order.
private var expectedPreparations: [[ExpectedPreparation]]?
private var expectedIndexStoreUpdates: [[ExpectedIndexStoreUpdate]]?
/// Implicitly-unwrapped optional so we can reference `self` when creating `IndexTestHooks`.
/// `nonisolated(unsafe)` is fine because this is not modified after `testHooks` is created.
nonisolated(unsafe) var testHooks: IndexTestHooks!
init(
expectedPreparations: [[ExpectedPreparation]]? = nil,
expectedIndexStoreUpdates: [[ExpectedIndexStoreUpdate]]? = nil
) {
self.expectedPreparations = expectedPreparations
self.expectedIndexStoreUpdates = expectedIndexStoreUpdates
self.testHooks = IndexTestHooks(
preparationTaskDidStart: { [weak self] in
await self?.preparationTaskDidStart(taskDescription: $0)
},
preparationTaskDidFinish: { [weak self] in
await self?.preparationTaskDidFinish(taskDescription: $0)
},
updateIndexStoreTaskDidStart: { [weak self] in
await self?.updateIndexStoreTaskDidStart(taskDescription: $0)
},
updateIndexStoreTaskDidFinish: { [weak self] in
await self?.updateIndexStoreTaskDidFinish(taskDescription: $0)
}
)
}
func preparationTaskDidStart(taskDescription: PreparationTaskDescription) -> Void {
guard let expectedPreparations else {
return
}
if Task.isCancelled {
logger.debug("Ignoring preparation task start because task is cancelled: \(taskDescription.targetsToPrepare)")
return
}
guard let expectedTargetsToPrepare = expectedPreparations.first else {
return
}
for expectedPreparation in expectedTargetsToPrepare {
if taskDescription.targetsToPrepare.contains(expectedPreparation.configuredTarget) {
expectedPreparation.didStart?()
}
}
}
func preparationTaskDidFinish(taskDescription: PreparationTaskDescription) -> Void {
guard let expectedPreparations else {
return
}
if Task.isCancelled {
logger.debug("Ignoring preparation task finish because task is cancelled: \(taskDescription.targetsToPrepare)")
return
}
guard let expectedTargetsToPrepare = expectedPreparations.first else {
XCTFail("Didn't expect a preparation but received \(taskDescription.targetsToPrepare)")
return
}
guard Set(taskDescription.targetsToPrepare).isSubset(of: expectedTargetsToPrepare.map(\.configuredTarget)) else {
XCTFail("Received unexpected preparation of \(taskDescription.targetsToPrepare)")
return
}
var remainingExpectedTargetsToPrepare: [ExpectedPreparation] = []
for expectedPreparation in expectedTargetsToPrepare {
if taskDescription.targetsToPrepare.contains(expectedPreparation.configuredTarget) {
expectedPreparation.didFinish?()
} else {
remainingExpectedTargetsToPrepare.append(expectedPreparation)
}
}
if remainingExpectedTargetsToPrepare.isEmpty {
self.expectedPreparations!.remove(at: 0)
} else {
self.expectedPreparations![0] = remainingExpectedTargetsToPrepare
}
}
func updateIndexStoreTaskDidStart(taskDescription: UpdateIndexStoreTaskDescription) -> Void {
if Task.isCancelled {
logger.debug(
"""
Ignoring update indexstore start because task is cancelled: \
\(taskDescription.filesToIndex.map(\.file.sourceFile))
"""
)
return
}
guard let expectedFilesToIndex = expectedIndexStoreUpdates?.first else {
return
}
for expectedIndexStoreUpdate in expectedFilesToIndex {
if taskDescription.filesToIndex.contains(where: { $0.sourceFileName == expectedIndexStoreUpdate.sourceFileName })
{
expectedIndexStoreUpdate.didStart?()
}
}
}
func updateIndexStoreTaskDidFinish(taskDescription: UpdateIndexStoreTaskDescription) -> Void {
guard let expectedIndexStoreUpdates else {
return
}
if Task.isCancelled {
logger.debug(
"""
Ignoring update indexstore finish because task is cancelled: \
\(taskDescription.filesToIndex.map(\.file.sourceFile))
"""
)
return
}
guard let expectedFilesToIndex = expectedIndexStoreUpdates.first else {
XCTFail("Didn't expect an index store update but received \(taskDescription.filesToIndex.map(\.file.sourceFile))")
return
}
guard
Set(taskDescription.filesToIndex.map(\.sourceFileName)).isSubset(of: expectedFilesToIndex.map(\.sourceFileName))
else {
XCTFail("Received unexpected index store update of \(taskDescription.filesToIndex.map(\.file.sourceFile))")
return
}
var remainingExpectedFilesToIndex: [ExpectedIndexStoreUpdate] = []
for expectedIndexStoreUpdate in expectedFilesToIndex {
if taskDescription.filesToIndex.map(\.sourceFileName).contains(expectedIndexStoreUpdate.sourceFileName) {
expectedIndexStoreUpdate.didFinish?()
} else {
remainingExpectedFilesToIndex.append(expectedIndexStoreUpdate)
}
}
if remainingExpectedFilesToIndex.isEmpty {
self.expectedIndexStoreUpdates!.remove(at: 0)
} else {
self.expectedIndexStoreUpdates![0] = remainingExpectedFilesToIndex
}
}
nonisolated func keepAlive() {
withExtendedLifetime(self) { _ in }
}
deinit {
if let expectedPreparations = self.expectedPreparations {
XCTAssert(
expectedPreparations.isEmpty,
"ExpectedPreparationTracker destroyed with unfulfilled expected preparations: \(expectedPreparations)."
)
}
}
}
fileprivate extension FileAndTarget {
var sourceFileName: String? {
return self.file.sourceFile.fileURL?.lastPathComponent
}
}