Skip to content

Commit 35f2618

Browse files
authored
Make Test.Attachment generic. (#814)
This PR makes `Test.Attachment` generic over its attachable value type. It gains conditional conformance to `Copyable` and `Sendable` depending on the attachable value and if you call `attach()` on a move-only or non-sendable attachment, will eagerly serialize the attachable value at that point (rather than during initialization.) There are a few benefits here: 1. Callers can statically know the type of the attachable value in an attachment rather than needing to always deal with an existential box; 2. We can add associated types to `Test.Attachable` that will be readily accessible in `withUnsafeBufferPointer(for:_:)` again without needing an existential; and 3. When we eventually add support for image attachments, we won't need a bunch of additional initializers or intermediate box types or what-have-you; and 4. For Embedded Swift or other environments where existentials are problematic, we can eagerly serialize all attachments and pass a consistent type (~~`Test.Attachment<[UInt8]>`~~ `Test.Attachment<Test.AnyAttachable>`) to the event handler. There are also some drawbacks: 1. Because conformance to `Copyable` and `Sendable` is conditional, we lose a bit of flexibility if you have a non-sendable `Test.Attachment` instance or whatnot; 2. We still need a lazy, type-erased attachment type that can be passed to the event handler. I played around with `Test.Attachment<Any>` but that causes as many problems as it solves. ~~We end up with `Test.Attachment<any Test.Attachable & Sendable & Copyable>` but, because that's an existential type that doesn't conform to itself, the generic parameter `AttachableValue` is not constrained to `Test.Attachable`. We only provide initializers for types that do conform though (plus the existential one internally) so in practice it's not a huge issue.~~ I replaced `any Test.Attachable & Sendable & Copyable` with a dedicated type-erasing box type, `Test.AnyAttachable`, that wraps the existential. In Embedded Swift, it wraps a `[UInt8]` instead. 3. There is some code duplication necessary (i.e. multiple implementations of `attach()` ~~and `write()`~~.) 4. Non-final classes cannot conform to `Test.Attachable`. You can work around this with a box type that's generic over the class and conforms to `Test.Attachable`: the `Test.AttachableContainer` protocol now exists to make this easier. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 1e65cf1 commit 35f2618

File tree

13 files changed

+275
-184
lines changed

13 files changed

+275
-184
lines changed

Documentation/Proposals/0005-ranged-confirmations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ A new overload of `confirmation()` is added:
6565
/// - comment: An optional comment to apply to any issues generated by this
6666
/// function.
6767
/// - expectedCount: A range of integers indicating the number of times the
68-
/// expected event should occur when `body` is invoked.
68+
/// expected event should occur when `body` is invoked.
6969
/// - isolation: The actor to which `body` is isolated, if any.
7070
/// - sourceLocation: The source location to which any recorded issues should
7171
/// be attributed.
@@ -93,7 +93,7 @@ A new overload of `confirmation()` is added:
9393
/// let minBuns = 5
9494
/// let maxBuns = 10
9595
/// await confirmation(
96-
/// "Baked between \(minBuns) and \(maxBuns) buns",
96+
/// "Baked between \(minBuns) and \(maxBuns) buns",
9797
/// expectedCount: minBuns ... maxBuns
9898
/// ) { bunBaked in
9999
/// foodTruck.eventHandler = { event in

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ extension Array where Element == PackageDescription.SwiftSetting {
124124
availabilityMacroSettings + [
125125
.unsafeFlags(["-require-explicit-sendable"]),
126126
.enableUpcomingFeature("ExistentialAny"),
127+
.enableExperimentalFeature("SuppressedAssociatedTypes"),
127128

128129
.enableExperimentalFeature("AccessLevelOnImport"),
129130
.enableUpcomingFeature("InternalImportsByDefault"),

Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedAttachment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ extension ABIv0 {
2121
/// The path where the attachment was written.
2222
var path: String?
2323

24-
init(encoding attachment: borrowing Test.Attachment, in eventContext: borrowing Event.Context) {
24+
init(encoding attachment: borrowing Test.Attachment<Test.AnyAttachable>, in eventContext: borrowing Event.Context) {
2525
path = attachment.fileSystemPath
2626
}
2727
}

Sources/Testing/Attachments/Test.Attachable.swift

Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@ extension Test {
1515
///
1616
/// To attach an attachable value to a test report or test run output, use it
1717
/// to initialize a new instance of ``Test/Attachment``, then call
18-
/// ``Test/Attachment/attach()``. An attachment can only be attached once.
18+
/// ``Test/Attachment/attach(sourceLocation:)``. An attachment can only be
19+
/// attached once.
1920
///
2021
/// The testing library provides default conformances to this protocol for a
2122
/// variety of standard library types. Most user-defined types do not need to
2223
/// conform to this protocol.
2324
///
2425
/// A type should conform to this protocol if it can be represented as a
25-
/// sequence of bytes that would be diagnostically useful if a test fails.
26+
/// sequence of bytes that would be diagnostically useful if a test fails. If
27+
/// a type cannot conform directly to this protocol (such as a non-final class
28+
/// or a type declared in a third-party module), you can create a container
29+
/// type that conforms to ``Test/AttachableContainer`` to act as a proxy.
2630
public protocol Attachable: ~Copyable {
2731
/// An estimate of the number of bytes of memory needed to store this value
2832
/// as an attachment.
@@ -61,7 +65,7 @@ extension Test {
6165
/// the buffer to contain an image in PNG format, JPEG format, etc., but it
6266
/// would not be idiomatic for the buffer to contain a textual description
6367
/// of the image.
64-
borrowing func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R
68+
borrowing func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R
6569
}
6670
}
6771

@@ -103,56 +107,28 @@ extension Test.Attachable where Self: StringProtocol {
103107
// developers can attach raw data when needed.
104108
@_spi(Experimental)
105109
extension Array<UInt8>: Test.Attachable {
106-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
110+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
107111
try withUnsafeBytes(body)
108112
}
109113
}
110114

111115
@_spi(Experimental)
112116
extension ContiguousArray<UInt8>: Test.Attachable {
113-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
117+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
114118
try withUnsafeBytes(body)
115119
}
116120
}
117121

118122
@_spi(Experimental)
119123
extension ArraySlice<UInt8>: Test.Attachable {
120-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
124+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
121125
try withUnsafeBytes(body)
122126
}
123127
}
124128

125-
@_spi(Experimental)
126-
extension UnsafeBufferPointer<UInt8>: Test.Attachable {
127-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
128-
try body(.init(self))
129-
}
130-
}
131-
132-
@_spi(Experimental)
133-
extension UnsafeMutableBufferPointer<UInt8>: Test.Attachable {
134-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
135-
try body(.init(self))
136-
}
137-
}
138-
139-
@_spi(Experimental)
140-
extension UnsafeRawBufferPointer: Test.Attachable {
141-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
142-
try body(self)
143-
}
144-
}
145-
146-
@_spi(Experimental)
147-
extension UnsafeMutableRawBufferPointer: Test.Attachable {
148-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
149-
try body(.init(self))
150-
}
151-
}
152-
153129
@_spi(Experimental)
154130
extension String: Test.Attachable {
155-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
131+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
156132
var selfCopy = self
157133
return try selfCopy.withUTF8 { utf8 in
158134
try body(UnsafeRawBufferPointer(utf8))
@@ -162,7 +138,7 @@ extension String: Test.Attachable {
162138

163139
@_spi(Experimental)
164140
extension Substring: Test.Attachable {
165-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
141+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
166142
var selfCopy = self
167143
return try selfCopy.withUTF8 { utf8 in
168144
try body(UnsafeRawBufferPointer(utf8))
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
@_spi(Experimental)
12+
extension Test {
13+
/// A protocol describing a type that can be attached to a test report or
14+
/// written to disk when a test is run and which contains another value that
15+
/// it stands in for.
16+
///
17+
/// To attach an attachable value to a test report or test run output, use it
18+
/// to initialize a new instance of ``Test/Attachment``, then call
19+
/// ``Test/Attachment/attach(sourceLocation:)``. An attachment can only be
20+
/// attached once.
21+
///
22+
/// A type can conform to this protocol if it represents another type that
23+
/// cannot directly conform to ``Test/Attachable``, such as a non-final class
24+
/// or a type declared in a third-party module.
25+
public protocol AttachableContainer<AttachableValue>: Attachable, ~Copyable {
26+
#if hasFeature(SuppressedAssociatedTypes)
27+
/// The type of the attachable value represented by this type.
28+
associatedtype AttachableValue: ~Copyable
29+
#else
30+
/// The type of the attachable value represented by this type.
31+
associatedtype AttachableValue
32+
#endif
33+
34+
/// The attachable value represented by this instance.
35+
var attachableValue: AttachableValue { get }
36+
}
37+
}

0 commit comments

Comments
 (0)