Skip to content

Commit

Permalink
Avoid runtime casts to parameterized protocol types, which require ne…
Browse files Browse the repository at this point in the history
…wer OS versions

This fixes a build failure seen when attempting to build for macOS/iOS versions older than 13.0/16.0, respectively, which was introduced in #104. This introduces a type-erasing wrapper, which fixes the failure and also improves the `Test.testCases` derived property by allowing it to use `some` instead of `any` and further constraining the return type to `Sendable`.
  • Loading branch information
stmontgomery committed Nov 16, 2023
1 parent 62b35e3 commit 6d234d9
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 20 deletions.
2 changes: 1 addition & 1 deletion Sources/Testing/Test+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ extension Test {
sourceLocation: SourceLocation
) -> Self {
let typeName = _typeName(containingType, qualified: false)
return Self(name: typeName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, testCases: nil)
return Self(name: typeName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType)
}
}

Expand Down
51 changes: 49 additions & 2 deletions Sources/Testing/Test.Case.Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,53 @@ extension Test.Case.Generator: Sequence {
}
}

// MARK: - TestCases
// MARK: - Type-erasing to Sequence<Test.Case>

extension Test.Case.Generator: TestCases {}
/// A type-erased protocol describing a sequence of ``Test/Case`` instances.
///
/// This protocol is necessary because it is not currently possible to express
/// `Sequence<Test.Case> & Sendable` as an existential (`any`)
/// ([96960993](rdar://96960993)). It is also not possible to have a value of
/// an underlying generic sequence type without specifying its generic
/// parameters.
private protocol _TestCases: Sequence<Test.Case> & Sendable {}

extension Test.Case.Generator: _TestCases {}

extension Test {
/// A type-erasing wrapper for a `_TestCases`-conforming type.
///
/// See the documentation for the `_TestCases` protocol explaining why this
/// type erasure is necessary.
struct Cases: Sequence, Sendable {
/// The type-erased sequence of test cases this instance wraps.
private var _sequence: any _TestCases

init<S>(_ testCases: Test.Case.Generator<S>) {
_sequence = testCases
}

/// A type-erasing wrapper for an iterator of a `_TestCases`-conforming
/// type.
struct Iterator: IteratorProtocol {
/// The type-erased test case iterator this instance wraps.
private var _iterator: any IteratorProtocol<Test.Case>

fileprivate init(iterator: any IteratorProtocol<Test.Case>) {
_iterator = iterator
}

mutating func next() -> Test.Case? {
_iterator.next()
}
}

func makeIterator() -> Iterator {
Iterator(iterator: _sequence.makeIterator())
}

var underestimatedCount: Int {
_sequence.underestimatedCount
}
}
}
9 changes: 0 additions & 9 deletions Sources/Testing/Test.Case.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,3 @@ extension Test {
public var secondName: String?
}
}

/// A type-erased protocol describing a sequence of ``Test/Case`` instances.
///
/// This protocol is necessary because it is not currently possible to express
/// `Sequence<Test.Case> & Sendable` as an existential (`any`)
/// ([96960993](rdar://96960993)). It is also not possible to have a value of
/// an underlying generic sequence type without specifying its generic
/// parameters.
protocol TestCases: Sequence<Test.Case> & Sendable {}
30 changes: 22 additions & 8 deletions Sources/Testing/Test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,17 @@ public struct Test: Sendable {
public var xcTestCompatibleSelector: __XCTestCompatibleSelector?

/// Storage for the ``testCases`` property.
private var _testCases: (any TestCases)?
private var _testCases: Test.Cases?

/// The set of test cases associated with this test, if any.
///
/// For parameterized tests, each test case is associated with a single
/// combination of parameterized inputs. For non-parameterized tests, a single
/// test case is synthesized. For test suite types (as opposed to test
/// functions), the value of this property is `nil`.
///
/// The value of this property is guaranteed to be `Sendable`.
@_spi(ExperimentalParameterizedTesting)
public var testCases: (any Sequence<Test.Case>)? {
_testCases as? any Sequence<Test.Case>
public var testCases: (some Sequence<Test.Case> & Sendable)? {
_testCases
}

/// Whether or not this test is parameterized.
Expand Down Expand Up @@ -139,23 +137,39 @@ public struct Test: Sendable {
containingType != nil && testCases == nil
}

/// Initialize an instance of this type representing a test suite type.
init(
name: String,
displayName: String? = nil,
traits: [any Trait],
sourceLocation: SourceLocation,
containingType: Any.Type
) {
self.name = name
self.displayName = displayName
self.traits = traits
self.sourceLocation = sourceLocation
self.containingType = containingType
}

/// Initialize an instance of this type representing a test function.
init<S>(
name: String,
displayName: String? = nil,
traits: [any Trait],
sourceLocation: SourceLocation,
containingType: Any.Type? = nil,
xcTestCompatibleSelector: __XCTestCompatibleSelector? = nil,
testCases: (any TestCases)? = nil,
parameters: [ParameterInfo]? = nil
testCases: Test.Case.Generator<S>,
parameters: [ParameterInfo]
) {
self.name = name
self.displayName = displayName
self.traits = traits
self.sourceLocation = sourceLocation
self.containingType = containingType
self.xcTestCompatibleSelector = xcTestCompatibleSelector
self._testCases = testCases
self._testCases = Test.Cases(testCases)
self.parameters = parameters
}
}
Expand Down

0 comments on commit 6d234d9

Please sign in to comment.