diff --git a/Sources/SKTestSupport/MultiFileTestProject.swift b/Sources/SKTestSupport/MultiFileTestProject.swift index 909d177e..7620da21 100644 --- a/Sources/SKTestSupport/MultiFileTestProject.swift +++ b/Sources/SKTestSupport/MultiFileTestProject.swift @@ -139,6 +139,7 @@ package class MultiFileTestProject { enableBackgroundIndexing: Bool = false, usePullDiagnostics: Bool = true, preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil, + postInitialization: (@Sendable (InitializeResult) -> Void)? = nil, testScratchDir overrideTestScratchDir: URL? = nil, cleanUp: (@Sendable () -> Void)? = nil, testName: String = #function @@ -156,6 +157,7 @@ package class MultiFileTestProject { enableBackgroundIndexing: enableBackgroundIndexing, workspaceFolders: workspaces(scratchDirectory), preInitialization: preInitialization, + postInitialization: postInitialization, cleanUp: { [scratchDirectory] in if cleanScratchDirectories { try? FileManager.default.removeItem(at: scratchDirectory) diff --git a/Sources/SKTestSupport/SwiftPMTestProject.swift b/Sources/SKTestSupport/SwiftPMTestProject.swift index 3d913510..da91c48e 100644 --- a/Sources/SKTestSupport/SwiftPMTestProject.swift +++ b/Sources/SKTestSupport/SwiftPMTestProject.swift @@ -190,6 +190,7 @@ package class SwiftPMTestProject: MultiFileTestProject { usePullDiagnostics: Bool = true, pollIndex: Bool = true, preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil, + postInitialization: (@Sendable (InitializeResult) -> Void)? = nil, cleanUp: (@Sendable () -> Void)? = nil, testName: String = #function ) async throws { @@ -231,6 +232,7 @@ package class SwiftPMTestProject: MultiFileTestProject { enableBackgroundIndexing: enableBackgroundIndexing, usePullDiagnostics: usePullDiagnostics, preInitialization: preInitialization, + postInitialization: postInitialization, cleanUp: cleanUp, testName: testName ) diff --git a/Sources/SKTestSupport/TestSourceKitLSPClient.swift b/Sources/SKTestSupport/TestSourceKitLSPClient.swift index a41041e2..a10ffa0e 100644 --- a/Sources/SKTestSupport/TestSourceKitLSPClient.swift +++ b/Sources/SKTestSupport/TestSourceKitLSPClient.swift @@ -142,6 +142,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable { enableBackgroundIndexing: Bool = false, workspaceFolders: [WorkspaceFolder]? = nil, preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil, + postInitialization: (@Sendable (InitializeResult) -> Void)? = nil, cleanUp: @Sendable @escaping () -> Void = {} ) async throws { var options = @@ -201,7 +202,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable { if initialize { let capabilities = capabilities try await withTimeout(defaultTimeoutDuration) { - _ = try await self.send( + let initializeResult = try await self.send( InitializeRequest( processId: nil, rootPath: nil, @@ -212,6 +213,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable { workspaceFolders: workspaceFolders ) ) + postInitialization?(initializeResult) } } } diff --git a/Tests/SourceKitLSPTests/CodeLensTests.swift b/Tests/SourceKitLSPTests/CodeLensTests.swift index cba757da..6c3b9bc2 100644 --- a/Tests/SourceKitLSPTests/CodeLensTests.swift +++ b/Tests/SourceKitLSPTests/CodeLensTests.swift @@ -16,53 +16,6 @@ import SKTestSupport import ToolchainRegistry import XCTest -fileprivate extension Toolchain { - #if compiler(>=6.4) - #warning( - "Once we require swift-play in the toolchain that's used to test SourceKit-LSP, we can just use `forTesting`" - ) - #endif - static var forTestingWithSwiftPlay: Toolchain { - get async throws { - let toolchain = try await unwrap(ToolchainRegistry.forTesting.default) - return Toolchain( - identifier: "\(toolchain.identifier)-swift-swift", - displayName: "\(toolchain.identifier) with swift-play", - path: toolchain.path, - clang: toolchain.clang, - swift: toolchain.swift, - swiftc: toolchain.swiftc, - swiftPlay: URL(fileURLWithPath: "/dummy/usr/bin/swift-play"), - clangd: toolchain.clangd, - sourcekitd: toolchain.sourcekitd, - sourceKitClientPlugin: toolchain.sourceKitClientPlugin, - sourceKitServicePlugin: toolchain.sourceKitServicePlugin, - libIndexStore: toolchain.libIndexStore - ) - } - } - - static var forTestingWithoutSwiftPlay: Toolchain { - get async throws { - let toolchain = try await unwrap(ToolchainRegistry.forTesting.default) - return Toolchain( - identifier: "\(toolchain.identifier)-no-swift-swift", - displayName: "\(toolchain.identifier) without swift-play", - path: toolchain.path, - clang: toolchain.clang, - swift: toolchain.swift, - swiftc: toolchain.swiftc, - swiftPlay: nil, - clangd: toolchain.clangd, - sourcekitd: toolchain.sourcekitd, - sourceKitClientPlugin: toolchain.sourceKitClientPlugin, - sourceKitServicePlugin: toolchain.sourceKitServicePlugin, - libIndexStore: toolchain.libIndexStore - ) - } - } -} - final class CodeLensTests: SourceKitLSPTestCase { func testNoLenses() async throws { diff --git a/Tests/SourceKitLSPTests/WorkspacePlaygroundDiscoveryTests.swift b/Tests/SourceKitLSPTests/WorkspacePlaygroundDiscoveryTests.swift new file mode 100644 index 00000000..256ec81b --- /dev/null +++ b/Tests/SourceKitLSPTests/WorkspacePlaygroundDiscoveryTests.swift @@ -0,0 +1,232 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 +// +//===----------------------------------------------------------------------===// + +@_spi(Testing) import BuildServerIntegration +import Foundation +@_spi(SourceKitLSP) import LanguageServerProtocol +import SKLogging +import SKTestSupport +import SemanticIndex +@_spi(Testing) import SourceKitLSP +import SwiftExtensions +import ToolchainRegistry +@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions +import XCTest + +import struct TSCBasic.AbsolutePath + +extension Toolchain { + #if compiler(>=6.4) + #warning( + "Once we require swift-play in the toolchain that's used to test SourceKit-LSP, we can just use `forTesting`" + ) + #endif + static var forTestingWithSwiftPlay: Toolchain { + get async throws { + let toolchain = try await unwrap(ToolchainRegistry.forTesting.default) + return Toolchain( + identifier: "\(toolchain.identifier)-swift-swift", + displayName: "\(toolchain.identifier) with swift-play", + path: toolchain.path, + clang: toolchain.clang, + swift: toolchain.swift, + swiftc: toolchain.swiftc, + swiftPlay: URL(fileURLWithPath: "/dummy/usr/bin/swift-play"), + clangd: toolchain.clangd, + sourcekitd: toolchain.sourcekitd, + sourceKitClientPlugin: toolchain.sourceKitClientPlugin, + sourceKitServicePlugin: toolchain.sourceKitServicePlugin, + libIndexStore: toolchain.libIndexStore + ) + } + } + + static var forTestingWithoutSwiftPlay: Toolchain { + get async throws { + let toolchain = try await unwrap(ToolchainRegistry.forTesting.default) + return Toolchain( + identifier: "\(toolchain.identifier)-no-swift-swift", + displayName: "\(toolchain.identifier) without swift-play", + path: toolchain.path, + clang: toolchain.clang, + swift: toolchain.swift, + swiftc: toolchain.swiftc, + swiftPlay: nil, + clangd: toolchain.clangd, + sourcekitd: toolchain.sourcekitd, + sourceKitClientPlugin: toolchain.sourceKitClientPlugin, + sourceKitServicePlugin: toolchain.sourceKitServicePlugin, + libIndexStore: toolchain.libIndexStore + ) + } + } +} + +final class WorkspacePlaygroundDiscoveryTests: SourceKitLSPTestCase { + + private var workspaceFiles: [RelativeFileLocation: String] = [ + "Sources/MyLibrary/Test.swift": """ + import Playgrounds + + public func foo() -> String { + "bar" + } + + 1️⃣#Playground("foo") { + print(foo()) + }2️⃣ + + 3️⃣#Playground { + print(foo()) + }4️⃣ + + public func bar(_ i: Int, _ j: Int) -> Int { + i + j + } + + 5️⃣#Playground("bar") { + var i = bar(1, 2) + i = i + 1 + print(i) + }6️⃣ + """, + "Sources/MyLibrary/TestNoImport.swift": """ + #Playground("fooNoImport") { + print(foo()) + } + + #Playground { + print(foo()) + } + + #Playground("barNoImport") { + var i = bar(1, 2) + i = i + 1 + print(i) + } + """, + "Sources/MyLibrary/bar.swift": """ + import Playgrounds + + 1️⃣#Playground("bar2") { + print(foo()) + }2️⃣ + """, + "Sources/MyApp/baz.swift": """ + import Playgrounds + + 1️⃣#Playground("baz") { + print("baz") + }2️⃣ + """, + ] + + private let packageManifestWithTestTarget = """ + let package = Package( + name: "MyLibrary", + targets: [.target(name: "MyLibrary"), .target(name: "MyApp")] + ) + """ + + func testWorkspacePlaygroundsScanned() async throws { + let toolchainRegistry = ToolchainRegistry(toolchains: [try await Toolchain.forTestingWithSwiftPlay]) + let project = try await SwiftPMTestProject( + files: workspaceFiles, + manifest: packageManifestWithTestTarget, + toolchainRegistry: toolchainRegistry + ) + + let response = try await project.testClient.send( + WorkspacePlaygroundsRequest() + ) + + let (testUri, testPositions) = try project.openDocument("Test.swift") + let (barUri, barPositions) = try project.openDocument("bar.swift") + let (bazUri, bazPositions) = try project.openDocument("baz.swift") + + // Notice sorted order + XCTAssertEqual( + response, + [ + Playground( + id: "MyApp/baz.swift:3:2", + label: "baz", + location: .init(uri: bazUri, range: bazPositions["1️⃣"]..(initialValue: nil) + let _ = try await SwiftPMTestProject( + files: workspaceFiles, + manifest: packageManifestWithTestTarget, + toolchainRegistry: toolchainRegistry, + postInitialization: { result in + initializeResult.withLock { + $0 = result + } + } + ) + + switch initializeResult.value?.capabilities.experimental { + case .dictionary(let dict): + XCTAssertNotEqual(dict[WorkspacePlaygroundsRequest.method], nil) + default: + XCTFail("Experminental capabilities is not a dictionary") + } + } + + func testWorkspacePlaygroundsCapabilityNoSwiftPlay() async throws { + let toolchainRegistry = ToolchainRegistry(toolchains: [try await Toolchain.forTestingWithoutSwiftPlay]) + let initializeResult = ThreadSafeBox(initialValue: nil) + let _ = try await SwiftPMTestProject( + files: workspaceFiles, + manifest: packageManifestWithTestTarget, + toolchainRegistry: toolchainRegistry, + postInitialization: { result in + initializeResult.withLock { + $0 = result + } + } + ) + + switch initializeResult.value?.capabilities.experimental { + case .dictionary(let dict): + XCTAssertEqual(dict[WorkspacePlaygroundsRequest.method], nil) + default: + XCTFail("Experminental capabilities is not a dictionary") + } + } +}