Skip to content
Open
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
3 changes: 2 additions & 1 deletion Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,8 @@ extension Test {
traits: traits,
sourceLocation: sourceLocation,
containingTypeInfo: typeInfo,
isSynthesized: true
isSynthesized: true,
isPolymorphic: false
)
case .function:
let parameters = test._parameters.map { parameters in
Expand Down
1 change: 1 addition & 0 deletions Sources/Testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ add_library(Testing
Support/Graph.swift
Support/JSON.swift
Support/Serializer.swift
Support/SubclassCache.swift
Support/VersionNumber.swift
Support/Versions.swift
Discovery+Macro.swift
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,27 @@ extension Test {
///
/// - Returns: The name of this test, suitable for display to the user.
func humanReadableName(withVerbosity verbosity: Int = 0) -> String {
switch displayName {
var result = switch displayName {
case let .some(displayName) where verbosity > 0:
#""\#(displayName)" (aka '\#(name)')"#
case let .some(displayName):
#""\#(displayName)""#
default:
name
}
if isPolymorphic, let clazz = containingTypeInfo {
let className = if verbosity > 0 {
clazz.fullyQualifiedName
} else {
clazz.unqualifiedName
}
if wasInherited {
result = "\(result) (inherited by '\(className)')"
} else {
result = "\(result) (implemented in '\(className)')"
}
}
return result
}
}

Expand Down
16 changes: 14 additions & 2 deletions Sources/Testing/Parameterization/TypeInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,25 @@ public struct TypeInfo: Sendable {
return nil
}

/// The described type, if available.
///
/// The value of this property is `nil` if the described type is not a class,
/// as well as under any conditions where ``type`` is `nil`.
var `class`: AnyClass? {
if let type {
// FIXME: casting `any (~).Type` to `AnyClass` warns that it always fails
return unsafeBitCast(type, to: Any.Type.self) as? AnyClass
}
return nil
}

/// Initialize an instance of this type with the specified names.
///
/// - Parameters:
/// - fullyQualifiedNameComponents: The fully-qualified name components of
/// the type.
/// the type.
/// - unqualifiedName: The unqualified name of the type. If `nil`, the last
/// string in `fullyQualifiedNameComponents` is used instead.
/// string in `fullyQualifiedNameComponents` is used instead.
/// - mangled: The mangled name of the type, if available.
init(fullyQualifiedNameComponents: [String], unqualifiedName: String? = nil, mangledName: String? = nil) {
let unqualifiedName = unqualifiedName ?? fullyQualifiedNameComponents.last ?? fullyQualifiedNameComponents.joined(separator: ".")
Expand Down
84 changes: 76 additions & 8 deletions Sources/Testing/Running/Runner.Plan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,78 @@ extension Runner.Plan {
// source location, so we use the source location of a close descendant
// test. We do this instead of falling back to some "unknown"
// placeholder in an attempt to preserve the correct sort ordering.
graph.value = Test(traits: [], sourceLocation: sourceLocation, containingTypeInfo: typeInfo, isSynthesized: true)
graph.value = Test(traits: [], sourceLocation: sourceLocation, containingTypeInfo: typeInfo, isSynthesized: true, isPolymorphic: false)
}
}

var sourceLocation: SourceLocation?
synthesizeSuites(in: &graph, sourceLocation: &sourceLocation)
}

/// Add copies of test functions in `@polymorphic` suites to all known
/// subclasses of said suites.
///
/// - Parameters:
/// - testGraph: The graph of tests to modify.
private static func _synthesizePolymorphicTests(in testGraph: inout Graph<String, Test?>) {
// First, recursively find all classes associated with polymorphic test
// suites (as determined at macro expansion time).
var subclassCache = SubclassCache(
testGraph
.compactMap { $0.value }
.filter(\.isPolymorphic)
.compactMap { $0.containingTypeInfo?.class }
)

// The set of all copied tests we created while recursing through the graph.
var testCopies = [Test]()

// Recursively walk through the graph looking for test functions that are
// associated with classes in the set we created above. Any such test
// functions are implicitly polymorphic themselves.
func makePolymorphicCopies(in testGraph: inout Graph<String, Test?>) {
if var test = testGraph.value.take() {
defer {
testGraph.value = test
}

// If this test is a test function and its type is marked polymorphic,
// mark the test function as polymorphic too and make copies of it to
// insert into the graph after recursion is complete.
if !test.isSuite,
let clazz = test.containingTypeInfo?.class,
subclassCache.contains(clazz) {
test.isPolymorphic = true
testCopies += subclassCache.subclasses(of: clazz).lazy
.map { subclass in
let subtypeInfo = TypeInfo(describing: subclass)
var testCopy = test
testCopy.containingTypeInfo = subtypeInfo
if testCopy.isSuite {
testCopy.name = subtypeInfo.unqualifiedName
}
testCopy.isSynthesized = true
testCopy.wasInherited = true
return testCopy
}
}
}

// Recurse into child nodes.
testGraph.children = testGraph.children.mapValues { child in
var child = child
makePolymorphicCopies(in: &child)
return child
}
}
makePolymorphicCopies(in: &testGraph)

// Insert the copied tests into the graph.
for testCopy in testCopies {
testGraph.insertValue(testCopy, at: testCopy.id.keyPathRepresentation)
}
}

/// Given an array of tests, synthesize any containing suites that are not
/// already represented in that array.
///
Expand Down Expand Up @@ -314,16 +378,11 @@ extension Runner.Plan {
Backtrace.flushThrownErrorCache()
}

// Convert the list of test into a graph of steps. The actions for these
// steps will all be .run() *unless* an error was thrown while examining
// them, in which case it will be .recordIssue().
// Convert the list of test into a graph of steps.
let runAction = _runAction
var testGraph = Graph<String, Test?>()
var actionGraph = Graph<String, Action>(value: runAction)
for test in tests {
let idComponents = test.id.keyPathRepresentation
testGraph.insertValue(test, at: idComponents)
actionGraph.insertValue(runAction, at: idComponents, intermediateValue: runAction)
testGraph.insertValue(test, at: test.id.keyPathRepresentation)
}

// Remove any tests that should be filtered out per the runner's
Expand All @@ -346,6 +405,15 @@ extension Runner.Plan {
// Synthesize suites for nodes in the test graph for which they are missing.
_recursivelySynthesizeSuites(in: &testGraph)

// Synthesize test functions inherited by subclasses of polymorphic test
// suites.
_synthesizePolymorphicTests(in: &testGraph)

// Generate the initial action graph. The actions for these steps will all
// be .run() *unless* an error was thrown while examining them, in which
// case they will be .recordIssue().
var actionGraph = testGraph.mapValues { _, _ in runAction }

// Recursively apply all recursive suite traits to children.
//
// This must be done _before_ calling `prepare(for:)` on the traits below.
Expand Down
42 changes: 22 additions & 20 deletions Sources/Testing/Running/Runner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ extension Runner {
/// - body: The function to invoke.
///
/// - Throws: Whatever is thrown by `body`.
private static func _forEach<E>(
private nonisolated(nonsending) static func _forEach<E>(
in sequence: some Sequence<E>,
namingTasksWith taskNamer: (borrowing E) -> (taskName: String, action: String?)?,
_ body: @Sendable @escaping (borrowing E) async throws -> Void
Expand Down Expand Up @@ -370,27 +370,29 @@ extension Runner {
private static func _runTestCases(_ testCases: some Sequence<Test.Case>, within step: Plan.Step, context: _Context) async {
let configuration = _configuration

// Apply the configuration's test case filter.
let testCaseFilter = configuration.testCaseFilter
let testCases = testCases.lazy.filter { testCase in
testCaseFilter(testCase, step.test)
}

// Figure out how to name child tasks.
let testName = "test \(step.test.humanReadableName())"
let taskNamer: (Int, Test.Case) -> (String, String?)? = if step.test.isParameterized {
{ i, _ in (testName, "running test case #\(i + 1)") }
} else {
{ _, _ in (testName, "running") }
}
await withCurrentPolymorphicSubclassIfNeeded(for: step.test) {
// Apply the configuration's test case filter.
let testCaseFilter = configuration.testCaseFilter
let testCases = testCases.lazy.filter { testCase in
testCaseFilter(testCase, step.test)
}

await _forEach(in: testCases.enumerated(), namingTasksWith: taskNamer) { _, testCase in
if let testCaseSerializer = context.testCaseSerializer {
// Note that if .serialized is applied to an inner scope, we still use
// this serializer (if set) so that we don't overcommit.
await testCaseSerializer.run { await _runTestCase(testCase, within: step, context: context) }
// Figure out how to name child tasks.
let testName = "test \(step.test.humanReadableName())"
let taskNamer: (Int, Test.Case) -> (String, String?)? = if step.test.isParameterized {
{ i, _ in (testName, "running test case #\(i + 1)") }
} else {
await _runTestCase(testCase, within: step, context: context)
{ _, _ in (testName, "running") }
}

await _forEach(in: testCases.enumerated(), namingTasksWith: taskNamer) { _, testCase in
if let testCaseSerializer = context.testCaseSerializer {
// Note that if .serialized is applied to an inner scope, we still use
// this serializer (if set) so that we don't overcommit.
await testCaseSerializer.run { await _runTestCase(testCase, within: step, context: context) }
} else {
await _runTestCase(testCase, within: step, context: context)
}
}
}
}
Expand Down
137 changes: 137 additions & 0 deletions Sources/Testing/Support/SubclassCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 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
//

#if _runtime(_ObjC)
private import ObjectiveC
#else
private import _TestDiscovery
#endif

/// A type that contains a cache of classes and their known subclasses.
///
/// - Note: In general, this type is not able to dynamically discover generic
/// classes that are subclasses of a given class.
struct SubclassCache {
#if !_runtime(_ObjC)
/// A dictionary keyed by classes whose values are arrays of all known
/// subclasses of those classes.
///
/// This dictionary is constructed in reverse by walking all known classes in
/// the current process and recursively querying each one for its immediate
/// superclass. This is less efficient than the Objective-C-based
/// implementation (which can avoid realizing classes that aren't of
/// interest to us). BUG: rdar://172942099
private static let _allSubclasses: [TypeInfo: [AnyClass]] = {
var result = [TypeInfo: [AnyClass]]()

for clazz in allClasses() {
let superclasses = sequence(first: clazz, next: _getSuperclass).dropFirst()
for superclass in superclasses {
let typeInfo = TypeInfo(describing: superclass)
result[typeInfo, default: []].append(clazz)
}
}

return result
}()
#endif

/// An entry in the subclass cache.
private struct _CacheEntry {
/// Whether or not the represented type belongs in the cache.
var inCache: Bool

/// The set of known subclasses for this entry, if cached.
var subclasses: [AnyClass]?
}

/// The set of cached information, keyed by type (class).
///
/// Negative entries (`inCache = false`) indicate that a type is known _not_
/// to be contained in this cache (after considering superclasses and
/// subclasses).
private var _cache: [TypeInfo: _CacheEntry]

/// Initialize an instance of this type to provide information for the given
/// set of base classes.
///
/// - Parameters:
/// - baseClasses: The set of base classes for which this instance will
/// cache information.
init(_ baseClasses: some Sequence<AnyClass>) {
let baseClasses = Set(baseClasses.lazy.map { TypeInfo(describing: $0) })
_cache = Dictionary(uniqueKeysWithValues: baseClasses.lazy.map { ($0, _CacheEntry(inCache: true)) })
}

/// Look up the given type in the cache.
///
/// - Parameters:
/// - typeInfo: The type to look up.
///
/// - Returns: Whether or not the given type is contained in this cache.
///
/// If `typeInfo` represents a class, and one of that class' superclasses is
/// contained in this cache, then that class is _also_ considered to be
/// contained in the cache.
private mutating func _find(_ typeInfo: TypeInfo) -> _CacheEntry? {
if let cached = _cache[typeInfo] {
return cached.inCache ? cached : nil
}

var superclassFound = false
if let clazz = typeInfo.class, let superclass = _getSuperclass(clazz) {
superclassFound = _find(TypeInfo(describing: superclass)) != nil
}
let result = _CacheEntry(inCache: superclassFound)
_cache[typeInfo] = result
return result
}

/// Check whether or not a given class is contained in this cache.
///
/// - Parameters:
/// - clazz: The class to look up.
///
/// - Returns: Whether or not the given class is contained in this cache.
///
/// If one of the superclasses of `clazz` is contained in this cache, then
/// `clazz` is _also_ considered to be contained in the cache.
mutating func contains(_ clazz: AnyClass) -> Bool {
_find(TypeInfo(describing: clazz)) != nil
}

/// Look up all known subclasses of a given class.
///
/// - Parameters:
/// - clazz: The base class of interest.
///
/// - Returns: An array of all known subclasses of the given class.
///
/// If `clazz` or a superclass thereof was not passed to ``init(_:)``, this
/// function returns the empty array.
mutating func subclasses(of clazz: AnyClass) -> [AnyClass] {
let typeInfo = TypeInfo(describing: clazz)

guard let cached = _find(typeInfo) else {
return []
}

if let result = cached.subclasses {
return result
}
#if _runtime(_ObjC)
let result = Array(objc_enumerateClasses(subclassing: clazz))
#else
let result = Self._allSubclasses[typeInfo] ?? []
#endif
_cache[typeInfo]!.subclasses = result
return result
}
}
Loading
Loading