From fd84e29a37f2ad193e0cb2ac640e6cba74717a06 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 7 Oct 2025 14:32:56 -0400 Subject: [PATCH] Apply changes from ST-NNNN. This PR applies the changes from [ST-NNNN](https://github.com/swiftlang/swift-evolution/pull/2985). It merges `AttachableAsCGImage` and `AttachableAsIWICBitmapSource` into a single `AttachableAsImage` protocol and it adjusts the interfaces of `AttachableImageFormat` and `Attachment where AttachableValue: AttachableAsImage`. --- ....swift => NSImage+AttachableAsImage.swift} | 8 +- .../Overlays/_Testing_AppKit/CMakeLists.txt | 2 +- .../Attachments/AttachableAsCGImage.swift | 93 ++++----- .../AttachableImageFormat+UTType.swift | 72 ++----- ....swift => CGImage+AttachableAsImage.swift} | 4 +- ...chableImageWrapper+AttachableWrapper.swift | 92 ++++----- .../_Testing_CoreGraphics/CMakeLists.txt | 4 +- ....swift => CIImage+AttachableAsImage.swift} | 4 +- .../_Testing_CoreImage/CMakeLists.txt | 2 +- ....swift => UIImage+AttachableAsImage.swift} | 13 +- .../Overlays/_Testing_UIKit/CMakeLists.txt | 2 +- .../AttachableAsIWICBitmapSource.swift | 181 +++++++++++------- .../AttachableImageFormat+CLSID.swift | 62 +----- ...HBITMAP+AttachableAsIWICBitmapSource.swift | 1 - .../HICON+AttachableAsIWICBitmapSource.swift | 1 - ...pSource+AttachableAsIWICBitmapSource.swift | 1 - ...Pointer+AttachableAsIWICBitmapSource.swift | 18 +- ...chableImageWrapper+AttachableWrapper.swift | 134 +++---------- .../Support/Additions/GUIDAdditions.swift | 34 ++-- .../Images/AttachableAsImage.swift | 131 +++++++++++++ .../Images/AttachableImageFormat.swift | 47 ++++- ...ift => Attachment+AttachableAsImage.swift} | 9 +- .../Images/_AttachableAsImage.swift | 66 ------- .../Images/_AttachableImageWrapper.swift | 2 +- Sources/Testing/CMakeLists.txt | 4 +- Sources/Testing/Testing.docc/Attachments.md | 5 +- Tests/TestingTests/AttachmentTests.swift | 34 +++- 27 files changed, 505 insertions(+), 521 deletions(-) rename Sources/Overlays/_Testing_AppKit/Attachments/{NSImage+AttachableAsCGImage.swift => NSImage+AttachableAsImage.swift} (92%) rename Sources/Overlays/_Testing_CoreGraphics/Attachments/{CGImage+AttachableAsCGImage.swift => CGImage+AttachableAsImage.swift} (84%) rename Sources/Overlays/_Testing_CoreImage/Attachments/{CIImage+AttachableAsCGImage.swift => CIImage+AttachableAsImage.swift} (90%) rename Sources/Overlays/_Testing_UIKit/Attachments/{UIImage+AttachableAsCGImage.swift => UIImage+AttachableAsImage.swift} (85%) create mode 100644 Sources/Testing/Attachments/Images/AttachableAsImage.swift rename Sources/Testing/Attachments/Images/{Attachment+_AttachableAsImage.swift => Attachment+AttachableAsImage.swift} (97%) delete mode 100644 Sources/Testing/Attachments/Images/_AttachableAsImage.swift diff --git a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsImage.swift similarity index 92% rename from Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift rename to Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsImage.swift index ef601f46f..0db26feb6 100644 --- a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsImage.swift @@ -36,13 +36,13 @@ extension NSImageRep { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } -extension NSImage: AttachableAsCGImage { +extension NSImage: AttachableAsImage, AttachableAsCGImage { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } - public var attachableCGImage: CGImage { + package var attachableCGImage: CGImage { get throws { - let ctm = AffineTransform(scale: _attachmentScaleFactor) as NSAffineTransform + let ctm = AffineTransform(scale: attachmentScaleFactor) as NSAffineTransform guard let result = cgImage(forProposedRect: nil, context: nil, hints: [.ctm: ctm]) else { throw ImageAttachmentError.couldNotCreateCGImage } @@ -50,7 +50,7 @@ extension NSImage: AttachableAsCGImage { } } - public var _attachmentScaleFactor: CGFloat { + package var attachmentScaleFactor: CGFloat { let maxRepWidth = representations.lazy .map { CGFloat($0.pixelsWide) / $0.size.width } .filter { $0 > 0.0 } diff --git a/Sources/Overlays/_Testing_AppKit/CMakeLists.txt b/Sources/Overlays/_Testing_AppKit/CMakeLists.txt index e864509f4..4ce37cfb7 100644 --- a/Sources/Overlays/_Testing_AppKit/CMakeLists.txt +++ b/Sources/Overlays/_Testing_AppKit/CMakeLists.txt @@ -8,7 +8,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") add_library(_Testing_AppKit - Attachments/NSImage+AttachableAsCGImage.swift + Attachments/NSImage+AttachableAsImage.swift ReexportTesting.swift) target_link_libraries(_Testing_AppKit PUBLIC diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index bae0b38f5..57d38a36d 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -9,42 +9,21 @@ // #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -public import CoreGraphics -private import ImageIO +package import CoreGraphics +package import ImageIO +private import UniformTypeIdentifiers /// A protocol describing images that can be converted to instances of -/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment). +/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment) +/// and which can be represented as instances of [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage). /// -/// Instances of types conforming to this protocol do not themselves conform to -/// [`Attachable`](https://developer.apple.com/documentation/testing/attachable). -/// Instead, the testing library provides additional initializers on [`Attachment`](https://developer.apple.com/documentation/testing/attachment) -/// that take instances of such types and handle converting them to image data when needed. -/// -/// You can attach instances of the following system-provided image types to a -/// test: -/// -/// | Platform | Supported Types | -/// |-|-| -/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | -/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | -/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | -/// -/// You do not generally need to add your own conformances to this protocol. If -/// you have an image in another format that needs to be attached to a test, -/// first convert it to an instance of one of the types above. -/// -/// @Metadata { -/// @Available(Swift, introduced: 6.3) -/// } +/// This protocol is not part of the public interface of the testing library. It +/// encapsulates Apple-specific logic for image attachments. @available(_uttypesAPI, *) -public protocol AttachableAsCGImage: _AttachableAsImage, SendableMetatype { +package protocol AttachableAsCGImage: AttachableAsImage, SendableMetatype { /// An instance of `CGImage` representing this image. /// /// - Throws: Any error that prevents the creation of an image. - /// - /// @Metadata { - /// @Available(Swift, introduced: 6.3) - /// } var attachableCGImage: CGImage { get throws } /// The orientation of the image. @@ -53,9 +32,9 @@ public protocol AttachableAsCGImage: _AttachableAsImage, SendableMetatype { /// `CGImagePropertyOrientation`. The default value of this property is /// `.up`. /// - /// This property is not part of the public interface of the testing - /// library. It may be removed in a future update. - var _attachmentOrientation: UInt32 { get } + /// This property is not part of the public interface of the testing library. + /// It may be removed in a future update. + var attachmentOrientation: CGImagePropertyOrientation { get } /// The scale factor of the image. /// @@ -63,28 +42,54 @@ public protocol AttachableAsCGImage: _AttachableAsImage, SendableMetatype { /// originates from a Retina Display screenshot or similar. The default value /// of this property is `1.0`. /// - /// This property is not part of the public interface of the testing - /// library. It may be removed in a future update. - var _attachmentScaleFactor: CGFloat { get } + /// This property is not part of the public interface of the testing library. + /// It may be removed in a future update. + var attachmentScaleFactor: CGFloat { get } } @available(_uttypesAPI, *) extension AttachableAsCGImage { - public var _attachmentOrientation: UInt32 { - CGImagePropertyOrientation.up.rawValue + package var attachmentOrientation: CGImagePropertyOrientation { + .up } - public var _attachmentScaleFactor: CGFloat { + package var attachmentScaleFactor: CGFloat { 1.0 } - public func _deinitializeAttachableValue() {} -} + public func withUnsafeBytes(as imageFormat: AttachableImageFormat, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + let data = NSMutableData() -@available(_uttypesAPI, *) -extension AttachableAsCGImage where Self: Sendable { - public func _copyAttachableValue() -> Self { - self + // Convert the image to a CGImage. + let attachableCGImage = try attachableCGImage + + // Create the image destination. + guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, imageFormat.contentType.identifier as CFString, 1, nil) else { + throw ImageAttachmentError.couldNotCreateImageDestination + } + + // Configure the properties of the image conversion operation. + let orientation = attachmentOrientation + let scaleFactor = attachmentScaleFactor + let properties: [CFString: Any] = [ + kCGImageDestinationLossyCompressionQuality: CGFloat(imageFormat.encodingQuality), + kCGImagePropertyOrientation: orientation, + kCGImagePropertyDPIWidth: 72.0 * scaleFactor, + kCGImagePropertyDPIHeight: 72.0 * scaleFactor, + ] + + // Perform the image conversion. + CGImageDestinationAddImage(dest, attachableCGImage, properties as CFDictionary) + guard CGImageDestinationFinalize(dest) else { + throw ImageAttachmentError.couldNotConvertImage + } + + // Pass the bits of the image out to the body. Note that we have an + // NSMutableData here so we have to use slightly different API than we would + // with an instance of Data. + return try withExtendedLifetime(data) { + try body(UnsafeRawBufferPointer(start: data.bytes, count: data.length)) + } } } #endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift index a427ceec9..f582bc26d 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift @@ -9,58 +9,10 @@ // #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -public import Testing - public import UniformTypeIdentifiers @available(_uttypesAPI, *) extension AttachableImageFormat { - /// Get the content type to use when encoding the image, substituting a - /// concrete type for `UTType.image` in particular. - /// - /// - Parameters: - /// - imageFormat: The image format to use, or `nil` if the developer did - /// not specify one. - /// - preferredName: The preferred name of the image for which a type is - /// needed. - /// - /// - Returns: An instance of `UTType` referring to a concrete image type. - /// - /// This function is not part of the public interface of the testing library. - static func computeContentType(for imageFormat: Self?, withPreferredName preferredName: String) -> UTType { - guard let imageFormat else { - // The developer didn't specify a type. Substitute the generic `.image` - // and solve for that instead. - return computeContentType(for: Self(.image, encodingQuality: 1.0), withPreferredName: preferredName) - } - - switch imageFormat.kind { - case .png: - return .png - case .jpeg: - return .jpeg - case let .systemValue(contentType): - let contentType = contentType as! UTType - if contentType != .image { - // The developer explicitly specified a type. - return contentType - } - - // The developer didn't specify a concrete type, so try to derive one from - // the preferred name's path extension. - let pathExtension = (preferredName as NSString).pathExtension - if !pathExtension.isEmpty, - let contentType = UTType(filenameExtension: pathExtension, conformingTo: .image), - contentType.isDeclared { - return contentType - } - - // We couldn't derive a concrete type from the path extension, so pick - // between PNG and JPEG based on the encoding quality. - return imageFormat.encodingQuality < 1.0 ? .jpeg : .png - } - } - /// The content type corresponding to this image format. /// /// For example, if this image format equals ``png``, the value of this @@ -100,12 +52,19 @@ extension AttachableImageFormat { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } - public init(_ contentType: UTType, encodingQuality: Float = 1.0) { - precondition( - contentType.conforms(to: .image), - "An image cannot be attached as an instance of type '\(contentType.identifier)'. Use a type that conforms to 'public.image' instead." - ) - self.init(kind: .systemValue(contentType), encodingQuality: encodingQuality) + public init(contentType: UTType, encodingQuality: Float = 1.0) { + switch contentType { + case .png: + self.init(kind: .png, encodingQuality: encodingQuality) + case .jpeg: + self.init(kind: .jpeg, encodingQuality: encodingQuality) + default: + precondition( + contentType.conforms(to: .image), + "An image cannot be attached as an instance of type '\(contentType.identifier)'. Use a type that conforms to 'public.image' instead." + ) + self.init(kind: .systemValue(contentType), encodingQuality: encodingQuality) + } } /// Construct an instance of this type with the given path extension and @@ -135,11 +94,12 @@ extension AttachableImageFormat { public init?(pathExtension: String, encodingQuality: Float = 1.0) { let pathExtension = pathExtension.drop { $0 == "." } - guard let contentType = UTType(filenameExtension: String(pathExtension), conformingTo: .image) else { + guard let contentType = UTType(filenameExtension: String(pathExtension), conformingTo: .image), + contentType.isDeclared else { return nil } - self.init(contentType, encodingQuality: encodingQuality) + self.init(contentType: contentType, encodingQuality: encodingQuality) } } #endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsImage.swift similarity index 84% rename from Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift rename to Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsImage.swift index c4a4fd630..dedff803b 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsImage.swift @@ -14,11 +14,11 @@ public import CoreGraphics /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } -extension CGImage: AttachableAsCGImage { +extension CGImage: AttachableAsImage, AttachableAsCGImage { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } - public var attachableCGImage: CGImage { + package var attachableCGImage: CGImage { self } } diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index 3281de11a..16c165ae1 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -9,76 +9,52 @@ // #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -public import Testing private import CoreGraphics -private import ImageIO private import UniformTypeIdentifiers -/// ## Why can't images directly conform to Attachable? -/// -/// Three reasons: -/// -/// 1. Several image classes are not marked `Sendable`, which means that as far -/// as Swift is concerned, they cannot be safely passed to Swift Testing's -/// 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 -/// 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 -/// classes to conform to the `Attachable` protocol. We could get around this -/// 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.) - @available(_uttypesAPI, *) -extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsCGImage { - public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - let data = NSMutableData() - - // Convert the image to a CGImage. - let attachableCGImage = try wrappedValue.attachableCGImage - - // Create the image destination. - let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: attachment.preferredName) - guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, contentType.identifier as CFString, 1, nil) else { - throw ImageAttachmentError.couldNotCreateImageDestination +extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsImage { + /// Get the image format to use when encoding an image, substituting a + /// concrete type for `UTType.image` in particular. + /// + /// - Parameters: + /// - preferredName: The preferred name of the image for which a type is + /// needed. + /// + /// - Returns: An instance of ``AttachableImageFormat`` referring to a + /// concrete image type. + /// + /// This function is not part of the public interface of the testing library. + private func _imageFormat(forPreferredName preferredName: String) -> AttachableImageFormat { + if let imageFormat, case let contentType = imageFormat.contentType, contentType != .image { + // The developer explicitly specified a type. + return imageFormat } - // Configure the properties of the image conversion operation. - let orientation = wrappedValue._attachmentOrientation - let scaleFactor = wrappedValue._attachmentScaleFactor - let properties: [CFString: Any] = [ - kCGImageDestinationLossyCompressionQuality: CGFloat(imageFormat?.encodingQuality ?? 1.0), - kCGImagePropertyOrientation: orientation, - kCGImagePropertyDPIWidth: 72.0 * scaleFactor, - kCGImagePropertyDPIHeight: 72.0 * scaleFactor, - ] - - // Perform the image conversion. - CGImageDestinationAddImage(dest, attachableCGImage, properties as CFDictionary) - guard CGImageDestinationFinalize(dest) else { - throw ImageAttachmentError.couldNotConvertImage + // The developer didn't specify a concrete type, so try to derive one from + // the preferred name's path extension. + let pathExtension = (preferredName as NSString).pathExtension + if !pathExtension.isEmpty, + let contentType = UTType(filenameExtension: pathExtension, conformingTo: .image), + contentType.isDeclared { + return AttachableImageFormat(contentType: contentType) } - // Pass the bits of the image out to the body. Note that we have an - // NSMutableData here so we have to use slightly different API than we would - // with an instance of Data. - return try withExtendedLifetime(data) { - try body(UnsafeRawBufferPointer(start: data.bytes, count: data.length)) - } + // We couldn't derive a concrete type from the path extension, so pick + // between PNG and JPEG based on the encoding quality. + let encodingQuality = imageFormat?.encodingQuality ?? 1.0 + return encodingQuality < 1.0 ? .jpeg : .png + } + + public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + let imageFormat = _imageFormat(forPreferredName: attachment.preferredName) + return try wrappedValue.withUnsafeBytes(as: imageFormat, body) } public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String { - let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: suggestedName) - return (suggestedName as NSString).appendingPathExtension(for: contentType) + let imageFormat = _imageFormat(forPreferredName: suggestedName) + return (suggestedName as NSString).appendingPathExtension(for: imageFormat.contentType) } } #endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt b/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt index 567428150..e408fa517 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt +++ b/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt @@ -8,10 +8,10 @@ if(APPLE) add_library(_Testing_CoreGraphics - Attachments/_AttachableImageWrapper+AttachableWrapper.swift Attachments/AttachableAsCGImage.swift + Attachments/_AttachableImageWrapper+AttachableWrapper.swift Attachments/AttachableImageFormat+UTType.swift - Attachments/CGImage+AttachableAsCGImage.swift + Attachments/CGImage+AttachableAsImage.swift ReexportTesting.swift) target_link_libraries(_Testing_CoreGraphics PUBLIC diff --git a/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsImage.swift similarity index 90% rename from Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift rename to Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsImage.swift index 581de2c7c..130e7e831 100644 --- a/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsImage.swift @@ -15,11 +15,11 @@ public import _Testing_CoreGraphics /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } -extension CIImage: AttachableAsCGImage { +extension CIImage: AttachableAsImage, AttachableAsCGImage { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } - public var attachableCGImage: CGImage { + package var attachableCGImage: CGImage { get throws { guard let result = CIContext().createCGImage(self, from: extent) else { throw ImageAttachmentError.couldNotCreateCGImage diff --git a/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt b/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt index 8c8076b8b..0295fedc7 100644 --- a/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt +++ b/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt @@ -8,7 +8,7 @@ if(APPLE) add_library(_Testing_CoreImage - Attachments/CIImage+AttachableAsCGImage.swift + Attachments/CIImage+AttachableAsImage.swift ReexportTesting.swift) target_link_libraries(_Testing_CoreImage PUBLIC diff --git a/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsImage.swift similarity index 85% rename from Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift rename to Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsImage.swift index 3766b3095..6e9ca6838 100644 --- a/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsImage.swift @@ -12,7 +12,7 @@ public import UIKit public import _Testing_CoreGraphics -private import ImageIO +package import ImageIO #if canImport(UIKitCore_Private) private import UIKitCore_Private #endif @@ -20,11 +20,11 @@ private import UIKitCore_Private /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } -extension UIImage: AttachableAsCGImage { +extension UIImage: AttachableAsImage, AttachableAsCGImage { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } - public var attachableCGImage: CGImage { + package var attachableCGImage: CGImage { get throws { #if canImport(UIKitCore_Private) // _UIImageGetCGImageRepresentation() is an internal UIKit function that @@ -49,8 +49,8 @@ extension UIImage: AttachableAsCGImage { } } - public var _attachmentOrientation: UInt32 { - let result: CGImagePropertyOrientation = switch imageOrientation { + package var attachmentOrientation: CGImagePropertyOrientation { + switch imageOrientation { case .up: .up case .down: .down case .left: .left @@ -61,10 +61,9 @@ extension UIImage: AttachableAsCGImage { case .rightMirrored: .rightMirrored @unknown default: .up } - return result.rawValue } - public var _attachmentScaleFactor: CGFloat { + package var attachmentScaleFactor: CGFloat { scale } } diff --git a/Sources/Overlays/_Testing_UIKit/CMakeLists.txt b/Sources/Overlays/_Testing_UIKit/CMakeLists.txt index e6f4ae9d5..908824704 100644 --- a/Sources/Overlays/_Testing_UIKit/CMakeLists.txt +++ b/Sources/Overlays/_Testing_UIKit/CMakeLists.txt @@ -8,7 +8,7 @@ if(APPLE) add_library(_Testing_UIKit - Attachments/UIImage+AttachableAsCGImage.swift + Attachments/UIImage+AttachableAsImage.swift ReexportTesting.swift) target_link_libraries(_Testing_UIKit PUBLIC diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift index 94dfe4299..130703183 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift @@ -13,25 +13,11 @@ private import Testing public import WinSDK /// A protocol describing images that can be converted to instances of -/// ``Testing/Attachment``. +/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment) +/// and which can be represented as instances of [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) +/// by address. /// -/// Instances of types conforming to this protocol do not themselves conform to -/// ``Testing/Attachable``. Instead, the testing library provides additional -/// initializers on ``Testing/Attachment`` that take instances of such types and -/// handle converting them to image data when needed. -/// -/// You can attach instances of the following system-provided image types to a -/// test: -/// -/// | Platform | Supported Types | -/// |-|-| -/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | -/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | -/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | -/// -/// You do not generally need to add your own conformances to this protocol. If -/// you have an image in another format that needs to be attached to a test, -/// first convert it to an instance of one of the types above. +/// This protocol is not part of the public interface of the testing library. public protocol _AttachableByAddressAsIWICBitmapSource { /// Create a WIC bitmap source representing an instance of this type at the /// given address. @@ -92,42 +78,12 @@ public protocol _AttachableByAddressAsIWICBitmapSource { } /// A protocol describing images that can be converted to instances of -/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment). -/// -/// Instances of types conforming to this protocol do not themselves conform to -/// [`Attachable`](https://developer.apple.com/documentation/testing/attachable). -/// Instead, the testing library provides additional initializers on [`Attachment`](https://developer.apple.com/documentation/testing/attachment) -/// that take instances of such types and handle converting them to image data when needed. -/// -/// You can attach instances of the following system-provided image types to a -/// test: -/// -/// | Platform | Supported Types | -/// |-|-| -/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | -/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | -/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | -/// -/// You do not generally need to add your own conformances to this protocol. If -/// you have an image in another format that needs to be attached to a test, -/// first convert it to an instance of one of the types above. +/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment) +/// and which can be represented as instances of [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource). /// -/// @Metadata { -/// @Available(Swift, introduced: 6.3) -/// } -public protocol AttachableAsIWICBitmapSource: _AttachableAsImage, SendableMetatype { - /// Create a WIC bitmap source representing an instance of this type. - /// - /// - Returns: A pointer to a new WIC bitmap source representing this image. - /// The caller is responsible for releasing this image when done with it. - /// - /// - Throws: Any error that prevented the creation of the WIC bitmap source. - /// - /// @Metadata { - /// @Available(Swift, introduced: 6.3) - /// } - func copyAttachableIWICBitmapSource() throws -> UnsafeMutablePointer - +/// This protocol is not part of the public interface of the testing library. It +/// encapsulates Windows-specific logic for image attachments. +package protocol AttachableAsIWICBitmapSource: AttachableAsImage, SendableMetatype { /// Create a WIC bitmap representing an instance of this type. /// /// - Parameters: @@ -138,26 +94,115 @@ public protocol AttachableAsIWICBitmapSource: _AttachableAsImage, SendableMetaty /// caller is responsible for releasing this image when done with it. /// /// - Throws: Any error that prevented the creation of the WIC bitmap. - /// - /// The default implementation of this function ignores `factory` and calls - /// ``copyAttachableIWICBitmapSource()``. If your implementation of - /// ``copyAttachableIWICBitmapSource()`` needs to create a WIC imaging factory - /// in order to return a result, it is more efficient to implement this - /// function too so that the testing library can pass the WIC imaging factory - /// it creates. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - func _copyAttachableIWICBitmapSource( + func copyAttachableIWICBitmapSource( using factory: UnsafeMutablePointer ) throws -> UnsafeMutablePointer } extension AttachableAsIWICBitmapSource { - public func _copyAttachableIWICBitmapSource( - using factory: UnsafeMutablePointer - ) throws -> UnsafeMutablePointer { - try copyAttachableIWICBitmapSource() + public func withUnsafeBytes(as imageFormat: AttachableImageFormat, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + // Create an in-memory stream to write the image data to. Note that Windows + // documentation recommends SHCreateMemStream() instead, but that function + // does not provide a mechanism to access the underlying memory directly. + var stream: UnsafeMutablePointer? + let rCreateStream = CreateStreamOnHGlobal(nil, true, &stream) + guard S_OK == rCreateStream, let stream else { + throw ImageAttachmentError.comObjectCreationFailed(IStream.self, rCreateStream) + } + defer { + _ = stream.pointee.lpVtbl.pointee.Release(stream) + } + + // Get an imaging factory to create the WIC bitmap and encoder. + let factory = try IWICImagingFactory.create() + defer { + _ = factory.pointee.lpVtbl.pointee.Release(factory) + } + + // Create the bitmap and downcast it to an IWICBitmapSource for later use. + let bitmap = try copyAttachableIWICBitmapSource(using: factory) + defer { + _ = bitmap.pointee.lpVtbl.pointee.Release(bitmap) + } + + // Create the encoder. + let encoder = try withUnsafePointer(to: IID_IWICBitmapEncoder) { IID_IWICBitmapEncoder in + var encoderCLSID = imageFormat.encoderCLSID + var encoder: UnsafeMutableRawPointer? + let rCreate = CoCreateInstance( + &encoderCLSID, + nil, + DWORD(CLSCTX_INPROC_SERVER.rawValue), + IID_IWICBitmapEncoder, + &encoder + ) + guard rCreate == S_OK, let encoder = encoder?.assumingMemoryBound(to: IWICBitmapEncoder.self) else { + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmapEncoder.self, rCreate) + } + return encoder + } + defer { + _ = encoder.pointee.lpVtbl.pointee.Release(encoder) + } + _ = encoder.pointee.lpVtbl.pointee.Initialize(encoder, stream, WICBitmapEncoderNoCache) + + // Create the frame into which the bitmap will be composited. + var frame: UnsafeMutablePointer? + var propertyBag: UnsafeMutablePointer? + let rCreateFrame = encoder.pointee.lpVtbl.pointee.CreateNewFrame(encoder, &frame, &propertyBag) + guard rCreateFrame == S_OK, let frame, let propertyBag else { + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmapFrameEncode.self, rCreateFrame) + } + defer { + _ = frame.pointee.lpVtbl.pointee.Release(frame) + _ = propertyBag.pointee.lpVtbl.pointee.Release(propertyBag) + } + + // Set properties. The only property we currently set is image quality. + do { + try propertyBag.write(imageFormat.encodingQuality, named: "ImageQuality") + } catch ImageAttachmentError.propertyBagWritingFailed(_, HRESULT(bitPattern: 0x80004005)) { + // E_FAIL: This property is not supported for the current encoder/format. + // Eat this error silently as it's not useful to the test author. + } + _ = frame.pointee.lpVtbl.pointee.Initialize(frame, propertyBag) + + // Write the image! + let rWrite = frame.pointee.lpVtbl.pointee.WriteSource(frame, bitmap, nil) + guard rWrite == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rWrite) + } + + // Commit changes through the various layers. + var rCommit = frame.pointee.lpVtbl.pointee.Commit(frame) + guard rCommit == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rCommit) + } + rCommit = encoder.pointee.lpVtbl.pointee.Commit(encoder) + guard rCommit == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rCommit) + } + rCommit = stream.pointee.lpVtbl.pointee.Commit(stream, DWORD(STGC_DEFAULT.rawValue)) + guard rCommit == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rCommit) + } + + // Extract the serialized image and pass it back to the caller. We hold the + // HGLOBAL locked while calling `body`, but nothing else should have a + // reference to it. + var global: HGLOBAL? + let rGetGlobal = GetHGlobalFromStream(stream, &global) + guard S_OK == rGetGlobal else { + throw ImageAttachmentError.globalFromStreamFailed(rGetGlobal) + } + guard let baseAddress = GlobalLock(global) else { + throw Win32Error(rawValue: GetLastError()) + } + defer { + GlobalUnlock(global) + } + let byteCount = GlobalSize(global) + return try body(UnsafeRawBufferPointer(start: baseAddress, count: Int(byteCount))) } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 37e7c8f9f..089b97f39 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -9,12 +9,11 @@ // #if os(Windows) -public import Testing public import WinSDK extension AttachableImageFormat { - private static let _encoderPathExtensionsByCLSID = Result<[UInt128: [String]], any Error> { - var result = [UInt128: [String]]() + private static let _encoderPathExtensionsByCLSID = Result { + var result = [GUID: [String]]() // Create an imaging factory. let factory = try IWICImagingFactory.create() @@ -67,7 +66,7 @@ extension AttachableImageFormat { continue } let extensions = _pathExtensions(for: info) - result[UInt128(clsid)] = extensions + result[clsid] = extensions } return result @@ -134,21 +133,7 @@ extension AttachableImageFormat { 0 == _wcsicmp(pathExtension, encoderExt) } } - }.map { CLSID($0.key) } - } - - /// Get the `CLSID` value of the WIC image encoder corresponding to the same - /// image format as the given path extension. - /// - /// - Parameters: - /// - pathExtension: The path extension for which a `CLSID` value is needed. - /// - /// - Returns: An instance of `CLSID` referring to a a WIC image encoder, or - /// `nil` if one could not be determined. - private static func _computeEncoderCLSID(forPathExtension pathExtension: String) -> CLSID? { - pathExtension.withCString(encodedAs: UTF16.self) { pathExtension in - _computeEncoderCLSID(forPathExtension: pathExtension) - } + }.map { $0.key } } /// Get the `CLSID` value of the WIC image encoder corresponding to the same @@ -160,7 +145,7 @@ extension AttachableImageFormat { /// /// - Returns: An instance of `CLSID` referring to a a WIC image encoder, or /// `nil` if one could not be determined. - private static func _computeEncoderCLSID(forPreferredName preferredName: String) -> CLSID? { + static func computeEncoderCLSID(forPreferredName preferredName: String) -> CLSID? { preferredName.withCString(encodedAs: UTF16.self) { (preferredName) -> CLSID? in // Get the path extension on the preferred name, if any. var dot: PCWSTR? @@ -171,37 +156,6 @@ extension AttachableImageFormat { } } - /// Get the `CLSID` value of the WIC image encoder to use when encoding an - /// image. - /// - /// - Parameters: - /// - imageFormat: The image format to use, or `nil` if the developer did - /// not specify one. - /// - preferredName: The preferred name of the image for which a type is - /// needed. - /// - /// - Returns: An instance of `CLSID` referring to a a WIC image encoder. If - /// none could be derived from `imageFormat` or `preferredName`, the PNG - /// encoder is used. - /// - /// This function is not part of the public interface of the testing library. - static func computeEncoderCLSID(for imageFormat: Self?, withPreferredName preferredName: String) -> CLSID { - if let clsid = imageFormat?.encoderCLSID { - return clsid - } - - // The developer didn't specify a CLSID, or we couldn't figure one out from - // context, so try to derive one from the preferred name's path extension. - if let inferredCLSID = _computeEncoderCLSID(forPreferredName: preferredName) { - return inferredCLSID - } - - // We couldn't derive a concrete type from the path extension, so default - // to PNG. Unlike Apple platforms, there's no abstract "image" type on - // Windows so we don't need to make any more decisions. - return CLSID_WICPngEncoder - } - /// Append the path extension preferred by WIC for the image format /// corresponding to the given `CLSID` value or the given filename. /// @@ -215,13 +169,13 @@ extension AttachableImageFormat { static func appendPathExtension(for clsid: CLSID, to preferredName: String) -> String { // If there's already a CLSID associated with the filename, and it matches // the one passed to us, no changes are needed. - if let existingCLSID = _computeEncoderCLSID(forPreferredName: preferredName), clsid == existingCLSID { + if let existingCLSID = computeEncoderCLSID(forPreferredName: preferredName), clsid == existingCLSID { return preferredName } // Find the preferred path extension for the encoder with the given CLSID. let encoderPathExtensionsByCLSID = (try? _encoderPathExtensionsByCLSID.get()) ?? [:] - if let ext = encoderPathExtensionsByCLSID[UInt128(clsid)]?.first { + if let ext = encoderPathExtensionsByCLSID[clsid]?.first { return "\(preferredName).\(ext)" } @@ -248,7 +202,7 @@ extension AttachableImageFormat { case .jpeg: CLSID_WICJpegEncoder case let .systemValue(clsid): - clsid as! CLSID + clsid as! GUID } } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift index baccf2663..0982234cc 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift @@ -9,7 +9,6 @@ // #if os(Windows) -private import Testing public import WinSDK extension HBITMAP__: _AttachableByAddressAsIWICBitmapSource { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift index 8884d713a..4e6addfa3 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift @@ -9,7 +9,6 @@ // #if os(Windows) -private import Testing public import WinSDK extension HICON__: _AttachableByAddressAsIWICBitmapSource { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift index d900baa46..733b72bb7 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift @@ -9,7 +9,6 @@ // #if os(Windows) -private import Testing public import WinSDK /// - Important: The casts in this file to `IUnknown` are safe insofar as we use diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift index 448c2151e..a7487c1cd 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift @@ -9,25 +9,13 @@ // #if os(Windows) -private import Testing -public import WinSDK +package import WinSDK /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } -extension UnsafeMutablePointer: _AttachableAsImage, AttachableAsIWICBitmapSource where Pointee: _AttachableByAddressAsIWICBitmapSource { - /// @Metadata { - /// @Available(Swift, introduced: 6.3) - /// } - public func copyAttachableIWICBitmapSource() throws -> UnsafeMutablePointer { - let factory = try IWICImagingFactory.create() - defer { - _ = factory.pointee.lpVtbl.pointee.Release(factory) - } - return try _copyAttachableIWICBitmapSource(using: factory) - } - - public func _copyAttachableIWICBitmapSource(using factory: UnsafeMutablePointer) throws -> UnsafeMutablePointer { +extension UnsafeMutablePointer: AttachableAsImage, AttachableAsIWICBitmapSource where Pointee: _AttachableByAddressAsIWICBitmapSource { + package func copyAttachableIWICBitmapSource(using factory: UnsafeMutablePointer) throws -> UnsafeMutablePointer { try Pointee._copyAttachableIWICBitmapSource(from: self, using: factory) } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index 33026fc9a..3437f2deb 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -9,115 +9,43 @@ // #if os(Windows) -public import Testing private import WinSDK -extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsIWICBitmapSource { - public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - // Create an in-memory stream to write the image data to. Note that Windows - // documentation recommends SHCreateMemStream() instead, but that function - // does not provide a mechanism to access the underlying memory directly. - var stream: UnsafeMutablePointer? - let rCreateStream = CreateStreamOnHGlobal(nil, true, &stream) - guard S_OK == rCreateStream, let stream else { - throw ImageAttachmentError.comObjectCreationFailed(IStream.self, rCreateStream) - } - defer { - _ = stream.pointee.lpVtbl.pointee.Release(stream) - } - - // Get an imaging factory to create the WIC bitmap and encoder. - let factory = try IWICImagingFactory.create() - defer { - _ = factory.pointee.lpVtbl.pointee.Release(factory) - } - - // Create the bitmap and downcast it to an IWICBitmapSource for later use. - let bitmap = try wrappedValue._copyAttachableIWICBitmapSource(using: factory) - defer { - _ = bitmap.pointee.lpVtbl.pointee.Release(bitmap) - } - - // Create the encoder. - let encoder = try withUnsafePointer(to: IID_IWICBitmapEncoder) { [preferredName = attachment.preferredName] IID_IWICBitmapEncoder in - var encoderCLSID = AttachableImageFormat.computeEncoderCLSID(for: imageFormat, withPreferredName: preferredName) - var encoder: UnsafeMutableRawPointer? - let rCreate = CoCreateInstance( - &encoderCLSID, - nil, - DWORD(CLSCTX_INPROC_SERVER.rawValue), - IID_IWICBitmapEncoder, - &encoder - ) - guard rCreate == S_OK, let encoder = encoder?.assumingMemoryBound(to: IWICBitmapEncoder.self) else { - throw ImageAttachmentError.comObjectCreationFailed(IWICBitmapEncoder.self, rCreate) - } - return encoder - } - defer { - _ = encoder.pointee.lpVtbl.pointee.Release(encoder) - } - _ = encoder.pointee.lpVtbl.pointee.Initialize(encoder, stream, WICBitmapEncoderNoCache) - - // Create the frame into which the bitmap will be composited. - var frame: UnsafeMutablePointer? - var propertyBag: UnsafeMutablePointer? - let rCreateFrame = encoder.pointee.lpVtbl.pointee.CreateNewFrame(encoder, &frame, &propertyBag) - guard rCreateFrame == S_OK, let frame, let propertyBag else { - throw ImageAttachmentError.comObjectCreationFailed(IWICBitmapFrameEncode.self, rCreateFrame) - } - defer { - _ = frame.pointee.lpVtbl.pointee.Release(frame) - _ = propertyBag.pointee.lpVtbl.pointee.Release(propertyBag) - } - - // Set properties. The only property we currently set is image quality. - if let encodingQuality = imageFormat?.encodingQuality { - try propertyBag.write(encodingQuality, named: "ImageQuality") - } - _ = frame.pointee.lpVtbl.pointee.Initialize(frame, propertyBag) - - // Write the image! - let rWrite = frame.pointee.lpVtbl.pointee.WriteSource(frame, bitmap, nil) - guard rWrite == S_OK else { - throw ImageAttachmentError.imageWritingFailed(rWrite) - } - - // Commit changes through the various layers. - var rCommit = frame.pointee.lpVtbl.pointee.Commit(frame) - guard rCommit == S_OK else { - throw ImageAttachmentError.imageWritingFailed(rCommit) - } - rCommit = encoder.pointee.lpVtbl.pointee.Commit(encoder) - guard rCommit == S_OK else { - throw ImageAttachmentError.imageWritingFailed(rCommit) - } - rCommit = stream.pointee.lpVtbl.pointee.Commit(stream, DWORD(STGC_DEFAULT.rawValue)) - guard rCommit == S_OK else { - throw ImageAttachmentError.imageWritingFailed(rCommit) - } +extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsImage { + /// Get the image format to use when encoding an image. + /// + /// - Parameters: + /// - preferredName: The preferred name of the image for which a type is + /// needed. + /// + /// - Returns: An instance of ``AttachableImageFormat`` referring to a + /// concrete image type. + /// + /// This function is not part of the public interface of the testing library. + private func _imageFormat(forPreferredName preferredName: String) -> AttachableImageFormat { + if let imageFormat { + // The developer explicitly specified a type. + return imageFormat + } + + if let clsid = AttachableImageFormat.computeEncoderCLSID(forPreferredName: preferredName) { + return AttachableImageFormat(encoderCLSID: clsid) + } + + // We couldn't derive a concrete type from the path extension, so pick + // between PNG and JPEG based on the encoding quality. + let encodingQuality = imageFormat?.encodingQuality ?? 1.0 + return encodingQuality < 1.0 ? .jpeg : .png + } - // Extract the serialized image and pass it back to the caller. We hold the - // HGLOBAL locked while calling `body`, but nothing else should have a - // reference to it. - var global: HGLOBAL? - let rGetGlobal = GetHGlobalFromStream(stream, &global) - guard S_OK == rGetGlobal else { - throw ImageAttachmentError.globalFromStreamFailed(rGetGlobal) - } - guard let baseAddress = GlobalLock(global) else { - throw Win32Error(rawValue: GetLastError()) - } - defer { - GlobalUnlock(global) - } - let byteCount = GlobalSize(global) - return try body(UnsafeRawBufferPointer(start: baseAddress, count: Int(byteCount))) + public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + let imageFormat = _imageFormat(forPreferredName: attachment.preferredName) + return try wrappedValue.withUnsafeBytes(as: imageFormat, body) } public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String { - let clsid = AttachableImageFormat.computeEncoderCLSID(for: imageFormat, withPreferredName: suggestedName) - return AttachableImageFormat.appendPathExtension(for: clsid, to: suggestedName) + let imageFormat = _imageFormat(forPreferredName: suggestedName) + return AttachableImageFormat.appendPathExtension(for: imageFormat.encoderCLSID, to: suggestedName) } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift b/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift index c58eca577..76b16a554 100644 --- a/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift +++ b/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift @@ -8,30 +8,28 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if os(Windows) -internal import WinSDK +#if os(Windows) && compiler(<6.4) +public import WinSDK -extension UInt128 { - init(_ guid: GUID) { - self = withUnsafeBytes(of: guid) { buffer in - buffer.baseAddress!.loadUnaligned(as: Self.self) +// Retroactively add conformance to `Equatable` and `Hashable` until +// https://github.com/swiftlang/swift/pull/84792 is merged into the WinSDK Swift +// overlay. + +@_spi(_) +extension GUID: @retroactive Equatable, @retroactive Hashable { + /// This GUID as an integer. + private var _uint128Value: UInt128 { + withUnsafeBytes(of: self) { buffer in + buffer.baseAddress!.loadUnaligned(as: UInt128.self) } } -} -extension GUID { - init(_ uint128Value: UInt128) { - self = withUnsafeBytes(of: uint128Value) { buffer in - buffer.baseAddress!.loadUnaligned(as: Self.self) - } + public static func ==(lhs: Self, rhs: Self) -> Bool { + lhs._uint128Value == rhs._uint128Value } - static func ==(lhs: Self, rhs: Self) -> Bool { - withUnsafeBytes(of: lhs) { lhs in - withUnsafeBytes(of: rhs) { rhs in - lhs.elementsEqual(rhs) - } - } + public func hash(into hasher: inout Hasher) { + hasher.combine(_uint128Value) } } #endif diff --git a/Sources/Testing/Attachments/Images/AttachableAsImage.swift b/Sources/Testing/Attachments/Images/AttachableAsImage.swift new file mode 100644 index 000000000..515233d19 --- /dev/null +++ b/Sources/Testing/Attachments/Images/AttachableAsImage.swift @@ -0,0 +1,131 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024–2025 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 || os(Windows) +// These platforms support image attachments. +#elseif !SWT_NO_IMAGE_ATTACHMENTS +#error("Platform-specific misconfiguration: support for image attachments requires a platform-specific implementation") +#endif + +/// ## Why can't images directly conform to Attachable? +/// +/// Three reasons: +/// +/// 1. Several image classes are not marked `Sendable`, which means that as far +/// as Swift is concerned, they cannot be safely passed to Swift Testing's +/// 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 +/// 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 +/// classes to conform to the `Attachable` protocol. We could get around this +/// 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 protocol describing images that can be converted to instances of +/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment). +/// +/// Instances of types conforming to this protocol do not themselves conform to +/// [`Attachable`](https://developer.apple.com/documentation/testing/attachable). +/// Instead, the testing library provides additional initializers on [`Attachment`](https://developer.apple.com/documentation/testing/attachment) +/// that take instances of such types and handle converting them to image data when needed. +/// +/// You can attach instances of the following system-provided image types to a +/// test: +/// +/// | Platform | Supported Types | +/// |-|-| +/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | +/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | +/// @Comment { +/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | +/// } +/// +/// You do not generally need to add your own conformances to this protocol. If +/// you have an image in another format that needs to be attached to a test, +/// first convert it to an instance of one of the types above. +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } +#if SWT_NO_IMAGE_ATTACHMENTS +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif +@available(_uttypesAPI, *) +public protocol AttachableAsImage: SendableMetatype { + /// Encode a representation of this image in a given image format. + /// + /// - Parameters: + /// - imageFormat: The image format to use when encoding this image. + /// - body: A function to call. A temporary buffer containing a data + /// representation of this instance is passed to it. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`, or any error that prevented the + /// creation of the buffer. + /// + /// The testing library uses this function when saving an image as an + /// attachment. The implementation should use `imageFormat` to determine what + /// encoder to use. + borrowing func withUnsafeBytes(as imageFormat: AttachableImageFormat, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R + + /// Make a copy of this instance to pass to an attachment. + /// + /// - Returns: A copy of `self`, or `self` if no copy is needed. + /// + /// The testing library uses this function to take ownership of image + /// resources that test authors pass to it. If possible, make a copy of or add + /// a reference to `self`. If this type does not support making copies, return + /// `self` verbatim. + /// + /// The default implementation of this function when `Self` conforms to + /// `Sendable` simply returns `self`. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + func _copyAttachableValue() -> Self + + /// Manually deinitialize any resources associated with this image. + /// + /// The implementation of this function cleans up any resources (such as + /// handles or COM objects) associated with this image. The testing library + /// automatically invokes this function as needed. + /// + /// This function is not responsible for releasing the image returned from + /// `_copyAttachableIWICBitmapSource(using:)`. + /// + /// The default implementation of this function when `Self` conforms to + /// `Sendable` does nothing. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + func _deinitializeAttachableValue() +} + +#if SWT_NO_IMAGE_ATTACHMENTS +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif +@available(_uttypesAPI, *) +extension AttachableAsImage { + public func _copyAttachableValue() -> Self { + self + } + + public func _deinitializeAttachableValue() {} +} diff --git a/Sources/Testing/Attachments/Images/AttachableImageFormat.swift b/Sources/Testing/Attachments/Images/AttachableImageFormat.swift index 350ef849e..e32df7e71 100644 --- a/Sources/Testing/Attachments/Images/AttachableImageFormat.swift +++ b/Sources/Testing/Attachments/Images/AttachableImageFormat.swift @@ -12,7 +12,7 @@ /// when attaching an image to a test. /// /// When you attach an image to a test, you can pass an instance of this type to -/// `Attachment.record(_:named:as:sourceLocation:)` so that the testing +/// ``Attachment/record(_:named:as:sourceLocation:)`` so that the testing /// library knows the image format you'd like to use. If you don't pass an /// instance of this type, the testing library infers which format to use based /// on the attachment's preferred name. @@ -53,7 +53,7 @@ public struct AttachableImageFormat: Sendable { /// /// On Apple platforms, `value` should be an instance of `UTType`. On /// Windows, it should be an instance of `CLSID`. - case systemValue(_ value: any Sendable) + case systemValue(_ value: any Sendable & Equatable & Hashable) } /// The kind of image format represented by this instance. @@ -69,7 +69,7 @@ public struct AttachableImageFormat: Sendable { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } - public internal(set) var encodingQuality: Float = 1.0 + public private(set) var encodingQuality: Float = 1.0 package init(kind: Kind, encodingQuality: Float) { self.kind = kind @@ -77,6 +77,47 @@ public struct AttachableImageFormat: Sendable { } } +// MARK: - Equatable, Hashable + +#if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif +@available(_uttypesAPI, *) +extension AttachableImageFormat: Equatable, Hashable {} + +#if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif +@available(_uttypesAPI, *) +extension AttachableImageFormat.Kind: Equatable, Hashable { + public static func ==(lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.png, .png), (.jpeg, .jpeg): + return true + case let (.systemValue(lhs), .systemValue(rhs)): + func open(_ lhs: T) -> Bool where T: Equatable { + lhs == (rhs as? T) + } + return open(lhs) + default: + return false + } + } + + public func hash(into hasher: inout Hasher) { + switch self { + case .png: + hasher.combine("png") + case .jpeg: + hasher.combine("jpeg") + case let .systemValue(systemValue): + hasher.combine(systemValue) + } + } +} + // MARK: - #if SWT_NO_IMAGE_ATTACHMENTS diff --git a/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift b/Sources/Testing/Attachments/Images/Attachment+AttachableAsImage.swift similarity index 97% rename from Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift rename to Sources/Testing/Attachments/Images/Attachment+AttachableAsImage.swift index dde24f6d7..045b5938e 100644 --- a/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift +++ b/Sources/Testing/Attachments/Images/Attachment+AttachableAsImage.swift @@ -100,14 +100,17 @@ extension Attachment { // MARK: - -@_spi(Experimental) // STOP: not part of ST-0014 #if SWT_NO_IMAGE_ATTACHMENTS @_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif @available(_uttypesAPI, *) -extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: _AttachableAsImage { - /// The image format to use when encoding the represented image. +extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: AttachableAsImage { + /// The image format to use when encoding the represented image, if specified. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } @_disfavoredOverload public var imageFormat: AttachableImageFormat? { // FIXME: no way to express `where AttachableValue == _AttachableImageWrapper` on a property (see rdar://47559973) (attachableValue as? _AttachableImageWrapper)?.imageFormat diff --git a/Sources/Testing/Attachments/Images/_AttachableAsImage.swift b/Sources/Testing/Attachments/Images/_AttachableAsImage.swift deleted file mode 100644 index ca8efacf2..000000000 --- a/Sources/Testing/Attachments/Images/_AttachableAsImage.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024–2025 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 -// Image attachments on Apple platforms conform to AttachableAsCGImage. -#elseif os(Windows) -// Image attachments on Windows platforms conform to AttachableAsIWICBitmapSource. -#elseif !SWT_NO_IMAGE_ATTACHMENTS -#error("Platform-specific misconfiguration: support for image attachments requires a platform-specific implementation") -#endif - -/// A protocol describing images that can be converted to instances of -/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment). -/// -/// This protocol acts as an abstract, platform-independent base protocol for -/// ``AttachableAsCGImage`` and ``AttachableAsIWICBitmapSource``. -/// -/// @Comment { -/// A future Swift Evolution proposal will promote this protocol to API so -/// that we don't need to underscore its name. -/// } -#if SWT_NO_IMAGE_ATTACHMENTS -@_unavailableInEmbedded -@available(*, unavailable, message: "Image attachments are not available on this platform.") -#endif -@available(_uttypesAPI, *) -public protocol _AttachableAsImage: SendableMetatype { - /// Make a copy of this instance to pass to an attachment. - /// - /// - Returns: A copy of `self`, or `self` if no copy is needed. - /// - /// The testing library uses this function to take ownership of image - /// resources that test authors pass to it. If possible, make a copy of or add - /// a reference to `self`. If this type does not support making copies, return - /// `self` verbatim. - /// - /// The default implementation of this function when `Self` conforms to - /// `Sendable` simply returns `self`. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - func _copyAttachableValue() -> Self - - /// Manually deinitialize any resources associated with this image. - /// - /// The implementation of this function cleans up any resources (such as - /// handles or COM objects) associated with this image. The testing library - /// automatically invokes this function as needed. - /// - /// This function is not responsible for releasing the image returned from - /// `_copyAttachableIWICBitmapSource(using:)`. - /// - /// The default implementation of this function when `Self` conforms to - /// `Sendable` does nothing. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - func _deinitializeAttachableValue() -} diff --git a/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift index dfde12587..7a099ba77 100644 --- a/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift +++ b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift @@ -23,7 +23,7 @@ @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif @available(_uttypesAPI, *) -public final class _AttachableImageWrapper: Sendable where Image: _AttachableAsImage { +public final class _AttachableImageWrapper: Sendable where Image: AttachableAsImage { /// The underlying image. private nonisolated(unsafe) let _image: Image diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 751bf1f64..549d2d337 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -21,10 +21,10 @@ add_library(Testing ABI/Encoded/ABI.EncodedIssue.swift ABI/Encoded/ABI.EncodedMessage.swift ABI/Encoded/ABI.EncodedTest.swift - Attachments/Images/_AttachableAsImage.swift + Attachments/Images/AttachableAsImage.swift Attachments/Images/_AttachableImageWrapper.swift Attachments/Images/AttachableImageFormat.swift - Attachments/Images/Attachment+_AttachableAsImage.swift + Attachments/Images/Attachment+AttachableAsImage.swift Attachments/Images/ImageAttachmentError.swift Attachments/Attachable.swift Attachments/AttachableWrapper.swift diff --git a/Sources/Testing/Testing.docc/Attachments.md b/Sources/Testing/Testing.docc/Attachments.md index aa754cc76..b84a50e13 100644 --- a/Sources/Testing/Testing.docc/Attachments.md +++ b/Sources/Testing/Testing.docc/Attachments.md @@ -34,10 +34,7 @@ protocol to create your own attachable types. ### Attaching images to tests - +- ``AttachableAsImage`` - ``AttachableImageFormat`` - ``Attachment/init(_:named:as:sourceLocation:)`` - ``Attachment/record(_:named:as:sourceLocation:)`` diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 7695dd634..b7051be46 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -572,7 +572,7 @@ extension AttachmentTests { @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 { let image = try Self.cgImage.get() - let format = type.map { AttachableImageFormat($0, encodingQuality: quality) } + let format = type.map { AttachableImageFormat(contentType: $0, encodingQuality: quality) } let attachment = Attachment(image, named: "diamond", as: format) #expect(attachment.attachableValue === image) try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in @@ -584,7 +584,7 @@ extension AttachmentTests { } @available(_uttypesAPI, *) - @Test(arguments: [AttachableImageFormat.png, .jpeg, .jpeg(withEncodingQuality: 0.5), .init(.tiff)]) + @Test(arguments: [AttachableImageFormat.png, .jpeg, .jpeg(withEncodingQuality: 0.5), .init(contentType: .tiff)]) func attachCGImage(format: AttachableImageFormat) throws { let image = try Self.cgImage.get() let attachment = Attachment(image, named: "diamond", as: format) @@ -601,7 +601,7 @@ extension AttachmentTests { @available(_uttypesAPI, *) @Test func cannotAttachCGImageWithNonImageType() async { await #expect(processExitsWith: .failure) { - let format = AttachableImageFormat(.mp3) + let format = AttachableImageFormat(contentType: .mp3) let attachment = Attachment(try Self.cgImage.get(), named: "diamond", as: format) try attachment.attachableValue.withUnsafeBytes(for: attachment) { _ in } } @@ -840,6 +840,34 @@ extension AttachmentTests { @Test func imageFormatFromPathExtension() { let format = AttachableImageFormat(pathExtension: "png") #expect(format != nil) + #expect(format == .png) + + let badFormat = AttachableImageFormat(pathExtension: "no-such-image-format") + #expect(badFormat == nil) + } + + @available(_uttypesAPI, *) + @Test func imageFormatEquatableConformance() { + let format1 = AttachableImageFormat.png + let format2 = AttachableImageFormat.jpeg +#if canImport(CoreGraphics) && canImport(_Testing_CoreGraphics) + let format3 = AttachableImageFormat(contentType: .tiff) +#elseif canImport(WinSDK) && canImport(_Testing_WinSDK) + let format3 = AttachableImageFormat(encoderCLSID: CLSID_WICTiffEncoder) +#endif + #expect(format1 == format1) + #expect(format2 == format2) + #expect(format3 == format3) + #expect(format1 != format2) + #expect(format2 != format3) + #expect(format1 != format3) + + #expect(format1.hashValue == format1.hashValue) + #expect(format2.hashValue == format2.hashValue) + #expect(format3.hashValue == format3.hashValue) + #expect(format1.hashValue != format2.hashValue) + #expect(format2.hashValue != format3.hashValue) + #expect(format1.hashValue != format3.hashValue) } #endif }