Skip to content

[WIP] Add an associated AttachmentMetadata type to Attachable. #824

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics)
@_spi(Experimental) public import Testing

public import UniformTypeIdentifiers

extension Attachment {
/// Initialize an instance of this type that encloses the given image.
///
Expand All @@ -22,47 +20,9 @@ extension Attachment {
/// - preferredName: The preferred name of the attachment when writing it
/// to a test report or to disk. If `nil`, the testing library attempts
/// to derive a reasonable filename for the attached value.
/// - contentType: The image format with which to encode `attachableValue`.
/// If this type does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
/// the result is undefined. Pass `nil` to let the testing library decide
/// which image format to use.
/// - encodingQuality: The encoding quality to use when encoding the image.
/// If the image format used for encoding (specified by the `contentType`
/// argument) does not support variable-quality encoding, the value of
/// this argument is ignored.
/// - sourceLocation: The source location of the call to this initializer.
/// This value is used when recording issues associated with the
/// attachment.
///
/// This is the designated initializer for this type when attaching an image
/// that conforms to ``AttachableAsCGImage``.
fileprivate init<T>(
attachableValue: T,
named preferredName: String?,
contentType: (any Sendable)?,
encodingQuality: Float,
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.
///
/// - Parameters:
/// - attachableValue: The value that will be attached to the output of
/// the test run.
/// - preferredName: The preferred name of the attachment when writing it
/// to a test report or to disk. If `nil`, the testing library attempts
/// to derive a reasonable filename for the attached value.
/// - contentType: The image format with which to encode `attachableValue`.
/// If this type does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
/// the result is undefined. Pass `nil` to let the testing library decide
/// which image format to use.
/// - encodingQuality: The encoding quality to use when encoding the image.
/// If the image format used for encoding (specified by the `contentType`
/// argument) does not support variable-quality encoding, the value of
/// this argument is ignored.
/// - metadata: Optional metadata such as the image format to use when
/// encoding `image`. If `nil`, the testing library will infer the format
/// and other metadata.
/// - sourceLocation: The source location of the call to this initializer.
/// This value is used when recording issues associated with the
/// attachment.
Expand All @@ -72,45 +32,14 @@ extension Attachment {
///
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage)
@_spi(Experimental)
@available(_uttypesAPI, *)
public init<T>(
_ attachableValue: T,
named preferredName: String? = nil,
as contentType: UTType?,
encodingQuality: Float = 1.0,
sourceLocation: SourceLocation = #_sourceLocation
) where AttachableValue == _AttachableImageWrapper<T> {
self.init(attachableValue: attachableValue, named: preferredName, contentType: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation)
}

/// Initialize an instance of this type that encloses the given image.
///
/// - Parameters:
/// - attachableValue: The value that will be attached to the output of
/// the test run.
/// - preferredName: The preferred name of the attachment when writing it
/// to a test report or to disk. If `nil`, the testing library attempts
/// to derive a reasonable filename for the attached value.
/// - encodingQuality: The encoding quality to use when encoding the image.
/// If the image format used for encoding (specified by the `contentType`
/// argument) does not support variable-quality encoding, the value of
/// this argument is ignored.
/// - sourceLocation: The source location of the call to this initializer.
/// This value is used when recording issues associated with the
/// attachment.
///
/// The following system-provided image types conform to the
/// ``AttachableAsCGImage`` protocol and can be attached to a test:
///
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage)
@_spi(Experimental)
public init<T>(
_ attachableValue: T,
named preferredName: String? = nil,
encodingQuality: Float = 1.0,
named preferredName: String?,
metadata: ImageAttachmentMetadata = .init(),
sourceLocation: SourceLocation = #_sourceLocation
) where AttachableValue == _AttachableImageWrapper<T> {
self.init(attachableValue: attachableValue, named: preferredName, contentType: nil, encodingQuality: encodingQuality, sourceLocation: sourceLocation)
let imageContainer = _AttachableImageWrapper(attachableValue)
self.init(imageContainer, named: preferredName, metadata: metadata, sourceLocation: sourceLocation)
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics)
@_spi(Experimental) public import Testing
private import CoreGraphics

public import UniformTypeIdentifiers

/// A type defining metadata used when attaching an image to a test.
///
/// The following system-provided image types can be attached to a test:
///
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage)
@_spi(Experimental)
public struct ImageAttachmentMetadata: Sendable {
/// The encoding quality to use when encoding the represented image.
///
/// If the image format used for encoding (specified by the ``contentType``
/// property) does not support variable-quality encoding, the value of this
/// property is ignored.
public var encodingQuality: Float

/// Storage for ``contentType``.
private var _contentType: (any Sendable)?

/// The content type to use when encoding the image.
///
/// The testing library uses this property to determine which image format to
/// encode the associated image as when it is attached to a test.
///
/// If the value of this property does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
/// the result is undefined.
@available(_uttypesAPI, *)
var contentType: UTType {
get {
if let contentType = _contentType as? UTType {
return contentType
} else {
return encodingQuality < 1.0 ? .jpeg : .png
}
}
set {
lazy var newValueDescription = newValue.localizedDescription ?? newValue.identifier
precondition(
newValue.conforms(to: .image),
"An image cannot be attached as an instance of type '\(newValueDescription)'. Use a type that conforms to 'public.image' instead."
)
_contentType = newValue
}
}

/// The content type to use when encoding the image, substituting a concrete
/// type for `UTType.image`.
///
/// This property is not part of the public interface of the testing library.
@available(_uttypesAPI, *)
var computedContentType: UTType {
if let contentType = _contentType as? UTType, contentType != .image {
contentType
} else {
encodingQuality < 1.0 ? .jpeg : .png
}
}

/// The type identifier (as a `CFString`) corresponding to this instance's
/// ``computedContentType`` property.
///
/// The value of this property is used by ImageIO when serializing an image.
///
/// This property is not part of the public interface of the testing library.
/// It is used by ImageIO below.
var typeIdentifier: CFString {
if #available(_uttypesAPI, *) {
computedContentType.identifier as CFString
} else {
encodingQuality < 1.0 ? kUTTypeJPEG : kUTTypePNG
}
}

public init(encodingQuality: Float = 1.0) {
self.encodingQuality = encodingQuality
}

@available(_uttypesAPI, *)
public init(encodingQuality: Float = 1.0, contentType: UTType) {
self.encodingQuality = encodingQuality
self.contentType = contentType
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ public import Testing
private import CoreGraphics

private import ImageIO
import UniformTypeIdentifiers
private import UniformTypeIdentifiers

#if canImport(CoreServices_Private)
private import CoreServices_Private
#endif

/// ## Why can't images directly conform to Attachable?
///
Expand All @@ -24,10 +28,7 @@ import UniformTypeIdentifiers
/// event handler (primarily because `Event` is `Sendable`.) So we would have
/// to eagerly serialize them, which is unnecessarily expensive if we know
/// they're actually concurrency-safe.
/// 2. We would have no place to store metadata such as the encoding quality
/// (although in the future we may introduce a "metadata" associated type to
/// `Attachable` that could store that info.)
/// 3. `Attachable` has a requirement with `Self` in non-parameter, non-return
/// 2. `Attachable` has a requirement with `Self` in non-parameter, non-return
/// position. As far as Swift is concerned, a non-final class cannot satisfy
/// such a requirement, and all image types we care about are non-final
/// classes. Thus, the compiler will steadfastly refuse to allow non-final
Expand Down Expand Up @@ -57,71 +58,8 @@ public struct _AttachableImageWrapper<Image>: Sendable where Image: AttachableAs
/// instances of this type it creates hold "safe" `NSImage` instances.
nonisolated(unsafe) var image: Image

/// The encoding quality to use when encoding the represented image.
var encodingQuality: Float

/// Storage for ``contentType``.
private var _contentType: (any Sendable)?

/// The content type to use when encoding the image.
///
/// The testing library uses this property to determine which image format to
/// encode the associated image as when it is attached to a test.
///
/// If the value of this property does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
/// the result is undefined.
@available(_uttypesAPI, *)
var contentType: UTType {
get {
if let contentType = _contentType as? UTType {
return contentType
} else {
return encodingQuality < 1.0 ? .jpeg : .png
}
}
set {
precondition(
newValue.conforms(to: .image),
"An image cannot be attached as an instance of type '\(newValue.identifier)'. Use a type that conforms to 'public.image' instead."
)
_contentType = newValue
}
}

/// The content type to use when encoding the image, substituting a concrete
/// type for `UTType.image`.
///
/// This property is not part of the public interface of the testing library.
@available(_uttypesAPI, *)
var computedContentType: UTType {
if let contentType = _contentType as? UTType, contentType != .image {
contentType
} else {
encodingQuality < 1.0 ? .jpeg : .png
}
}

/// The type identifier (as a `CFString`) corresponding to this instance's
/// ``computedContentType`` property.
///
/// The value of this property is used by ImageIO when serializing an image.
///
/// This property is not part of the public interface of the testing library.
/// It is used by ImageIO below.
var typeIdentifier: CFString {
if #available(_uttypesAPI, *) {
computedContentType.identifier as CFString
} else {
encodingQuality < 1.0 ? kUTTypeJPEG : kUTTypePNG
}
}

init(image: Image, encodingQuality: Float, contentType: (any Sendable)?) {
init(_ image: borrowing Image) {
self.image = image._makeCopyForAttachment()
self.encodingQuality = encodingQuality
if #available(_uttypesAPI, *), let contentType = contentType as? UTType {
self.contentType = contentType
}
}
}

