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 (#121)

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 authored Nov 16, 2023
1 parent 62b35e3 commit 3767654
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 3767654

Please sign in to comment.