Skip to content

Commit 5e3e40b

Browse files
committed
[ST-NNNN] Revise Swift Testing's Attachment/Encodable interop
1 parent 18efc43 commit 5e3e40b

1 file changed

Lines changed: 357 additions & 0 deletions

File tree

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
# Revise Swift Testing's `Attachment`/`Encodable` interop
2+
3+
* Proposal: [ST-NNNN](NNNN-revise-attachment-encodable-interfaces.md)
4+
* Authors: [Jonathan Grynspan](https://github.com/grynspan)
5+
* Review Manager: TBD
6+
* Status: **Awaiting review**
7+
* Implementation: [swiftlang/swift-testing#1770](https://github.com/swiftlang/swift-testing/pull/1770)
8+
* Review: ([pitch](https://forums.swift.org/...))
9+
10+
## Introduction
11+
12+
We introduced **attachments** to Swift Testing in Swift 6.2 with [ST-0009][].
13+
As part of that initial introduction, we included default implementations of our
14+
[`Attachable`][] protocol's requirements for types that already conform to
15+
[`Encodable`][] or [`NSSecureCoding`][].
16+
17+
This proposal revises the interfaces for attaching a value that conforms to one
18+
of those protocols to resolve some constraints that come with the current
19+
interface and implementation.
20+
21+
## Motivation
22+
23+
We have identified several deficiencies in the current interface:
24+
25+
- Test authors must explicitly add conformance to [`Attachable`][] to their
26+
types that already conform to [`Encodable`][] or [`NSSecureCoding`][], which
27+
is boilerplate that doesn't really _do_ anything other than satisfy Swift's
28+
type system.
29+
- The only way to select between property list and JSON encodings is to specify
30+
a path extension in the attachment's preferred name.
31+
- There is no way to customize the encoder's settings. For example, there is no
32+
way to configure [`JSONEncoder.outputFormatting`](https://developer.apple.com/documentation/foundation/jsonencoder/outputformatting-swift.property).
33+
- Types that conform to _both_ [`Encodable`][] and [`NSSecureCoding`][] are
34+
ambiguously encoded. We document these types as using the [`Encodable`][]
35+
encoding by default, but there is no way to customize this behavior.
36+
37+
## Proposed solution
38+
39+
New [`Attachment`][] initializers are introduced that behave similarly to the
40+
initializers introduced for file URLs in [ST-0009][] and for [`Transferable`][]-conforming
41+
types in [ST-0023][].
42+
43+
The existing default implementations of [`Attachable`][]'s requirements are
44+
marked to-be-deprecated. Their documentation will automatically include
45+
migration information, but new diagnostics will not be emitted at build time
46+
until a future Swift release.
47+
48+
## Detailed design
49+
50+
The following extension to `Attachment` is added and is available when
51+
test authors import both `Testing` and `Foundation` in a Swift file:
52+
53+
```swift
54+
extension Attachment {
55+
/// Initialize an instance of this type representing a value that conforms to
56+
/// the [`Encodable`][]
57+
/// protocol.
58+
///
59+
/// - Parameters:
60+
/// - encodableValue: The value to encode and attach.
61+
/// - encodingFormat: The encoding format to use to encode `encodableValue`.
62+
/// - preferredName: The preferred name of the attachment when writing it to
63+
/// a test report or to disk.
64+
/// - sourceLocation: The source location of the call to this initializer.
65+
/// This value is used when recording issues associated with the
66+
/// attachment.
67+
///
68+
/// - Throws: If an appropriate encoder could not be found given the
69+
/// `encodingFormat` and `preferredName` arguments.
70+
///
71+
/// Use this initializer to create an instance of ``Attachment`` from a value
72+
/// that conforms to the [`Encodable`][]
73+
/// protocol:
74+
///
75+
/// ```swift
76+
/// let menu = FoodTruck.currentMenu
77+
/// let attachment = try Attachment(encoding: menu, as: .json)
78+
/// Attachment.record(attachment)
79+
/// ```
80+
///
81+
/// The encoding that the testing library uses depends on the `encodingFormat`
82+
/// argument. If the value of that argument is `nil`, the testing library
83+
/// derives the format from the path extension you specify in `preferredName`.
84+
///
85+
/// | Extension | Encoding Used | Encoder Used |
86+
/// |-|-|-|
87+
/// | `".xml"` | XML property list | `PropertyListEncoder` |
88+
/// | `".plist"` | Binary property list | `PropertyListEncoder` |
89+
/// | None, `".json"` | JSON | `JSONEncoder` |
90+
///
91+
/// - Important: OpenStep-style property lists are not supported.
92+
///
93+
/// If the values of both the `encodingFormat` and `preferredName` arguments
94+
/// are `nil`, the testing library encodes `encodableValue` as JSON.
95+
public init<T>(
96+
encoding encodableValue: T,
97+
as encodingFormat: AttachableEncodingFormat? = nil,
98+
named preferredName: String? = nil,
99+
sourceLocation: SourceLocation = #_sourceLocation
100+
) throws where AttachableValue == _AttachableEncodableWrapper<T, Void>, T: Encodable
101+
102+
#if canImport(Combine)
103+
/// Initialize an instance of this type representing a value that conforms to
104+
/// the [`Encodable`][]
105+
/// protocol.
106+
///
107+
/// - Parameters:
108+
/// - encodableValue: The value to encode and attach.
109+
/// - encoder: The encoder to use to encode `encodableValue`.
110+
/// - preferredName: The preferred name of the attachment when writing it to
111+
/// a test report or to disk.
112+
/// - sourceLocation: The source location of the call to this initializer.
113+
/// This value is used when recording issues associated with the
114+
/// attachment.
115+
///
116+
/// - Throws: If `encoder` cannot be used to encode `encodableValue`.
117+
///
118+
/// Use this initializer to create an instance of ``Attachment`` from a value
119+
/// that conforms to the [`Encodable`][]
120+
/// protocol:
121+
///
122+
/// ```swift
123+
/// let menu = FoodTruck.currentMenu
124+
/// let encoder = JSONEncoder()
125+
/// let attachment = try Attachment(encoding: menu, using: encoder)
126+
/// Attachment.record(attachment)
127+
/// ```
128+
public init<T, E>(
129+
encoding encodableValue: T,
130+
using encoder: E,
131+
named preferredName: String? = nil,
132+
sourceLocation: SourceLocation = #_sourceLocation
133+
) throws where AttachableValue == _AttachableEncodableWrapper<T, E>, T: Encodable, E: TopLevelEncoder, E.Output: ContiguousBytes
134+
#endif
135+
136+
/// Initialize an instance of this type representing a value that conforms to
137+
/// the [`NSSecureCoding`][]
138+
/// protocol.
139+
///
140+
/// - Parameters:
141+
/// - encodableValue: The value to encode and attach.
142+
/// - propertyListFormat: The property list format to use to encode
143+
/// `encodableValue`.
144+
/// - preferredName: The preferred name of the attachment when writing it to
145+
/// a test report or to disk.
146+
/// - sourceLocation: The source location of the call to this initializer.
147+
/// This value is used when recording issues associated with the
148+
/// attachment.
149+
///
150+
/// - Throws: If an appropriate encoder could not be found given the
151+
/// `propertyListFormat` and `preferredName` arguments.
152+
///
153+
/// Use this initializer to create an instance of ``Attachment`` from a value
154+
/// that conforms to the [`NSSecureCoding`][]
155+
/// protocol:
156+
///
157+
/// ```swift
158+
/// let menu = FoodTruck.currentMenu
159+
/// let attachment = try Attachment(encoding: menu, as: .xml)
160+
/// Attachment.record(attachment)
161+
/// ```
162+
///
163+
/// The encoding that the testing library uses depends on the
164+
/// `propertyListFormat` argument. If the value of that argument is `nil`, the
165+
/// testing library derives the format from the path extension you specify in
166+
/// `preferredName`.
167+
///
168+
/// | Extension | Encoding Used | Encoder Used |
169+
/// |-|-|-|
170+
/// | `".xml"` | XML property list | `NSKeyedArchiver` |
171+
/// | None, `".plist"` | Binary property list | `NSKeyedArchiver` |
172+
///
173+
/// - Important: OpenStep-style property lists are not supported.
174+
///
175+
/// If the values of both the `propertyListFormat` and `preferredName`
176+
/// arguments are `nil`, the testing library encodes `encodableValue` as a
177+
/// binary property list.
178+
public init<T>(
179+
encoding encodableValue: T,
180+
as propertyListFormat: PropertyListSerialization.PropertyListFormat? = nil,
181+
named preferredName: String? = nil,
182+
sourceLocation: SourceLocation = #_sourceLocation
183+
) throws where AttachableValue == _AttachableEncodableWrapper<T, NSKeyedArchiver>, T: NSSecureCoding
184+
}
185+
```
186+
187+
> [!NOTE]
188+
> The `_AttachableEncodableWrapper` is a type that conforms to the
189+
> [`AttachableWrapper`][] protocol and is an implementation detail. Test authors
190+
> do not need to use this type directly.
191+
192+
The `AttachableEncodingFormat` type is declared as follows:
193+
194+
```swift
195+
/// An enumeration describing the encoding formats that you can use when
196+
/// attaching a value that conforms to [`Encodable`](https://developer.apple.com/documentation/swift/encodable).
197+
///
198+
/// Pass an instance of this type to ``Testing/Attachment/init(encoding:as:named:sourceLocation:)``
199+
/// to specify what encoder and format to use when the testing library saves the
200+
/// resulting attachment.
201+
///
202+
/// If you want to attach a value that conforms to [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding),
203+
/// use [`PropertyListFormat`](https://developer.apple.com/documentation/foundation/propertylistserialization/propertylistformat)
204+
/// instead.
205+
public struct AttachableEncodingFormat: Sendable, Equatable {
206+
/// Create an instance of this type representing a property list format.
207+
///
208+
/// - Parameters:
209+
/// - format: The corresponding property list format.
210+
///
211+
/// - Returns: An instance of this type representing `format`.
212+
public static func propertyListFormat(_ format: PropertyListSerialization.PropertyListFormat) -> Self
213+
214+
/// An instance of this type representing the JSON format.
215+
public static var json: Self { get }
216+
}
217+
```
218+
219+
Test authors can use the new initializers to create attachments from any value
220+
that conforms to [`Encodable`][] or [`NSSecureCoding`][]. These values do not
221+
need a _pro forma_ conformance to [`Attachable`][]:
222+
223+
```swift
224+
@Test func `Bake an apple pie`() throws {
225+
let oven = Oven()
226+
oven.warm(to: .fahrenheit(400))
227+
...
228+
let attachment = try Attachment(encoding: recipe, as: .propertyListFormat(.binary))
229+
Attachment.record(attachment)
230+
...
231+
#expect(pie.isDelicious)
232+
}
233+
234+
// OR:
235+
236+
@Test func `Bake an apple pie`() throws {
237+
let oven = Oven()
238+
oven.warm(to: .fahrenheit(400))
239+
...
240+
let encoder = JSONEncoder()
241+
encoder.outputFormatting = [.prettyPrinted, ...]
242+
let attachment = try Attachment(encoding: recipe, using: encoder)
243+
Attachment.record(attachment)
244+
...
245+
#expect(pie.isScrumptious)
246+
}
247+
```
248+
249+
If the test author does not specify an encoding format or encoder, Swift Testing
250+
makes a best effort to derive the encoding format from the attachment's
251+
preferred name (as with the existing interface). If the test author doesn't
252+
specify a preferred name either, Swift Testing defaults to JSON (for [`Encodable`][]
253+
types) or the binary property list format (for [`NSSecureCoding`][] types).
254+
255+
### Deprecation of existing interfaces
256+
257+
The existing default implementations are marked to-be-deprecated:
258+
259+
```swift
260+
extension Attachable where Self: Encodable {
261+
/// @DeprecationSummary {
262+
/// Use ``Attachment/init(encoding:as:named:sourceLocation:)`` instead:
263+
///
264+
/// let attachment = try Attachment(encoding: someValue, as: .json)
265+
/// Attachment.record(attachment)
266+
/// }
267+
@available(swift, introduced: 6.2, deprecated: 100000.0, message: "Use 'Attachment.init(encoding:as:named:sourceLocation:)' instead")
268+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R
269+
}
270+
271+
extension Attachable where Self: NSSecureCoding {
272+
/// @DeprecationSummary {
273+
/// Use ``Attachment/init(encoding:as:named:sourceLocation:)`` instead:
274+
///
275+
/// let attachment = try Attachment(encoding: someValue, as: .binary)
276+
/// Attachment.record(attachment)
277+
/// }
278+
@available(swift, introduced: 6.2, deprecated: 100000.0, message: "Use 'Attachment.init(encoding:as:named:sourceLocation:)' instead")
279+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R
280+
}
281+
```
282+
283+
### `TopLevelEncoder` on non-Apple platforms
284+
285+
[`TopLevelEncoder`][] is declared in the Combine framework which is part of
286+
Apple's SDKs but not part of the open source Swift project. As such, it is not
287+
available on non-Apple platforms. See **future directions** for more details; in
288+
the mean time, concrete overloads of `init(encoding:using:named:sourceLocation:)`
289+
are added on non-Apple platforms to cover the common use cases:
290+
291+
```swift
292+
#if !canImport(Combine)
293+
extension Attachment {
294+
public init<T, E>(
295+
encoding encodableValue: T,
296+
using encoder: E,
297+
named preferredName: String? = nil,
298+
sourceLocation: SourceLocation = #_sourceLocation
299+
) throws where AttachableValue == _AttachableEncodableWrapper<T, E>, T: Encodable, E: PropertyListEncoder
300+
301+
public init<T, E>(
302+
encoding encodableValue: T,
303+
using encoder: E,
304+
named preferredName: String? = nil,
305+
sourceLocation: SourceLocation = #_sourceLocation
306+
) throws where AttachableValue == _AttachableEncodableWrapper<T, E>, T: Encodable, E: JSONEncoder
307+
}
308+
#endif
309+
```
310+
311+
## Source compatibility
312+
313+
These changes are additive and should not impact existing code.
314+
315+
## Integration with supporting tools
316+
317+
No changes. Supporting tools that consume attachments should "just work".
318+
319+
## Future directions
320+
321+
- **Lowering [`TopLevelEncoder`][] to the standard library.** If we do this, then
322+
the protocol becomes available on non-Apple platforms and our hard-coded
323+
workarounds can be removed. (The author intends to propose a change here, but
324+
it is beyond the scope of _this_ proposal).
325+
326+
- **Formally deprecating the existing interface.** As discussed above, a future
327+
Swift toolchain release will emit deprecation warnings at build time when a
328+
type conforms to both [`Encodable`][]/[`NSSecureCoding`][] and [`Attachable`][]
329+
and relies on the default implementations of [`Attachable`][]'s requirements.
330+
331+
## Alternatives considered
332+
333+
- **Exposing new overloads of `Attachment.record()` instead of `Attachment.init()`.**
334+
See [ST-0023][] for more information why we prefer to overload `init()` here
335+
instead of `record()`.
336+
337+
- **Dropping support for [`NSSecureCoding`][].** While [`Encodable`][] is the
338+
preferred serialization protocol in Swift, [`NSSecureCoding`][] remains
339+
supported at the Foundation layer.
340+
341+
- **Leaving the existing interfaces undeprecated while adding the new ones.**
342+
The existing interfaces have the aforementioned ergonomic deficiencies. We
343+
don't want test authors to add conformances to [`Attachable`][] by rote, and
344+
we don't want to support two entirely distinct mechanisms for attaching
345+
encodable values when only one is needed (let alone where only one is
346+
_recommended_).
347+
348+
[`Attachable`]: https://developer.apple.com/documentation/testing/attachable
349+
[`AttachableWrapper`]: https://developer.apple.com/documentation/testing/attachablewrapper
350+
[`Attachment`]: https://developer.apple.com/documentation/testing/attachment
351+
[`Encodable`]: https://developer.apple.com/documentation/swift/encodable
352+
[`NSSecureCoding`]: https://developer.apple.com/documentation/foundation/nssecurecoding
353+
[`TopLevelEncoder`]: https://developer.apple.com/documentation/combine/toplevelencoder
354+
[`Transferable`]: https://developer.apple.com/documentation/coretransferable/transferable
355+
356+
[ST-0009]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0009-attachments.md
357+
[ST-0023]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0023-attachments-transferable.md

0 commit comments

Comments
 (0)