From c996602f3a3becec5ff5151d4ef6daa9e0b6c41a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 13 Sep 2024 12:47:48 -0400 Subject: [PATCH 1/2] Remove the `Confirmation.ExpectedCount` marker protocol. This PR replaces uses of `Confirmation.ExpectedCount` in our API surface with `RangeExpression & Sendable`. A composed protocol type `P & Q` can only be expressed as `some`, not `any` as the latter is not yet implemented in the compiler (rdar://96960993), so we were using a separate protocol in place of that combination. The downside of doing so is that it makes the legibility of the interface worse. Xcode and other IDEs will offer you `confirmation(expectedCount: Confirmation.ExpectedCount)` but don't tell you that you need to write some range expression such as `0...2`. You have to dig into the protocol definition/documentation to figure it out. So this change changes the signature of the experimental `confirmation()` overload to take a `some RangeExpression & Sendable` and plumbs that through everywhere it's valid. Once we get to generating an `Issue`, we need an existential box (`any`) at which point we will just drop the `` bit until the compiler feature is added. We have a parameterized unit test that takes an array of these values, so to keep it building and passing, I moved a copy of the `ExpectedCount` protocol to the fixtures section of the test file containing that test; it's present only in our test target and not in our API surface. --- Sources/Testing/Issues/Confirmation.swift | 34 ++-------------------- Sources/Testing/Issues/Issue.swift | 2 +- Tests/TestingTests/ConfirmationTests.swift | 16 ++++++++-- 3 files changed, 17 insertions(+), 35 deletions(-) diff --git a/Sources/Testing/Issues/Confirmation.swift b/Sources/Testing/Issues/Confirmation.swift index 5848dfb50..c3f841ca8 100644 --- a/Sources/Testing/Issues/Confirmation.swift +++ b/Sources/Testing/Issues/Confirmation.swift @@ -164,7 +164,7 @@ public func confirmation( @_spi(Experimental) public func confirmation( _ comment: Comment? = nil, - expectedCount: some Confirmation.ExpectedCount, + expectedCount: some RangeExpression & Sendable, isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation = #_sourceLocation, _ body: (Confirmation) async throws -> sending R @@ -200,22 +200,7 @@ public func confirmation( fatalError("Unsupported") } -@_spi(Experimental) -extension Confirmation { - /// A protocol that describes a range expression that can be used with - /// ``confirmation(_:expectedCount:isolation:sourceLocation:_:)-9rt6m``. - /// - /// This protocol represents any expression that describes a range of - /// confirmation counts. For example, the expression `1 ..< 10` automatically - /// conforms to it. - /// - /// You do not generally need to add conformances to this type yourself. It is - /// used by the testing library to abstract away the different range types - /// provided by the Swift standard library. - public protocol ExpectedCount: Sendable, RangeExpression {} -} - -extension Confirmation.ExpectedCount { +extension RangeExpression where Bound == Int, Self: Sendable { /// Get an instance of ``Issue/Kind-swift.enum`` corresponding to this value. /// /// - Parameters: @@ -233,18 +218,3 @@ extension Confirmation.ExpectedCount { } } } - -@_spi(Experimental) -extension ClosedRange: Confirmation.ExpectedCount {} - -@_spi(Experimental) -extension PartialRangeFrom: Confirmation.ExpectedCount {} - -@_spi(Experimental) -extension PartialRangeThrough: Confirmation.ExpectedCount {} - -@_spi(Experimental) -extension PartialRangeUpTo: Confirmation.ExpectedCount {} - -@_spi(Experimental) -extension Range: Confirmation.ExpectedCount {} diff --git a/Sources/Testing/Issues/Issue.swift b/Sources/Testing/Issues/Issue.swift index 91602ef7c..1795a5362 100644 --- a/Sources/Testing/Issues/Issue.swift +++ b/Sources/Testing/Issues/Issue.swift @@ -52,7 +52,7 @@ public struct Issue: Sendable { /// the confirmation passed to these functions' `body` closures is confirmed /// too few or too many times. @_spi(Experimental) - indirect case confirmationOutOfRange(actual: Int, expected: any Confirmation.ExpectedCount) + indirect case confirmationOutOfRange(actual: Int, expected: any RangeExpression & Sendable) /// An issue due to an `Error` being thrown by a test function and caught by /// the testing library. diff --git a/Tests/TestingTests/ConfirmationTests.swift b/Tests/TestingTests/ConfirmationTests.swift index 11d04b48c..7c2dca474 100644 --- a/Tests/TestingTests/ConfirmationTests.swift +++ b/Tests/TestingTests/ConfirmationTests.swift @@ -116,16 +116,28 @@ struct UnsuccessfulConfirmationTests { } @Test(.hidden, arguments: [ - 1 ... 2 as any Confirmation.ExpectedCount, + 1 ... 2 as any ExpectedCount, 1 ..< 2, 1 ..< 3, ..<2, ...2, 999..., ]) - func confirmedOutOfRange(_ range: any Confirmation.ExpectedCount) async { + func confirmedOutOfRange(_ range: any ExpectedCount) async { await confirmation(expectedCount: range) { (thingHappened) async in thingHappened(count: 3) } } } + +// MARK: - + +/// Needed since we don't have generic test functions, so we need a concrete +/// argument type for `confirmedOutOfRange(_:)`, but we can't write +/// `any RangeExpression & Sendable`. ([96960993](rdar://96960993)) +protocol ExpectedCount: RangeExpression, Sendable where Bound == Int {} +extension ClosedRange: ExpectedCount {} +extension PartialRangeFrom: ExpectedCount {} +extension PartialRangeThrough: ExpectedCount {} +extension PartialRangeUpTo: ExpectedCount {} +extension Range: ExpectedCount {} From ea09cd18e09662fc6779b943b4b11e818f47202f Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 13 Sep 2024 13:04:50 -0400 Subject: [PATCH 2/2] Work around poor diagnostic messaging in Xcode 16 (which doesn't know how to handle .confirmationOutOfRange) --- Sources/Testing/Issues/Issue.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Issues/Issue.swift b/Sources/Testing/Issues/Issue.swift index 1795a5362..3ad035114 100644 --- a/Sources/Testing/Issues/Issue.swift +++ b/Sources/Testing/Issues/Issue.swift @@ -227,8 +227,14 @@ extension Issue { /// /// - Parameter issue: The original issue that gets snapshotted. public init(snapshotting issue: borrowing Issue) { - self.kind = Issue.Kind.Snapshot(snapshotting: issue.kind) - self.comments = issue.comments + if case .confirmationOutOfRange = issue.kind { + // Work around poor stringification of this issue kind in Xcode 16. + self.kind = .unconditional + self.comments = CollectionOfOne("\(issue.kind)") + issue.comments + } else { + self.kind = Issue.Kind.Snapshot(snapshotting: issue.kind) + self.comments = issue.comments + } self.sourceContext = issue.sourceContext self.isKnown = issue.isKnown }