Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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