Skip to content

Commit 993c7cb

Browse files
authored
Add a preferredName(for:basedOn:) member to Attachable to allow customizing filenames. (#854)
This PR introduces a new optional member of `Attachable`, `preferredName(for:basedOn:)`, that we use when writing the corresponding attachment to disk/test reports/etc. in order to allow attachable types to customize the name independently of what the user specifies. For example: ```swift let a = Attachment(x) let b = Attachment(y, named: "hello") ``` In both the attachments created above, the file name is incompletely specified. `a` has a default name, and both `a` and `b` have no path extension (which is important for the OS to correctly recognize the produced file's type.) By adding this new function to `Attachable`, we give `x` and `y` the opportunity to say "this is JPEG data" or "this is plain text" (and so forth.) The new function is implemented by `_AttachableImageContainer`. I've also created `_AttachableURLContainer` to represent files mapped from disk for attachment (instead of directly passing them around as `Data`.) Third-party conforming types will generally want to use Foundation's `NSString` or `URL` API to append path extensions (etc.) > [!NOTE] > Attachments remain experimental. ### 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 a239222 commit 993c7cb

File tree

13 files changed

+207
-144
lines changed

13 files changed

+207
-144
lines changed

Documentation/StyleGuide.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,23 @@ Symbols marked `private` should be given a leading underscore to emphasize that
4646
they are private. Symbols marked `fileprivate`, `internal`, etc. should not have
4747
a leading underscore (except for those `public` symbols mentioned above.)
4848

49+
Symbols that provide storage for higher-visibility symbols can be underscored if
50+
their preferred names would otherwise conflict. For example:
51+
52+
```swift
53+
private var _errorCount: Int
54+
55+
public var errorCount: Int {
56+
get {
57+
_errorCount
58+
}
59+
set {
60+
precondition(newValue >= 0, "Error count cannot be negative")
61+
_errorCount = newValue
62+
}
63+
}
64+
```
65+
4966
Exported C and C++ symbols that are exported should be given the prefix `swt_`
5067
and should otherwise be named using the same lowerCamelCase naming rules as in
5168
Swift. Use the `SWT_EXTERN` macro to ensure that symbols are consistently

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

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -43,38 +43,7 @@ extension Attachment {
4343
encodingQuality: Float,
4444
sourceLocation: SourceLocation
4545
) where AttachableValue == _AttachableImageContainer<T> {
46-
var imageContainer = _AttachableImageContainer(image: attachableValue, encodingQuality: encodingQuality)
47-
48-
// Update the preferred name to include an extension appropriate for the
49-
// given content type. (Note the `else` branch duplicates the logic in
50-
// `preferredContentType(forEncodingQuality:)` but will go away once our
51-
// minimum deployment targets include the UniformTypeIdentifiers framework.)
52-
var preferredName = preferredName ?? Self.defaultPreferredName
53-
if #available(_uttypesAPI, *) {
54-
let contentType: UTType = contentType
55-
.map { $0 as! UTType }
56-
.flatMap { contentType in
57-
if UTType.image.conforms(to: contentType) {
58-
// This type is an abstract base type of .image (or .image itself.)
59-
// We'll infer the concrete type based on other arguments.
60-
return nil
61-
}
62-
return contentType
63-
} ?? .preferred(forEncodingQuality: encodingQuality)
64-
preferredName = (preferredName as NSString).appendingPathExtension(for: contentType)
65-
imageContainer.contentType = contentType
66-
} else {
67-
// The caller can't provide a content type, so we'll pick one for them.
68-
let ext = if encodingQuality < 1.0 {
69-
"jpg"
70-
} else {
71-
"png"
72-
}
73-
if (preferredName as NSString).pathExtension.caseInsensitiveCompare(ext) != .orderedSame {
74-
preferredName = (preferredName as NSString).appendingPathExtension(ext) ?? preferredName
75-
}
76-
}
77-
46+
let imageContainer = _AttachableImageContainer(image: attachableValue, encodingQuality: encodingQuality, contentType: contentType)
7847
self.init(imageContainer, named: preferredName, sourceLocation: sourceLocation)
7948
}
8049

Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@
1212
/// A type representing an error that can occur when attaching an image.
1313
@_spi(ForSwiftTestingOnly)
1414
public enum ImageAttachmentError: Error, CustomStringConvertible {
15-
/// The specified content type did not conform to `.image`.
16-
case contentTypeDoesNotConformToImage
17-
1815
/// The image could not be converted to an instance of `CGImage`.
1916
case couldNotCreateCGImage
2017

@@ -24,11 +21,8 @@ public enum ImageAttachmentError: Error, CustomStringConvertible {
2421
/// The image could not be converted.
2522
case couldNotConvertImage
2623

27-
@_spi(ForSwiftTestingOnly)
2824
public var description: String {
2925
switch self {
30-
case .contentTypeDoesNotConformToImage:
31-
"The specified type does not represent an image format."
3226
case .couldNotCreateCGImage:
3327
"Could not create the corresponding Core Graphics image."
3428
case .couldNotCreateImageDestination:

Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -58,53 +58,75 @@ public struct _AttachableImageContainer<Image>: Sendable where Image: Attachable
5858
nonisolated(unsafe) var image: Image
5959

6060
/// The encoding quality to use when encoding the represented image.
61-
public var encodingQuality: Float
61+
var encodingQuality: Float
6262

6363
/// Storage for ``contentType``.
6464
private var _contentType: (any Sendable)?
6565

6666
/// The content type to use when encoding the image.
6767
///
68-
/// This property should eventually move up to ``Attachment``. It is not part
69-
/// of the public interface of the testing library.
68+
/// The testing library uses this property to determine which image format to
69+
/// encode the associated image as when it is attached to a test.
70+
///
71+
/// If the value of this property does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
72+
/// the result is undefined.
7073
@available(_uttypesAPI, *)
71-
var contentType: UTType? {
74+
var contentType: UTType {
7275
get {
73-
_contentType as? UTType
76+
if let contentType = _contentType as? UTType {
77+
return contentType
78+
} else {
79+
return encodingQuality < 1.0 ? .jpeg : .png
80+
}
7481
}
7582
set {
83+
precondition(
84+
newValue.conforms(to: .image),
85+
"An image cannot be attached as an instance of type '\(newValue.identifier)'. Use a type that conforms to 'public.image' instead."
86+
)
7687
_contentType = newValue
7788
}
7889
}
7990

80-
init(image: Image, encodingQuality: Float) {
81-
self.image = image._makeCopyForAttachment()
82-
self.encodingQuality = encodingQuality
91+
/// The content type to use when encoding the image, substituting a concrete
92+
/// type for `UTType.image`.
93+
///
94+
/// This property is not part of the public interface of the testing library.
95+
@available(_uttypesAPI, *)
96+
var computedContentType: UTType {
97+
if let contentType = _contentType as? UTType, contentType != .image {
98+
contentType
99+
} else {
100+
encodingQuality < 1.0 ? .jpeg : .png
101+
}
83102
}
84-
}
85-
86-
// MARK: -
87103

88-
@available(_uttypesAPI, *)
89-
extension UTType {
90-
/// Determine the preferred content type to encode this image as for a given
91-
/// encoding quality.
104+
/// The type identifier (as a `CFString`) corresponding to this instance's
105+
/// ``computedContentType`` property.
92106
///
93-
/// - Parameters:
94-
/// - encodingQuality: The encoding quality to use when encoding the image.
107+
/// The value of this property is used by ImageIO when serializing an image.
95108
///
96-
/// - Returns: The type to encode this image as.
97-
static func preferred(forEncodingQuality encodingQuality: Float) -> Self {
98-
// If the caller wants lossy encoding, use JPEG.
99-
if encodingQuality < 1.0 {
100-
return .jpeg
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
101116
}
117+
}
102118

103-
// Lossless encoding implies PNG.
104-
return .png
119+
init(image: Image, encodingQuality: Float, contentType: (any Sendable)?) {
120+
self.image = image._makeCopyForAttachment()
121+
self.encodingQuality = encodingQuality
122+
if #available(_uttypesAPI, *), let contentType = contentType as? UTType {
123+
self.contentType = contentType
124+
}
105125
}
106126
}
107127

128+
// MARK: -
129+
108130
extension _AttachableImageContainer: AttachableContainer {
109131
public var attachableValue: Image {
110132
image
@@ -116,21 +138,6 @@ extension _AttachableImageContainer: AttachableContainer {
116138
// Convert the image to a CGImage.
117139
let attachableCGImage = try image.attachableCGImage
118140

119-
// Get the type to encode as. (Note the `else` branches duplicate the logic
120-
// in `preferredContentType(forEncodingQuality:)` but will go away once our
121-
// minimum deployment targets include the UniformTypeIdentifiers framework.)
122-
let typeIdentifier: CFString
123-
if #available(_uttypesAPI, *), let contentType {
124-
guard contentType.conforms(to: .image) else {
125-
throw ImageAttachmentError.contentTypeDoesNotConformToImage
126-
}
127-
typeIdentifier = contentType.identifier as CFString
128-
} else if encodingQuality < 1.0 {
129-
typeIdentifier = kUTTypeJPEG
130-
} else {
131-
typeIdentifier = kUTTypePNG
132-
}
133-
134141
// Create the image destination.
135142
guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, typeIdentifier, 1, nil) else {
136143
throw ImageAttachmentError.couldNotCreateImageDestination
@@ -159,5 +166,13 @@ extension _AttachableImageContainer: AttachableContainer {
159166
try body(UnsafeRawBufferPointer(start: data.bytes, count: data.length))
160167
}
161168
}
169+
170+
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
176+
}
162177
}
163178
#endif

Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,6 @@ extension Attachable where Self: Encodable {
8686
/// _and_ [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding),
8787
/// the default implementation of this function uses the value's conformance
8888
/// to `Encodable`.
89-
///
90-
/// - Note: On Apple platforms, if the attachment's preferred name includes
91-
/// some other path extension, that path extension must represent a type
92-
/// that conforms to [`UTType.propertyList`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/propertylist)
93-
/// or to [`UTType.json`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/json).
9489
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
9590
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)
9691
}

Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,6 @@ extension Attachable where Self: NSSecureCoding {
4646
/// _and_ [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding),
4747
/// the default implementation of this function uses the value's conformance
4848
/// to `Encodable`.
49-
///
50-
/// - Note: On Apple platforms, if the attachment's preferred name includes
51-
/// some other path extension, that path extension must represent a type
52-
/// that conforms to [`UTType.propertyList`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/propertylist).
5349
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
5450
let format = try EncodingFormat(for: attachment)
5551

Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ extension URL {
3737
}
3838

3939
@_spi(Experimental)
40-
extension Attachment where AttachableValue == Data {
40+
extension Attachment where AttachableValue == _AttachableURLContainer {
4141
#if SWT_TARGET_OS_APPLE
4242
/// An operation queue to use for asynchronously reading data from disk.
4343
private static let _operationQueue = OperationQueue()
@@ -65,30 +65,12 @@ extension Attachment where AttachableValue == Data {
6565
throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "Attaching downloaded files is not supported"])
6666
}
6767

68+
// If the user did not provide a preferred name, derive it from the URL.
69+
let preferredName = preferredName ?? url.lastPathComponent
70+
6871
let url = url.resolvingSymlinksInPath()
6972
let isDirectory = try url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory!
7073

71-
// Determine the preferred name of the attachment if one was not provided.
72-
var preferredName = if let preferredName {
73-
preferredName
74-
} else if case let lastPathComponent = url.lastPathComponent, !lastPathComponent.isEmpty {
75-
lastPathComponent
76-
} else {
77-
Self.defaultPreferredName
78-
}
79-
80-
if isDirectory {
81-
// Ensure the preferred name of the archive has an appropriate extension.
82-
preferredName = {
83-
#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
84-
if #available(_uttypesAPI, *) {
85-
return (preferredName as NSString).appendingPathExtension(for: .zip)
86-
}
87-
#endif
88-
return (preferredName as NSString).appendingPathExtension("zip") ?? preferredName
89-
}()
90-
}
91-
9274
#if SWT_TARGET_OS_APPLE
9375
let data: Data = try await withCheckedThrowingContinuation { continuation in
9476
let fileCoordinator = NSFileCoordinator()
@@ -113,7 +95,8 @@ extension Attachment where AttachableValue == Data {
11395
}
11496
#endif
11597

116-
self.init(data, named: preferredName, sourceLocation: sourceLocation)
98+
let urlContainer = _AttachableURLContainer(url: url, data: data, isCompressedDirectory: isDirectory)
99+
self.init(urlContainer, named: preferredName, sourceLocation: sourceLocation)
117100
}
118101
}
119102

Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@
1212
@_spi(Experimental) import Testing
1313
import Foundation
1414

15-
#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
16-
private import UniformTypeIdentifiers
17-
#endif
18-
1915
/// An enumeration describing the encoding formats we support for `Encodable`
2016
/// and `NSSecureCoding` types that conform to `Attachable`.
2117
enum EncodingFormat {
@@ -43,30 +39,6 @@ enum EncodingFormat {
4339
/// - Throws: If the attachment's content type or media type is unsupported.
4440
init(for attachment: borrowing Attachment<some Attachable>) throws {
4541
let ext = (attachment.preferredName as NSString).pathExtension
46-
47-
#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
48-
// If the caller explicitly wants to encode their data as either XML or as a
49-
// property list, use PropertyListEncoder. Otherwise, we'll fall back to
50-
// JSONEncoder below.
51-
if #available(_uttypesAPI, *), let contentType = UTType(filenameExtension: ext) {
52-
if contentType == .data {
53-
self = .default
54-
} else if contentType.conforms(to: .json) {
55-
self = .json
56-
} else if contentType.conforms(to: .xml) {
57-
self = .propertyListFormat(.xml)
58-
} else if contentType.conforms(to: .binaryPropertyList) || contentType == .propertyList {
59-
self = .propertyListFormat(.binary)
60-
} else if contentType.conforms(to: .propertyList) {
61-
self = .propertyListFormat(.openStep)
62-
} else {
63-
let contentTypeDescription = contentType.localizedDescription ?? contentType.identifier
64-
throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "The content type '\(contentTypeDescription)' cannot be used to attach an instance of \(type(of: self)) to a test."])
65-
}
66-
return
67-
}
68-
#endif
69-
7042
if ext.isEmpty {
7143
// No path extension? No problem! Default data.
7244
self = .default

0 commit comments

Comments
 (0)