From 281c3e0e2e33df26318b7875a78fce1dc00da2f8 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Thu, 27 Jun 2024 11:49:00 -0400 Subject: [PATCH 1/5] Prepend module name to TestItem IDs It is possible to have two identically named suites in two different test targets. These were being erroniously rolled up in to the same parent TestItem. Disambiguate these TestItems by prepending the module name. This has the added benefit of making the TestItem IDs a fully qualified name that can be passed to `swift test`. The module name is pulled from the compiler arguments for the target. If no module name can be found we fall back to the `targetID` for the `ConfiguredTarget`. --- Sources/SKCore/BuildSystemManager.swift | 12 + Sources/SourceKitLSP/TestDiscovery.swift | 39 +- .../WorkspaceTestDiscoveryTests.swift | 360 ++++++++---------- 3 files changed, 207 insertions(+), 204 deletions(-) diff --git a/Sources/SKCore/BuildSystemManager.swift b/Sources/SKCore/BuildSystemManager.swift index 3f6fc7815..137226ed8 100644 --- a/Sources/SKCore/BuildSystemManager.swift +++ b/Sources/SKCore/BuildSystemManager.swift @@ -154,6 +154,18 @@ extension BuildSystemManager { .first } + /// Returns the target's module name as parsed from the `ConfiguredTarget`'s compiler arguments. + public func moduleName(for document: DocumentURI, in target: ConfiguredTarget) async -> String? { + guard let buildSettings = await buildSettings(for: document, in: target, language: .swift), + let moduleNameFlagIndex = buildSettings.compilerArguments.firstIndex(of: "-module-name") + else { + return nil + } + + let moduleNameIndex = buildSettings.compilerArguments.index(after: moduleNameFlagIndex) + return buildSettings.compilerArguments[moduleNameIndex] + } + /// Returns the build settings for `document` from `buildSystem`. /// /// Implementation detail of `buildSettings(for:language:)`. diff --git a/Sources/SourceKitLSP/TestDiscovery.swift b/Sources/SourceKitLSP/TestDiscovery.swift index dcab15fcf..1b5c2df4b 100644 --- a/Sources/SourceKitLSP/TestDiscovery.swift +++ b/Sources/SourceKitLSP/TestDiscovery.swift @@ -260,7 +260,7 @@ extension SourceKitLSPServer { func workspaceTests(_ req: WorkspaceTestsRequest) async throws -> [TestItem] { return await self.workspaces - .concurrentMap { await self.tests(in: $0) } + .concurrentMap { await self.tests(in: $0).prefixTestsWithModuleName(workspace: $0) } .flatMap { $0 } .sorted { $0.testItem.location < $1.testItem.location } .mergingTestsInExtensions() @@ -272,6 +272,7 @@ extension SourceKitLSPServer { languageService: LanguageService ) async throws -> [TestItem] { return try await documentTestsWithoutMergingExtensions(req, workspace: workspace, languageService: languageService) + .prefixTestsWithModuleName(workspace: workspace) .mergingTestsInExtensions() } @@ -476,6 +477,42 @@ fileprivate extension Array { } return result } + + func prefixTestsWithModuleName(workspace: Workspace) async -> Self { + return await self.asyncMap({ + // If the module name can't be determined we return the test item without a prefixed id. + guard let moduleName = await self.moduleName(from: workspace, for: $0.testItem.location.uri) else { + return $0 + } + var newTest = $0.testItem + newTest.id = "\(moduleName).\(newTest.id)" + newTest.children = await prefixTestsWithModuleName(workspace: workspace, newTest.children) + return AnnotatedTestItem(testItem: newTest, isExtension: $0.isExtension) + }) + } + + private func prefixTestsWithModuleName(workspace: Workspace, _ tests: [TestItem]) async -> [TestItem] { + return await tests.asyncMap({ + guard let moduleName = await self.moduleName(from: workspace, for: $0.location.uri) else { + return $0 + } + + var newTest = $0 + newTest.id = "\(moduleName).\(newTest.id)" + newTest.children = await prefixTestsWithModuleName(workspace: workspace, newTest.children) + return newTest + }) + } + + private func moduleName(from workspace: Workspace, for uri: DocumentURI) async -> String? { + guard let configuredTarget = await workspace.buildSystemManager.canonicalConfiguredTarget(for: uri) else { + return nil + } + // If for whatever reason we can't get a module name from the build system, fall back + // to using the targetID as this would be used when there is no command line arguments + // to define the module name as something other than the targetID. + return await workspace.buildSystemManager.moduleName(for: uri, in: configuredTarget) ?? configuredTarget.targetID + } } extension SwiftLanguageService { diff --git a/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift b/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift index 9cf2911e4..13afe2872 100644 --- a/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift +++ b/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift @@ -50,29 +50,22 @@ final class WorkspaceTestDiscoveryTests: XCTestCase { tests, [ TestItem( - id: "MyTests", + id: "MyLibraryTests.MyTests", label: "MyTests", - disabled: false, - style: TestStyle.xcTest, location: Location( uri: try project.uri(for: "MyTests.swift"), range: Range(try project.position(of: "1️⃣", in: "MyTests.swift")) ), children: [ TestItem( - id: "MyTests/testMyLibrary()", + id: "MyLibraryTests.MyTests/testMyLibrary()", label: "testMyLibrary()", - disabled: false, - style: TestStyle.xcTest, location: Location( uri: try project.uri(for: "MyTests.swift"), range: Range(try project.position(of: "2️⃣", in: "MyTests.swift")) - ), - children: [], - tags: [] + ) ) - ], - tags: [] + ] ) ] ) @@ -99,23 +92,16 @@ final class WorkspaceTestDiscoveryTests: XCTestCase { tests, [ TestItem( - id: "MyTests", + id: "MyLibraryTests.MyTests", label: "MyTests", - disabled: false, - style: TestStyle.xcTest, location: try project.location(from: "1️⃣", to: "4️⃣", in: "MyTests.swift"), children: [ TestItem( - id: "MyTests/testMyLibrary()", + id: "MyLibraryTests.MyTests/testMyLibrary()", label: "testMyLibrary()", - disabled: false, - style: TestStyle.xcTest, - location: try project.location(from: "2️⃣", to: "3️⃣", in: "MyTests.swift"), - children: [], - tags: [] + location: try project.location(from: "2️⃣", to: "3️⃣", in: "MyTests.swift") ) - ], - tags: [] + ] ) ] ) @@ -149,29 +135,22 @@ final class WorkspaceTestDiscoveryTests: XCTestCase { tests, [ TestItem( - id: "MyTests", + id: "MyLibraryTests.MyTests", label: "MyTests", - disabled: false, - style: TestStyle.xcTest, location: Location( uri: myTestsUri, range: Range(try project.position(of: "1️⃣", in: "MyTests.swift")) ), children: [ TestItem( - id: "MyTests/testMyLibrary()", + id: "MyLibraryTests.MyTests/testMyLibrary()", label: "testMyLibrary()", - disabled: false, - style: TestStyle.xcTest, location: Location( uri: myTestsUri, range: Range(try project.position(of: "2️⃣", in: "MyTests.swift")) - ), - children: [], - tags: [] + ) ) - ], - tags: [] + ] ) ] ) @@ -198,29 +177,22 @@ final class WorkspaceTestDiscoveryTests: XCTestCase { testsAfterDocumentChanged, [ TestItem( - id: "NotQuiteTests", + id: "MyLibraryTests.NotQuiteTests", label: "NotQuiteTests", - disabled: false, - style: TestStyle.xcTest, location: Location( uri: myTestsUri, range: newFilePositions["3️⃣"].. Date: Fri, 28 Jun 2024 09:38:59 -0400 Subject: [PATCH 2/5] Parse module name for obj-c targets Parse the obj-c module name for targets by looking at the `-fmodule-name` flag. --- Sources/SKCore/BuildSystemManager.swift | 28 ++++++++++-- Sources/SourceKitLSP/TestDiscovery.swift | 44 +++++++------------ Sources/SwiftExtensions/Array+Safe.swift | 19 ++++++++ .../WorkspaceTestDiscoveryTests.swift | 8 ++-- 4 files changed, 63 insertions(+), 36 deletions(-) create mode 100644 Sources/SwiftExtensions/Array+Safe.swift diff --git a/Sources/SKCore/BuildSystemManager.swift b/Sources/SKCore/BuildSystemManager.swift index 137226ed8..92a824fad 100644 --- a/Sources/SKCore/BuildSystemManager.swift +++ b/Sources/SKCore/BuildSystemManager.swift @@ -14,6 +14,7 @@ import BuildServerProtocol import Dispatch import LSPLogging import LanguageServerProtocol +import SwiftExtensions import struct TSCBasic.AbsolutePath @@ -156,14 +157,33 @@ extension BuildSystemManager { /// Returns the target's module name as parsed from the `ConfiguredTarget`'s compiler arguments. public func moduleName(for document: DocumentURI, in target: ConfiguredTarget) async -> String? { - guard let buildSettings = await buildSettings(for: document, in: target, language: .swift), - let moduleNameFlagIndex = buildSettings.compilerArguments.firstIndex(of: "-module-name") + guard let language = await self.defaultLanguage(for: document), + let buildSettings = await buildSettings(for: document, in: target, language: language) else { return nil } - let moduleNameIndex = buildSettings.compilerArguments.index(after: moduleNameFlagIndex) - return buildSettings.compilerArguments[moduleNameIndex] + switch language { + case .swift: + // Module name is specified in the form -module-name MyLibrary + guard let moduleNameFlagIndex = buildSettings.compilerArguments.firstIndex(of: "-module-name") else { + return nil + } + return buildSettings.compilerArguments[safe: moduleNameFlagIndex + 1] + case .objective_c: + // Specified in the form -fmodule-name=MyLibrary + guard + let moduleNameArgument = buildSettings.compilerArguments.first(where: { + $0.starts(with: "-fmodule-name=") + }), + let moduleName = moduleNameArgument.split(separator: "=").last + else { + return nil + } + return String(moduleName) + default: + return nil + } } /// Returns the build settings for `document` from `buildSystem`. diff --git a/Sources/SourceKitLSP/TestDiscovery.swift b/Sources/SourceKitLSP/TestDiscovery.swift index 1b5c2df4b..5cda9c633 100644 --- a/Sources/SourceKitLSP/TestDiscovery.swift +++ b/Sources/SourceKitLSP/TestDiscovery.swift @@ -480,38 +480,26 @@ fileprivate extension Array { func prefixTestsWithModuleName(workspace: Workspace) async -> Self { return await self.asyncMap({ - // If the module name can't be determined we return the test item without a prefixed id. - guard let moduleName = await self.moduleName(from: workspace, for: $0.testItem.location.uri) else { - return $0 - } - var newTest = $0.testItem - newTest.id = "\(moduleName).\(newTest.id)" - newTest.children = await prefixTestsWithModuleName(workspace: workspace, newTest.children) - return AnnotatedTestItem(testItem: newTest, isExtension: $0.isExtension) - }) - } - - private func prefixTestsWithModuleName(workspace: Workspace, _ tests: [TestItem]) async -> [TestItem] { - return await tests.asyncMap({ - guard let moduleName = await self.moduleName(from: workspace, for: $0.location.uri) else { - return $0 - } - - var newTest = $0 - newTest.id = "\(moduleName).\(newTest.id)" - newTest.children = await prefixTestsWithModuleName(workspace: workspace, newTest.children) - return newTest + return AnnotatedTestItem( + testItem: await $0.testItem.prefixIDWithModuleName(workspace: workspace), + isExtension: $0.isExtension + ) }) } +} - private func moduleName(from workspace: Workspace, for uri: DocumentURI) async -> String? { - guard let configuredTarget = await workspace.buildSystemManager.canonicalConfiguredTarget(for: uri) else { - return nil +extension TestItem { + fileprivate func prefixIDWithModuleName(workspace: Workspace) async -> TestItem { + guard let configuredTarget = await workspace.buildSystemManager.canonicalConfiguredTarget(for: self.location.uri), + let moduleName = await workspace.buildSystemManager.moduleName(for: self.location.uri, in: configuredTarget) + else { + return self } - // If for whatever reason we can't get a module name from the build system, fall back - // to using the targetID as this would be used when there is no command line arguments - // to define the module name as something other than the targetID. - return await workspace.buildSystemManager.moduleName(for: uri, in: configuredTarget) ?? configuredTarget.targetID + + var newTest = self + newTest.id = "\(moduleName).\(newTest.id)" + newTest.children = await newTest.children.asyncMap({ await $0.prefixIDWithModuleName(workspace: workspace) }) + return newTest } } diff --git a/Sources/SwiftExtensions/Array+Safe.swift b/Sources/SwiftExtensions/Array+Safe.swift new file mode 100644 index 000000000..974607382 --- /dev/null +++ b/Sources/SwiftExtensions/Array+Safe.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2018 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 +// +//===----------------------------------------------------------------------===// + +extension Array { + /// Returns the element at the specified index if it is within the Array's + /// bounds, otherwise `nil`. + public subscript(safe index: Index) -> Element? { + return index > 0 && index < count ? self[index] : nil + } +} diff --git a/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift b/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift index 13afe2872..226ffbe32 100644 --- a/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift +++ b/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift @@ -655,12 +655,12 @@ final class WorkspaceTestDiscoveryTests: XCTestCase { tests, [ TestItem( - id: "dummy.MyTests", + id: "MyTests", label: "MyTests", location: Location(uri: project.fileURI, range: Range(project.positions["1️⃣"])), children: [ TestItem( - id: "dummy.MyTests/testSomething()", + id: "MyTests/testSomething()", label: "testSomething()", location: Location(uri: project.fileURI, range: Range(project.positions["2️⃣"])) ) @@ -712,12 +712,12 @@ final class WorkspaceTestDiscoveryTests: XCTestCase { tests, [ TestItem( - id: "dummy.MyTests", + id: "MyTests", label: "MyTests", location: try project.location(from: "1️⃣", to: "4️⃣", in: "MyTests.swift"), children: [ TestItem( - id: "dummy.MyTests/testSomething()", + id: "MyTests/testSomething()", label: "testSomething()", location: try project.location(from: "2️⃣", to: "3️⃣", in: "MyTests.swift") ) From bc86ff8e6a8c4ceb394b9064b094cb0db3b77b7d Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Fri, 28 Jun 2024 12:45:14 -0400 Subject: [PATCH 3/5] Address comments --- Sources/SwiftExtensions/Array+Safe.swift | 2 +- Sources/SwiftExtensions/CMakeLists.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftExtensions/Array+Safe.swift b/Sources/SwiftExtensions/Array+Safe.swift index 974607382..09435e86f 100644 --- a/Sources/SwiftExtensions/Array+Safe.swift +++ b/Sources/SwiftExtensions/Array+Safe.swift @@ -14,6 +14,6 @@ extension Array { /// Returns the element at the specified index if it is within the Array's /// bounds, otherwise `nil`. public subscript(safe index: Index) -> Element? { - return index > 0 && index < count ? self[index] : nil + return index >= 0 && index < count ? self[index] : nil } } diff --git a/Sources/SwiftExtensions/CMakeLists.txt b/Sources/SwiftExtensions/CMakeLists.txt index 7b024d4d1..f459a0b16 100644 --- a/Sources/SwiftExtensions/CMakeLists.txt +++ b/Sources/SwiftExtensions/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(SwiftExtensions STATIC + Array+Safe.swift AsyncQueue.swift AsyncUtils.swift Collection+Only.swift From 14b81cfe6cfde2ff5b0fb192b136e6e7347b9a00 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Fri, 28 Jun 2024 14:42:00 -0400 Subject: [PATCH 4/5] Fixup DocumentTestDiscoveryTests, make TestStyle an enum --- .../SupportTypes/TestItem.swift | 13 +- .../Swift/SwiftTestingScanner.swift | 4 +- .../Swift/SyntacticSwiftXCTestScanner.swift | 4 +- Sources/SourceKitLSP/TestDiscovery.swift | 11 +- .../DocumentTestDiscoveryTests.swift | 351 +++++------------- .../WorkspaceTestDiscoveryTests.swift | 26 +- 6 files changed, 135 insertions(+), 274 deletions(-) diff --git a/Sources/LanguageServerProtocol/SupportTypes/TestItem.swift b/Sources/LanguageServerProtocol/SupportTypes/TestItem.swift index a764dabc6..107bc37d0 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/TestItem.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/TestItem.swift @@ -19,6 +19,15 @@ public struct TestTag: Codable, Equatable, Sendable { } } +/// An enum representing the different styles of tests that can be found in a document. +public enum TestStyle: String, Codable, Sendable { + /// XCTest: https://developer.apple.com/documentation/xctest + case xcTest = "XCTest" + + /// Swift Testing: https://swiftpackageindex.com/apple/swift-testing/main/documentation/testing + case swiftTesting = "swift-testing" +} + /// A test item that can be shown an a client's test explorer or used to identify tests alongside a source file. /// /// A `TestItem` can represent either a test suite or a test itself, since they both have similar capabilities. @@ -43,7 +52,7 @@ public struct TestItem: ResponseType, Equatable { public var disabled: Bool /// The type of test, eg. the testing framework that was used to declare the test. - public var style: String + public var style: TestStyle /// The location of the test item in the source code. public var location: Location @@ -62,7 +71,7 @@ public struct TestItem: ResponseType, Equatable { description: String? = nil, sortText: String? = nil, disabled: Bool, - style: String, + style: TestStyle, location: Location, children: [TestItem], tags: [TestTag] diff --git a/Sources/SourceKitLSP/Swift/SwiftTestingScanner.swift b/Sources/SourceKitLSP/Swift/SwiftTestingScanner.swift index c2d33835c..00d48947d 100644 --- a/Sources/SourceKitLSP/Swift/SwiftTestingScanner.swift +++ b/Sources/SourceKitLSP/Swift/SwiftTestingScanner.swift @@ -257,7 +257,7 @@ final class SyntacticSwiftTestingTestScanner: SyntaxVisitor { id: (parentTypeNames + typeNames).joined(separator: "/"), label: attributeData?.displayName ?? typeNames.last!, disabled: (attributeData?.isDisabled ?? false) || allTestsDisabled, - style: TestStyle.swiftTesting, + style: .swiftTesting, location: Location(uri: snapshot.uri, range: range), children: memberScanner.result.map(\.testItem), tags: attributeData?.tags.map(TestTag.init(id:)) ?? [] @@ -330,7 +330,7 @@ final class SyntacticSwiftTestingTestScanner: SyntaxVisitor { id: (parentTypeNames + [name]).joined(separator: "/"), label: attributeData.displayName ?? name, disabled: attributeData.isDisabled || allTestsDisabled, - style: TestStyle.swiftTesting, + style: .swiftTesting, location: Location(uri: snapshot.uri, range: range), children: [], tags: attributeData.tags.map(TestTag.init(id:)) diff --git a/Sources/SourceKitLSP/Swift/SyntacticSwiftXCTestScanner.swift b/Sources/SourceKitLSP/Swift/SyntacticSwiftXCTestScanner.swift index bb5363f62..6314e9544 100644 --- a/Sources/SourceKitLSP/Swift/SyntacticSwiftXCTestScanner.swift +++ b/Sources/SourceKitLSP/Swift/SyntacticSwiftXCTestScanner.swift @@ -72,7 +72,7 @@ final class SyntacticSwiftXCTestScanner: SyntaxVisitor { id: "\(containerName)/\(function.name.text)()", label: "\(function.name.text)()", disabled: false, - style: TestStyle.xcTest, + style: .xcTest, location: Location(uri: snapshot.uri, range: range), children: [], tags: [] @@ -106,7 +106,7 @@ final class SyntacticSwiftXCTestScanner: SyntaxVisitor { id: node.name.text, label: node.name.text, disabled: false, - style: TestStyle.xcTest, + style: .xcTest, location: Location(uri: snapshot.uri, range: range), children: testMethods, tags: [] diff --git a/Sources/SourceKitLSP/TestDiscovery.swift b/Sources/SourceKitLSP/TestDiscovery.swift index 5cda9c633..f4bc6cc3c 100644 --- a/Sources/SourceKitLSP/TestDiscovery.swift +++ b/Sources/SourceKitLSP/TestDiscovery.swift @@ -16,11 +16,6 @@ import LanguageServerProtocol import SemanticIndex import SwiftSyntax -public enum TestStyle { - public static let xcTest = "XCTest" - public static let swiftTesting = "swift-testing" -} - fileprivate extension SymbolOccurrence { /// Assuming that this is a symbol occurrence returned by the index, return whether it can constitute the definition /// of a test case. @@ -155,7 +150,7 @@ extension SourceKitLSPServer { id: id, label: testSymbolOccurrence.symbol.name, disabled: false, - style: TestStyle.xcTest, + style: .xcTest, location: location, children: children.map(\.testItem), tags: [] @@ -221,7 +216,7 @@ extension SourceKitLSPServer { testsFromSyntacticIndex .compactMap { (item) -> AnnotatedTestItem? in let testItem = item.testItem - if testItem.style == TestStyle.swiftTesting { + if testItem.style == .swiftTesting { // Swift-testing tests aren't part of the semantic index. Always include them. return item } @@ -297,7 +292,7 @@ extension SourceKitLSPServer { if let index = workspace.index(checkedFor: indexCheckLevel) { var syntacticSwiftTestingTests: [AnnotatedTestItem] { - syntacticTests?.filter { $0.testItem.style == TestStyle.swiftTesting } ?? [] + syntacticTests?.filter { $0.testItem.style == .swiftTesting } ?? [] } let testSymbols = diff --git a/Tests/SourceKitLSPTests/DocumentTestDiscoveryTests.swift b/Tests/SourceKitLSPTests/DocumentTestDiscoveryTests.swift index ce50b1dd7..f7f5b187c 100644 --- a/Tests/SourceKitLSPTests/DocumentTestDiscoveryTests.swift +++ b/Tests/SourceKitLSPTests/DocumentTestDiscoveryTests.swift @@ -53,23 +53,16 @@ final class DocumentTestDiscoveryTests: XCTestCase { tests, [ TestItem( - id: "MyTests", + id: "MyLibraryTests.MyTests", label: "MyTests", - disabled: false, - style: TestStyle.xcTest, location: Location(uri: uri, range: positions["1️⃣"].. Date: Mon, 1 Jul 2024 09:31:00 -0400 Subject: [PATCH 5/5] Revert TestStyle enum changes --- .../SupportTypes/TestItem.swift | 13 +-- .../Swift/SwiftTestingScanner.swift | 4 +- .../Swift/SyntacticSwiftXCTestScanner.swift | 4 +- Sources/SourceKitLSP/TestDiscovery.swift | 11 ++- .../DocumentTestDiscoveryTests.swift | 90 +++++++++---------- .../WorkspaceTestDiscoveryTests.swift | 18 ++-- 6 files changed, 67 insertions(+), 73 deletions(-) diff --git a/Sources/LanguageServerProtocol/SupportTypes/TestItem.swift b/Sources/LanguageServerProtocol/SupportTypes/TestItem.swift index 107bc37d0..a764dabc6 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/TestItem.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/TestItem.swift @@ -19,15 +19,6 @@ public struct TestTag: Codable, Equatable, Sendable { } } -/// An enum representing the different styles of tests that can be found in a document. -public enum TestStyle: String, Codable, Sendable { - /// XCTest: https://developer.apple.com/documentation/xctest - case xcTest = "XCTest" - - /// Swift Testing: https://swiftpackageindex.com/apple/swift-testing/main/documentation/testing - case swiftTesting = "swift-testing" -} - /// A test item that can be shown an a client's test explorer or used to identify tests alongside a source file. /// /// A `TestItem` can represent either a test suite or a test itself, since they both have similar capabilities. @@ -52,7 +43,7 @@ public struct TestItem: ResponseType, Equatable { public var disabled: Bool /// The type of test, eg. the testing framework that was used to declare the test. - public var style: TestStyle + public var style: String /// The location of the test item in the source code. public var location: Location @@ -71,7 +62,7 @@ public struct TestItem: ResponseType, Equatable { description: String? = nil, sortText: String? = nil, disabled: Bool, - style: TestStyle, + style: String, location: Location, children: [TestItem], tags: [TestTag] diff --git a/Sources/SourceKitLSP/Swift/SwiftTestingScanner.swift b/Sources/SourceKitLSP/Swift/SwiftTestingScanner.swift index 00d48947d..c2d33835c 100644 --- a/Sources/SourceKitLSP/Swift/SwiftTestingScanner.swift +++ b/Sources/SourceKitLSP/Swift/SwiftTestingScanner.swift @@ -257,7 +257,7 @@ final class SyntacticSwiftTestingTestScanner: SyntaxVisitor { id: (parentTypeNames + typeNames).joined(separator: "/"), label: attributeData?.displayName ?? typeNames.last!, disabled: (attributeData?.isDisabled ?? false) || allTestsDisabled, - style: .swiftTesting, + style: TestStyle.swiftTesting, location: Location(uri: snapshot.uri, range: range), children: memberScanner.result.map(\.testItem), tags: attributeData?.tags.map(TestTag.init(id:)) ?? [] @@ -330,7 +330,7 @@ final class SyntacticSwiftTestingTestScanner: SyntaxVisitor { id: (parentTypeNames + [name]).joined(separator: "/"), label: attributeData.displayName ?? name, disabled: attributeData.isDisabled || allTestsDisabled, - style: .swiftTesting, + style: TestStyle.swiftTesting, location: Location(uri: snapshot.uri, range: range), children: [], tags: attributeData.tags.map(TestTag.init(id:)) diff --git a/Sources/SourceKitLSP/Swift/SyntacticSwiftXCTestScanner.swift b/Sources/SourceKitLSP/Swift/SyntacticSwiftXCTestScanner.swift index 6314e9544..bb5363f62 100644 --- a/Sources/SourceKitLSP/Swift/SyntacticSwiftXCTestScanner.swift +++ b/Sources/SourceKitLSP/Swift/SyntacticSwiftXCTestScanner.swift @@ -72,7 +72,7 @@ final class SyntacticSwiftXCTestScanner: SyntaxVisitor { id: "\(containerName)/\(function.name.text)()", label: "\(function.name.text)()", disabled: false, - style: .xcTest, + style: TestStyle.xcTest, location: Location(uri: snapshot.uri, range: range), children: [], tags: [] @@ -106,7 +106,7 @@ final class SyntacticSwiftXCTestScanner: SyntaxVisitor { id: node.name.text, label: node.name.text, disabled: false, - style: .xcTest, + style: TestStyle.xcTest, location: Location(uri: snapshot.uri, range: range), children: testMethods, tags: [] diff --git a/Sources/SourceKitLSP/TestDiscovery.swift b/Sources/SourceKitLSP/TestDiscovery.swift index f4bc6cc3c..5cda9c633 100644 --- a/Sources/SourceKitLSP/TestDiscovery.swift +++ b/Sources/SourceKitLSP/TestDiscovery.swift @@ -16,6 +16,11 @@ import LanguageServerProtocol import SemanticIndex import SwiftSyntax +public enum TestStyle { + public static let xcTest = "XCTest" + public static let swiftTesting = "swift-testing" +} + fileprivate extension SymbolOccurrence { /// Assuming that this is a symbol occurrence returned by the index, return whether it can constitute the definition /// of a test case. @@ -150,7 +155,7 @@ extension SourceKitLSPServer { id: id, label: testSymbolOccurrence.symbol.name, disabled: false, - style: .xcTest, + style: TestStyle.xcTest, location: location, children: children.map(\.testItem), tags: [] @@ -216,7 +221,7 @@ extension SourceKitLSPServer { testsFromSyntacticIndex .compactMap { (item) -> AnnotatedTestItem? in let testItem = item.testItem - if testItem.style == .swiftTesting { + if testItem.style == TestStyle.swiftTesting { // Swift-testing tests aren't part of the semantic index. Always include them. return item } @@ -292,7 +297,7 @@ extension SourceKitLSPServer { if let index = workspace.index(checkedFor: indexCheckLevel) { var syntacticSwiftTestingTests: [AnnotatedTestItem] { - syntacticTests?.filter { $0.testItem.style == .swiftTesting } ?? [] + syntacticTests?.filter { $0.testItem.style == TestStyle.swiftTesting } ?? [] } let testSymbols = diff --git a/Tests/SourceKitLSPTests/DocumentTestDiscoveryTests.swift b/Tests/SourceKitLSPTests/DocumentTestDiscoveryTests.swift index f7f5b187c..199466337 100644 --- a/Tests/SourceKitLSPTests/DocumentTestDiscoveryTests.swift +++ b/Tests/SourceKitLSPTests/DocumentTestDiscoveryTests.swift @@ -191,13 +191,13 @@ final class DocumentTestDiscoveryTests: XCTestCase { TestItem( id: "MyTests", label: "MyTests", - style: .swiftTesting, + style: TestStyle.swiftTesting, location: Location(uri: uri, range: positions["1️⃣"]..