Skip to content

Require UTType for image attachments. #1192

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 4 commits into from
Jul 2, 2025
Merged
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 @@ -13,6 +13,8 @@

public import UniformTypeIdentifiers

@_spi(Experimental)
@available(_uttypesAPI, *)
extension Attachment {
/// Initialize an instance of this type that encloses the given image.
///
Expand All @@ -23,46 +25,9 @@ extension Attachment {
/// 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.
/// For the lowest supported quality, pass `0.0`. For the highest
/// supported quality, pass `1.0`.
/// - sourceLocation: The source location of the call to this initializer.
/// This value is used when recording issues associated with the
/// attachment.
Expand All @@ -71,46 +36,72 @@ extension Attachment {
/// ``AttachableAsCGImage`` protocol and can be attached to a test:
///
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage)
@_spi(Experimental)
@available(_uttypesAPI, *)
///
/// The testing library uses the image format specified by `contentType`. Pass
/// `nil` to let the testing library decide which image format to use. If you
/// pass `nil`, then the image format that the testing library uses depends on
/// the path extension you specify in `preferredName`, if any. If you do not
/// specify a path extension, or if the path extension you specify doesn't
/// correspond to an image format the operating system knows how to write, the
/// testing library selects an appropriate image format for you.
///
/// If the target image format does not support variable-quality encoding,
/// the value of the `encodingQuality` argument is ignored. If `contentType`
/// is not `nil` and does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
/// the result is undefined.
public init<T>(
_ attachableValue: T,
named preferredName: String? = nil,
as contentType: UTType?,
as contentType: UTType? = nil,
encodingQuality: Float = 1.0,
sourceLocation: SourceLocation = #_sourceLocation
) where AttachableValue == _AttachableImageWrapper<T> {
self.init(attachableValue: attachableValue, named: preferredName, contentType: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation)
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.
/// Attach an image to the current test.
///
/// - 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.
/// - image: The value to attach.
/// - 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`.
/// - 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.
/// For the lowest supported quality, pass `0.0`. For the highest
/// supported quality, pass `1.0`.
/// - sourceLocation: The source location of the call to this function.
///
/// This function creates a new instance of ``Attachment`` wrapping `image`
/// and immediately attaches it to the current test.
///
/// 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,
///
/// The testing library uses the image format specified by `contentType`. Pass
/// `nil` to let the testing library decide which image format to use. If you
/// pass `nil`, then the image format that the testing library uses depends on
/// the path extension you specify in `preferredName`, if any. If you do not
/// specify a path extension, or if the path extension you specify doesn't
/// correspond to an image format the operating system knows how to write, the
/// testing library selects an appropriate image format for you.
///
/// If the target image format does not support variable-quality encoding,
/// the value of the `encodingQuality` argument is ignored. If `contentType`
/// is not `nil` and does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
/// the result is undefined.
public static func record<T>(
_ image: consuming T,
named preferredName: String? = nil,
as contentType: UTType? = nil,
encodingQuality: Float = 1.0,
sourceLocation: SourceLocation = #_sourceLocation
) where AttachableValue == _AttachableImageWrapper<T> {
self.init(attachableValue: attachableValue, named: preferredName, contentType: nil, encodingQuality: encodingQuality, sourceLocation: sourceLocation)
let attachment = Self(image, named: preferredName, as: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation)
Self.record(attachment, sourceLocation: sourceLocation)
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import UniformTypeIdentifiers
///
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage)
@_spi(Experimental)
@available(_uttypesAPI, *)
public struct _AttachableImageWrapper<Image>: Sendable where Image: AttachableAsCGImage {
/// The underlying image.
///
Expand All @@ -61,7 +62,7 @@ public struct _AttachableImageWrapper<Image>: Sendable where Image: AttachableAs
var encodingQuality: Float

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

/// The content type to use when encoding the image.
///
Expand All @@ -70,14 +71,9 @@ public struct _AttachableImageWrapper<Image>: Sendable where Image: AttachableAs
///
/// 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
}
_contentType ?? .image
}
set {
precondition(
Expand All @@ -92,41 +88,25 @@ public struct _AttachableImageWrapper<Image>: Sendable where Image: AttachableAs
/// 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
if contentType == .image {
return encodingQuality < 1.0 ? .jpeg : .png
}
return contentType
}

/// 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: Image, encodingQuality: Float, contentType: UTType?) {
self.image = image._makeCopyForAttachment()
self.encodingQuality = encodingQuality
if #available(_uttypesAPI, *), let contentType = contentType as? UTType {
if let contentType {
self.contentType = contentType
}
}
}

// MARK: -

@available(_uttypesAPI, *)
extension _AttachableImageWrapper: AttachableWrapper {
public var wrappedValue: Image {
image
Expand All @@ -139,6 +119,7 @@ extension _AttachableImageWrapper: AttachableWrapper {
let attachableCGImage = try image.attachableCGImage

// Create the image destination.
let typeIdentifier = computedContentType.identifier as CFString
guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, typeIdentifier, 1, nil) else {
throw ImageAttachmentError.couldNotCreateImageDestination
}
Expand Down Expand Up @@ -168,11 +149,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
(suggestedName as NSString).appendingPathExtension(for: computedContentType)
}
}
#endif
17 changes: 17 additions & 0 deletions Tests/TestingTests/AttachmentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,23 @@ extension AttachmentTests {
Attachment.record(attachment)
}

@available(_uttypesAPI, *)
@Test func attachCGImageDirectly() async throws {
await confirmation("Attachment detected") { valueAttached in
var configuration = Configuration()
configuration.eventHandler = { event, _ in
if case .valueAttached = event.kind {
valueAttached()
}
}

await Test {
let image = try Self.cgImage.get()
Attachment.record(image, named: "diamond.jpg")
}.run(configuration: configuration)
}
}

@available(_uttypesAPI, *)
@Test(arguments: [Float(0.0).nextUp, 0.25, 0.5, 0.75, 1.0], [.png as UTType?, .jpeg, .gif, .image, nil])
func attachCGImage(quality: Float, type: UTType?) throws {
Expand Down