Expand All @@ -132,13 +70,16 @@ extension _AttachableImageWrapper: AttachableWrapper {
image
}

public typealias AttachmentMetadata = ImageAttachmentMetadata

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

// Convert the image to a CGImage.
let attachableCGImage = try image.attachableCGImage

// Create the image destination.
let typeIdentifier = attachment.metadata.typeIdentifier
guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, typeIdentifier, 1, nil) else {
throw ImageAttachmentError.couldNotCreateImageDestination
}
Expand All @@ -147,7 +88,7 @@ extension _AttachableImageWrapper: AttachableWrapper {
let orientation = image._attachmentOrientation
let scaleFactor = image._attachmentScaleFactor
let properties: [CFString: Any] = [
kCGImageDestinationLossyCompressionQuality: CGFloat(encodingQuality),
kCGImageDestinationLossyCompressionQuality: CGFloat(attachment.metadata.encodingQuality),
kCGImagePropertyOrientation: orientation,
kCGImagePropertyDPIWidth: 72.0 * scaleFactor,
kCGImagePropertyDPIHeight: 72.0 * scaleFactor,
Expand All @@ -169,7 +110,7 @@ extension _AttachableImageWrapper: AttachableWrapper {

public borrowing func preferredName(for attachment: borrowing Attachment<Self>, basedOn suggestedName: String) -> String {
if #available(_uttypesAPI, *) {
return (suggestedName as NSString).appendingPathExtension(for: computedContentType)
return (suggestedName as NSString).appendingPathExtension(for: attachment.metadata.computedContentType)
}

return suggestedName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ public import Foundation
/// @Metadata {
/// @Available(Swift, introduced: 6.2)
/// }
extension Attachable where Self: Encodable & NSSecureCoding {
extension Attachable where Self: Encodable & NSSecureCoding, AttachmentMetadata == EncodableAttachmentMetadata? {
@_documentation(visibility: private)
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try _Testing_Foundation.withUnsafeBytes(encoding: self, for: attachment, body)
}

@_documentation(visibility: private)
public func preferredName(for attachment: borrowing Attachment<Self>, basedOn suggestedName: String) -> String {
makePreferredName(from: suggestedName, for: attachment, defaultFormat: .json)
}
}
#endif
Loading