Skip to content

Promote attachments to API #973

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Documentation/ABI/JSON.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,19 +188,24 @@ sufficient information to display the event in a human-readable format.
"kind": <event-kind>,
"instant": <instant>, ; when the event occurred
["issue": <issue>,] ; the recorded issue (if "kind" is "issueRecorded")
["attachment": <attachment>,] ; the attachment (if kind is "valueAttached")
"messages": <array:message>,
["testID": <test-id>,]
}

<event-kind> ::= "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

<issue> ::= {
"isKnown": <bool>, ; is this a known issue or not?
["sourceLocation": <source-location>,] ; where the issue occurred, if known
}

<attachment> ::= {
"path": <string>, ; the absolute path to the attachment on disk
}

<message> ::= {
"symbol": <message-symbol>,
"text": <string>, ; the human-readable text of this message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ extension Attachment {
contentType: (any Sendable)?,
encodingQuality: Float,
sourceLocation: SourceLocation
) where AttachableValue == _AttachableImageContainer<T> {
let imageContainer = _AttachableImageContainer(image: attachableValue, encodingQuality: encodingQuality, contentType: contentType)
self.init(imageContainer, named: preferredName, sourceLocation: sourceLocation)
) where AttachableValue == _AttachableImageWrapper<T> {
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.
Expand Down Expand Up @@ -79,7 +79,7 @@ extension Attachment {
as contentType: UTType?,
encodingQuality: Float = 1.0,
sourceLocation: SourceLocation = #_sourceLocation
) where AttachableValue == _AttachableImageContainer<T> {
) where AttachableValue == _AttachableImageWrapper<T> {
self.init(attachableValue: attachableValue, named: preferredName, contentType: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation)
}

Expand Down Expand Up @@ -109,7 +109,7 @@ extension Attachment {
named preferredName: String? = nil,
encodingQuality: Float = 1.0,
sourceLocation: SourceLocation = #_sourceLocation
) where AttachableValue == _AttachableImageContainer<T> {
) where AttachableValue == _AttachableImageWrapper<T> {
self.init(attachableValue: attachableValue, named: preferredName, contentType: nil, encodingQuality: encodingQuality, sourceLocation: sourceLocation)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -48,7 +48,7 @@ import UniformTypeIdentifiers
///
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage)
@_spi(Experimental)
public struct _AttachableImageContainer<Image>: Sendable where Image: AttachableAsCGImage {
public struct _AttachableImageWrapper<Image>: Sendable where Image: AttachableAsCGImage {
/// The underlying image.
///
/// `CGImage` and `UIImage` are sendable, but `NSImage` is not. `NSImage`
Expand Down Expand Up @@ -127,8 +127,8 @@ public struct _AttachableImageContainer<Image>: Sendable where Image: Attachable

// MARK: -

extension _AttachableImageContainer: AttachableContainer {
public var attachableValue: Image {
extension _AttachableImageWrapper: AttachableWrapper {
public var wrappedValue: Image {
image
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -53,7 +53,10 @@ func withUnsafeBytes<E, R>(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),
Expand Down Expand Up @@ -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<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try _Testing_Foundation.withUnsafeBytes(encoding: self, for: attachment, body)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
let format = try EncodingFormat(for: attachment)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try withUnsafeBytes(body)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@
//

#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
/// attached indirectly.
///
/// 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

Expand All @@ -31,8 +30,8 @@ public struct _AttachableURLContainer: Sendable {

// MARK: -

extension _AttachableURLContainer: AttachableContainer {
public var attachableValue: URL {
extension _AttachableURLWrapper: AttachableWrapper {
public var wrappedValue: URL {
url
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Overlays/_Testing_Foundation/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 3 additions & 5 deletions Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ extension ABI {
case testStarted
case testCaseStarted
case issueRecorded
case valueAttached = "_valueAttached"
case valueAttached
case testCaseEnded
case testEnded
case testSkipped
Expand All @@ -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<V>?
var attachment: EncodedAttachment<V>?

/// Human-readable messages associated with this event that can be presented
/// to the user.
Expand Down Expand Up @@ -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
Expand Down
26 changes: 18 additions & 8 deletions Sources/Testing/Attachments/Attachable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R

/// Generate a preferred name for the given attachment.
Expand All @@ -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<Self>, basedOn suggestedName: String) -> String
}

Expand Down Expand Up @@ -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<UInt8>: Attachable {
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try withUnsafeBytes(body)
}
}

@_spi(Experimental)
extension ContiguousArray<UInt8>: Attachable {
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try withUnsafeBytes(body)
}
}

@_spi(Experimental)
extension ArraySlice<UInt8>: Attachable {
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try withUnsafeBytes(body)
}
}

@_spi(Experimental)
extension String: Attachable {
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
var selfCopy = self
Expand All @@ -150,7 +161,6 @@ extension String: Attachable {
}
}

@_spi(Experimental)
extension Substring: Attachable {
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
var selfCopy = self
Expand Down
Loading