Skip to content

Commit 1abfb65

Browse files
committed
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`.
1 parent c185099 commit 1abfb65

File tree

6 files changed

+291
-405
lines changed

6 files changed

+291
-405
lines changed

Sources/SKCore/BuildSystemManager.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import BuildServerProtocol
1414
import Dispatch
1515
import LSPLogging
1616
import LanguageServerProtocol
17+
import SwiftExtensions
1718

1819
import struct TSCBasic.AbsolutePath
1920

@@ -154,6 +155,37 @@ extension BuildSystemManager {
154155
.first
155156
}
156157

158+
/// Returns the target's module name as parsed from the `ConfiguredTarget`'s compiler arguments.
159+
public func moduleName(for document: DocumentURI, in target: ConfiguredTarget) async -> String? {
160+
guard let language = await self.defaultLanguage(for: document),
161+
let buildSettings = await buildSettings(for: document, in: target, language: language)
162+
else {
163+
return nil
164+
}
165+
166+
switch language {
167+
case .swift:
168+
// Module name is specified in the form -module-name MyLibrary
169+
guard let moduleNameFlagIndex = buildSettings.compilerArguments.firstIndex(of: "-module-name") else {
170+
return nil
171+
}
172+
return buildSettings.compilerArguments[safe: moduleNameFlagIndex + 1]
173+
case .objective_c:
174+
// Specified in the form -fmodule-name=MyLibrary
175+
guard
176+
let moduleNameArgument = buildSettings.compilerArguments.first(where: {
177+
$0.starts(with: "-fmodule-name=")
178+
}),
179+
let moduleName = moduleNameArgument.split(separator: "=").last
180+
else {
181+
return nil
182+
}
183+
return String(moduleName)
184+
default:
185+
return nil
186+
}
187+
}
188+
157189
/// Returns the build settings for `document` from `buildSystem`.
158190
///
159191
/// Implementation detail of `buildSettings(for:language:)`.

Sources/SourceKitLSP/TestDiscovery.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ extension SourceKitLSPServer {
260260

261261
func workspaceTests(_ req: WorkspaceTestsRequest) async throws -> [TestItem] {
262262
return await self.workspaces
263-
.concurrentMap { await self.tests(in: $0) }
263+
.concurrentMap { await self.tests(in: $0).prefixTestsWithModuleName(workspace: $0) }
264264
.flatMap { $0 }
265265
.sorted { $0.testItem.location < $1.testItem.location }
266266
.mergingTestsInExtensions()
@@ -272,6 +272,7 @@ extension SourceKitLSPServer {
272272
languageService: LanguageService
273273
) async throws -> [TestItem] {
274274
return try await documentTestsWithoutMergingExtensions(req, workspace: workspace, languageService: languageService)
275+
.prefixTestsWithModuleName(workspace: workspace)
275276
.mergingTestsInExtensions()
276277
}
277278

@@ -476,6 +477,30 @@ fileprivate extension Array<AnnotatedTestItem> {
476477
}
477478
return result
478479
}
480+
481+
func prefixTestsWithModuleName(workspace: Workspace) async -> Self {
482+
return await self.asyncMap({
483+
return AnnotatedTestItem(
484+
testItem: await $0.testItem.prefixIDWithModuleName(workspace: workspace),
485+
isExtension: $0.isExtension
486+
)
487+
})
488+
}
489+
}
490+
491+
extension TestItem {
492+
fileprivate func prefixIDWithModuleName(workspace: Workspace) async -> TestItem {
493+
guard let configuredTarget = await workspace.buildSystemManager.canonicalConfiguredTarget(for: self.location.uri),
494+
let moduleName = await workspace.buildSystemManager.moduleName(for: self.location.uri, in: configuredTarget)
495+
else {
496+
return self
497+
}
498+
499+
var newTest = self
500+
newTest.id = "\(moduleName).\(newTest.id)"
501+
newTest.children = await newTest.children.asyncMap({ await $0.prefixIDWithModuleName(workspace: workspace) })
502+
return newTest
503+
}
479504
}
480505

481506
extension SwiftLanguageService {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
extension Array {
14+
/// Returns the element at the specified index if it is within the Array's
15+
/// bounds, otherwise `nil`.
16+
public subscript(safe index: Index) -> Element? {
17+
return index >= 0 && index < count ? self[index] : nil
18+
}
19+
}

Sources/SwiftExtensions/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
add_library(SwiftExtensions STATIC
3+
Array+Safe.swift
34
AsyncQueue.swift
45
AsyncUtils.swift
56
Collection+Only.swift

0 commit comments

Comments
 (0)