Skip to content

Don’t run a swift build command to prepare a package manifest #1380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,11 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
singleTarget target: ConfiguredTarget,
indexProcessDidProduceResult: @Sendable (IndexProcessResult) -> Void
) async throws {
if target == .forPackageManifest {
// Nothing to prepare for package manifests.
return
}

// TODO (indexing): Add a proper 'prepare' job in SwiftPM instead of building the target.
// https://github.com/apple/sourcekit-lsp/issues/1254
guard let toolchain = await toolchainRegistry.default else {
Expand Down
43 changes: 33 additions & 10 deletions Sources/SKTestSupport/TestSourceKitLSPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ extension SourceKitLSPServer.Options {
public static let testDefault = Self(swiftPublishDiagnosticsDebounceDuration: 0)
}

fileprivate struct NotificationTimeoutError: Error, CustomStringConvertible {
var description: String = "Failed to receive next notification within timeout"
}

/// A mock SourceKit-LSP client (aka. a mock editor) that behaves like an editor
/// for testing purposes.
///
Expand Down Expand Up @@ -229,21 +233,17 @@ public final class TestSourceKitLSPClient: MessageHandler {
///
/// - Note: This also returns any notifications sent before the call to
/// `nextNotification`.
public func nextNotification(timeout: TimeInterval = defaultTimeout) async throws -> any NotificationType {
struct TimeoutError: Error, CustomStringConvertible {
var description: String = "Failed to receive next notification within timeout"
}

public func nextNotification(timeout: Duration = .seconds(defaultTimeout)) async throws -> any NotificationType {
return try await withThrowingTaskGroup(of: (any NotificationType).self) { taskGroup in
taskGroup.addTask {
for await notification in self.notifications {
return notification
}
throw TimeoutError()
throw NotificationTimeoutError()
}
taskGroup.addTask {
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
throw TimeoutError()
try await Task.sleep(for: timeout)
throw NotificationTimeoutError()
}
let result = try await taskGroup.next()!
taskGroup.cancelAll()
Expand All @@ -256,7 +256,7 @@ public final class TestSourceKitLSPClient: MessageHandler {
/// If the next notification is not a `PublishDiagnosticsNotification`, this
/// methods throws.
public func nextDiagnosticsNotification(
timeout: TimeInterval = defaultTimeout
timeout: Duration = .seconds(defaultTimeout)
) async throws -> PublishDiagnosticsNotification {
guard !usePullDiagnostics else {
struct PushDiagnosticsError: Error, CustomStringConvertible {
Expand All @@ -272,7 +272,7 @@ public final class TestSourceKitLSPClient: MessageHandler {
public func nextNotification<ExpectedNotificationType: NotificationType>(
ofType: ExpectedNotificationType.Type,
satisfying predicate: (ExpectedNotificationType) -> Bool = { _ in true },
timeout: TimeInterval = defaultTimeout
timeout: Duration = .seconds(defaultTimeout)
) async throws -> ExpectedNotificationType {
while true {
let nextNotification = try await nextNotification(timeout: timeout)
Expand All @@ -282,6 +282,29 @@ public final class TestSourceKitLSPClient: MessageHandler {
}
}

/// Asserts that the test client does not receive a notification of the given type and satisfying the given predicate
/// within the given duration.
///
/// For stable tests, the code that triggered the notification should be run before this assertion instead of relying
/// on the duration.
///
/// The duration should not be 0 because we need to allow `nextNotification` some time to get the notification out of
/// the `notifications` `AsyncStream`.
public func assertDoesNotReceiveNotification<ExpectedNotificationType: NotificationType>(
ofType: ExpectedNotificationType.Type,
satisfying predicate: (ExpectedNotificationType) -> Bool = { _ in true },
within duration: Duration = .seconds(0.2)
) async throws {
do {
let notification = try await nextNotification(
ofType: ExpectedNotificationType.self,
satisfying: predicate,
timeout: duration
)
XCTFail("Did not expect to receive notification but received \(notification)")
} catch is NotificationTimeoutError {}
}

/// Handle the next request of the given type that is sent to the client.
///
/// The request handler will only handle a single request. If the request is called again, the request handler won't
Expand Down
14 changes: 10 additions & 4 deletions Sources/SemanticIndex/SemanticIndexManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -394,24 +394,30 @@ public final actor SemanticIndexManager {
let id = UUID()
let task = Task(priority: priority) {
await withLoggingScope("preparation") {
defer {
if inProgressPrepareForEditorTask?.id == id {
inProgressPrepareForEditorTask = nil
self.indexProgressStatusDidChange()
}
}
// Should be kept in sync with `prepareFileForEditorFunctionality`
guard let target = await buildSystemManager.canonicalConfiguredTarget(for: uri) else {
return
}
if Task.isCancelled {
return
}
if await preparationUpToDateTracker.isUpToDate(target) {
// If the target is up-to-date, there is nothing to prepare.
return
}
if inProgressPrepareForEditorTask?.id == id {
if inProgressPrepareForEditorTask?.state != .determiningCanonicalConfiguredTarget {
logger.fault("inProgressPrepareForEditorTask is in unexpected state")
}
inProgressPrepareForEditorTask?.state = .preparingTarget
}
await self.prepare(targets: [target], priority: nil)
if inProgressPrepareForEditorTask?.id == id {
inProgressPrepareForEditorTask = nil
self.indexProgressStatusDidChange()
}
}
}
inProgressPrepareForEditorTask?.task.cancel()
Expand Down
4 changes: 2 additions & 2 deletions Tests/SourceKitDTests/CrashRecoveryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ final class CrashRecoveryTests: XCTestCase {

await swiftLanguageService._crash()

let crashedNotification = try await testClient.nextNotification(ofType: WorkDoneProgress.self, timeout: 5)
let crashedNotification = try await testClient.nextNotification(ofType: WorkDoneProgress.self, timeout: .seconds(5))
XCTAssertEqual(
crashedNotification.value,
.begin(
Expand All @@ -107,7 +107,7 @@ final class CrashRecoveryTests: XCTestCase {
_ = try? await testClient.send(hoverRequest)
let semanticFunctionalityRestoredNotification = try await testClient.nextNotification(
ofType: WorkDoneProgress.self,
timeout: 30
timeout: .seconds(30)
)
XCTAssertEqual(semanticFunctionalityRestoredNotification.value, .end(WorkDoneProgressEnd()))

Expand Down
23 changes: 23 additions & 0 deletions Tests/SourceKitLSPTests/BackgroundIndexingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -930,4 +930,27 @@ final class BackgroundIndexingTests: XCTestCase {
)
XCTAssertEqual((result?.changes?.keys).map(Set.init), [uri, try project.uri(for: "Client.swift")])
}

func testDontPreparePackageManifest() async throws {
let project = try await SwiftPMTestProject(
files: [
"Lib.swift": ""
],
enableBackgroundIndexing: true
)

_ = try await project.testClient.nextNotification(
ofType: LogMessageNotification.self,
satisfying: { $0.message.contains("Preparing MyLibrary") }
)

// Opening the package manifest shouldn't cause any `swift build` calls to prepare them because they are not part of
// a target that can be prepared.
let (uri, _) = try project.openDocument("Package.swift")
_ = try await project.testClient.send(DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri)))
try await project.testClient.assertDoesNotReceiveNotification(
ofType: LogMessageNotification.self,
satisfying: { $0.message.contains("Preparing") }
)
}
}
2 changes: 1 addition & 1 deletion Tests/SourceKitLSPTests/BuildSystemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ final class BuildSystemTests: XCTestCase {

var receivedCorrectDiagnostic = false
for _ in 0..<Int(defaultTimeout) {
let refreshedDiags = try await testClient.nextDiagnosticsNotification(timeout: 1)
let refreshedDiags = try await testClient.nextDiagnosticsNotification(timeout: .seconds(1))
if refreshedDiags.diagnostics.count == 0, try text == documentManager.latestSnapshot(doc).text {
receivedCorrectDiagnostic = true
break
Expand Down
2 changes: 1 addition & 1 deletion Tests/SourceKitLSPTests/LocalSwiftTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1427,6 +1427,6 @@ final class LocalSwiftTests: XCTestCase {
XCTAssertEqual(diag.message, "Cannot find 'bar' in scope")

// Ensure that we don't get a second `PublishDiagnosticsNotification`
await assertThrowsError(try await testClient.nextDiagnosticsNotification(timeout: 2))
await assertThrowsError(try await testClient.nextDiagnosticsNotification(timeout: .seconds(2)))
}
}
2 changes: 1 addition & 1 deletion Tests/SourceKitLSPTests/MainFilesProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ final class MainFilesProviderTests: XCTestCase {
// diagnostics.
var receivedCorrectDiagnostic = false
for _ in 0..<Int(defaultTimeout) {
let refreshedDiags = try await project.testClient.nextDiagnosticsNotification(timeout: 1)
let refreshedDiags = try await project.testClient.nextDiagnosticsNotification(timeout: .seconds(1))
if let diagnostic = refreshedDiags.diagnostics.only,
diagnostic.message == "Unused variable 'fromMyFancyLibrary'"
{
Expand Down