Skip to content
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

Promote attachments to API #973

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 @@ -32,11 +32,10 @@ import UniformTypeIdentifiers
/// such a requirement, and all image types we care about are non-final
/// classes. Thus, the compiler will steadfastly refuse to allow non-final
/// classes to conform to the `Attachable` protocol. We could get around this
/// by changing the signature of `withUnsafeBufferPointer()` so that the
/// generic parameter to `Attachment` is not `Self`, but that would defeat
/// much of the purpose of making `Attachment` generic in the first place.
/// (And no, the language does not let us write `where T: Self` anywhere
/// useful.)
/// by changing the signature of `withUnsafeBytes()` so that the generic
/// parameter to `Attachment` is not `Self`, but that would defeat much of
/// the purpose of making `Attachment` generic in the first place. (And no,
/// the language does not let us write `where T: Self` anywhere useful.)

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

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

// Convert the image to a CGImage.
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,11 +18,13 @@ 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 withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try _Testing_Foundation.withUnsafeBytes(encoding: self, for: attachment, body)
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
//

#if canImport(Foundation)
@_spi(Experimental) public import Testing
public import Testing
private import Foundation

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

let data: Data
Expand All @@ -53,7 +53,10 @@ func withUnsafeBufferPointer<E, R>(encoding attachableValue: borrowing E, for at
// 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,8 +89,12 @@ 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`.
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)
///
/// @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)
}
}
#endif
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,7 +49,11 @@ 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`.
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
///
/// @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)

var data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true)
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,7 +32,6 @@ extension URL {
}
}

@_spi(Experimental)
extension Attachment where AttachableValue == _AttachableURLContainer {
#if SWT_TARGET_OS_APPLE
/// An operation queue to use for asynchronously reading data from disk.
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
//

#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 {
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
/// @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,15 +9,14 @@
//

#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 {
/// The underlying URL.
var url: URL
Expand All @@ -36,7 +35,7 @@ extension _AttachableURLContainer: AttachableContainer {
url
}

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

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
36 changes: 23 additions & 13 deletions Sources/Testing/Attachments/Attachable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
/// 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)
///
/// @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 @@ -41,6 +44,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 @@ -63,7 +70,11 @@ 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.
borrowing func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R
///
/// @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 @@ -79,6 +90,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 All @@ -99,7 +114,7 @@ extension Attachable where Self: Collection, Element == UInt8 {
count
}

// We do not provide an implementation of withUnsafeBufferPointer(for:_:) here
// We do not provide an implementation of withUnsafeBytes(for:_:) here
// because there is no way in the standard library to statically detect if a
// collection can provide contiguous storage (_HasContiguousBytes is not API.)
// If withContiguousStorageIfAvailable(_:) fails, we don't want to make a
Expand All @@ -118,40 +133,35 @@ 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 withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
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 withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
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 withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
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 withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
var selfCopy = self
return try selfCopy.withUTF8 { utf8 in
try body(UnsafeRawBufferPointer(utf8))
}
}
}

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