Skip to content

Commit 7893f3f

Browse files
committed
[SWT-NNNN] Attachments
Test authors frequently need to include out-of-band data with tests that can be used to diagnose issues when a test fails. This proposal introduces a new API called "attachments" (analogous to the same-named feature in XCTest) as well as the infrastructure necessary to create new attachments and handle them in tools like VS Code. Read the full proposal [here]().
1 parent c488e8f commit 7893f3f

18 files changed

+685
-83
lines changed

Diff for: Documentation/ABI/JSON.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -188,19 +188,24 @@ sufficient information to display the event in a human-readable format.
188188
"kind": <event-kind>,
189189
"instant": <instant>, ; when the event occurred
190190
["issue": <issue>,] ; the recorded issue (if "kind" is "issueRecorded")
191+
["attachment": <attachment>,] ; the attachment (if kind is "valueAttached")
191192
"messages": <array:message>,
192193
["testID": <test-id>,]
193194
}
194195
195196
<event-kind> ::= "runStarted" | "testStarted" | "testCaseStarted" |
196197
"issueRecorded" | "testCaseEnded" | "testEnded" | "testSkipped" |
197-
"runEnded" ; additional event kinds may be added in the future
198+
"runEnded" | "valueAttached"; additional event kinds may be added in the future
198199
199200
<issue> ::= {
200201
"isKnown": <bool>, ; is this a known issue or not?
201202
["sourceLocation": <source-location>,] ; where the issue occurred, if known
202203
}
203204
205+
<attachment> ::= {
206+
"path": <string>, ; the absolute path to the attachment on disk
207+
}
208+
204209
<message> ::= {
205210
"symbol": <message-symbol>,
206211
"text": <string>, ; the human-readable text of this message

Diff for: Documentation/Proposals/NNNN-attachments.md

+436
Large diffs are not rendered by default.

Diff for: Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift

+5-6
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,10 @@ import UniformTypeIdentifiers
3232
/// such a requirement, and all image types we care about are non-final
3333
/// classes. Thus, the compiler will steadfastly refuse to allow non-final
3434
/// classes to conform to the `Attachable` protocol. We could get around this
35-
/// by changing the signature of `withUnsafeBufferPointer()` so that the
36-
/// generic parameter to `Attachment` is not `Self`, but that would defeat
37-
/// much of the purpose of making `Attachment` generic in the first place.
38-
/// (And no, the language does not let us write `where T: Self` anywhere
39-
/// useful.)
35+
/// by changing the signature of `withUnsafeBytes()` so that the generic
36+
/// parameter to `Attachment` is not `Self`, but that would defeat much of
37+
/// the purpose of making `Attachment` generic in the first place. (And no,
38+
/// the language does not let us write `where T: Self` anywhere useful.)
4039

4140
/// A wrapper type for image types such as `CGImage` and `NSImage` that can be
4241
/// attached indirectly.
@@ -132,7 +131,7 @@ extension _AttachableImageContainer: AttachableContainer {
132131
image
133132
}
134133

135-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
134+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
136135
let data = NSMutableData()
137136

138137
// Convert the image to a CGImage.

Diff for: Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift

+6-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//
1010

1111
#if canImport(Foundation)
12-
@_spi(Experimental) public import Testing
12+
public import Testing
1313
public import Foundation
1414

1515
// This implementation is necessary to let the compiler disambiguate when a type
@@ -18,11 +18,13 @@ public import Foundation
1818
// (which explicitly document what happens when a type conforms to both
1919
// protocols.)
2020

21-
@_spi(Experimental)
21+
/// @Metadata {
22+
/// @Available(Swift, introduced: 6.2)
23+
/// }
2224
extension Attachable where Self: Encodable & NSSecureCoding {
2325
@_documentation(visibility: private)
24-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
25-
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)
26+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
27+
try _Testing_Foundation.withUnsafeBytes(encoding: self, for: attachment, body)
2628
}
2729
}
2830
#endif

Diff for: Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift

+15-8
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
//
1010

1111
#if canImport(Foundation)
12-
@_spi(Experimental) public import Testing
12+
public import Testing
1313
private import Foundation
1414

