diff --git a/Package.swift b/Package.swift index 67c0614cd..3eadcff13 100644 --- a/Package.swift +++ b/Package.swift @@ -154,6 +154,7 @@ let package = Package( "_Testing_AppKit", "_Testing_CoreGraphics", "_Testing_CoreImage", + "_Testing_CoreTransferable", "_Testing_Foundation", "_Testing_UIKit", "_Testing_WinSDK", @@ -256,6 +257,15 @@ let package = Package( exclude: ["CMakeLists.txt"], swiftSettings: .packageSettings() + .enableLibraryEvolution() + .moduleABIName("_Testing_CoreImage") ), + .target( + name: "_Testing_CoreTransferable", + dependencies: [ + "Testing", + ], + path: "Sources/Overlays/_Testing_CoreTransferable", + exclude: ["CMakeLists.txt"], + swiftSettings: .packageSettings() + .enableLibraryEvolution() + .moduleABIName("_Testing_CoreTransferable") + ), .target( name: "_Testing_Foundation", dependencies: [ @@ -399,6 +409,7 @@ extension Array where Element == PackageDescription.SwiftSetting { .enableExperimentalFeature("AvailabilityMacro=_uttypesAPI:macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0"), .enableExperimentalFeature("AvailabilityMacro=_clockAPI:macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0"), .enableExperimentalFeature("AvailabilityMacro=_typedThrowsAPI:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"), + .enableExperimentalFeature("AvailabilityMacro=_transferableAPI:macOS 15.2, iOS 18.2, watchOS 11.2, tvOS 18.2, visionOS 2.2"), .enableExperimentalFeature("AvailabilityMacro=_castingWithNonCopyableGenerics:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"), .enableExperimentalFeature("AvailabilityMacro=_distantFuture:macOS 99.0, iOS 99.0, watchOS 99.0, tvOS 99.0, visionOS 99.0"), diff --git a/Sources/Overlays/CMakeLists.txt b/Sources/Overlays/CMakeLists.txt index 434b4d3ec..2a646dfd3 100644 --- a/Sources/Overlays/CMakeLists.txt +++ b/Sources/Overlays/CMakeLists.txt @@ -9,6 +9,7 @@ add_subdirectory(_Testing_AppKit) add_subdirectory(_Testing_CoreGraphics) add_subdirectory(_Testing_CoreImage) +add_subdirectory(_Testing_CoreTransferable) add_subdirectory(_Testing_Foundation) add_subdirectory(_Testing_UIKit) add_subdirectory(_Testing_WinSDK) diff --git a/Sources/Overlays/_Testing_CoreTransferable/Attachments/Attachment+Transferable.swift b/Sources/Overlays/_Testing_CoreTransferable/Attachments/Attachment+Transferable.swift new file mode 100644 index 000000000..1a8ed6431 --- /dev/null +++ b/Sources/Overlays/_Testing_CoreTransferable/Attachments/Attachment+Transferable.swift @@ -0,0 +1,87 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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(CoreTransferable) +public import Testing +public import CoreTransferable + +public import UniformTypeIdentifiers + +/// @Metadata { +/// @Available(Swift, introduced: 6.4) +/// } +@available(_transferableAPI, *) +extension Attachment { + /// Initialize an instance of this type that encloses the given transferable + /// value. + /// + /// - Parameters: + /// - transferableValue: The value that will be attached to the output of + /// the test run. + /// - contentType: The content type with which to export `transferableValue`. + /// If this argument is `nil`, the testing library calls [`exportedContentTypes(_:)`](https://developer.apple.com/documentation/coretransferable/transferable/exportedcontenttypes(_:)) + /// on `transferableValue` and uses the first type the function returns + /// that conforms to [`UTType.data`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/data). + /// - preferredName: The preferred name of the attachment to use when saving + /// it. If `nil`, the testing library attempts to generate a reasonable + /// filename for the attached value. + /// - sourceLocation: The source location of the call to this initializer. + /// This value is used when recording issues associated with the + /// attachment. + /// + /// - Throws: Any error that occurs while exporting `transferableValue`. + /// + /// Use this initializer to create an instance of ``Attachment`` from a value + /// that conforms to the [`Transferable`](https://developer.apple.com/documentation/coretransferable/transferable) + /// protocol. + /// + /// ```swift + /// let menu = FoodTruck.menu + /// let attachment = try await Attachment(exporting: menu, as: .pdf) + /// Attachment.record(attachment) + /// ``` + /// + /// When you call this initializer and pass it a transferable value, it + /// calls [`exported(as:)`](https://developer.apple.com/documentation/coretransferable/transferable/exported(as:)) + /// on that value. This operation may take some time, so this initializer + /// suspends the calling task until it is complete. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.4) + /// } + public init( + exporting transferableValue: T, + as contentType: UTType? = nil, + named preferredName: String? = nil, + sourceLocation: SourceLocation = #_sourceLocation + ) async throws where T: Transferable, AttachableValue == _AttachableTransferableWrapper { + let transferableWrapper = try await _AttachableTransferableWrapper(exporting: transferableValue, as: contentType) + self.init(transferableWrapper, named: preferredName, sourceLocation: sourceLocation) + } +} + +// MARK: - + +/// A type describing errors that can occur when attaching a transferable value. +enum TransferableAttachmentError: Error { + /// The developer did not pass a content type and the value did not list any + /// that conform to `UTType.data`. + case suitableContentTypeNotFound +} + +extension TransferableAttachmentError: CustomStringConvertible { + var description: String { + switch self { + case .suitableContentTypeNotFound: + "The value does not list any exported content types that conform to 'UTType.data'." + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_CoreTransferable/Attachments/_AttachableTransferableWrapper.swift b/Sources/Overlays/_Testing_CoreTransferable/Attachments/_AttachableTransferableWrapper.swift new file mode 100644 index 000000000..d26855681 --- /dev/null +++ b/Sources/Overlays/_Testing_CoreTransferable/Attachments/_AttachableTransferableWrapper.swift @@ -0,0 +1,81 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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(CoreTransferable) +public import Testing +public import CoreTransferable + +private import Foundation +import UniformTypeIdentifiers + +/// A wrapper type representing transferable values that can be attached +/// indirectly. +/// +/// You do not need to use this type directly. Instead, initialize an instance +/// of ``Attachment`` using an instance of a type conforming to the [`Transferable`](https://developer.apple.com/documentation/coretransferable/transferable) +/// protocol. +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.4) +/// } +@available(_transferableAPI, *) +public struct _AttachableTransferableWrapper: Sendable where T: Transferable { + /// The transferable value. + private var _transferableValue: T + + /// The content type used to export the transferable value. + private var _contentType: UTType + + /// The exported form of the transferable value. + private var _bytes: Data + + init(exporting transferableValue: T, as contentType: UTType?) async throws { + let contentType = contentType ?? transferableValue.exportedContentTypes() + .first { $0.conforms(to: .data) } + guard let contentType else { + throw TransferableAttachmentError.suitableContentTypeNotFound + } + + _transferableValue = transferableValue + _contentType = contentType + _bytes = try await transferableValue.exported(as: contentType) + } +} + +// MARK: - + +/// @Metadata { +/// @Available(Swift, introduced: 6.4) +/// } +@available(_transferableAPI, *) +extension _AttachableTransferableWrapper: AttachableWrapper { + /// @Metadata { + /// @Available(Swift, introduced: 6.4) + /// } + public var wrappedValue: T { + _transferableValue + } + + /// @Metadata { + /// @Available(Swift, introduced: 6.4) + /// } + public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try _bytes.withUnsafeBytes(body) + } + + /// @Metadata { + /// @Available(Swift, introduced: 6.4) + /// } + public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + let baseName = _transferableValue.suggestedFilename ?? suggestedName + return (baseName as NSString).appendingPathExtension(for: _contentType) + } +} +#endif diff --git a/Sources/Overlays/_Testing_CoreTransferable/CMakeLists.txt b/Sources/Overlays/_Testing_CoreTransferable/CMakeLists.txt new file mode 100644 index 000000000..2752a5c3f --- /dev/null +++ b/Sources/Overlays/_Testing_CoreTransferable/CMakeLists.txt @@ -0,0 +1,24 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2026 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 (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + include(ModuleABIName) + add_library(_Testing_CoreTransferable + Attachments/_AttachableTransferableWrapper.swift + Attachments/Attachment+Transferable.swift + ReexportTesting.swift) + + target_link_libraries(_Testing_CoreTransferable PUBLIC + Testing) + + target_compile_options(_Testing_CoreTransferable PRIVATE + -enable-library-evolution + -emit-module-interface -emit-module-interface-path $/_Testing_CoreTransferable.swiftinterface) + + _swift_testing_install_target(_Testing_CoreTransferable) +endif() diff --git a/Sources/Overlays/_Testing_CoreTransferable/ReexportTesting.swift b/Sources/Overlays/_Testing_CoreTransferable/ReexportTesting.swift new file mode 100644 index 000000000..20cbda91d --- /dev/null +++ b/Sources/Overlays/_Testing_CoreTransferable/ReexportTesting.swift @@ -0,0 +1,11 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// + +@_exported public import Testing diff --git a/Sources/Testing/Testing.swiftcrossimport/CoreTransferable.swiftoverlay b/Sources/Testing/Testing.swiftcrossimport/CoreTransferable.swiftoverlay new file mode 100644 index 000000000..727419b3b --- /dev/null +++ b/Sources/Testing/Testing.swiftcrossimport/CoreTransferable.swiftoverlay @@ -0,0 +1,3 @@ +version: 1 +modules: +- name: _Testing_CoreTransferable diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index f3bb8df12..9b986a54b 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -26,6 +26,10 @@ import CoreGraphics import CoreImage import _Testing_CoreImage #endif +#if canImport(CoreTransferable) && canImport(_Testing_CoreTransferable) +import CoreTransferable +@_spi(Experimental) import _Testing_CoreTransferable +#endif #if canImport(UIKit) && canImport(_Testing_UIKit) import UIKit import _Testing_UIKit @@ -258,7 +262,7 @@ struct AttachmentTests { #expect((attachment.attachableValue as Any) is AnyAttachable.Wrapped) #expect(attachment.sourceLocation.fileID == #fileID) - valueAttached() + valueAttached() } await Test { @@ -486,6 +490,45 @@ struct AttachmentTests { } } #endif + +#if canImport(CoreTransferable) && canImport(_Testing_CoreTransferable) + @available(_transferableAPI, *) + @Test("Attach Transferable-conformant value") + func transferable() async throws { + let value = MyTransferable() + let attachment = try await Attachment(exporting: value, as: .plainText) + #expect(value == attachment.attachableValue) + try attachment.withUnsafeBytes { bytes in + #expect(Array(bytes) == Array(MyTransferable.stringValue.utf8)) + } + } + + @available(_transferableAPI, *) + @Test("Attach Transferable-conformant value with a nonsensical type") + func transferableWithNonsensicalType() async throws { + let value = MyTransferable() + await #expect(throws: (any Error).self) { + _ = try await Attachment(exporting: value, as: .gif) + } + } + + @available(_transferableAPI, *) + @Test("Preferred name of Transferable-conformant value") + func transferablePreferredName() async throws { + let value = MyTransferable() + let attachment = try await Attachment(exporting: value) + #expect(attachment.preferredName == "untitled.txt") + } + + @available(_transferableAPI, *) + @Test("Attach Transferable-conformant value with no available type") + func transferableWithNoAvailableType() async throws { + let value = MyBadTransferable() + await #expect(throws: (any Error).self) { + _ = try await Attachment(exporting: value) + } + } +#endif } extension AttachmentTests { @@ -1083,6 +1126,26 @@ struct MySendableAttachableWithDefaultByteCount: Attachable, Sendable { } } +#if canImport(CoreTransferable) && canImport(_Testing_CoreTransferable) +struct MyTransferable: Transferable, Equatable { + static let stringValue = "This isn't even my exported form!" + + static var transferRepresentation: some TransferRepresentation { + DataRepresentation(exportedContentType: .plainText) { instance in + Data(Self.stringValue.utf8) + } + } +} + +struct MyBadTransferable: Transferable, Equatable { + static var transferRepresentation: some TransferRepresentation { + DataRepresentation(exportedContentType: .directory) { _ in + throw MyError() + } + } +} +#endif + #if canImport(Foundation) && canImport(_Testing_Foundation) struct MyCodableAttachable: Codable, Attachable, Sendable { var string: String diff --git a/cmake/modules/shared/AvailabilityDefinitions.cmake b/cmake/modules/shared/AvailabilityDefinitions.cmake index ebf196ffb..f26a159f6 100644 --- a/cmake/modules/shared/AvailabilityDefinitions.cmake +++ b/cmake/modules/shared/AvailabilityDefinitions.cmake @@ -13,5 +13,6 @@ add_compile_options( "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_uttypesAPI:macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_clockAPI:macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_typedThrowsAPI:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0\">" + "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_transferableAPI:macOS 15.2, iOS 18.2, watchOS 11.2, tvOS 18.2, visionOS 2.2\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_castingWithNonCopyableGenerics:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_distantFuture:macOS 99.0, iOS 99.0, watchOS 99.0, tvOS 99.0, visionOS 99.0\">")