File: CustomBuildServerTestProject.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (309 lines) | stat: -rw-r--r-- 11,495 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

package import BuildServerProtocol
import BuildSystemIntegration
package import Foundation
package import LanguageServerProtocol
import LanguageServerProtocolExtensions
import SKLogging
package import SKOptions
package import SourceKitLSP
import SwiftExtensions
import ToolchainRegistry
import XCTest

// MARK: - CustomBuildServer

package actor CustomBuildServerInProgressRequestTracker {
  private var inProgressRequests: [RequestID: Task<Void, Never>] = [:]
  private let queue = AsyncQueue<Serial>()

  package init() {}

  private func setInProgressRequestImpl(_ id: RequestID, task: Task<Void, Never>) {
    guard inProgressRequests[id] == nil else {
      logger.fault("Received duplicate request for id: \(id, privacy: .public)")
      return
    }
    inProgressRequests[id] = task
  }

  fileprivate nonisolated func setInProgressRequest(_ id: RequestID, task: Task<Void, Never>) {
    queue.async {
      await self.setInProgressRequestImpl(id, task: task)
    }
  }

  private func markTaskAsFinishedImpl(_ id: RequestID) {
    guard inProgressRequests[id] != nil else {
      logger.fault("Cannot mark request \(id, privacy: .public) as finished because it is not being tracked.")
      return
    }
    inProgressRequests[id] = nil
  }

  fileprivate nonisolated func markTaskAsFinished(_ id: RequestID) {
    queue.async {
      await self.markTaskAsFinishedImpl(id)
    }
  }

  private func cancelTaskImpl(_ id: RequestID) {
    guard let task = inProgressRequests[id] else {
      logger.fault("Cannot cancel task \(id, privacy: .public) because it isn't tracked")
      return
    }
    task.cancel()
  }

  fileprivate nonisolated func cancelTask(_ id: RequestID) {
    queue.async {
      await self.cancelTaskImpl(id)
    }
  }
}

/// A build server that can be injected into `CustomBuildServerTestProject`.
package protocol CustomBuildServer: MessageHandler {
  var inProgressRequestsTracker: CustomBuildServerInProgressRequestTracker { get }

  init(projectRoot: URL, connectionToSourceKitLSP: any Connection)

  func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse
  func onBuildInitialized(_ notification: OnBuildInitializedNotification) throws
  func buildShutdown(_ request: BuildShutdownRequest) async throws -> VoidResponse
  func onBuildExit(_ notification: OnBuildExitNotification) throws
  func workspaceBuildTargetsRequest(
    _ request: WorkspaceBuildTargetsRequest
  ) async throws -> WorkspaceBuildTargetsResponse
  func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse
  func textDocumentSourceKitOptionsRequest(
    _ request: TextDocumentSourceKitOptionsRequest
  ) async throws -> TextDocumentSourceKitOptionsResponse?
  func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse
  func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse
  nonisolated func onWatchedFilesDidChange(_ notification: OnWatchedFilesDidChangeNotification) throws
  func workspaceWaitForBuildSystemUpdatesRequest(
    _ request: WorkspaceWaitForBuildSystemUpdatesRequest
  ) async throws -> VoidResponse
  nonisolated func cancelRequest(_ notification: CancelRequestNotification) throws
}

extension CustomBuildServer {
  package nonisolated func handle(_ notification: some NotificationType) {
    do {
      switch notification {
      case let notification as CancelRequestNotification:
        try self.cancelRequest(notification)
      case let notification as OnBuildExitNotification:
        try self.onBuildExit(notification)
      case let notification as OnBuildInitializedNotification:
        try self.onBuildInitialized(notification)
      case let notification as OnWatchedFilesDidChangeNotification:
        try self.onWatchedFilesDidChange(notification)
      default:
        throw ResponseError.methodNotFound(type(of: notification).method)
      }
    } catch {
      logger.error("Error while handling BSP notification: \(error.forLogging)")
    }
  }

  package nonisolated func handle<Request: RequestType>(
    _ request: Request,
    id: RequestID,
    reply: @Sendable @escaping (LSPResult<Request.Response>) -> Void
  ) {
    func handle<R: RequestType>(_ request: R, using handler: @Sendable @escaping (R) async throws -> R.Response) {
      let task = Task {
        defer { inProgressRequestsTracker.markTaskAsFinished(id) }
        do {
          reply(.success(try await handler(request) as! Request.Response))
        } catch {
          reply(.failure(ResponseError(error)))
        }
      }
      inProgressRequestsTracker.setInProgressRequest(id, task: task)
    }

    switch request {
    case let request as BuildShutdownRequest:
      handle(request, using: self.buildShutdown(_:))
    case let request as BuildTargetSourcesRequest:
      handle(request, using: self.buildTargetSourcesRequest)
    case let request as InitializeBuildRequest:
      handle(request, using: self.initializeBuildRequest)
    case let request as TextDocumentSourceKitOptionsRequest:
      handle(request, using: self.textDocumentSourceKitOptionsRequest)
    case let request as WorkspaceBuildTargetsRequest:
      handle(request, using: self.workspaceBuildTargetsRequest)
    case let request as WorkspaceWaitForBuildSystemUpdatesRequest:
      handle(request, using: self.workspaceWaitForBuildSystemUpdatesRequest)
    case let request as BuildTargetPrepareRequest:
      handle(request, using: self.prepareTarget)
    default:
      reply(.failure(ResponseError.methodNotFound(type(of: request).method)))
    }
  }
}

