Skip to content

Commit 2ddb923

Browse files
committed
Make SKTestSupport build in Swift 6 mode
1 parent 52636dd commit 2ddb923

File tree

3 files changed

+52
-40
lines changed

3 files changed

+52
-40
lines changed

Sources/SKSupport/ThreadSafeBox.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ public class ThreadSafeBox<T>: @unchecked Sendable {
4747
_value = initialValue
4848
}
4949

50+
public func withLock<Result>(_ body: (inout T) -> Result) -> Result {
51+
return lock.withLock {
52+
return body(&_value)
53+
}
54+
}
55+
5056
/// If the value in the box is an optional, return it and reset it to `nil`
5157
/// in an atomic operation.
5258
public func takeValue<U>() -> T where U? == T {

Sources/SKTestSupport/SkipUnless.swift

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@ import enum TSCBasic.ProcessEnv
2727
// MARK: - Skip checks
2828

2929
/// Namespace for functions that are used to skip unsupported tests.
30-
public enum SkipUnless {
30+
public actor SkipUnless {
3131
private enum FeatureCheckResult {
3232
case featureSupported
3333
case featureUnsupported(skipMessage: String)
3434
}
3535

36+
private static let shared = SkipUnless()
37+
3638
/// For any feature that has already been evaluated, the result of whether or not it should be skipped.
37-
private static var checkCache: [String: FeatureCheckResult] = [:]
39+
private var checkCache: [String: FeatureCheckResult] = [:]
3840

3941
/// Throw an `XCTSkip` if any of the following conditions hold
4042
/// - The Swift version of the toolchain used for testing (`ToolchainRegistry.forTesting.default`) is older than
@@ -49,7 +51,7 @@ public enum SkipUnless {
4951
///
5052
/// Independently of these checks, the tests are never skipped in Swift CI (identified by the presence of the `SWIFTCI_USE_LOCAL_DEPS` environment). Swift CI is assumed to always build its own toolchain, which is thus
5153
/// guaranteed to be up-to-date.
52-
private static func skipUnlessSupportedByToolchain(
54+
private func skipUnlessSupportedByToolchain(
5355
swiftVersion: SwiftVersion,
5456
featureName: String = #function,
5557
file: StaticString,
@@ -96,10 +98,10 @@ public enum SkipUnless {
9698
}
9799

98100
public static func sourcekitdHasSemanticTokensRequest(
99-
file: StaticString = #file,
101+
file: StaticString = #filePath,
100102
line: UInt = #line
101103
) async throws {
102-
try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) {
104+
try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) {
103105
let testClient = try await TestSourceKitLSPClient()
104106
let uri = DocumentURI.for(.swift)
105107
testClient.openDocument("0.bitPattern", uri: uri)
@@ -127,10 +129,10 @@ public enum SkipUnless {
127129
}
128130

129131
public static func sourcekitdSupportsRename(
130-
file: StaticString = #file,
132+
file: StaticString = #filePath,
131133
line: UInt = #line
132134
) async throws {
133-
try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) {
135+
try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) {
134136
let testClient = try await TestSourceKitLSPClient()
135137
let uri = DocumentURI.for(.swift)
136138
let positions = testClient.openDocument("func 1️⃣test() {}", uri: uri)
@@ -147,10 +149,10 @@ public enum SkipUnless {
147149

148150
/// Whether clangd has support for the `workspace/indexedRename` request.
149151
public static func clangdSupportsIndexBasedRename(
150-
file: StaticString = #file,
152+
file: StaticString = #filePath,
151153
line: UInt = #line
152154
) async throws {
153-
try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) {
155+
try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) {
154156
let testClient = try await TestSourceKitLSPClient()
155157
let uri = DocumentURI.for(.c)
156158
let positions = testClient.openDocument("void 1️⃣test() {}", uri: uri)
@@ -177,10 +179,10 @@ public enum SkipUnless {
177179
/// toolchain’s SwiftPM stores the Swift modules on the top level but we synthesize compiler arguments expecting the
178180
/// modules to be in a `Modules` subdirectory.
179181
public static func swiftpmStoresModulesInSubdirectory(
180-
file: StaticString = #file,
182+
file: StaticString = #filePath,
181183
line: UInt = #line
182184
) async throws {
183-
try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) {
185+
try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) {
184186
let workspace = try await SwiftPMTestProject(
185187
files: ["test.swift": ""],
186188
build: true
@@ -195,21 +197,21 @@ public enum SkipUnless {
195197
}
196198

197199
public static func toolchainContainsSwiftFormat(
198-
file: StaticString = #file,
200+
file: StaticString = #filePath,
199201
line: UInt = #line
200202
) async throws {
201-
try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) {
203+
try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) {
202204
return await ToolchainRegistry.forTesting.default?.swiftFormat != nil
203205
}
204206
}
205207

206208
public static func sourcekitdReturnsRawDocumentationResponse(
207-
file: StaticString = #file,
209+
file: StaticString = #filePath,
208210
line: UInt = #line
209211
) async throws {
210212
struct ExpectedMarkdownContentsError: Error {}
211213

212-
return try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 0), file: file, line: line) {
214+
return try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 0), file: file, line: line) {
213215
// The XML-based doc comment conversion did not preserve `Precondition`.
214216
let testClient = try await TestSourceKitLSPClient()
215217
let uri = DocumentURI.for(.swift)
@@ -235,10 +237,10 @@ public enum SkipUnless {
235237
/// Checks whether the index contains a fix that prevents it from adding relations to non-indexed locals
236238
/// (https://github.com/apple/swift/pull/72930).
237239
public static func indexOnlyHasContainedByRelationsToIndexedDecls(
238-
file: StaticString = #file,
240+
file: StaticString = #filePath,
239241
line: UInt = #line
240242
) async throws {
241-
return try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 0), file: file, line: line) {
243+
return try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 0), file: file, line: line) {
242244
let project = try await IndexedSingleSwiftFileTestProject(
243245
"""
244246
func foo() {}

Sources/SKTestSupport/TestSourceKitLSPClient.swift

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import CAtomics
1314
import Foundation
1415
import LSPTestSupport
1516
import LanguageServerProtocol
@@ -35,7 +36,8 @@ public final class TestSourceKitLSPClient: MessageHandler {
3536
public typealias RequestHandler<Request: RequestType> = (Request) -> Request.Response
3637

3738
/// The ID that should be assigned to the next request sent to the `server`.
38-
private var nextRequestID: Int = 0
39+
/// `nonisolated(unsafe)` is fine because `nextRequestID` is atomic.
40+
private nonisolated(unsafe) var nextRequestID = AtomicUInt32(initialValue: 0)
3941

4042
/// If the server is not using the global module cache, the path of the local
4143
/// module cache.
@@ -66,12 +68,12 @@ public final class TestSourceKitLSPClient: MessageHandler {
6668
///
6769
/// Conceptually, this is an array of `RequestHandler<any RequestType>` but
6870
/// since we can't express this in the Swift type system, we use `[Any]`.
69-
private var requestHandlers: [Any] = []
71+
private nonisolated(unsafe) var requestHandlers = ThreadSafeBox<[Any]>(initialValue: [])
7072

7173
/// A closure that is called when the `TestSourceKitLSPClient` is destructed.
7274
///
7375
/// This allows e.g. a `IndexedSingleSwiftFileTestProject` to delete its temporary files when they are no longer needed.
74-
private let cleanUp: () -> Void
76+
private let cleanUp: @Sendable () -> Void
7577

7678
/// - Parameters:
7779
/// - serverOptions: The equivalent of the command line options with which sourcekit-lsp should be started
@@ -94,7 +96,7 @@ public final class TestSourceKitLSPClient: MessageHandler {
9496
capabilities: ClientCapabilities = ClientCapabilities(),
9597
usePullDiagnostics: Bool = true,
9698
workspaceFolders: [WorkspaceFolder]? = nil,
97-
cleanUp: @escaping () -> Void = {}
99+
cleanUp: @Sendable @escaping () -> Void = {}
98100
) async throws {
99101
if !useGlobalModuleCache {
100102
moduleCache = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)
@@ -165,8 +167,7 @@ public final class TestSourceKitLSPClient: MessageHandler {
165167
// It's really unfortunate that there are no async deinits. If we had async
166168
// deinits, we could await the sending of a ShutdownRequest.
167169
let sema = DispatchSemaphore(value: 0)
168-
nextRequestID += 1
169-
server.handle(ShutdownRequest(), id: .number(nextRequestID)) { result in
170+
server.handle(ShutdownRequest(), id: .number(Int(nextRequestID.fetchAndIncrement()))) { result in
170171
sema.signal()
171172
}
172173
sema.wait()
@@ -182,9 +183,8 @@ public final class TestSourceKitLSPClient: MessageHandler {
182183

183184
/// Send the request to `server` and return the request result.
184185
public func send<R: RequestType>(_ request: R) async throws -> R.Response {
185-
nextRequestID += 1
186186
return try await withCheckedThrowingContinuation { continuation in
187-
server.handle(request, id: .number(self.nextRequestID)) { result in
187+
server.handle(request, id: .number(Int(nextRequestID.fetchAndIncrement()))) { result in
188188
continuation.resume(with: result)
189189
}
190190
}
@@ -269,7 +269,7 @@ public final class TestSourceKitLSPClient: MessageHandler {
269269
/// If the next request that is sent to the client is of a different kind than
270270
/// the given handler, `TestSourceKitLSPClient` will emit an `XCTFail`.
271271
public func handleNextRequest<R: RequestType>(_ requestHandler: @escaping RequestHandler<R>) {
272-
requestHandlers.append(requestHandler)
272+
requestHandlers.value.append(requestHandler)
273273
}
274274

275275
// MARK: - Conformance to MessageHandler
@@ -286,18 +286,20 @@ public final class TestSourceKitLSPClient: MessageHandler {
286286
id: LanguageServerProtocol.RequestID,
287287
reply: @escaping (LSPResult<Request.Response>) -> Void
288288
) {
289-
guard let requestHandler = requestHandlers.first else {
290-
reply(.failure(.methodNotFound(Request.method)))
291-
return
292-
}
293-
guard let requestHandler = requestHandler as? RequestHandler<Request> else {
294-
print("\(RequestHandler<Request>.self)")
295-
XCTFail("Received request of unexpected type \(Request.method)")
296-
reply(.failure(.methodNotFound(Request.method)))
297-
return
289+
requestHandlers.withLock { requestHandlers in
290+
guard let requestHandler = requestHandlers.first else {
291+
reply(.failure(.methodNotFound(Request.method)))
292+
return
293+
}
294+
guard let requestHandler = requestHandler as? RequestHandler<Request> else {
295+
print("\(RequestHandler<Request>.self)")
296+
XCTFail("Received request of unexpected type \(Request.method)")
297+
reply(.failure(.methodNotFound(Request.method)))
298+
return
299+
}
300+
reply(.success(requestHandler(params)))
301+
requestHandlers.removeFirst()
298302
}
299-
reply(.success(requestHandler(params)))
300-
requestHandlers.removeFirst()
301303
}
302304

303305
// MARK: - Convenience functions
@@ -391,8 +393,10 @@ public struct DocumentPositions {
391393
///
392394
/// This allows us to set the ``TestSourceKitLSPClient`` as the message handler of
393395
/// `SourceKitLSPServer` without retaining it.
394-
private class WeakMessageHandler: MessageHandler {
395-
private weak var handler: (any MessageHandler)?
396+
private final class WeakMessageHandler: MessageHandler, Sendable {
397+
// `nonisolated(unsafe)` is fine because `handler` is never modified, only if the weak reference is deallocated, which
398+
// is atomic.
399+
private nonisolated(unsafe) weak var handler: (any MessageHandler)?
396400

397401
init(_ handler: any MessageHandler) {
398402
self.handler = handler
@@ -405,7 +409,7 @@ private class WeakMessageHandler: MessageHandler {
405409
func handle<Request: RequestType>(
406410
_ params: Request,
407411
id: LanguageServerProtocol.RequestID,
408-
reply: @escaping (LanguageServerProtocol.LSPResult<Request.Response>) -> Void
412+
reply: @Sendable @escaping (LanguageServerProtocol.LSPResult<Request.Response>) -> Void
409413
) {
410414
guard let handler = handler else {
411415
reply(.failure(.unknown("Handler has been deallocated")))

0 commit comments

Comments
 (0)