Skip to content

Organize discovered tests in a runner plan based on the module they're implemented in #1131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Sources/Testing/Running/Runner.Plan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ extension Runner {
stepGraph.compactMap(\.value).sorted { $0.test.sourceLocation < $1.test.sourceLocation }
}

/// The tests this runner plan contains.
public var tests: some Collection<Test> {
steps.lazy.map(\.test)
}

/// Initialize an instance of this type with the specified graph of test
/// plan steps.
///
Expand Down
2 changes: 1 addition & 1 deletion Sources/Testing/Running/Runner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public struct Runner: Sendable {
public var plan: Plan

/// The set of tests this runner will run.
public var tests: [Test] { plan.steps.map(\.test) }
public var tests: [Test] { .init(plan.tests) }

/// The runner's configuration.
public var configuration: Configuration
Expand Down
12 changes: 11 additions & 1 deletion Sources/Testing/Test.ID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@

extension Test: Identifiable {
public struct ID: Sendable, Equatable, Hashable {
/// The name of the module containing the corresponding test.
/// The name of the module in which this test is defined.
///
/// This may be different than the name of the module this test's containing
/// suite type is declared in. For example, if the test is defined in an
/// extension of a type declared in an imported module, the value of this
/// property on the ID of the containing suite will be the name of the
/// imported module, but the value of this property for the ID of the test
/// within that extension will be the name of the module which declares the
/// extension.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Danger! This is potentially ambiguous. We may want to instead describe such a test in terms of both modules.

public var moduleName: String

/// The fully qualified name components (other than the module name) used to
Expand Down Expand Up @@ -123,6 +131,8 @@ extension Test: Identifiable {
var result = containingTypeInfo.map(ID.init)
?? ID(moduleName: sourceLocation.moduleName, nameComponents: [], sourceLocation: nil)

result.moduleName = sourceLocation.moduleName
Copy link
Contributor

@grynspan grynspan May 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we store this value in a separate field (if it differs from the inferred module)? extensionModuleName: String? perhaps.


if !isSuite {
result.nameComponents.append(name)
result.sourceLocation = sourceLocation
Expand Down
46 changes: 46 additions & 0 deletions Tests/TestingTests/Test.IDTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 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 Swift project authors
//

@testable @_spi(ForToolsIntegrationOnly) import Testing

@Suite("Test.ID Tests")
struct Test_IDTests {
@Test func topmostSuiteInCurrentModule() async throws {
let plan = await Runner.Plan(selecting: SomeSuite.self)

let suiteID = try #require(plan.tests.first { $0.name == "SomeSuite" }?.id)
#expect(suiteID.moduleName == .currentModuleName())

let functionID = try #require(plan.tests.first { $0.name == "example()" }?.id)
#expect(functionID.moduleName == .currentModuleName())
}

@Test func topmostSuiteInDifferentModule() async throws {
let plan = await Runner.Plan(selecting: String.AnotherSuite.self)

let suiteID = try #require(plan.tests.first { $0.name == "AnotherSuite" }?.id)
#expect(suiteID.moduleName == .currentModuleName())

let functionID = try #require(plan.tests.first { $0.name == "example()" }?.id)
#expect(functionID.moduleName == .currentModuleName())
}
}

// MARK: - Fixtures

@Suite(.hidden) struct SomeSuite {
@Test func example() {}
}

extension String {
@Suite(.hidden) struct AnotherSuite {
@Test func example() {}
}
}
37 changes: 32 additions & 5 deletions Tests/TestingTests/TestSupport/TestingAdditions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,16 @@ func testFunction(named name: String, in containingType: Any.Type) async -> Test
///
/// - Parameters:
/// - containingType: The type containing the tests that should be run.
/// - fileID: The `#fileID` string whose module should be used to locate
/// the test suite to run. If `nil`, the module which declares
/// `containingType` is used. The default value is the file ID of the file
/// in which this method is called.
/// - configuration: The configuration to use for running.
///
/// Any tests defined within `containingType` are also run. If no test is found
/// representing that type, nothing is run.
func runTest(for containingType: Any.Type, configuration: Configuration = .init()) async {
let plan = await Runner.Plan(selecting: containingType, configuration: configuration)
func runTest(for containingType: Any.Type, inModuleOf fileID: String? = #fileID, configuration: Configuration = .init()) async {
let plan = await Runner.Plan(selecting: containingType, inModuleOf: fileID, configuration: configuration)
let runner = Runner(plan: plan, configuration: configuration)
await runner.run()
}
Expand Down Expand Up @@ -123,11 +127,20 @@ extension Runner.Plan {
///
/// - Parameters:
/// - containingType: The suite type this plan should select.
/// - fileID: The `#fileID` string whose module should be used to locate
/// the test suite to select. If `nil`, the module which declares
/// `containingType` is used. The default value is the file ID of the file
/// in which this initializer is called.
/// - configuration: The configuration to use for planning.
init(selecting containingType: Any.Type, configuration: Configuration = .init()) async {
init(selecting containingType: Any.Type, inModuleOf fileID: String? = #fileID, configuration: Configuration = .init()) async {
var testID = Test.ID(type: containingType)

if let fileID {
testID.moduleName = String(fileID[..<fileID.lastIndex(of: "/")!])
}

var configuration = configuration
let selection = [Test.ID(type: containingType)]
configuration.setTestFilter(toInclude: selection, includeHiddenTests: true)
configuration.setTestFilter(toInclude: [testID], includeHiddenTests: true)

await self.init(configuration: configuration)
}
Expand Down Expand Up @@ -409,3 +422,17 @@ extension SourceContext {
self.init(backtrace: .current(), sourceLocation: sourceLocation)
}
}

extension String {
/// The name of the module this method is called from.
///
/// - Parameters:
/// - fileID: The `#fileID` of the file in which this method is called,
/// which is used to determine the module name.
///
/// - Returns: A string containing the name of the module this method is
/// called from.
static func currentModuleName(from fileID: String = #fileID) -> String {
String(fileID[..<fileID.lastIndex(of: "/")!])
}
}