diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift index ed1e6a2ee..6bb6f3744 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -13,6 +13,8 @@ public import UniformTypeIdentifiers +@_spi(Experimental) +@available(_uttypesAPI, *) extension Attachment { /// Initialize an instance of this type that encloses the given image. /// @@ -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( - attachableValue: T, - named preferredName: String?, - contentType: (any Sendable)?, - encodingQuality: Float, - 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. - /// - /// - 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. @@ -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( _ attachableValue: T, named preferredName: String? = nil, - as contentType: UTType?, + as contentType: UTType? = nil, encodingQuality: Float = 1.0, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - 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( - _ 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( + _ image: consuming T, named preferredName: String? = nil, + as contentType: UTType? = nil, encodingQuality: Float = 1.0, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - 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 diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift index 7aa1fd139..9b8ea6788 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift @@ -48,6 +48,7 @@ import UniformTypeIdentifiers /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) @_spi(Experimental) +@available(_uttypesAPI, *) public struct _AttachableImageWrapper: Sendable where Image: AttachableAsCGImage { /// The underlying image. /// @@ -61,7 +62,7 @@ public struct _AttachableImageWrapper: 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. /// @@ -70,14 +71,9 @@ public struct _AttachableImageWrapper: 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( @@ -92,34 +88,17 @@ public struct _AttachableImageWrapper: 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 } } @@ -127,6 +106,7 @@ public struct _AttachableImageWrapper: Sendable where Image: AttachableAs // MARK: - +@available(_uttypesAPI, *) extension _AttachableImageWrapper: AttachableWrapper { public var wrappedValue: Image { image @@ -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 } @@ -168,11 +149,7 @@ extension _AttachableImageWrapper: AttachableWrapper { } public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { - if #available(_uttypesAPI, *) { - return (suggestedName as NSString).appendingPathExtension(for: computedContentType) - } - - return suggestedName + (suggestedName as NSString).appendingPathExtension(for: computedContentType) } } #endif diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index be940371e..efa1eccfd 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -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 {