diff --git a/Documentation/ABI/JSON.md b/Documentation/ABI/JSON.md index f313ddc00..e4ff24a4b 100644 --- a/Documentation/ABI/JSON.md +++ b/Documentation/ABI/JSON.md @@ -188,19 +188,24 @@ sufficient information to display the event in a human-readable format. "kind": , "instant": , ; when the event occurred ["issue": ,] ; the recorded issue (if "kind" is "issueRecorded") + ["attachment": ,] ; the attachment (if kind is "valueAttached") "messages": , ["testID": ,] } ::= "runStarted" | "testStarted" | "testCaseStarted" | "issueRecorded" | "testCaseEnded" | "testEnded" | "testSkipped" | - "runEnded" ; additional event kinds may be added in the future + "runEnded" | "valueAttached"; additional event kinds may be added in the future ::= { "isKnown": , ; is this a known issue or not? ["sourceLocation": ,] ; where the issue occurred, if known } + ::= { + "path": , ; the absolute path to the attachment on disk +} + ::= { "symbol": , "text": , ; the human-readable text of this message diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift index ca520e0c0..ed1e6a2ee 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -42,9 +42,9 @@ extension Attachment { contentType: (any Sendable)?, encodingQuality: Float, sourceLocation: SourceLocation - ) where AttachableValue == _AttachableImageContainer { - let imageContainer = _AttachableImageContainer(image: attachableValue, encodingQuality: encodingQuality, contentType: contentType) - self.init(imageContainer, named: preferredName, sourceLocation: sourceLocation) + ) where AttachableValue == _AttachableImageWrapper { + let imageWrapper = _AttachableImageWrapper(image: attachableValue, encodingQuality: encodingQuality, contentType: contentType) + self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } /// Initialize an instance of this type that encloses the given image. @@ -79,7 +79,7 @@ extension Attachment { as contentType: UTType?, encodingQuality: Float = 1.0, sourceLocation: SourceLocation = #_sourceLocation - ) where AttachableValue == _AttachableImageContainer { + ) where AttachableValue == _AttachableImageWrapper { self.init(attachableValue: attachableValue, named: preferredName, contentType: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation) } @@ -109,7 +109,7 @@ extension Attachment { named preferredName: String? = nil, encodingQuality: Float = 1.0, sourceLocation: SourceLocation = #_sourceLocation - ) where AttachableValue == _AttachableImageContainer { + ) where AttachableValue == _AttachableImageWrapper { self.init(attachableValue: attachableValue, named: preferredName, contentType: nil, encodingQuality: encodingQuality, sourceLocation: sourceLocation) } } diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift similarity index 96% rename from Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift rename to Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift index 90d1c0c70..7aa1fd139 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift @@ -9,7 +9,7 @@ // #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -@_spi(Experimental) public import Testing +public import Testing private import CoreGraphics private import ImageIO @@ -48,7 +48,7 @@ import UniformTypeIdentifiers /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) @_spi(Experimental) -public struct _AttachableImageContainer: Sendable where Image: AttachableAsCGImage { +public struct _AttachableImageWrapper: Sendable where Image: AttachableAsCGImage { /// The underlying image. /// /// `CGImage` and `UIImage` are sendable, but `NSImage` is not. `NSImage` @@ -127,8 +127,8 @@ public struct _AttachableImageContainer: Sendable where Image: Attachable // MARK: - -extension _AttachableImageContainer: AttachableContainer { - public var attachableValue: Image { +extension _AttachableImageWrapper: AttachableWrapper { + public var wrappedValue: Image { image } diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift index cd26c24cc..46a1e11e6 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift @@ -9,7 +9,7 @@ // #if canImport(Foundation) -@_spi(Experimental) public import Testing +public import Testing public import Foundation // This implementation is necessary to let the compiler disambiguate when a type @@ -18,7 +18,9 @@ public import Foundation // (which explicitly document what happens when a type conforms to both // protocols.) -@_spi(Experimental) +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// } extension Attachable where Self: Encodable & NSSecureCoding { @_documentation(visibility: private) public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift index 812db0b70..683888801 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift @@ -9,7 +9,7 @@ // #if canImport(Foundation) -@_spi(Experimental) public import Testing +public import Testing private import Foundation /// A common implementation of ``withUnsafeBytes(for:_:)`` that is used when a @@ -53,7 +53,10 @@ func withUnsafeBytes(encoding attachableValue: borrowing E, for attachment // Implement the protocol requirements generically for any encodable value by // encoding to JSON. This lets developers provide trivial conformance to the // protocol for types that already support Codable. -@_spi(Experimental) + +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// } extension Attachable where Self: Encodable { /// Encode this value into a buffer using either [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder) /// or [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder), @@ -86,6 +89,10 @@ extension Attachable where Self: Encodable { /// _and_ [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding), /// the default implementation of this function uses the value's conformance /// to `Encodable`. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try _Testing_Foundation.withUnsafeBytes(encoding: self, for: attachment, body) } diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift index c2cc28ea0..4acbf4960 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift @@ -9,13 +9,16 @@ // #if canImport(Foundation) -@_spi(Experimental) public import Testing +public import Testing public import Foundation // As with Encodable, implement the protocol requirements for // NSSecureCoding-conformant classes by default. The implementation uses // NSKeyedArchiver for encoding. -@_spi(Experimental) + +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// } extension Attachable where Self: NSSecureCoding { /// Encode this object using [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver) /// into a buffer, then call a function and pass that buffer to it. @@ -46,6 +49,10 @@ extension Attachable where Self: NSSecureCoding { /// _and_ [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding), /// the default implementation of this function uses the value's conformance /// to `Encodable`. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { let format = try EncodingFormat(for: attachment) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index dbf7e2688..83c3909be 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -9,7 +9,7 @@ // #if canImport(Foundation) -@_spi(Experimental) public import Testing +public import Testing public import Foundation #if !SWT_NO_PROCESS_SPAWNING && os(Windows) @@ -32,8 +32,7 @@ extension URL { } } -@_spi(Experimental) -extension Attachment where AttachableValue == _AttachableURLContainer { +extension Attachment where AttachableValue == _AttachableURLWrapper { #if SWT_TARGET_OS_APPLE /// An operation queue to use for asynchronously reading data from disk. private static let _operationQueue = OperationQueue() @@ -51,6 +50,10 @@ extension Attachment where AttachableValue == _AttachableURLContainer { /// attachment. /// /// - Throws: Any error that occurs attempting to read from `url`. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } public init( contentsOf url: URL, named preferredName: String? = nil, @@ -91,8 +94,8 @@ extension Attachment where AttachableValue == _AttachableURLContainer { } #endif - let urlContainer = _AttachableURLContainer(url: url, data: data, isCompressedDirectory: isDirectory) - self.init(urlContainer, named: preferredName, sourceLocation: sourceLocation) + let urlWrapper = _AttachableURLWrapper(url: url, data: data, isCompressedDirectory: isDirectory) + self.init(urlWrapper, named: preferredName, sourceLocation: sourceLocation) } } diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift index 38233cd3c..ce7b719a9 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift @@ -9,11 +9,16 @@ // #if canImport(Foundation) -@_spi(Experimental) public import Testing +public import Testing public import Foundation -@_spi(Experimental) +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// } extension Data: Attachable { + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift b/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift index bbbe934ab..49499a8c2 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift @@ -9,7 +9,7 @@ // #if canImport(Foundation) -@_spi(Experimental) import Testing +import Testing import Foundation /// An enumeration describing the encoding formats we support for `Encodable` diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLContainer.swift b/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLWrapper.swift similarity index 90% rename from Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLContainer.swift rename to Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLWrapper.swift index c7a223a51..d6be53c80 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLContainer.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLWrapper.swift @@ -9,7 +9,7 @@ // #if canImport(Foundation) -@_spi(Experimental) public import Testing +public import Testing public import Foundation /// A wrapper type representing file system objects and URLs that can be @@ -17,8 +17,7 @@ public import Foundation /// /// You do not need to use this type directly. Instead, initialize an instance /// of ``Attachment`` using a file URL. -@_spi(Experimental) -public struct _AttachableURLContainer: Sendable { +public struct _AttachableURLWrapper: Sendable { /// The underlying URL. var url: URL @@ -31,8 +30,8 @@ public struct _AttachableURLContainer: Sendable { // MARK: - -extension _AttachableURLContainer: AttachableContainer { - public var attachableValue: URL { +extension _AttachableURLWrapper: AttachableWrapper { + public var wrappedValue: URL { url } diff --git a/Sources/Overlays/_Testing_Foundation/CMakeLists.txt b/Sources/Overlays/_Testing_Foundation/CMakeLists.txt index 54a340323..9343960ab 100644 --- a/Sources/Overlays/_Testing_Foundation/CMakeLists.txt +++ b/Sources/Overlays/_Testing_Foundation/CMakeLists.txt @@ -7,7 +7,7 @@ # See http://swift.org/CONTRIBUTORS.txt for Swift project authors add_library(_Testing_Foundation - Attachments/_AttachableURLContainer.swift + Attachments/_AttachableURLWrapper.swift Attachments/EncodingFormat.swift Attachments/Attachment+URL.swift Attachments/Attachable+NSSecureCoding.swift diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift index b8bafdde1..73e7db2ac 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift @@ -27,7 +27,7 @@ extension ABI { case testStarted case testCaseStarted case issueRecorded - case valueAttached = "_valueAttached" + case valueAttached case testCaseEnded case testEnded case testSkipped @@ -50,9 +50,7 @@ extension ABI { /// /// The value of this property is `nil` unless the value of the /// ``kind-swift.property`` property is ``Kind-swift.enum/valueAttached``. - /// - /// - Warning: Attachments are not yet part of the JSON schema. - var _attachment: EncodedAttachment? + var attachment: EncodedAttachment? /// Human-readable messages associated with this event that can be presented /// to the user. @@ -82,7 +80,7 @@ extension ABI { issue = EncodedIssue(encoding: recordedIssue, in: eventContext) case let .valueAttached(attachment): kind = .valueAttached - _attachment = EncodedAttachment(encoding: attachment, in: eventContext) + self.attachment = EncodedAttachment(encoding: attachment, in: eventContext) case .testCaseEnded: if eventContext.test?.isParameterized == false { return nil diff --git a/Sources/Testing/Attachments/Attachable.swift b/Sources/Testing/Attachments/Attachable.swift index 09a7e0b78..be466940b 100644 --- a/Sources/Testing/Attachments/Attachable.swift +++ b/Sources/Testing/Attachments/Attachable.swift @@ -24,9 +24,12 @@ /// A type should conform to this protocol if it can be represented as a /// sequence of bytes that would be diagnostically useful if a test fails. If a /// type cannot conform directly to this protocol (such as a non-final class or -/// a type declared in a third-party module), you can create a container type -/// that conforms to ``AttachableContainer`` to act as a proxy. -@_spi(Experimental) +/// a type declared in a third-party module), you can create a wrapper type that +/// conforms to ``AttachableWrapper`` to act as a proxy. +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// } public protocol Attachable: ~Copyable { /// An estimate of the number of bytes of memory needed to store this value as /// an attachment. @@ -42,6 +45,10 @@ public protocol Attachable: ~Copyable { /// /// - Complexity: O(1) unless `Self` conforms to `Collection`, in which case /// up to O(_n_) where _n_ is the length of the collection. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } var estimatedAttachmentByteCount: Int? { get } /// Call a function and pass a buffer representing this instance to it. @@ -64,6 +71,10 @@ public protocol Attachable: ~Copyable { /// the buffer to contain an image in PNG format, JPEG format, etc., but it /// would not be idiomatic for the buffer to contain a textual description of /// the image. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } borrowing func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R /// Generate a preferred name for the given attachment. @@ -80,6 +91,10 @@ public protocol Attachable: ~Copyable { /// when adding `attachment` to a test report or persisting it to storage. The /// default implementation of this function returns `suggestedName` without /// any changes. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String } @@ -119,28 +134,24 @@ extension Attachable where Self: StringProtocol { // Implement the protocol requirements for byte arrays and buffers so that // developers can attach raw data when needed. -@_spi(Experimental) extension Array: Attachable { public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } } -@_spi(Experimental) extension ContiguousArray: Attachable { public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } } -@_spi(Experimental) extension ArraySlice: Attachable { public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } } -@_spi(Experimental) extension String: Attachable { public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { var selfCopy = self @@ -150,7 +161,6 @@ extension String: Attachable { } } -@_spi(Experimental) extension Substring: Attachable { public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { var selfCopy = self diff --git a/Sources/Testing/Attachments/AttachableContainer.swift b/Sources/Testing/Attachments/AttachableWrapper.swift similarity index 70% rename from Sources/Testing/Attachments/AttachableContainer.swift rename to Sources/Testing/Attachments/AttachableWrapper.swift index e4d716e9c..81df52d4d 100644 --- a/Sources/Testing/Attachments/AttachableContainer.swift +++ b/Sources/Testing/Attachments/AttachableWrapper.swift @@ -21,11 +21,22 @@ /// A type can conform to this protocol if it represents another type that /// cannot directly conform to ``Attachable``, such as a non-final class or a /// type declared in a third-party module. -@_spi(Experimental) -public protocol AttachableContainer: Attachable, ~Copyable { - /// The type of the attachable value represented by this type. - associatedtype AttachableValue +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// } +public protocol AttachableWrapper: Attachable, ~Copyable { + /// The type of the underlying value represented by this type. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } + associatedtype Wrapped - /// The attachable value represented by this instance. - var attachableValue: AttachableValue { get } + /// The underlying value represented by this instance. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } + var wrappedValue: Wrapped { get } } diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index d7c1cddb7..c11ca7a50 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -18,7 +18,10 @@ private import _TestingInternals /// of some type that conforms to ``Attachable``. Initialize an instance of /// ``Attachment`` with that value and, optionally, a preferred filename to use /// when writing to disk. -@_spi(Experimental) +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// } public struct Attachment: ~Copyable where AttachableValue: Attachable & ~Copyable { /// Storage for ``attachableValue-7dyjv``. fileprivate var _attachableValue: AttachableValue @@ -51,6 +54,10 @@ public struct Attachment: ~Copyable where AttachableValue: Atta /// testing library may substitute a different filename as needed. If the /// value of this property has not been explicitly set, the testing library /// will attempt to generate its own value. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } public var preferredName: String { let suggestedName = if let _preferredName, !_preferredName.isEmpty { _preferredName @@ -90,6 +97,10 @@ extension Attachment where AttachableValue: ~Copyable { /// - sourceLocation: The source location of the call to this initializer. /// This value is used when recording issues associated with the /// attachment. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } public init(_ attachableValue: consuming AttachableValue, named preferredName: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) { self._attachableValue = attachableValue self._preferredName = preferredName @@ -97,7 +108,7 @@ extension Attachment where AttachableValue: ~Copyable { } } -@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +@_spi(ForToolsIntegrationOnly) extension Attachment where AttachableValue == AnyAttachable { /// Create a type-erased attachment from an instance of ``Attachment``. /// @@ -105,7 +116,7 @@ extension Attachment where AttachableValue == AnyAttachable { /// - attachment: The attachment to type-erase. fileprivate init(_ attachment: Attachment) { self.init( - _attachableValue: AnyAttachable(attachableValue: attachment.attachableValue), + _attachableValue: AnyAttachable(wrappedValue: attachment.attachableValue), fileSystemPath: attachment.fileSystemPath, _preferredName: attachment._preferredName, sourceLocation: attachment.sourceLocation @@ -114,7 +125,7 @@ extension Attachment where AttachableValue == AnyAttachable { } #endif -/// A type-erased container type that represents any attachable value. +/// A type-erased wrapper type that represents any attachable value. /// /// This type is not generally visible to developers. It is used when posting /// events of kind ``Event/Kind/valueAttached(_:)``. Test tools authors who use @@ -125,54 +136,55 @@ extension Attachment where AttachableValue == AnyAttachable { // Swift's type system requires that this type be at least as visible as // `Event.Kind.valueAttached(_:)`, otherwise it would be declared private. // } -@_spi(Experimental) @_spi(ForToolsIntegrationOnly) -public struct AnyAttachable: AttachableContainer, Copyable, Sendable { +@_spi(ForToolsIntegrationOnly) +public struct AnyAttachable: AttachableWrapper, Copyable, Sendable { #if !SWT_NO_LAZY_ATTACHMENTS - public typealias AttachableValue = any Attachable & Sendable /* & Copyable rdar://137614425 */ + public typealias Wrapped = any Attachable & Sendable /* & Copyable rdar://137614425 */ #else - public typealias AttachableValue = [UInt8] + public typealias Wrapped = [UInt8] #endif - public var attachableValue: AttachableValue + public var wrappedValue: Wrapped - init(attachableValue: AttachableValue) { - self.attachableValue = attachableValue + init(wrappedValue: Wrapped) { + self.wrappedValue = wrappedValue } public var estimatedAttachmentByteCount: Int? { - attachableValue.estimatedAttachmentByteCount + wrappedValue.estimatedAttachmentByteCount } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - func open(_ attachableValue: T, for attachment: borrowing Attachment) throws -> R where T: Attachable & Sendable & Copyable { + func open(_ wrappedValue: T, for attachment: borrowing Attachment) throws -> R where T: Attachable & Sendable & Copyable { let temporaryAttachment = Attachment( - _attachableValue: attachableValue, + _attachableValue: wrappedValue, fileSystemPath: attachment.fileSystemPath, _preferredName: attachment._preferredName, sourceLocation: attachment.sourceLocation ) return try temporaryAttachment.withUnsafeBytes(body) } - return try open(attachableValue, for: attachment) + return try open(wrappedValue, for: attachment) } public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { - func open(_ attachableValue: T, for attachment: borrowing Attachment) -> String where T: Attachable & Sendable & Copyable { + func open(_ wrappedValue: T, for attachment: borrowing Attachment) -> String where T: Attachable & Sendable & Copyable { let temporaryAttachment = Attachment( - _attachableValue: attachableValue, + _attachableValue: wrappedValue, fileSystemPath: attachment.fileSystemPath, _preferredName: attachment._preferredName, sourceLocation: attachment.sourceLocation ) return temporaryAttachment.preferredName } - return open(attachableValue, for: attachment) + return open(wrappedValue, for: attachment) } } // MARK: - Describing an attachment extension Attachment where AttachableValue: ~Copyable { + @_documentation(visibility: private) public var description: String { let typeInfo = TypeInfo(describing: AttachableValue.self) return #""\#(preferredName)": instance of '\#(typeInfo.unqualifiedName)'"# @@ -180,6 +192,9 @@ extension Attachment where AttachableValue: ~Copyable { } extension Attachment: CustomStringConvertible { + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } public var description: String { #""\#(preferredName)": \#(String(describingForTest: attachableValue))"# } @@ -187,9 +202,12 @@ extension Attachment: CustomStringConvertible { // MARK: - Getting an attachable value from an attachment -@_spi(Experimental) extension Attachment where AttachableValue: ~Copyable { /// The value of this attachment. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } @_disfavoredOverload public var attachableValue: AttachableValue { _read { yield _attachableValue @@ -197,21 +215,24 @@ extension Attachment where AttachableValue: ~Copyable { } } -@_spi(Experimental) -extension Attachment where AttachableValue: AttachableContainer & ~Copyable { +extension Attachment where AttachableValue: AttachableWrapper & ~Copyable { /// The value of this attachment. /// - /// When the attachable value's type conforms to ``AttachableContainer``, the - /// value of this property equals the container's underlying attachable value. + /// When the attachable value's type conforms to ``AttachableWrapper``, the + /// value of this property equals the wrapper's underlying attachable value. /// To access the attachable value as an instance of `T` (where `T` conforms - /// to ``AttachableContainer``), specify the type explicitly: + /// to ``AttachableWrapper``), specify the type explicitly: /// /// ```swift /// let attachableValue = attachment.attachableValue as T /// ``` - public var attachableValue: AttachableValue.AttachableValue { + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } + public var attachableValue: AttachableValue.Wrapped { _read { - yield attachableValue.attachableValue + yield attachableValue.wrappedValue } } } @@ -235,6 +256,10 @@ extension Attachment where AttachableValue: Sendable & Copyable { /// disk. /// /// An attachment can only be attached once. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } @_documentation(visibility: private) public static func record(_ attachment: consuming Self, sourceLocation: SourceLocation = #_sourceLocation) { var attachmentCopy = Attachment(attachment) @@ -263,6 +288,10 @@ extension Attachment where AttachableValue: Sendable & Copyable { /// attaches it to the current test. /// /// An attachment can only be attached once. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } @_documentation(visibility: private) public static func record(_ attachableValue: consuming AttachableValue, named preferredName: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) { record(Self(attachableValue, named: preferredName, sourceLocation: sourceLocation), sourceLocation: sourceLocation) @@ -286,12 +315,16 @@ extension Attachment where AttachableValue: ~Copyable { /// disk. /// /// An attachment can only be attached once. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } public static func record(_ attachment: consuming Self, sourceLocation: SourceLocation = #_sourceLocation) { do { let attachmentCopy = try attachment.withUnsafeBytes { buffer in - let attachableContainer = AnyAttachable(attachableValue: Array(buffer)) + let attachableWrapper = AnyAttachable(wrappedValue: Array(buffer)) return Attachment( - _attachableValue: attachableContainer, + _attachableValue: attachableWrapper, fileSystemPath: attachment.fileSystemPath, _preferredName: attachment.preferredName, // invokes preferredName(for:basedOn:) sourceLocation: sourceLocation @@ -325,6 +358,10 @@ extension Attachment where AttachableValue: ~Copyable { /// attaches it to the current test. /// /// An attachment can only be attached once. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } public static func record(_ attachableValue: consuming AttachableValue, named preferredName: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) { record(Self(attachableValue, named: preferredName, sourceLocation: sourceLocation), sourceLocation: sourceLocation) } @@ -349,6 +386,10 @@ extension Attachment where AttachableValue: ~Copyable { /// test report or to a file on disk. This function calls the /// ``Attachable/withUnsafeBytes(for:_:)`` function on this attachment's /// ``attachableValue-2tnj5`` property. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } @inlinable public borrowing func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try attachableValue.withUnsafeBytes(for: self, body) } @@ -382,7 +423,7 @@ extension Attachment where AttachableValue: ~Copyable { /// This function is provided as a convenience to allow tools authors to write /// attachments to persistent storage the same way that Swift Package Manager /// does. You are not required to use this function. - @_spi(Experimental) @_spi(ForToolsIntegrationOnly) + @_spi(ForToolsIntegrationOnly) public borrowing func write(toFileInDirectoryAtPath directoryPath: String) throws -> String { try write( toFileInDirectoryAtPath: directoryPath, diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 7e07636d5..81a0c550a 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -22,7 +22,7 @@ add_library(Testing ABI/Encoded/ABI.EncodedMessage.swift ABI/Encoded/ABI.EncodedTest.swift Attachments/Attachable.swift - Attachments/AttachableContainer.swift + Attachments/AttachableWrapper.swift Attachments/Attachment.swift Events/Clock.swift Events/Event.swift diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index b81f1c2c7..0be14ae88 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -102,7 +102,6 @@ public struct Event: Sendable { /// /// - Parameters: /// - attachment: The attachment that was created. - @_spi(Experimental) indirect case valueAttached(_ attachment: Attachment) /// A test ended. diff --git a/Sources/Testing/Events/Recorder/Event.Symbol.swift b/Sources/Testing/Events/Recorder/Event.Symbol.swift index 846fb2d4d..3354691fb 100644 --- a/Sources/Testing/Events/Recorder/Event.Symbol.swift +++ b/Sources/Testing/Events/Recorder/Event.Symbol.swift @@ -44,7 +44,6 @@ extension Event { case details /// The symbol to use when describing an instance of ``Attachment``. - @_spi(Experimental) case attachment } } diff --git a/Sources/Testing/Issues/Issue.swift b/Sources/Testing/Issues/Issue.swift index 5d7449b7b..d210ecec7 100644 --- a/Sources/Testing/Issues/Issue.swift +++ b/Sources/Testing/Issues/Issue.swift @@ -65,7 +65,6 @@ public struct Issue: Sendable { /// /// - Parameters: /// - error: The error which was associated with this issue. - @_spi(Experimental) case valueAttachmentFailed(_ error: any Error) /// An issue occurred due to misuse of the testing library. diff --git a/Sources/Testing/Running/Configuration.swift b/Sources/Testing/Running/Configuration.swift index 30a2ce303..b8c48aa79 100644 --- a/Sources/Testing/Running/Configuration.swift +++ b/Sources/Testing/Running/Configuration.swift @@ -237,7 +237,6 @@ public struct Configuration: Sendable { /// The value of this property must refer to a directory on the local file /// system that already exists and which the current user can write to. If it /// is a relative path, it is resolved to an absolute path automatically. - @_spi(Experimental) public var attachmentsPath: String? { get { _attachmentsPath diff --git a/Sources/Testing/Testing.docc/Attachments.md b/Sources/Testing/Testing.docc/Attachments.md new file mode 100644 index 000000000..0da40c201 --- /dev/null +++ b/Sources/Testing/Testing.docc/Attachments.md @@ -0,0 +1,32 @@ +# Attachments + + + +Attach values to tests to help diagnose issues and gather feedback. + +## Overview + +Attach values such as strings and files to tests. Implement the ``Attachable`` +protocol to create your own attachable types. + +## Topics + +### Attaching values to tests + +- ``Attachment`` +- ``Attachable`` +- ``AttachableWrapper`` + + diff --git a/Sources/Testing/Testing.docc/Documentation.md b/Sources/Testing/Testing.docc/Documentation.md index 901c0e3a6..cc4001889 100644 --- a/Sources/Testing/Testing.docc/Documentation.md +++ b/Sources/Testing/Testing.docc/Documentation.md @@ -69,3 +69,7 @@ their problems. ### Test customization - + +### Data collection + +- diff --git a/Sources/Testing/Testing.docc/MigratingFromXCTest.md b/Sources/Testing/Testing.docc/MigratingFromXCTest.md index e3a9d961f..60744ba7a 100644 --- a/Sources/Testing/Testing.docc/MigratingFromXCTest.md +++ b/Sources/Testing/Testing.docc/MigratingFromXCTest.md @@ -742,6 +742,66 @@ suite serially: For more information, see . +### Attach values + +In XCTest, you can create an instance of [`XCTAttachment`](https://developer.apple.com/documentation/xctest/xctattachment) +representing arbitrary data, files, property lists, encodable objects, images, +and other types of information that would be useful to have available if a test +fails. Swift Testing has an ``Attachment`` type that serves much the same +purpose. + +To attach a value from a test to the output of a test run, that value must +conform to the ``Attachable`` protocol. The testing library provides default +conformances for various standard library and Foundation types. + +If you want to attach a value of another type, and that type already conforms to +[`Encodable`](https://developer.apple.com/documentation/swift/encodable) or to +[`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding), +the testing library automatically provides a default implementation when you +import Foundation: + +@Row { + @Column { + ```swift + // Before + import Foundation + + class Tortilla: NSSecureCoding { /* ... */ } + + func testTortillaIntegrity() async { + let tortilla = Tortilla(diameter: .large) + ... + let attachment = XCTAttachment( + archivableObject: tortilla + ) + self.add(attachment) + } + ``` + } + @Column { + ```swift + // After + import Foundation + + struct Tortilla: Codable, Attachable { /* ... */ } + + @Test func tortillaIntegrity() async { + let tortilla = Tortilla(diameter: .large) + ... + Attachment.record(tortilla) + } + ``` + } +} + +If you have a type that does not (or cannot) conform to `Encodable` or +`NSSecureCoding`, or if you want fine-grained control over how it is serialized +when attaching it to a test, you can provide your own implementation of +``Attachable/withUnsafeBytes(for:_:)``. + + + ## See Also - diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 126633776..0281b4091 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -8,11 +8,11 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing +@testable @_spi(ForToolsIntegrationOnly) import Testing private import _TestingInternals #if canImport(Foundation) import Foundation -@_spi(Experimental) import _Testing_Foundation +import _Testing_Foundation #endif #if canImport(CoreGraphics) import CoreGraphics