Skip to content

Commit 533538b

Browse files
authored
Attachment lifetimes (#1319)
This PR introduces a new experimental trait, `.savingAttachments(if:)`, that can be used to control whether a test's attachments are saved or not. XCTest has API around the [`XCTAttachment.Lifetime`](https://developer.apple.com/documentation/xctest/xctattachment/lifetime-swift.enum) enumeration that developers can use to control whether attachments are saved to a test report in Xcode. This enumeration has two cases: ```objc /* * Attachment will be kept regardless of the outcome of the test. */ XCTAttachmentLifetimeKeepAlways = 0, /* * Attachment will only be kept when the test fails, and deleted otherwise. */ XCTAttachmentLifetimeDeleteOnSuccess = 1 ``` I've opted to implement something a bit more granular. A developer can specify `.savingAttachments(if: .testFails)` and `.savingAttachments(if: .testPasses)` or can call some custom function of their own design like `runningInCI` or `hasPlentyOfFloppyDiskSpace`. The default behaviour if this trait is not used is to always save attachments, which is equivalent to `XCTAttachmentLifetimeKeepAlways`. `XCTAttachmentLifetimeDeleteOnSuccess` is, in effect, equivalent to `.savingAttachments(if: .testFails)`, but I hope reads a bit more clearly in context. Here's a usage example: ```swift @test(.savingAttachments(if: .testFails)) func `best test ever`() { Attachment.record("...") // only saves to the test report or to disk if the // next line is uncommented. // Issue.record("sadness") } ``` I've taken the opportunity to update existing documentation for `Attachment` and `Attachable` to try to use more consistent language: a test records an attachment and then the testing library saves it (somewhere). I'm sure I've missed some spots, so please point them out if you see them. Resolves rdar://138921461. ### 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 81dd324 commit 533538b

File tree

9 files changed

+601
-88
lines changed

9 files changed

+601
-88
lines changed

Sources/Testing/Attachments/Attachable.swift

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11-
/// A protocol describing a type that can be attached to a test report or
12-
/// written to disk when a test is run.
11+
private import _TestingInternals
12+
13+
/// A protocol describing a type whose instances can be recorded and saved as
14+
/// part of a test run.
1315
///
1416
/// To attach an attachable value to a test, pass it to ``Attachment/record(_:named:sourceLocation:)``.
1517
/// To further configure an attachable value before you attach it, use it to
1618
/// initialize an instance of ``Attachment`` and set its properties before
17-
/// passing it to ``Attachment/record(_:sourceLocation:)``. An attachable
18-
/// value can only be attached to a test once.
19+
/// passing it to ``Attachment/record(_:sourceLocation:)``.
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
@@ -36,8 +37,8 @@ public protocol Attachable: ~Copyable {
3637
/// an attachment.
3738
///
3839
/// The testing library uses this property to determine if an attachment
39-
/// should be held in memory or should be immediately persisted to storage.
40-
/// Larger attachments are more likely to be persisted, but the algorithm the
40+
/// should be held in memory or should be immediately saved. Larger
41+
/// attachments are more likely to be saved immediately, but the algorithm the
4142
/// testing library uses is an implementation detail and is subject to change.
4243
///
4344
/// The value of this property is approximately equal to the number of bytes
@@ -66,13 +67,12 @@ public protocol Attachable: ~Copyable {
6667
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
6768
/// creation of the buffer.
6869
///
69-
/// The testing library uses this function when writing an attachment to a
70-
/// test report or to a file on disk. The format of the buffer is
71-
/// implementation-defined, but should be "idiomatic" for this type: for
72-
/// example, if this type represents an image, it would be appropriate for
73-
/// the buffer to contain an image in PNG format, JPEG format, etc., but it
74-
/// would not be idiomatic for the buffer to contain a textual description of
75-
/// the image.
70+
/// The testing library uses this function when saving an attachment. The
71+
/// format of the buffer is implementation-defined, but should be "idiomatic"
72+
/// for this type: for example, if this type represents an image, it would be
73+
/// appropriate for the buffer to contain an image in PNG format, JPEG format,
74+
/// etc., but it would not be idiomatic for the buffer to contain a textual
75+
/// description of the image.
7676
///
7777
/// @Metadata {
7878
/// @Available(Swift, introduced: 6.2)
@@ -91,9 +91,8 @@ public protocol Attachable: ~Copyable {
9191
/// - Returns: The preferred name for `attachment`.
9292
///
9393
/// The testing library uses this function to determine the best name to use
94-
/// when adding `attachment` to a test report or persisting it to storage. The
95-
/// default implementation of this function returns `suggestedName` without
96-
/// any changes.
94+
/// when saving `attachment`. The default implementation of this function
95+
/// returns `suggestedName` without any changes.
9796
///
9897
/// @Metadata {
9998
/// @Available(Swift, introduced: 6.2)

Sources/Testing/Attachments/AttachableWrapper.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11-
/// A protocol describing a type that can be attached to a test report or
12-
/// written to disk when a test is run and which contains another value that it
13-
/// stands in for.
11+
/// A protocol describing a type whose instances can be recorded and saved as
12+
/// part of a test run and which contains another value that it stands in for.
1413
///
1514
/// To attach an attachable value to a test, pass it to ``Attachment/record(_:named:sourceLocation:)``.
1615
/// To further configure an attachable value before you attach it, use it to

Sources/Testing/Attachments/Attachment.swift

Lines changed: 67 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,24 @@ private import _TestingInternals
1313
/// A type describing values that can be attached to the output of a test run
1414
/// and inspected later by the user.
1515
///
16-
/// Attachments are included in test reports in Xcode or written to disk when
17-
/// tests are run at the command line. To create an attachment, you need a value
18-
/// of some type that conforms to ``Attachable``. Initialize an instance of
19-
/// ``Attachment`` with that value and, optionally, a preferred filename to use
20-
/// when writing to disk.
16+
/// To create an attachment, you need a value of some type that conforms to
17+
/// ``Attachable``. Initialize an instance of ``Attachment`` with that value
18+
/// and, optionally, a preferred filename to use when saving the attachment. To
19+
/// record the attachment, call ``Attachment/record(_:sourceLocation:)``.
20+
/// Alternatively, pass your attachable value directly to ``Attachment/record(_:named:sourceLocation:)``.
21+
///
22+
/// By default, the testing library saves your attachments as soon as you call
23+
/// ``Attachment/record(_:sourceLocation:)`` or
24+
/// ``Attachment/record(_:named:sourceLocation:)``. You can access saved
25+
/// attachments after your tests finish running:
26+
///
27+
/// - When using Xcode, you can access attachments from the test report.
28+
/// - When using Visual Studio Code, the testing library saves attachments to
29+
/// `.build/attachments` by default. Visual Studio Code reports the paths to
30+
/// individual attachments in its Tests Results panel.
31+
/// - When using Swift Package Manager's `swift test` command, you can pass the
32+
/// `--attachments-path` option. The testing library saves attachments to the
33+
/// specified directory.
2134
///
2235
/// @Metadata {
2336
/// @Available(Swift, introduced: 6.2)
@@ -41,16 +54,17 @@ public struct Attachment<AttachableValue> where AttachableValue: Attachable & ~C
4154
/// Storage for ``attachableValue-7dyjv``.
4255
private var _storage: Storage
4356

44-
/// The path to which the this attachment was written, if any.
57+
/// The path to which the this attachment was saved, if any.
4558
///
4659
/// If a developer sets the ``Configuration/attachmentsPath`` property of the
4760
/// current configuration before running tests, or if a developer passes
4861
/// `--attachments-path` on the command line, then attachments will be
49-
/// automatically written to disk when they are attached and the value of this
50-
/// property will describe the path where they were written.
62+
/// automatically saved when they are attached and the value of this property
63+
/// will describe the paths where they were saved. A developer can use the
64+
/// ``AttachmentSavingTrait`` trait type to defer or skip saving attachments.
5165
///
52-
/// If no destination path is set, or if an error occurred while writing this
53-
/// attachment to disk, the value of this property is `nil`.
66+
/// If no destination path is set, or if an error occurred while saving this
67+
/// attachment, the value of this property is `nil`.
5468
@_spi(ForToolsIntegrationOnly)
5569
public var fileSystemPath: String?
5670

@@ -62,8 +76,7 @@ public struct Attachment<AttachableValue> where AttachableValue: Attachable & ~C
6276
/// Storage for ``preferredName``.
6377
fileprivate var _preferredName: String?
6478

65-
/// A filename to use when writing this attachment to a test report or to a
66-
/// file on disk.
79+
/// A filename to use when saving this attachment.
6780
///
6881
/// The value of this property is used as a hint to the testing library. The
6982
/// testing library may substitute a different filename as needed. If the
@@ -106,9 +119,9 @@ extension Attachment where AttachableValue: ~Copyable {
106119
/// - Parameters:
107120
/// - attachableValue: The value that will be attached to the output of the
108121
/// test run.
109-
/// - preferredName: The preferred name of the attachment when writing it to
110-
/// a test report or to disk. If `nil`, the testing library attempts to
111-
/// derive a reasonable filename for the attached value.
122+
/// - preferredName: The preferred name of the attachment to use when saving
123+
/// it. If `nil`, the testing library attempts to generate a reasonable
124+
/// filename for the attached value.
112125
/// - sourceLocation: The source location of the call to this initializer.
113126
/// This value is used when recording issues associated with the
114127
/// attachment.
@@ -248,11 +261,11 @@ extension Attachment where AttachableValue: Sendable & ~Copyable {
248261
///
249262
/// When `attachableValue` is an instance of a type that does not conform to
250263
/// the [`Sendable`](https://developer.apple.com/documentation/swift/sendable)
251-
/// protocol, the testing library encodes it as data immediately. If
252-
/// `attachableValue` throws an error when the testing library attempts to
253-
/// encode it, the testing library records that error as an issue in the
254-
/// current test and does not write the attachment to the test report or to
255-
/// persistent storage.
264+
/// protocol, the testing library calls its ``Attachable/withUnsafeBytes(for:_:)``
265+
/// immediately and records a copy of the resulting buffer instead. If
266+
/// `attachableValue` throws an error when the testing library calls its
267+
/// ``Attachable/withUnsafeBytes(for:_:)`` function, the testing library
268+
/// records that error as an issue in the current test.
256269
///
257270
/// @Metadata {
258271
/// @Available(Swift, introduced: 6.2)
@@ -273,18 +286,18 @@ extension Attachment where AttachableValue: Sendable & ~Copyable {
273286
///
274287
/// - Parameters:
275288
/// - attachableValue: The value to attach.
276-
/// - preferredName: The preferred name of the attachment when writing it to
277-
/// a test report or to disk. If `nil`, the testing library attempts to
278-
/// derive a reasonable filename for the attached value.
289+
/// - preferredName: The preferred name of the attachment to use when saving
290+
/// it. If `nil`, the testing library attempts to generate a reasonable
291+
/// filename for the attached value.
279292
/// - sourceLocation: The source location of the call to this function.
280293
///
281294
/// When `attachableValue` is an instance of a type that does not conform to
282295
/// the [`Sendable`](https://developer.apple.com/documentation/swift/sendable)
283-
/// protocol, the testing library encodes it as data immediately. If
284-
/// `attachableValue` throws an error when the testing library attempts to
285-
/// encode it, the testing library records that error as an issue in the
286-
/// current test and does not write the attachment to the test report or to
287-
/// persistent storage.
296+
/// protocol, the testing library calls its ``Attachable/withUnsafeBytes(for:_:)``
297+
/// immediately and records a copy of the resulting buffer instead. If
298+
/// `attachableValue` throws an error when the testing library calls its
299+
/// ``Attachable/withUnsafeBytes(for:_:)`` function, the testing library
300+
/// records that error as an issue in the current test.
288301
///
289302
/// This function creates a new instance of ``Attachment`` and immediately
290303
/// attaches it to the current test.
@@ -308,11 +321,11 @@ extension Attachment where AttachableValue: ~Copyable {
308321
///
309322
/// When `attachableValue` is an instance of a type that does not conform to
310323
/// the [`Sendable`](https://developer.apple.com/documentation/swift/sendable)
311-
/// protocol, the testing library encodes it as data immediately. If
312-
/// `attachableValue` throws an error when the testing library attempts to
313-
/// encode it, the testing library records that error as an issue in the
314-
/// current test and does not write the attachment to the test report or to
315-
/// persistent storage.
324+
/// protocol, the testing library calls its ``Attachable/withUnsafeBytes(for:_:)``
325+
/// immediately and records a copy of the resulting buffer instead. If
326+
/// `attachableValue` throws an error when the testing library calls its
327+
/// ``Attachable/withUnsafeBytes(for:_:)`` function, the testing library
328+
/// records that error as an issue in the current test.
316329
///
317330
/// @Metadata {
318331
/// @Available(Swift, introduced: 6.2)
@@ -332,18 +345,18 @@ extension Attachment where AttachableValue: ~Copyable {
332345
///
333346
/// - Parameters:
334347
/// - attachableValue: The value to attach.
335-
/// - preferredName: The preferred name of the attachment when writing it to
336-
/// a test report or to disk. If `nil`, the testing library attempts to
337-
/// derive a reasonable filename for the attached value.
348+
/// - preferredName: The preferred name of the attachment to use when saving
349+
/// it. If `nil`, the testing library attempts to generate a reasonable
350+
/// filename for the attached value.
338351
/// - sourceLocation: The source location of the call to this function.
339352
///
340353
/// When `attachableValue` is an instance of a type that does not conform to
341354
/// the [`Sendable`](https://developer.apple.com/documentation/swift/sendable)
342-
/// protocol, the testing library encodes it as data immediately. If
343-
/// `attachableValue` throws an error when the testing library attempts to
344-
/// encode it, the testing library records that error as an issue in the
345-
/// current test and does not write the attachment to the test report or to
346-
/// persistent storage.
355+
/// protocol, the testing library calls its ``Attachable/withUnsafeBytes(for:_:)``
356+
/// immediately and records a copy of the resulting buffer instead. If
357+
/// `attachableValue` throws an error when the testing library calls its
358+
/// ``Attachable/withUnsafeBytes(for:_:)`` function, the testing library
359+
/// records that error as an issue in the current test.
347360
///
348361
/// This function creates a new instance of ``Attachment`` and immediately
349362
/// attaches it to the current test.
@@ -372,10 +385,9 @@ extension Attachment where AttachableValue: ~Copyable {
372385
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
373386
/// creation of the buffer.
374387
///
375-
/// The testing library uses this function when writing an attachment to a
376-
/// test report or to a file on disk. This function calls the
377-
/// ``Attachable/withUnsafeBytes(for:_:)`` function on this attachment's
378-
/// ``attachableValue-2tnj5`` property.
388+
/// The testing library uses this function when saving an attachment. This
389+
/// function calls the ``Attachable/withUnsafeBytes(for:_:)`` function on this
390+
/// attachment's ``attachableValue-2tnj5`` property.
379391
///
380392
/// @Metadata {
381393
/// @Available(Swift, introduced: 6.2)
@@ -404,16 +416,16 @@ extension Attachment where AttachableValue: ~Copyable {
404416
/// is derived from the value of the ``Attachment/preferredName`` property.
405417
///
406418
/// If you pass `--attachments-path` to `swift test`, the testing library
407-
/// automatically uses this function to persist attachments to the directory
408-
/// you specify.
419+
/// automatically uses this function to save attachments to the directory you
420+
/// specify.
409421
///
410422
/// This function does not get or set the value of the attachment's
411423
/// ``fileSystemPath`` property. The caller is responsible for setting the
412424
/// value of this property if needed.
413425
///
414-
/// This function is provided as a convenience to allow tools authors to write
415-
/// attachments to persistent storage the same way that Swift Package Manager
416-
/// does. You are not required to use this function.
426+
/// This function is provided as a convenience to allow tools authors to save
427+
/// attachments the same way that Swift Package Manager does. You are not
428+
/// required to use this function.
417429
@_spi(ForToolsIntegrationOnly)
418430
public borrowing func write(toFileInDirectoryAtPath directoryPath: String) throws -> String {
419431
try write(
@@ -505,9 +517,9 @@ extension Configuration {
505517
/// - Returns: Whether or not to continue handling the event.
506518
///
507519
/// This function is called automatically by ``handleEvent(_:in:)``. You do
508-
/// not need to call it elsewhere. It automatically persists the attachment
520+
/// not need to call it elsewhere. It automatically saves the attachment
509521
/// associated with `event` and modifies `event` to include the path where the
510-
/// attachment was stored.
522+
/// attachment was saved.
511523
func handleValueAttachedEvent(_ event: inout Event, in eventContext: borrowing Event.Context) -> Bool {
512524
guard let attachmentsPath else {
513525
// If there is no path to which attachments should be written, there's
@@ -519,9 +531,9 @@ extension Configuration {
519531
preconditionFailure("Passed the wrong kind of event to \(#function) (expected valueAttached, got \(event.kind)). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new")
520532
}
521533
if attachment.fileSystemPath != nil {
522-
// Somebody already persisted this attachment. This isn't necessarily a
523-
// logic error in the testing library, but it probably means we shouldn't
524-
// persist it again. Suppress the event.
534+
// Somebody already saved this attachment. This isn't necessarily a logic
535+
// error in the testing library, but it probably means we shouldn't save
536+
// it again. Suppress the event.
525537
return false
526538
}
527539

Sources/Testing/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ add_library(Testing
100100
Test+Discovery.swift
101101
Test+Discovery+Legacy.swift
102102
Test+Macro.swift
103+
Traits/AttachmentSavingTrait.swift
103104
Traits/Bug.swift
104105
Traits/Comment.swift
105106
Traits/Comment+Macro.swift

Sources/Testing/Events/Event.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,16 @@ public struct Event: Sendable {
191191
/// The instant at which the event occurred.
192192
public var instant: Test.Clock.Instant
193193

194+
#if DEBUG
195+
/// Whether or not this event was deferred.
196+
///
197+
/// A deferred event is handled significantly later than when was posted.
198+
///
199+
/// We currently use this property in our tests, but do not expose it as API
200+
/// or SPI. We can expose it in the future if tools need it.
201+
var wasDeferred: Bool = false
202+
#endif
203+
194204
/// Initialize an instance of this type.
195205
///
196206
/// - Parameters:

Sources/Testing/Running/Configuration+EventHandling.swift

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,6 @@ extension Configuration {
2323
var contextCopy = copy context
2424
contextCopy.configuration = self
2525
contextCopy.configuration?.eventHandler = { _, _ in }
26-
27-
#if !SWT_NO_FILE_IO
28-
if case .valueAttached = event.kind {
29-
var eventCopy = copy event
30-
guard handleValueAttachedEvent(&eventCopy, in: contextCopy) else {
31-
// The attachment could not be handled, so suppress this event.
32-
return
33-
}
34-
return eventHandler(eventCopy, contextCopy)
35-
}
36-
#endif
37-
3826
return eventHandler(event, contextCopy)
3927
}
4028
}

Sources/Testing/Running/Runner.RuntimeState.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,19 @@ extension Runner {
4747
return
4848
}
4949

50-
configuration.eventHandler = { [eventHandler = configuration.eventHandler] event, context in
50+
configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in
51+
#if !SWT_NO_FILE_IO
52+
var event = copy event
53+
if case .valueAttached = event.kind {
54+
guard let configuration = context.configuration,
55+
configuration.handleValueAttachedEvent(&event, in: context) else {
56+
// The attachment could not be handled, so suppress this event.
57+
return
58+
}
59+
}
60+
#endif
5161
RuntimeState.$current.withValue(existingRuntimeState) {
52-
eventHandler(event, context)
62+
oldEventHandler(event, context)
5363
}
5464
}
5565
}

0 commit comments

Comments
 (0)