Skip to content

Commit 84ed952

Browse files
authored
Require UTType for image attachments. (#1192)
This PR simplifies our image attachment code so that it always requires `UTType` instead of providing implementations for older Apple platforms. For more information about the `UTType` API, watch [this](https://developer.apple.com/videos/play/tech-talks/10696/) video. There is a kitty. Image attachments depend on `CGImage` and are Apple-specific at this time. Non-Apple image attachment support is a future direction. > [!NOTE] > Image attachments are an experimental feature. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 79c22ad commit 84ed952

File tree

3 files changed

+78
-93
lines changed

3 files changed

+78
-93
lines changed

Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift

Lines changed: 50 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
public import UniformTypeIdentifiers
1515

16+
@_spi(Experimental)
17+
@available(_uttypesAPI, *)
1618
extension Attachment {
1719
/// Initialize an instance of this type that encloses the given image.
1820
///
@@ -23,46 +25,9 @@ extension Attachment {
2325
/// to a test report or to disk. If `nil`, the testing library attempts
2426
/// to derive a reasonable filename for the attached value.
2527
/// - contentType: The image format with which to encode `attachableValue`.
26-
/// If this type does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
27-
/// the result is undefined. Pass `nil` to let the testing library decide
28-
/// which image format to use.
2928
/// - encodingQuality: The encoding quality to use when encoding the image.
30-
/// If the image format used for encoding (specified by the `contentType`
31-
/// argument) does not support variable-quality encoding, the value of
32-
/// this argument is ignored.
33-
/// - sourceLocation: The source location of the call to this initializer.
34-
/// This value is used when recording issues associated with the
35-
/// attachment.
36-
///
37-
/// This is the designated initializer for this type when attaching an image
38-
/// that conforms to ``AttachableAsCGImage``.
39-
fileprivate init<T>(
40-
attachableValue: T,
41-
named preferredName: String?,
42-
contentType: (any Sendable)?,
43-
encodingQuality: Float,
44-
sourceLocation: SourceLocation
45-
) where AttachableValue == _AttachableImageWrapper<T> {
46-
let imageWrapper = _AttachableImageWrapper(image: attachableValue, encodingQuality: encodingQuality, contentType: contentType)
47-
self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation)
48-
}
49-
50-
/// Initialize an instance of this type that encloses the given image.
51-
///
52-
/// - Parameters:
53-
/// - attachableValue: The value that will be attached to the output of
54-
/// the test run.
55-
/// - preferredName: The preferred name of the attachment when writing it
56-
/// to a test report or to disk. If `nil`, the testing library attempts
57-
/// to derive a reasonable filename for the attached value.
58-
/// - contentType: The image format with which to encode `attachableValue`.
59-
/// If this type does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
60-
/// the result is undefined. Pass `nil` to let the testing library decide
61-
/// which image format to use.
62-
/// - encodingQuality: The encoding quality to use when encoding the image.
63-
/// If the image format used for encoding (specified by the `contentType`
64-
/// argument) does not support variable-quality encoding, the value of
65-
/// this argument is ignored.
29+
/// For the lowest supported quality, pass `0.0`. For the highest
30+
/// supported quality, pass `1.0`.
6631
/// - sourceLocation: The source location of the call to this initializer.
6732
/// This value is used when recording issues associated with the
6833
/// attachment.
@@ -71,46 +36,72 @@ extension Attachment {
7136
/// ``AttachableAsCGImage`` protocol and can be attached to a test:
7237
///
7338
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage)
74-
@_spi(Experimental)
75-
@available(_uttypesAPI, *)
39+
///
40+
/// The testing library uses the image format specified by `contentType`. Pass
41+
/// `nil` to let the testing library decide which image format to use. If you
42+
/// pass `nil`, then the image format that the testing library uses depends on
43+
/// the path extension you specify in `preferredName`, if any. If you do not
44+
/// specify a path extension, or if the path extension you specify doesn't
45+
/// correspond to an image format the operating system knows how to write, the
46+
/// testing library selects an appropriate image format for you.
47+
///
48+
/// If the target image format does not support variable-quality encoding,
49+
/// the value of the `encodingQuality` argument is ignored. If `contentType`
50+
/// is not `nil` and does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
51+
/// the result is undefined.
7652
public init<T>(
7753
_ attachableValue: T,
7854
named preferredName: String? = nil,
79-
as contentType: UTType?,
55+
as contentType: UTType? = nil,
8056
encodingQuality: Float = 1.0,
8157
sourceLocation: SourceLocation = #_sourceLocation
8258
) where AttachableValue == _AttachableImageWrapper<T> {
83-
self.init(attachableValue: attachableValue, named: preferredName, contentType: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation)
59+
let imageWrapper = _AttachableImageWrapper(image: attachableValue, encodingQuality: encodingQuality, contentType: contentType)
60+
self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation)
8461
}
8562

86-
/// Initialize an instance of this type that encloses the given image.
63+
/// Attach an image to the current test.
8764
///
8865
/// - Parameters:
89-
/// - attachableValue: The value that will be attached to the output of
90-
/// the test run.
91-
/// - preferredName: The preferred name of the attachment when writing it
92-
/// to a test report or to disk. If `nil`, the testing library attempts
93-
/// to derive a reasonable filename for the attached value.
66+
/// - image: The value to attach.
67+
/// - preferredName: The preferred name of the attachment when writing it to
68+
/// a test report or to disk. If `nil`, the testing library attempts to
69+
/// derive a reasonable filename for the attached value.
70+
/// - contentType: The image format with which to encode `attachableValue`.
9471
/// - encodingQuality: The encoding quality to use when encoding the image.
95-
/// If the image format used for encoding (specified by the `contentType`
96-
/// argument) does not support variable-quality encoding, the value of
97-
/// this argument is ignored.
98-
/// - sourceLocation: The source location of the call to this initializer.
99-
/// This value is used when recording issues associated with the
100-
/// attachment.
72+
/// For the lowest supported quality, pass `0.0`. For the highest
73+
/// supported quality, pass `1.0`.
74+
/// - sourceLocation: The source location of the call to this function.
75+
///
76+
/// This function creates a new instance of ``Attachment`` wrapping `image`
77+
/// and immediately attaches it to the current test.
10178
///
10279
/// The following system-provided image types conform to the
10380
/// ``AttachableAsCGImage`` protocol and can be attached to a test:
10481
///
10582
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage)
106-
@_spi(Experimental)
107-
public init<T>(
108-
_ attachableValue: T,
83+
///
84+
/// The testing library uses the image format specified by `contentType`. Pass
85+
/// `nil` to let the testing library decide which image format to use. If you
86+
/// pass `nil`, then the image format that the testing library uses depends on
87+
/// the path extension you specify in `preferredName`, if any. If you do not
88+
/// specify a path extension, or if the path extension you specify doesn't
89+
/// correspond to an image format the operating system knows how to write, the
90+
/// testing library selects an appropriate image format for you.
91+
///
92+
/// If the target image format does not support variable-quality encoding,
93+
/// the value of the `encodingQuality` argument is ignored. If `contentType`
94+
/// is not `nil` and does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
95+
/// the result is undefined.
96+
public static func record<T>(
97+
_ image: consuming T,
10998
named preferredName: String? = nil,
99+
as contentType: UTType? = nil,
110100
encodingQuality: Float = 1.0,
111101
sourceLocation: SourceLocation = #_sourceLocation
112102
) where AttachableValue == _AttachableImageWrapper<T> {
113-
self.init(attachableValue: attachableValue, named: preferredName, contentType: nil, encodingQuality: encodingQuality, sourceLocation: sourceLocation)
103+
let attachment = Self(image, named: preferredName, as: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation)
104+
Self.record(attachment, sourceLocation: sourceLocation)
114105
}
115106
}
116107
#endif

Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import UniformTypeIdentifiers
4848
///
4949
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage)
5050
@_spi(Experimental)
51+
@available(_uttypesAPI, *)
5152
public struct _AttachableImageWrapper<Image>: Sendable where Image: AttachableAsCGImage {
5253
/// The underlying image.
5354
///
@@ -61,7 +62,7 @@ public struct _AttachableImageWrapper<Image>: Sendable where Image: AttachableAs
6162
var encodingQuality: Float
6263

6364
/// Storage for ``contentType``.
64-
private var _contentType: (any Sendable)?
65+
private var _contentType: UTType?
6566

6667
/// The content type to use when encoding the image.
6768
///
@@ -70,14 +71,9 @@ public struct _AttachableImageWrapper<Image>: Sendable where Image: AttachableAs
7071
///
7172
/// If the value of this property does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
7273
/// the result is undefined.
73-
@available(_uttypesAPI, *)
7474
var contentType: UTType {
7575
get {
76-
if let contentType = _contentType as? UTType {
77-
return contentType
78-
} else {
79-
return encodingQuality < 1.0 ? .jpeg : .png
80-
}
76+
_contentType ?? .image
8177
}
8278
set {
8379
precondition(
@@ -92,41 +88,25 @@ public struct _AttachableImageWrapper<Image>: Sendable where Image: AttachableAs
9288
/// type for `UTType.image`.
9389
///
9490
/// This property is not part of the public interface of the testing library.
95-
@available(_uttypesAPI, *)
9691
var computedContentType: UTType {
97-
if let contentType = _contentType as? UTType, contentType != .image {
98-
contentType
99-
} else {
100-
encodingQuality < 1.0 ? .jpeg : .png
92+
if contentType == .image {
93+
return encodingQuality < 1.0 ? .jpeg : .png
10194
}
95+
return contentType
10296
}
10397

104-
/// The type identifier (as a `CFString`) corresponding to this instance's
105-
/// ``computedContentType`` property.
106-
///
107-
/// The value of this property is used by ImageIO when serializing an image.
108-
///
109-
/// This property is not part of the public interface of the testing library.
110-
/// It is used by ImageIO below.
111-
var typeIdentifier: CFString {
112-
if #available(_uttypesAPI, *) {
113-
computedContentType.identifier as CFString
114-
} else {
115-
encodingQuality < 1.0 ? kUTTypeJPEG : kUTTypePNG
116-
}
117-
}
118-
119-
init(image: Image, encodingQuality: Float, contentType: (any Sendable)?) {
98+
init(image: Image, encodingQuality: Float, contentType: UTType?) {
12099
self.image = image._makeCopyForAttachment()
121100
self.encodingQuality = encodingQuality
122-
if #available(_uttypesAPI, *), let contentType = contentType as? UTType {
101+
if let contentType {
123102
self.contentType = contentType
124103
}
125104
}
126105
}
127106

128107
// MARK: -
129108

109+
@available(_uttypesAPI, *)
130110
extension _AttachableImageWrapper: AttachableWrapper {
131111
public var wrappedValue: Image {
132112
image
@@ -139,6 +119,7 @@ extension _AttachableImageWrapper: AttachableWrapper {
139119
let attachableCGImage = try image.attachableCGImage
140120

141121
// Create the image destination.
122+
let typeIdentifier = computedContentType.identifier as CFString
142123
guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, typeIdentifier, 1, nil) else {
143124
throw ImageAttachmentError.couldNotCreateImageDestination
144125
}
@@ -168,11 +149,7 @@ extension _AttachableImageWrapper: AttachableWrapper {
168149
}
169150

170151
public borrowing func preferredName(for attachment: borrowing Attachment<Self>, basedOn suggestedName: String) -> String {
171-
if #available(_uttypesAPI, *) {
172-
return (suggestedName as NSString).appendingPathExtension(for: computedContentType)
173-
}
174-
175-
return suggestedName
152+
(suggestedName as NSString).appendingPathExtension(for: computedContentType)
176153
}
177154
}
178155
#endif

Tests/TestingTests/AttachmentTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,23 @@ extension AttachmentTests {
537537
Attachment.record(attachment)
538538
}
539539

540+
@available(_uttypesAPI, *)
541+
@Test func attachCGImageDirectly() async throws {
542+
await confirmation("Attachment detected") { valueAttached in
543+
var configuration = Configuration()
544+
configuration.eventHandler = { event, _ in
545+
if case .valueAttached = event.kind {
546+
valueAttached()
547+
}
548+
}
549+
550+
await Test {
551+
let image = try Self.cgImage.get()
552+
Attachment.record(image, named: "diamond.jpg")
553+
}.run(configuration: configuration)
554+
}
555+
}
556+
540557
@available(_uttypesAPI, *)
541558
@Test(arguments: [Float(0.0).nextUp, 0.25, 0.5, 0.75, 1.0], [.png as UTType?, .jpeg, .gif, .image, nil])
542559
func attachCGImage(quality: Float, type: UTType?) throws {

0 commit comments

Comments
 (0)