diff --git a/Documentation/StyleGuide.md b/Documentation/StyleGuide.md index 8002463f1..f20c4d7e1 100644 --- a/Documentation/StyleGuide.md +++ b/Documentation/StyleGuide.md @@ -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 diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift index f93afb7f7..ddc876442 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -43,38 +43,7 @@ extension Attachment { encodingQuality: Float, sourceLocation: SourceLocation ) where AttachableValue == _AttachableImageContainer { - 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) } diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift index fdd0b3f3e..b63831317 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift @@ -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 @@ -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: diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift index 5e8fcd227..9db225826 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift @@ -58,53 +58,75 @@ public struct _AttachableImageContainer: 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 @@ -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 @@ -159,5 +166,13 @@ extension _AttachableImageContainer: AttachableContainer { try body(UnsafeRawBufferPointer(start: data.bytes, count: data.length)) } } + + public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + if #available(_uttypesAPI, *) { + return (suggestedName as NSString).appendingPathExtension(for: computedContentType) + } + + return suggestedName + } } #endif diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift index 3e26f7ead..cfae97ca7 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift @@ -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(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body) } diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift index 622787384..c6916ec39 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift @@ -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(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { let format = try EncodingFormat(for: attachment) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index 815fcfd18..9bfa027d9 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -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() @@ -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() @@ -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) } } diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift b/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift index b60a54882..bbbe934ab 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift @@ -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 { @@ -43,30 +39,6 @@ enum EncodingFormat { /// - Throws: If the attachment's content type or media type is unsupported. init(for attachment: borrowing Attachment) 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 diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLContainer.swift b/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLContainer.swift new file mode 100644 index 000000000..38f21d4d3 --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLContainer.swift @@ -0,0 +1,68 @@ +// +// 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 canImport(Foundation) +@_spi(Experimental) public import Testing +public import Foundation + +/// A wrapper type representing file system objects and URLs that can be +/// attached indirectly. +/// +/// You do not need to use this type directly. Instead, initialize an instance +/// of ``Attachment`` using a file URL. +@_spi(Experimental) +public struct _AttachableURLContainer: Sendable { + /// The underlying URL. + var url: URL + + /// The data contained at ``url``. + var data: Data + + /// Whether or not this instance represents a compressed directory. + var isCompressedDirectory: Bool +} + +// MARK: - + +extension _AttachableURLContainer: AttachableContainer { + public var attachableValue: URL { + url + } + + public func withUnsafeBufferPointer(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try data.withUnsafeBytes(body) + } + + public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + // What extension should we have on the filename so that it has the same + // type as the original file (or, in the case of a compressed directory, is + // a zip file?) + let preferredPathExtension = if isCompressedDirectory { + "zip" + } else { + url.pathExtension + } + + // What path extension is on the suggested name already? + let nsSuggestedName = suggestedName as NSString + let suggestedPathExtension = nsSuggestedName.pathExtension + + // If the suggested name's extension isn't what we would prefer, append the + // preferred extension. + if !preferredPathExtension.isEmpty, + suggestedPathExtension.caseInsensitiveCompare(preferredPathExtension) != .orderedSame, + let result = nsSuggestedName.appendingPathExtension(preferredPathExtension) { + return result + } + + return suggestedName + } +} +#endif diff --git a/Sources/Overlays/_Testing_Foundation/CMakeLists.txt b/Sources/Overlays/_Testing_Foundation/CMakeLists.txt index 0c942cfa3..54a340323 100644 --- a/Sources/Overlays/_Testing_Foundation/CMakeLists.txt +++ b/Sources/Overlays/_Testing_Foundation/CMakeLists.txt @@ -7,6 +7,7 @@ # See http://swift.org/CONTRIBUTORS.txt for Swift project authors add_library(_Testing_Foundation + Attachments/_AttachableURLContainer.swift Attachments/EncodingFormat.swift Attachments/Attachment+URL.swift Attachments/Attachable+NSSecureCoding.swift diff --git a/Sources/Testing/Attachments/Attachable.swift b/Sources/Testing/Attachments/Attachable.swift index 990f80dee..4a1d775a5 100644 --- a/Sources/Testing/Attachments/Attachable.swift +++ b/Sources/Testing/Attachments/Attachable.swift @@ -64,6 +64,22 @@ public protocol Attachable: ~Copyable { /// would not be idiomatic for the buffer to contain a textual description of /// the image. borrowing func withUnsafeBufferPointer(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R + + /// Generate a preferred name for the given attachment. + /// + /// - Parameters: + /// - attachment: The attachment that needs to be named. + /// - suggestedName: A suggested name to use as the basis of the preferred + /// name. This string was provided by the developer when they initialized + /// `attachment`. + /// + /// - Returns: The preferred name for `attachment`. + /// + /// The testing library uses this function to determine the best name to use + /// when adding `attachment` to a test report or persisting it to storage. The + /// default implementation of this function returns `suggestedName` without + /// any changes. + borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String } // MARK: - Default implementations @@ -72,6 +88,10 @@ extension Attachable where Self: ~Copyable { public var estimatedAttachmentByteCount: Int? { nil } + + public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + suggestedName + } } extension Attachable where Self: Collection, Element == UInt8 { diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index 034f5e03c..f69df3679 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -42,6 +42,9 @@ public struct Attachment: ~Copyable where AttachableValue: Atta "untitled" } + /// Storage for ``preferredName``. + fileprivate var _preferredName: String? + /// A filename to use when writing this attachment to a test report or to a /// file on disk. /// @@ -49,7 +52,14 @@ public struct Attachment: ~Copyable where AttachableValue: Atta /// testing library may substitute a different filename as needed. If the /// value of this property has not been explicitly set, the testing library /// will attempt to generate its own value. - public var preferredName: String + public var preferredName: String { + let suggestedName = if let _preferredName, !_preferredName.isEmpty { + _preferredName + } else { + Self.defaultPreferredName + } + return attachableValue.preferredName(for: self, basedOn: suggestedName) + } /// The source location of this instance. /// @@ -83,7 +93,7 @@ extension Attachment where AttachableValue: ~Copyable { /// attachment. public init(_ attachableValue: consuming AttachableValue, named preferredName: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) { self._attachableValue = attachableValue - self.preferredName = preferredName ?? Self.defaultPreferredName + self._preferredName = preferredName self.sourceLocation = sourceLocation } } @@ -98,7 +108,7 @@ extension Attachment where AttachableValue == AnyAttachable { self.init( _attachableValue: AnyAttachable(attachableValue: attachment.attachableValue), fileSystemPath: attachment.fileSystemPath, - preferredName: attachment.preferredName, + _preferredName: attachment._preferredName, sourceLocation: attachment.sourceLocation ) } @@ -139,13 +149,26 @@ public struct AnyAttachable: AttachableContainer, Copyable, Sendable { let temporaryAttachment = Attachment( _attachableValue: attachableValue, fileSystemPath: attachment.fileSystemPath, - preferredName: attachment.preferredName, + _preferredName: attachment._preferredName, sourceLocation: attachment.sourceLocation ) return try temporaryAttachment.withUnsafeBufferPointer(body) } return try open(attachableValue, for: attachment) } + + public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + func open(_ attachableValue: T, for attachment: borrowing Attachment) -> String where T: Attachable & Sendable & Copyable { + let temporaryAttachment = Attachment( + _attachableValue: attachableValue, + fileSystemPath: attachment.fileSystemPath, + _preferredName: attachment._preferredName, + sourceLocation: attachment.sourceLocation + ) + return temporaryAttachment.preferredName + } + return open(attachableValue, for: attachment) + } } // MARK: - Describing an attachment @@ -232,7 +255,12 @@ extension Attachment where AttachableValue: ~Copyable { do { let attachmentCopy = try withUnsafeBufferPointer { buffer in let attachableContainer = AnyAttachable(attachableValue: Array(buffer)) - return Attachment(_attachableValue: attachableContainer, fileSystemPath: fileSystemPath, preferredName: preferredName, sourceLocation: sourceLocation) + return Attachment( + _attachableValue: attachableContainer, + fileSystemPath: fileSystemPath, + _preferredName: preferredName, // invokes preferredName(for:basedOn:) + sourceLocation: sourceLocation + ) } Event.post(.valueAttached(attachmentCopy)) } catch { diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 4013882a1..98ecc668d 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -533,7 +533,7 @@ extension AttachmentTests { } @available(_uttypesAPI, *) - @Test(arguments: [Float(0.0).nextUp, 0.25, 0.5, 0.75, 1.0], [.png as UTType?, .jpeg, .gif, .image, .data, nil]) + @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 attachment = Attachment(image, named: "diamond", as: type, encodingQuality: quality) @@ -541,15 +541,20 @@ extension AttachmentTests { try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { buffer in #expect(buffer.count > 32) } + if let ext = type?.preferredFilenameExtension { + #expect(attachment.preferredName == ("diamond" as NSString).appendingPathExtension(ext)) + } } +#if !SWT_NO_EXIT_TESTS @available(_uttypesAPI, *) @Test func cannotAttachCGImageWithNonImageType() async { - #expect(throws: ImageAttachmentError.contentTypeDoesNotConformToImage) { + await #expect(exitsWith: .failure) { let attachment = Attachment(try Self.cgImage.get(), named: "diamond", as: .mp3) try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { _ in } } } +#endif #endif } }