Skip to content

Add a preferredName(for:basedOn:) member to Attachable to allow customizing filenames. #854

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 12, 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
17 changes: 17 additions & 0 deletions Documentation/StyleGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ Symbols marked `private` should be given a leading underscore to emphasize that
they are private. Symbols marked `fileprivate`, `internal`, etc. should not have
a leading underscore (except for those `public` symbols mentioned above.)

Symbols that provide storage for higher-visibility symbols can be underscored if
their preferred names would otherwise conflict. For example:

```swift
private var _errorCount: Int

public var errorCount: Int {
get {
_errorCount
}
set {
precondition(newValue >= 0, "Error count cannot be negative")
_errorCount = newValue
}
}
```

Exported C and C++ symbols that are exported should be given the prefix `swt_`
and should otherwise be named using the same lowerCamelCase naming rules as in
Swift. Use the `SWT_EXTERN` macro to ensure that symbols are consistently
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,38 +43,7 @@ extension Attachment {
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
}
}

let imageContainer = _AttachableImageContainer(image: attachableValue, encodingQuality: encodingQuality, contentType: contentType)
self.init(imageContainer, named: preferredName, sourceLocation: sourceLocation)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
/// 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

Expand All @@ -24,11 +21,8 @@ public enum ImageAttachmentError: Error, CustomStringConvertible {
/// 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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,53 +58,75 @@ public struct _AttachableImageContainer<Image>: Sendable where Image: Attachable
nonisolated(unsafe) var image: Image

/// The encoding quality to use when encoding the represented image.
public var encodingQuality: Float
var encodingQuality: Float

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

/// The content type to use when encoding the image.
///
/// This property should eventually move up to ``Attachment``. It is not part
/// of the public interface of the testing library.
/// The testing library uses this property to determine which image format to
/// encode the associated image as when it is attached to a test.
///
/// If the value of this property does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
/// the result is undefined.
@available(_uttypesAPI, *)
var contentType: UTType? {
var contentType: UTType {
get {
_contentType as? UTType
if let contentType = _contentType as? UTType {
return contentType
} else {
return encodingQuality < 1.0 ? .jpeg : .png
}
}
set {
precondition(
newValue.conforms(to: .image),
"An image cannot be attached as an instance of type '\(newValue.identifier)'. Use a type that conforms to 'public.image' instead."
)
_contentType = newValue
}
}

init(image: Image, encodingQuality: Float) {
self.image = image._makeCopyForAttachment()
self.encodingQuality = encodingQuality
/// The content type to use when encoding the image, substituting a concrete
/// type for `UTType.image`.
///
/// This property is not part of the public interface of the testing library.
@available(_uttypesAPI, *)
var computedContentType: UTType {
if let contentType = _contentType as? UTType, contentType != .image {
contentType
} else {
encodingQuality < 1.0 ? .jpeg : .png
}
}
}

// MARK: -

@available(_uttypesAPI, *)
extension UTType {
/// Determine the preferred content type to encode this image as for a given
/// encoding quality.
/// The type identifier (as a `CFString`) corresponding to this instance's
/// ``computedContentType`` property.
///
/// - Parameters:
/// - encodingQuality: The encoding quality to use when encoding the image.
/// The value of this property is used by ImageIO when serializing an image.
///
/// - Returns: The type to encode this image as.
static func preferred(forEncodingQuality encodingQuality: Float) -> Self {
// If the caller wants lossy encoding, use JPEG.
if encodingQuality < 1.0 {
return .jpeg
/// This property is not part of the public interface of the testing library.
/// It is used by ImageIO below.
var typeIdentifier: CFString {
if #available(_uttypesAPI, *) {
computedContentType.identifier as CFString
} else {
encodingQuality < 1.0 ? kUTTypeJPEG : kUTTypePNG
}
}

// Lossless encoding implies PNG.
return .png
init(image: Image, encodingQuality: Float, contentType: (any Sendable)?) {
self.image = image._makeCopyForAttachment()
self.encodingQuality = encodingQuality
if #available(_uttypesAPI, *), let contentType = contentType as? UTType {
self.contentType = contentType
}
}
}

// MARK: -

extension _AttachableImageContainer: AttachableContainer {
public var attachableValue: Image {
image
Expand All @@ -116,21 +138,6 @@ extension _AttachableImageContainer: AttachableContainer {
// Convert the image to a CGImage.
let attachableCGImage = try image.attachableCGImage

// Get the type to encode as. (Note the `else` branches duplicate the logic
// in `preferredContentType(forEncodingQuality:)` but will go away once our
// minimum deployment targets include the UniformTypeIdentifiers framework.)
let typeIdentifier: CFString
if #available(_uttypesAPI, *), let contentType {
guard contentType.conforms(to: .image) else {
throw ImageAttachmentError.contentTypeDoesNotConformToImage
}
typeIdentifier = contentType.identifier as CFString
} else if encodingQuality < 1.0 {
typeIdentifier = kUTTypeJPEG
} else {
typeIdentifier = kUTTypePNG
}

// Create the image destination.
guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, typeIdentifier, 1, nil) else {
throw ImageAttachmentError.couldNotCreateImageDestination
Expand Down Expand Up @@ -159,5 +166,13 @@ extension _AttachableImageContainer: AttachableContainer {
try body(UnsafeRawBufferPointer(start: data.bytes, count: data.length))
}
}

public borrowing func preferredName(for attachment: borrowing Attachment<Self>, basedOn suggestedName: String) -> String {
if #available(_uttypesAPI, *) {
return (suggestedName as NSString).appendingPathExtension(for: computedContentType)
}

return suggestedName
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,6 @@ extension Attachable where Self: Encodable {
/// _and_ [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding),
/// the default implementation of this function uses the value's conformance
/// to `Encodable`.
///
/// - Note: On Apple platforms, if the attachment's preferred name includes
/// some other path extension, that path extension must represent a type
/// that conforms to [`UTType.propertyList`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/propertylist)
/// or to [`UTType.json`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/json).
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ extension Attachable where Self: NSSecureCoding {
/// _and_ [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding),
/// the default implementation of this function uses the value's conformance
/// to `Encodable`.
///
/// - Note: On Apple platforms, if the attachment's preferred name includes
/// some other path extension, that path extension must represent a type
/// that conforms to [`UTType.propertyList`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/propertylist).
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
let format = try EncodingFormat(for: attachment)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ extension URL {
}

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

// If the user did not provide a preferred name, derive it from the URL.
let preferredName = preferredName ?? url.lastPathComponent

let url = url.resolvingSymlinksInPath()
let isDirectory = try url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory!

// Determine the preferred name of the attachment if one was not provided.
var preferredName = if let preferredName {
preferredName
} else if case let lastPathComponent = url.lastPathComponent, !lastPathComponent.isEmpty {
lastPathComponent
} else {
Self.defaultPreferredName
}

if isDirectory {
// Ensure the preferred name of the archive has an appropriate extension.
preferredName = {
#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
if #available(_uttypesAPI, *) {
return (preferredName as NSString).appendingPathExtension(for: .zip)
}
#endif
return (preferredName as NSString).appendingPathExtension("zip") ?? preferredName
}()
}

#if SWT_TARGET_OS_APPLE
let data: Data = try await withCheckedThrowingContinuation { continuation in
let fileCoordinator = NSFileCoordinator()
Expand All @@ -113,7 +95,8 @@ extension Attachment where AttachableValue == Data {
}
#endif

self.init(data, named: preferredName, sourceLocation: sourceLocation)
let urlContainer = _AttachableURLContainer(url: url, data: data, isCompressedDirectory: isDirectory)
self.init(urlContainer, named: preferredName, sourceLocation: sourceLocation)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@
@_spi(Experimental) import Testing
import Foundation

#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
private import UniformTypeIdentifiers
#endif

/// An enumeration describing the encoding formats we support for `Encodable`
/// and `NSSecureCoding` types that conform to `Attachable`.
enum EncodingFormat {
Expand Down Expand Up @@ -43,30 +39,6 @@ enum EncodingFormat {
/// - Throws: If the attachment's content type or media type is unsupported.
init(for attachment: borrowing Attachment<some Attachable>) throws {
let ext = (attachment.preferredName as NSString).pathExtension

#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
// If the caller explicitly wants to encode their data as either XML or as a
// property list, use PropertyListEncoder. Otherwise, we'll fall back to
// JSONEncoder below.
if #available(_uttypesAPI, *), let contentType = UTType(filenameExtension: ext) {
if contentType == .data {
self = .default
} else if contentType.conforms(to: .json) {
self = .json
} else if contentType.conforms(to: .xml) {
self = .propertyListFormat(.xml)
} else if contentType.conforms(to: .binaryPropertyList) || contentType == .propertyList {
self = .propertyListFormat(.binary)
} else if contentType.conforms(to: .propertyList) {
self = .propertyListFormat(.openStep)
} else {
let contentTypeDescription = contentType.localizedDescription ?? contentType.identifier
throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "The content type '\(contentTypeDescription)' cannot be used to attach an instance of \(type(of: self)) to a test."])
}
return
}
#endif

if ext.isEmpty {
// No path extension? No problem! Default data.
self = .default
Expand Down
Loading