Skip to content

Add a CoreGraphics cross-import overlay with support for attaching CGImages. #827

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 3 commits into from
Dec 10, 2024
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
9 changes: 9 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ let package = Package(
name: "TestingTests",
dependencies: [
"Testing",
"_Testing_CoreGraphics",
"_Testing_Foundation",
],
swiftSettings: .packageSettings
Expand Down Expand Up @@ -91,6 +92,14 @@ let package = Package(
),

// Cross-import overlays (not supported by Swift Package Manager)
.target(
name: "_Testing_CoreGraphics",
dependencies: [
"Testing",
],
path: "Sources/Overlays/_Testing_CoreGraphics",
swiftSettings: .packageSettings
),
.target(
name: "_Testing_Foundation",
dependencies: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics)
public import CoreGraphics
private import ImageIO

/// A protocol describing images that can be converted to instances of
/// ``Testing/Attachment``.
///
/// 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.
///
/// The following system-provided image types conform to this protocol and can
/// be attached to a test:
///
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage)
///
/// 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.
@_spi(Experimental)
public protocol AttachableAsCGImage {
/// An instance of `CGImage` representing this image.
///
/// - Throws: Any error that prevents the creation of an image.
var attachableCGImage: CGImage { get throws }

/// The orientation of the image.
///
/// The value of this property is the raw value of an instance of
/// `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 }

/// The scale factor of the image.
///
/// The value of this property is typically greater than `1.0` when an image
/// 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 }

/// Make a copy of this instance to pass to an attachment.
///
/// - Returns: A copy of `self`, or `self` if no copy is needed.
///
/// Several system image types do not conform to `Sendable`; use this
/// function to make copies of such images that will not be shared outside
/// of an attachment and so can be generally safely stored.
///
/// 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 _makeCopyForAttachment() -> Self
}

extension AttachableAsCGImage {
public var _attachmentOrientation: UInt32 {
CGImagePropertyOrientation.up.rawValue
}

public var _attachmentScaleFactor: CGFloat {
1.0
}
}

extension AttachableAsCGImage where Self: Sendable {
public func _makeCopyForAttachment() -> Self {
self
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

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

public import UniformTypeIdentifiers

extension Attachment {
/// 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.
/// - 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 == _AttachableImageContainer<T> {
var imageContainer = _AttachableImageContainer(image: attachableValue, encodingQuality: encodingQuality)

// Update the preferred name to include an extension appropriate for the
// given content type. (Note the `else` branch duplicates the logic in
// `preferredContentType(forEncodingQuality:)` but will go away once our
// minimum deployment targets include the UniformTypeIdentifiers framework.)
var preferredName = preferredName ?? Self.defaultPreferredName
if #available(_uttypesAPI, *) {
let contentType: UTType = contentType
.map { $0 as! UTType }
.flatMap { contentType in
if UTType.image.conforms(to: contentType) {
// This type is an abstract base type of .image (or .image itself.)
// We'll infer the concrete type based on other arguments.
return nil
}
return contentType
} ?? .preferred(forEncodingQuality: encodingQuality)
preferredName = (preferredName as NSString).appendingPathExtension(for: contentType)
imageContainer.contentType = contentType
} else {
// The caller can't provide a content type, so we'll pick one for them.
let ext = if encodingQuality < 1.0 {
"jpg"
} else {
"png"
}
if (preferredName as NSString).pathExtension.caseInsensitiveCompare(ext) != .orderedSame {
preferredName = (preferredName as NSString).appendingPathExtension(ext) ?? preferredName
}
}

self.init(imageContainer, 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.
/// - sourceLocation: The source location of the call to this initializer.
/// This value is used when recording issues associated with the
/// attachment.
///
/// The following system-provided image types conform to the
/// ``AttachableAsCGImage`` protocol and can be attached to a test:
///
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage)
@_spi(Experimental)
@available(_uttypesAPI, *)
public init<T>(
_ attachableValue: T,
named preferredName: String? = nil,
as contentType: UTType?,
encodingQuality: Float = 1.0,
sourceLocation: SourceLocation = #_sourceLocation
) where AttachableValue == _AttachableImageContainer<T> {
self.init(attachableValue: attachableValue, named: preferredName, contentType: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation)
}

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

#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics)
public import CoreGraphics

@_spi(Experimental)
extension CGImage: AttachableAsCGImage {
public var attachableCGImage: CGImage {
self
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics)
/// A type representing an error that can occur when attaching an image.
@_spi(ForSwiftTestingOnly)
public enum ImageAttachmentError: Error, CustomStringConvertible {
/// The specified content type did not conform to `.image`.
case contentTypeDoesNotConformToImage

/// The image could not be converted to an instance of `CGImage`.
case couldNotCreateCGImage

/// The image destination could not be created.
case couldNotCreateImageDestination

/// The image could not be converted.
case couldNotConvertImage

@_spi(ForSwiftTestingOnly)
public var description: String {
switch self {
case .contentTypeDoesNotConformToImage:
"The specified type does not represent an image format."
case .couldNotCreateCGImage:
"Could not create the corresponding Core Graphics image."
case .couldNotCreateImageDestination:
"Could not create the Core Graphics image destination to encode this image."
case .couldNotConvertImage:
"Could not convert the image to the specified format."
}
}
}
#endif
Loading