Skip to content

Commit 252512a

Browse files
committed
Attachments!
This PR introduces a new experimental feature, attachments. With this feature, you can "attach" values that conform to a new `Test.Attachable` protocol to a test. With the right command-line incantation (TBD), Swift Testing will automatically write attachments to disk for you. > ![NOTE] > This PR does not teach Xcode or VS Code how to handle attachments produced by > Swift Testing, nor does it add the necessary command-line arguments to the > `swift test` command-line tool. This PR is one of a series that I'll be posting to build out this feature. As always, keep in mind that symbols marked `@_spi(Experimental)` are subject to change or removal without notice. Resolves #714. Resolves rdar://88648735.
1 parent 3fb80df commit 252512a

17 files changed

+690
-4
lines changed

Documentation/ABI/JSON.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ sufficient information to display the event in a human-readable format.
193193
}
194194
195195
<event-kind> ::= "runStarted" | "testStarted" | "testCaseStarted" |
196-
"issueRecorded" | "testCaseEnded" | "testEnded" | "testSkipped" |
197-
"runEnded" ; additional event kinds may be added in the future
196+
"issueRecorded" | "valueAttached" | "testCaseEnded" | "testEnded" |
197+
"testSkipped" | "runEnded" ; additional event kinds may be added in the future
198198
199199
<issue> ::= {
200200
"isKnown": <bool>, ; is this a known issue or not?
@@ -207,7 +207,7 @@ sufficient information to display the event in a human-readable format.
207207
}
208208
209209
<message-symbol> ::= "default" | "skip" | "pass" | "passWithKnownIssue" |
210-
"fail" | "difference" | "warning" | "details"
210+
"fail" | "difference" | "warning" | "details" | "attachment"
211211
```
212212

213213
<!--

Sources/Testing/ABI/EntryPoints/EntryPoint.swift

+17
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,9 @@ public struct __CommandLineArguments_v0: Sendable {
275275

276276
/// The value of the `--repeat-until` argument.
277277
public var repeatUntil: String?
278+
279+
/// The value of the `--experimental-attachment-path` argument.
280+
public var experimentalAttachmentPath: String?
278281
}
279282

280283
extension __CommandLineArguments_v0: Codable {
@@ -295,6 +298,7 @@ extension __CommandLineArguments_v0: Codable {
295298
case skip
296299
case repetitions
297300
case repeatUntil
301+
case experimentalAttachmentPath
298302
}
299303
}
300304

@@ -355,6 +359,11 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
355359
if let xunitOutputIndex = args.firstIndex(of: "--xunit-output"), !isLastArgument(at: xunitOutputIndex) {
356360
result.xunitOutput = args[args.index(after: xunitOutputIndex)]
357361
}
362+
363+
// Attachment output
364+
if let attachmentPathIndex = args.firstIndex(of: "--experimental-attachment-path"), !isLastArgument(at: attachmentPathIndex) {
365+
result.experimentalAttachmentPath = args[args.index(after: attachmentPathIndex)]
366+
}
358367
#endif
359368

360369
if args.contains("--list-tests") {
@@ -464,6 +473,14 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr
464473
}
465474
}
466475

476+
// Attachment output.
477+
if let attachmentPath = args.experimentalAttachmentPath {
478+
guard fileExists(atPath: attachmentPath) else {
479+
throw _EntryPointError.invalidArgument("--experimental-attachment-path", value: attachmentPath)
480+
}
481+
configuration.attachmentDirectoryPath = attachmentPath
482+
}
483+
467484
#if canImport(Foundation)
468485
// Event stream output (experimental)
469486
if let eventStreamOutputPath = args.eventStreamOutputPath {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
extension ABIv0 {
12+
/// A type implementing the JSON encoding of ``Test/Attachment`` for the ABI
13+
/// entry point and event stream output.
14+
///
15+
/// This type is not part of the public interface of the testing library. It
16+
/// assists in converting values to JSON; clients that consume this JSON are
17+
/// expected to write their own decoders.
18+
///
19+
/// - Warning: Attachments are not yet part of the JSON schema.
20+
struct EncodedAttachment: Sendable {
21+
/// The path where the attachment was written.
22+
var path: String?
23+
24+
init(encoding attachment: borrowing Test.Attachment, in eventContext: borrowing Event.Context) {
25+
path = attachment.fileSystemPath
26+
}
27+
}
28+
}
29+
30+
// MARK: - Codable
31+
32+
extension ABIv0.EncodedAttachment: Codable {}

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

+12
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ extension ABIv0 {
2727
case testStarted
2828
case testCaseStarted
2929
case issueRecorded
30+
case valueAttached
3031
case testCaseEnded
3132
case testEnded
3233
case testSkipped
@@ -45,6 +46,14 @@ extension ABIv0 {
4546
/// ``kind-swift.property`` property is ``Kind-swift.enum/issueRecorded``.
4647
var issue: EncodedIssue?
4748

49+
/// The value that was attached, if any.
50+
///
51+
/// The value of this property is `nil` unless the value of the
52+
/// ``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?
56+
4857
/// Human-readable messages associated with this event that can be presented
4958
/// to the user.
5059
var messages: [EncodedMessage]
@@ -71,6 +80,9 @@ extension ABIv0 {
7180
case let .issueRecorded(recordedIssue):
7281
kind = .issueRecorded
7382
issue = EncodedIssue(encoding: recordedIssue, in: eventContext)
83+
case let .valueAttached(attachment):
84+
kind = .valueAttached
85+
_attachment = EncodedAttachment(encoding: attachment, in: eventContext)
7486
case .testCaseEnded:
7587
if eventContext.test?.isParameterized == false {
7688
return nil

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

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ extension ABIv0 {
3030
case difference
3131
case warning
3232
case details
33+
case attachment
3334

3435
init(encoding symbol: Event.Symbol) {
3536
self = switch symbol {
@@ -51,6 +52,8 @@ extension ABIv0 {
5152
.warning
5253
case .details:
5354
.details
55+
case .attachment:
56+
.attachment
5457
}
5558
}
5659
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.
15+
///
16+
/// To attach an attachable value to a test report or test run output, use it
17+
/// to initialize a new instance of ``Test/Attachment``, then call
18+
/// ``Test/Attachment/attach()``. An attachment can only be attached once.
19+
///
20+
/// Generally speaking, you should not need to make new types conform to this
21+
/// protocol.
22+
// TODO: write more about this protocol, how it works, and list conforming
23+
// types (including discussion of the Foundation cross-import overlay.)
24+
public protocol Attachable: ~Copyable {
25+
/// Call a function and pass a buffer representing this instance to it.
26+
///
27+
/// - Parameters:
28+
/// - attachment: The attachment that is requesting a buffer (that is, the
29+
/// attachment containing this instance.)
30+
/// - body: A function to call. A temporary buffer containing a data
31+
/// representation of this instance is passed to it.
32+
///
33+
/// - Returns: Whatever is returned by `body`.
34+
///
35+
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
36+
/// creation of the buffer.
37+
///
38+
/// The testing library uses this function when writing an attachment to a
39+
/// test report or to a file on disk. The format of the buffer is
40+
/// implementation-defined, but should be "idiomatic" for this type: for
41+
/// example, if this type represents an image, it would be appropriate for
42+
/// the buffer to contain an image in PNG format, JPEG format, etc., but it
43+
/// would not be idiomatic for the buffer to contain a textual description
44+
/// of the image.
45+
borrowing func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R
46+
}
47+
}
48+
49+
// MARK: - Default implementations
50+
51+
// Implement the protocol requirements for byte arrays and buffers so that
52+
// developers can attach raw data when needed.
53+
@_spi(Experimental)
54+
extension [UInt8]: Test.Attachable {
55+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
56+
try withUnsafeBytes(body)
57+
}
58+
}
59+
60+
@_spi(Experimental)
61+
extension UnsafeBufferPointer<UInt8>: Test.Attachable {
62+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
63+
try body(.init(self))
64+
}
65+
}
66+
67+
@_spi(Experimental)
68+
extension UnsafeRawBufferPointer: Test.Attachable {
69+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
70+
try body(self)
71+
}
72+
}

0 commit comments

Comments
 (0)