package extension CustomBuildServer {
  // MARK: Helper functions for the implementation of BSP methods

  func initializationResponse(
    initializeData: SourceKitInitializeBuildResponseData = .init(sourceKitOptionsProvider: true)
  ) -> InitializeBuildResponse {
    InitializeBuildResponse(
      displayName: "\(type(of: self))",
      version: "",
      bspVersion: "2.2.0",
      capabilities: BuildServerCapabilities(),
      dataKind: .sourceKit,
      data: initializeData.encodeToLSPAny()
    )
  }

  func initializationResponseSupportingBackgroundIndexing(
    projectRoot: URL,
    outputPathsProvider: Bool
  ) throws -> InitializeBuildResponse {
    return initializationResponse(
      initializeData: SourceKitInitializeBuildResponseData(
        indexDatabasePath: try projectRoot.appendingPathComponent("index-db").filePath,
        indexStorePath: try projectRoot.appendingPathComponent("index-store").filePath,
        outputPathsProvider: outputPathsProvider,
        prepareProvider: true,
        sourceKitOptionsProvider: true
      )
    )
  }

  /// Returns a fake path that is unique to this target and file combination and can be used to identify this
  /// combination in a unit's output path.
  func fakeOutputPath(for file: String, in target: String) -> String {
    #if os(Windows)
    return #"C:\"# + target + #"\"# + file + ".o"
    #else
    return "/" + target + "/" + file + ".o"
    #endif
  }

  func sourceItem(for url: URL, outputPath: String) -> SourceItem {
    SourceItem(
      uri: URI(url),
      kind: .file,
      generated: false,
      dataKind: .sourceKit,
      data: SourceKitSourceItemData(outputPath: outputPath).encodeToLSPAny()
    )
  }

  func dummyTargetSourcesResponse(files: some Sequence<DocumentURI>) -> BuildTargetSourcesResponse {
    return BuildTargetSourcesResponse(items: [
      SourcesItem(target: .dummy, sources: files.map { SourceItem(uri: $0, kind: .file, generated: false) })
    ])
  }

  // MARK: Default implementation for all build server methods that usually don't need customization.

  func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse {
    return initializationResponse()
  }

  nonisolated func onBuildInitialized(_ notification: OnBuildInitializedNotification) throws {}

  func buildShutdown(_ request: BuildShutdownRequest) async throws -> VoidResponse {
    return VoidResponse()
  }

  nonisolated func onBuildExit(_ notification: OnBuildExitNotification) throws {}

  func workspaceBuildTargetsRequest(
    _ request: WorkspaceBuildTargetsRequest
  ) async throws -> WorkspaceBuildTargetsResponse {
    return WorkspaceBuildTargetsResponse(targets: [
      BuildTarget(
        id: .dummy,
        capabilities: BuildTargetCapabilities(),
        languageIds: [],
        dependencies: []
      )
    ])
  }

  func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse {
    return VoidResponse()
  }

  func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse {
    return VoidResponse()
  }

  nonisolated func onWatchedFilesDidChange(_ notification: OnWatchedFilesDidChangeNotification) throws {}

  func workspaceWaitForBuildSystemUpdatesRequest(
    _ request: WorkspaceWaitForBuildSystemUpdatesRequest
  ) async throws -> VoidResponse {
    return VoidResponse()
  }

  nonisolated func cancelRequest(_ notification: CancelRequestNotification) throws {
    inProgressRequestsTracker.cancelTask(notification.id)
  }
}

// MARK: - CustomBuildServerTestProject

/// A test project that launches a custom build server in-process.
///
/// In contrast to `ExternalBuildServerTestProject`, the custom build system runs in-process and is implemented in
/// Swift.
package final class CustomBuildServerTestProject<BuildServer: CustomBuildServer>: MultiFileTestProject {
  private let buildServerBox = ThreadSafeBox<BuildServer?>(initialValue: nil)

  package init(
    files: [RelativeFileLocation: String],
    buildServer buildServerType: BuildServer.Type,
    options: SourceKitLSPOptions? = nil,
    hooks: Hooks = Hooks(),
    enableBackgroundIndexing: Bool = false,
    pollIndex: Bool = true,
    testScratchDir: URL? = nil,
    testName: String = #function
  ) async throws {
    var hooks = hooks
    XCTAssertNil(hooks.buildSystemHooks.injectBuildServer)
    hooks.buildSystemHooks.injectBuildServer = { [buildServerBox] projectRoot, connectionToSourceKitLSP in
      let buildServer = BuildServer(projectRoot: projectRoot, connectionToSourceKitLSP: connectionToSourceKitLSP)
      buildServerBox.value = buildServer
      return LocalConnection(receiverName: "TestBuildSystem", handler: buildServer)
    }
    try await super.init(
      files: files,
      options: options,
      hooks: hooks,
      enableBackgroundIndexing: enableBackgroundIndexing,
      testScratchDir: testScratchDir,
      testName: testName
    )

    if pollIndex {
      // Wait for the indexstore-db to finish indexing
      try await testClient.send(SynchronizeRequest(index: true))
    }
  }

  package func buildServer(file: StaticString = #filePath, line: UInt = #line) throws -> BuildServer {
    try XCTUnwrap(buildServerBox.value, "Accessing build server before it has been created", file: file, line: line)
  }
}