15-
/// A common implementation of ``withUnsafeBufferPointer(for:_:)`` that is
16-
/// used when a type conforms to `Encodable`, whether or not it also conforms
17-
/// to `NSSecureCoding`.
15+
/// A common implementation of ``withUnsafeBytes(for:_:)`` that is used when a
16+
/// type conforms to `Encodable`, whether or not it also conforms to
17+
/// `NSSecureCoding`.
1818
///
1919
/// - Parameters:
2020
/// - attachableValue: The value to encode.
@@ -27,7 +27,7 @@ private import Foundation
2727
///
2828
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
2929
/// creation of the buffer.
30-
func withUnsafeBufferPointer<E, R>(encoding attachableValue: borrowing E, for attachment: borrowing Attachment<E>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R where E: Attachable & Encodable {
30+
func withUnsafeBytes<E, R>(encoding attachableValue: borrowing E, for attachment: borrowing Attachment<E>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R where E: Attachable & Encodable {
3131
let format = try EncodingFormat(for: attachment)
3232

3333
let data: Data
@@ -53,7 +53,10 @@ func withUnsafeBufferPointer<E, R>(encoding attachableValue: borrowing E, for at
5353
// Implement the protocol requirements generically for any encodable value by
5454
// encoding to JSON. This lets developers provide trivial conformance to the
5555
// protocol for types that already support Codable.
56-
@_spi(Experimental)
56+
57+
/// @Metadata {
58+
/// @Available(Swift, introduced: 6.2)
59+
/// }
5760
extension Attachable where Self: Encodable {
5861
/// Encode this value into a buffer using either [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder)
5962
/// or [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder),
@@ -86,8 +89,12 @@ extension Attachable where Self: Encodable {
8689
/// _and_ [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding),
8790
/// the default implementation of this function uses the value's conformance
8891
/// to `Encodable`.
89-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
90-
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)
92+
///
93+
/// @Metadata {
94+
/// @Available(Swift, introduced: 6.2)
95+
/// }
96+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
97+
try _Testing_Foundation.withUnsafeBytes(encoding: self, for: attachment, body)
9198
}
9299
}
93100
#endif

Diff for: Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift

+10-3
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
//
1010

1111
#if canImport(Foundation)
12-
@_spi(Experimental) public import Testing
12+
public import Testing
1313
public import Foundation
1414

1515
// As with Encodable, implement the protocol requirements for
1616
// NSSecureCoding-conformant classes by default. The implementation uses
1717
// NSKeyedArchiver for encoding.
18-
@_spi(Experimental)
18+
19+
/// @Metadata {
20+
/// @Available(Swift, introduced: 6.2)
21+
/// }
1922
extension Attachable where Self: NSSecureCoding {
2023
/// Encode this object using [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver)
2124
/// into a buffer, then call a function and pass that buffer to it.
@@ -46,7 +49,11 @@ extension Attachable where Self: NSSecureCoding {
4649
/// _and_ [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding),
4750
/// the default implementation of this function uses the value's conformance
4851
/// to `Encodable`.
49-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
52+
///
53+
/// @Metadata {
54+
/// @Available(Swift, introduced: 6.2)
55+
/// }
56+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
5057
let format = try EncodingFormat(for: attachment)
5158

5259
var data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true)

Diff for: Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//
1010

1111
#if canImport(Foundation)
12-
@_spi(Experimental) public import Testing
12+
public import Testing
1313
public import Foundation
1414

1515
#if !SWT_NO_PROCESS_SPAWNING && os(Windows)
@@ -32,7 +32,6 @@ extension URL {
3232
}
3333
}
3434

35-
@_spi(Experimental)
3635
extension Attachment where AttachableValue == _AttachableURLContainer {
3736
#if SWT_TARGET_OS_APPLE
3837
/// An operation queue to use for asynchronously reading data from disk.
@@ -51,6 +50,10 @@ extension Attachment where AttachableValue == _AttachableURLContainer {
5150
/// attachment.
5251
///
5352
/// - Throws: Any error that occurs attempting to read from `url`.
53+
///
54+
/// @Metadata {
55+
/// @Available(Swift, introduced: 6.2)
56+
/// }
5457
public init(
5558
contentsOf url: URL,
5659
named preferredName: String? = nil,

Diff for: Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift

+8-3
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@
99
//
1010

1111
#if canImport(Foundation)
12-
@_spi(Experimental) public import Testing
12+
public import Testing
1313
public import Foundation
1414

15-
@_spi(Experimental)
15+
/// @Metadata {
16+
/// @Available(Swift, introduced: 6.2)
17+
/// }
1618
extension Data: Attachable {
17-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
19+
/// @Metadata {
20+
/// @Available(Swift, introduced: 6.2)
21+
/// }
22+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
1823
try withUnsafeBytes(body)
1924
}
2025
}

Diff for: Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//
1010

1111
#if canImport(Foundation)
12-
@_spi(Experimental) import Testing
12+
import Testing
1313
import Foundation
1414

1515
/// An enumeration describing the encoding formats we support for `Encodable`

Diff for: Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLContainer.swift

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@
99
//
1010

1111
#if canImport(Foundation)
12-
@_spi(Experimental) public import Testing
12+
public import Testing
1313
public import Foundation
1414

1515
/// A wrapper type representing file system objects and URLs that can be
1616
/// attached indirectly.
1717
///
1818
/// You do not need to use this type directly. Instead, initialize an instance
1919
/// of ``Attachment`` using a file URL.
20-
@_spi(Experimental)
2120
public struct _AttachableURLContainer: Sendable {
2221
/// The underlying URL.
2322
var url: URL
@@ -36,7 +35,7 @@ extension _AttachableURLContainer: AttachableContainer {
3635
url
3736
}
3837

39-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
38+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
4039
try data.withUnsafeBytes(body)
4140
}
4241

Diff for: Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift

+3-5
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ extension ABI {
2727
case testStarted
2828
case testCaseStarted
2929
case issueRecorded
30-
case valueAttached = "_valueAttached"
30+
case valueAttached
3131
case testCaseEnded
3232
case testEnded
3333
case testSkipped
@@ -50,9 +50,7 @@ extension ABI {
5050
///
5151
/// The value of this property is `nil` unless the value of the
5252
/// ``kind-swift.property`` property is ``Kind-swift.enum/valueAttached``.
53-
///
54-
/// - Warning: Attachments are not yet part of the JSON schema.
55-
var _attachment: EncodedAttachment<V>?
53+
var attachment: EncodedAttachment<V>?
5654

5755
/// Human-readable messages associated with this event that can be presented
5856
/// to the user.
@@ -82,7 +80,7 @@ extension ABI {
8280
issue = EncodedIssue(encoding: recordedIssue, in: eventContext)
8381
case let .valueAttached(attachment):
8482
kind = .valueAttached
85-
_attachment = EncodedAttachment(encoding: attachment, in: eventContext)
83+
self.attachment = EncodedAttachment(encoding: attachment, in: eventContext)
8684
case .testCaseEnded:
8785
if eventContext.test?.isParameterized == false {
8886
return nil

Diff for: Sources/Testing/Attachments/Attachable.swift

+23-13
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
/// type cannot conform directly to this protocol (such as a non-final class or
2626
/// a type declared in a third-party module), you can create a container type
2727
/// that conforms to ``AttachableContainer`` to act as a proxy.
28-
@_spi(Experimental)
28+
///
29+
/// @Metadata {
30+
/// @Available(Swift, introduced: 6.2)
31+
/// }
2932
public protocol Attachable: ~Copyable {
3033
/// An estimate of the number of bytes of memory needed to store this value as
3134
/// an attachment.
@@ -41,6 +44,10 @@ public protocol Attachable: ~Copyable {
4144
///
4245
/// - Complexity: O(1) unless `Self` conforms to `Collection`, in which case
4346
/// up to O(_n_) where _n_ is the length of the collection.
47+
///
48+
/// @Metadata {
49+
/// @Available(Swift, introduced: 6.2)
50+
/// }
4451
var estimatedAttachmentByteCount: Int? { get }
4552

4653
/// Call a function and pass a buffer representing this instance to it.
@@ -63,7 +70,11 @@ public protocol Attachable: ~Copyable {
6370
/// the buffer to contain an image in PNG format, JPEG format, etc., but it
6471
/// would not be idiomatic for the buffer to contain a textual description of
6572
/// the image.
66-
borrowing func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R
73+
///
74+
/// @Metadata {
75+
/// @Available(Swift, introduced: 6.2)
76+
/// }
77+
borrowing func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R
6778

6879
/// Generate a preferred name for the given attachment.
6980
///
@@ -79,6 +90,10 @@ public protocol Attachable: ~Copyable {
7990
/// when adding `attachment` to a test report or persisting it to storage. The
8091
/// default implementation of this function returns `suggestedName` without
8192
/// any changes.
93+
///
94+
/// @Metadata {
95+
/// @Available(Swift, introduced: 6.2)
96+
/// }
8297
borrowing func preferredName(for attachment: borrowing Attachment<Self>, basedOn suggestedName: String) -> String
8398
}
8499

@@ -99,7 +114,7 @@ extension Attachable where Self: Collection, Element == UInt8 {
99114
count
100115
}
101116

102-
// We do not provide an implementation of withUnsafeBufferPointer(for:_:) here
117+
// We do not provide an implementation of withUnsafeBytes(for:_:) here
103118
// because there is no way in the standard library to statically detect if a
104119
// collection can provide contiguous storage (_HasContiguousBytes is not API.)
105120
// If withContiguousStorageIfAvailable(_:) fails, we don't want to make a
@@ -118,40 +133,35 @@ extension Attachable where Self: StringProtocol {
118133

119134
// Implement the protocol requirements for byte arrays and buffers so that
120135
// developers can attach raw data when needed.
121-
@_spi(Experimental)
122136
extension Array<UInt8>: Attachable {
123-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
137+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
124138
try withUnsafeBytes(body)
125139
}
126140
}
127141

128-
@_spi(Experimental)
129142
extension ContiguousArray<UInt8>: Attachable {
130-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
143+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
131144
try withUnsafeBytes(body)
132145
}
133146
}
134147

135-
@_spi(Experimental)
136148
extension ArraySlice<UInt8>: Attachable {
137-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
149+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
138150
try withUnsafeBytes(body)
139151
}
140152
}
141153

142-
@_spi(Experimental)
143154
extension String: Attachable {
144-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
155+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
145156
var selfCopy = self
146157
return try selfCopy.withUTF8 { utf8 in
147158
try body(UnsafeRawBufferPointer(utf8))
148159
}
149160
}
150161
}
151162

152-
@_spi(Experimental)
153163
extension Substring: Attachable {
154-
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
164+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
155165
var selfCopy = self
156166
return try selfCopy.withUTF8 { utf8 in
157167
try body(UnsafeRawBufferPointer(utf8))

0 commit comments

Comments
 (0)