From 65e006eb1ab8cc0840bdbf16e26fa0af25734dec Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 19 Sep 2024 14:25:02 -0400 Subject: [PATCH 1/8] Add support for attachments to the Foundation cross-import overlay. This PR adds experimental support for attachments to some types in Foundation via the (non-functional) cross-import overlay. @stmontgomery is working on setting up said overlay so that it can actually be used; until then, the changes here are speculative only. --- Package.swift | 2 + .../Attachments/Data+Test.Attachable.swift | 21 ++ .../Attachments/EncodingFormat.swift | 84 ++++++ ....Attachable+Encodable&NSSecureCoding.swift | 22 ++ .../Test.Attachable+Encodable.swift | 97 +++++++ .../Test.Attachable+NSSecureCoding.swift | 78 ++++++ .../Attachments/Test.Attachment+URL.swift | 158 ++++++++++++ .../Events/Clock+Date.swift | 0 .../_Testing_Foundation/ReexportTesting.swift | 2 +- .../Testing/Attachments/Test.Attachable.swift | 143 +++++++++++ Tests/TestingTests/AttachmentTests.swift | 241 +++++++++++++++++- .../shared/AvailabilityDefinitions.cmake | 1 + 12 files changed, 838 insertions(+), 11 deletions(-) create mode 100644 Sources/Overlays/_Testing_Foundation/Attachments/Data+Test.Attachable.swift create mode 100644 Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift create mode 100644 Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift create mode 100644 Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable.swift create mode 100644 Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+NSSecureCoding.swift create mode 100644 Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift rename Sources/{ => Overlays}/_Testing_Foundation/Events/Clock+Date.swift (100%) rename Sources/{ => Overlays}/_Testing_Foundation/ReexportTesting.swift (91%) create mode 100644 Sources/Testing/Attachments/Test.Attachable.swift diff --git a/Package.swift b/Package.swift index 202b09e1f..d42d570c0 100644 --- a/Package.swift +++ b/Package.swift @@ -96,6 +96,7 @@ let package = Package( dependencies: [ "Testing", ], + path: "Sources/Overlays/_Testing_Foundation", swiftSettings: .packageSettings ), ], @@ -147,6 +148,7 @@ extension Array where Element == PackageDescription.SwiftSetting { private static var availabilityMacroSettings: Self { [ .enableExperimentalFeature("AvailabilityMacro=_mangledTypeNameAPI:macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0"), + .enableExperimentalFeature("AvailabilityMacro=_uttypesAPI:macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0"), .enableExperimentalFeature("AvailabilityMacro=_backtraceAsyncAPI:macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0"), .enableExperimentalFeature("AvailabilityMacro=_clockAPI:macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0"), .enableExperimentalFeature("AvailabilityMacro=_regexAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"), diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Data+Test.Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Test.Attachable.swift new file mode 100644 index 000000000..4c771562e --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Test.Attachable.swift @@ -0,0 +1,21 @@ +// +// 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 + +@_spi(Experimental) +extension Data: Test.Attachable { + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try withUnsafeBytes(body) + } +} +#endif diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift b/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift new file mode 100644 index 000000000..fb3a85413 --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift @@ -0,0 +1,84 @@ +// +// 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) 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 `Test.Attachable`. +enum EncodingFormat { + /// A property list format. + /// + /// - Parameters: + /// - format: The corresponding property list format. + case propertyListFormat(_ format: PropertyListSerialization.PropertyListFormat) + + /// The JSON format. + case json + + /// The encoding format to use by default. + /// + /// The specific format this case corresponds to depends on if we are encoding + /// an `Encodable` value or an `NSSecureCoding` value. + case `default` + + /// Initialize an instance of this type representing the content type or media + /// type of the specified attachment. + /// + /// - Parameters: + /// - attachment: The attachment that will be encoded. + /// + /// - Throws: If the attachment's content type or media type is unsupported. + init(for attachment: borrowing Test.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 + } else if ext.caseInsensitiveCompare("plist") == .orderedSame { + self = .propertyListFormat(.binary) + } else if ext.caseInsensitiveCompare("xml") == .orderedSame { + self = .propertyListFormat(.xml) + } else if ext.caseInsensitiveCompare("json") == .orderedSame { + self = .json + } else { + throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "The path extension '.\(ext)' cannot be used to attach an instance of \(type(of: self)) to a test."]) + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift new file mode 100644 index 000000000..4b1b95f22 --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift @@ -0,0 +1,22 @@ +// +// 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 + +@_spi(Experimental) +extension Test.Attachable where Self: Encodable & NSSecureCoding { + @_documentation(visibility: private) + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body) + } +} +#endif diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable.swift new file mode 100644 index 000000000..0bafb011e --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable.swift @@ -0,0 +1,97 @@ +// +// 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 +private import Foundation + +/// A common implementation of ``withUnsafeBufferPointer(for:_:)`` that is +/// used when a type conforms to `Encodable`, whether or not it also conforms +/// to `NSSecureCoding`. +/// +/// - Parameters: +/// - attachment: The attachment that is requesting a buffer (that is, the +/// attachment containing this instance.) +/// - body: A function to call. A temporary buffer containing a data +/// representation of this instance is passed to it. +/// +/// - Returns: Whatever is returned by `body`. +/// +/// - Throws: Whatever is thrown by `body`, or any error that prevented the +/// creation of the buffer. +func withUnsafeBufferPointer(encoding attachableValue: borrowing E, for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R where E: Test.Attachable & Encodable { + let format = try EncodingFormat(for: attachment) + + let data: Data + switch format { + case let .propertyListFormat(propertyListFormat): + let plistEncoder = PropertyListEncoder() + plistEncoder.outputFormat = propertyListFormat + data = try plistEncoder.encode(attachableValue) + case .default: + // The default format is JSON. + fallthrough + case .json: + // We cannot use our own JSON encoding wrapper here because that would + // require it be exported with (at least) package visibility which would + // create a visible external dependency on Foundation in the main testing + // library target. + data = try JSONEncoder().encode(attachableValue) + } + + return try data.withUnsafeBytes(body) +} + +// Implement the protocol requirements generically for any encodable value by +// encoding to JSON. This lets developers provide trivial conformance to the +// protocol for types that already support Codable. +@_spi(Experimental) +extension Test.Attachable where Self: Encodable { + /// Encode this value into a buffer using either [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder) + /// or [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder), + /// then call a function and pass that buffer to it. + /// + /// - Parameters: + /// - attachment: The attachment that is requesting a buffer (that is, the + /// attachment containing this instance.) + /// - body: A function to call. A temporary buffer containing a data + /// representation of this instance is passed to it. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`, or any error that prevented the + /// creation of the buffer. + /// + /// The testing library uses this function when writing an attachment to a + /// test report or to a file on disk. The encoding used depends on the path + /// extension specified by the value of `attachment`'s ``Testing/Test/Attachment/preferredName`` + /// property: + /// + /// | Extension | Encoding Used | Encoder Used | + /// |-|-|-| + /// | `".xml"` | XML property list | [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder) | + /// | `".plist"` | Binary property list | [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder) | + /// | None, `".json"` | JSON | [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder) | + /// + /// OpenStep-style property lists are not supported. If a value conforms to + /// _both_ [`Encodable`](https://developer.apple.com/documentation/swift/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 Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body) + } +} +#endif diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+NSSecureCoding.swift new file mode 100644 index 000000000..6a79a37ff --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+NSSecureCoding.swift @@ -0,0 +1,78 @@ +// +// 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 + +// As with Encodable, implement the protocol requirements for +// NSSecureCoding-conformant classes by default. The implementation uses +// NSKeyedArchiver for encoding. +@_spi(Experimental) +extension Test.Attachable where Self: NSSecureCoding { + /// Encode this object using [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver) + /// into a buffer, then call a function and pass that buffer to it. + /// + /// - Parameters: + /// - attachment: The attachment that is requesting a buffer (that is, the + /// attachment containing this instance.) + /// - body: A function to call. A temporary buffer containing a data + /// representation of this instance is passed to it. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`, or any error that prevented the + /// creation of the buffer. + /// + /// The testing library uses this function when writing an attachment to a + /// test report or to a file on disk. The encoding used depends on the path + /// extension specified by the value of `attachment`'s ``Testing/Test/Attachment/preferredName`` + /// property: + /// + /// | Extension | Encoding Used | Encoder Used | + /// |-|-|-| + /// | `".xml"` | XML property list | [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver) | + /// | None, `".plist"` | Binary property list | [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver) | + /// + /// OpenStep-style property lists are not supported. If a value conforms to + /// _both_ [`Encodable`](https://developer.apple.com/documentation/swift/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). + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + let format = try EncodingFormat(for: attachment) + + var data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true) + switch format { + case .default: + // The default format is just what NSKeyedArchiver produces. + break + case let .propertyListFormat(propertyListFormat): + // BUG: Foundation does not offer a variant of + // NSKeyedArchiver.archivedData(withRootObject:requiringSecureCoding:) + // that is Swift-safe (throws errors instead of exceptions) and lets the + // caller specify the output format. Work around this issue by decoding + // the archive re-encoding it manually. + if propertyListFormat != .binary { + let plist = try PropertyListSerialization.propertyList(from: data, format: nil) + data = try PropertyListSerialization.data(fromPropertyList: plist, format: propertyListFormat, options: 0) + } + case .json: + throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "An instance of \(type(of: self)) cannot be encoded as JSON. Specify a property list format instead."]) + } + + return try data.withUnsafeBytes(body) + } +} +#endif diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift new file mode 100644 index 000000000..5f56ad727 --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift @@ -0,0 +1,158 @@ +// +// 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 + +#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers) +private import UniformTypeIdentifiers +#endif + +#if !SWT_NO_FILE_IO +extension URL { + /// The file system path of the URL, equivalent to `path`. + var fileSystemPath: String { +#if os(Windows) + // BUG: `path` includes a leading slash which makes it invalid on Windows. + // SEE: https://github.com/swiftlang/swift-foundation/pull/964 + let path = path + if path.starts(with: /\/[A-Za-z]:\//) { + return String(path.dropFirst()) + } +#endif + return path + } +} + +#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers) +@available(_uttypesAPI, *) +extension UTType { + /// A type that represents a `.tgz` archive, or `nil` if the system does not + /// recognize that content type. + fileprivate static let tgz = UTType("org.gnu.gnu-zip-tar-archive") +} +#endif + +@_spi(Experimental) +extension Test.Attachment where AttachableValue == Data { + /// Initialize an instance of this type with the contents of the given URL. + /// + /// - Parameters: + /// - url: The URL containing the attachment's data. + /// - preferredName: The preferred name of the attachment when writing it + /// to a test report or to disk. If `nil`, the name of the attachment is + /// derived from the last path component of `url`. + /// - sourceLocation: The source location of the attachment. + /// + /// - Throws: Any error that occurs attempting to read from `url`. + public init( + contentsOf url: URL, + named preferredName: String? = nil + ) async throws { + guard url.isFileURL else { + // TODO: network URLs? + throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "Attaching downloaded files is not supported"]) + } + + // FIXME: use NSFileCoordinator on Darwin? + + 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, *), let tgz = UTType.tgz { + return (preferredName as NSString).appendingPathExtension(for: tgz) + } +#endif + return (preferredName as NSString).appendingPathExtension("tgz") ?? preferredName + }() + + try await self.init(Data(compressedContentsOfDirectoryAt: url), named: preferredName) + } else { + // Load the file. + try self.init(Data(contentsOf: url, options: [.mappedIfSafe]), named: preferredName) + } + } +} + +// MARK: - Attaching directories + +extension Data { + /// Initialize an instance of this type by compressing the contents of a + /// directory. + /// + /// - Parameters: + /// - directoryURL: A URL referring to the directory to attach. + /// + /// - Throws: Any error encountered trying to compress the directory, or if + /// directories cannot be compressed on this platform. + /// + /// This initializer asynchronously compresses the contents of `directoryURL` + /// into an archive (currently of `.tgz` format, although this is subject to + /// change) and stores a mapped copy of that archive. + init(compressedContentsOfDirectoryAt directoryURL: URL) async throws { + let temporaryName = "\(UUID().uuidString).tgz" + let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(temporaryName) + +#if !SWT_NO_PROCESS_SPAWNING +#if os(Windows) + let tarPath = #"C:\Windows\System32\tar.exe"# +#else + let tarPath = "/usr/bin/tar" +#endif + let sourcePath = directoryURL.fileSystemPath + let destinationPath = temporaryURL.fileSystemPath + defer { + try? FileManager().removeItem(at: temporaryURL) + } + + try await withCheckedThrowingContinuation { continuation in + do { + _ = try Process.run( + URL(fileURLWithPath: tarPath, isDirectory: false), + arguments: ["--create", "--gzip", "--directory", sourcePath, "--file", destinationPath, "."] + ) { process in + let terminationReason = process.terminationReason + let terminationStatus = process.terminationStatus + if terminationReason == .exit && terminationStatus == EXIT_SUCCESS { + continuation.resume() + } else { + let error = CocoaError(.fileWriteUnknown, userInfo: [ + NSLocalizedDescriptionKey: "The directory at '\(sourcePath)' could not be compressed.", + ]) + continuation.resume(throwing: error) + } + } + } catch { + continuation.resume(throwing: error) + } + } + + try self.init(contentsOf: temporaryURL, options: [.mappedIfSafe]) +#else + throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "This platform does not support attaching directories to tests."]) +#endif + } +} +#endif +#endif diff --git a/Sources/_Testing_Foundation/Events/Clock+Date.swift b/Sources/Overlays/_Testing_Foundation/Events/Clock+Date.swift similarity index 100% rename from Sources/_Testing_Foundation/Events/Clock+Date.swift rename to Sources/Overlays/_Testing_Foundation/Events/Clock+Date.swift diff --git a/Sources/_Testing_Foundation/ReexportTesting.swift b/Sources/Overlays/_Testing_Foundation/ReexportTesting.swift similarity index 91% rename from Sources/_Testing_Foundation/ReexportTesting.swift rename to Sources/Overlays/_Testing_Foundation/ReexportTesting.swift index d06def5b8..3faa622d7 100644 --- a/Sources/_Testing_Foundation/ReexportTesting.swift +++ b/Sources/Overlays/_Testing_Foundation/ReexportTesting.swift @@ -8,4 +8,4 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -@_exported import Testing +@_exported public import Testing diff --git a/Sources/Testing/Attachments/Test.Attachable.swift b/Sources/Testing/Attachments/Test.Attachable.swift new file mode 100644 index 000000000..3bd4e88af --- /dev/null +++ b/Sources/Testing/Attachments/Test.Attachable.swift @@ -0,0 +1,143 @@ +// +// 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 +// + +@_spi(Experimental) +extension Test { + /// A protocol describing a type that can be attached to a test report or + /// written to disk when a test is run. + /// + /// To attach an attachable value to a test report or test run output, use it + /// to initialize a new instance of ``Test/Attachment``, then call + /// ``Test/Attachment/attach(sourceLocation:)``. An attachment can only be + /// attached once. + /// + /// The testing library provides default conformances to this protocol for a + /// variety of standard library types. Most user-defined types do not need to + /// conform to this protocol. + /// + /// A type should conform to this protocol if it can be represented as a + /// sequence of bytes that would be diagnostically useful if a test fails. If + /// a type cannot conform directly to this protocol (such as a non-final class + /// or a type declared in a third-party module), you can create a container + /// type that conforms to ``Test/AttachableContainer`` to act as a proxy. + public protocol Attachable: ~Copyable { + /// An estimate of the number of bytes of memory needed to store this value + /// as an attachment. + /// + /// The testing library uses this property to determine if an attachment + /// should be held in memory or should be immediately persisted to storage. + /// Larger attachments are more likely to be persisted, but the algorithm + /// the testing library uses is an implementation detail and is subject to + /// change. + /// + /// The value of this property is approximately equal to the number of bytes + /// that will actually be needed, or `nil` if the value cannot be computed + /// efficiently. The default implementation of this property returns `nil`. + /// + /// - Complexity: O(1) unless `Self` conforms to `Collection`, in which case + /// up to O(_n_) where _n_ is the length of the collection. + var estimatedAttachmentByteCount: Int? { get } + + /// Call a function and pass a buffer representing this instance to it. + /// + /// - Parameters: + /// - attachment: The attachment that is requesting a buffer (that is, the + /// attachment containing this instance.) + /// - body: A function to call. A temporary buffer containing a data + /// representation of this instance is passed to it. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`, or any error that prevented the + /// creation of the buffer. + /// + /// The testing library uses this function when writing an attachment to a + /// test report or to a file on disk. The format of the buffer is + /// implementation-defined, but should be "idiomatic" for this type: for + /// example, if this type represents an image, it would be appropriate for + /// the buffer to contain an image in PNG format, JPEG format, etc., but it + /// would not be idiomatic for the buffer to contain a textual description + /// of the image. + borrowing func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R + } +} + +// MARK: - Default implementations + +extension Test.Attachable where Self: ~Copyable { + public var estimatedAttachmentByteCount: Int? { + nil + } +} + +extension Test.Attachable where Self: Collection, Element == UInt8 { + public var estimatedAttachmentByteCount: Int? { + count + } + + // We do not provide an implementation of withUnsafeBufferPointer(for:_:) here + // because there is no way in the standard library to statically detect if a + // collection can provide contiguous storage (_HasContiguousBytes is not API.) + // If withContiguousStorageIfAvailable(_:) fails, we don't want to make a + // (potentially expensive!) copy of the collection. +} + +extension Test.Attachable where Self: StringProtocol { + public var estimatedAttachmentByteCount: Int? { + // NOTE: utf8.count may be O(n) for foreign strings. + // SEE: https://github.com/swiftlang/swift/blob/main/stdlib/public/core/StringUTF8View.swift + utf8.count + } +} + +// MARK: - Default conformances + +// Implement the protocol requirements for byte arrays and buffers so that +// developers can attach raw data when needed. +@_spi(Experimental) +extension Array: Test.Attachable { + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try withUnsafeBytes(body) + } +} + +@_spi(Experimental) +extension ContiguousArray: Test.Attachable { + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try withUnsafeBytes(body) + } +} + +@_spi(Experimental) +extension ArraySlice: Test.Attachable { + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try withUnsafeBytes(body) + } +} + +@_spi(Experimental) +extension String: Test.Attachable { + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + var selfCopy = self + return try selfCopy.withUTF8 { utf8 in + try body(UnsafeRawBufferPointer(utf8)) + } + } +} + +@_spi(Experimental) +extension Substring: Test.Attachable { + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + var selfCopy = self + return try selfCopy.withUTF8 { utf8 in + try body(UnsafeRawBufferPointer(utf8)) + } + } +} diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 37430a6a4..111c70c4c 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -10,6 +10,10 @@ @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing private import _TestingInternals +#if canImport(Foundation) +import Foundation +@_spi(Experimental) import _Testing_Foundation +#endif @Suite("Attachment Tests") struct AttachmentTests { @@ -94,22 +98,14 @@ struct AttachmentTests { remove(filePath) } let fileName = try #require(filePath.split { $0 == "/" || $0 == #"\"# }.last) - #expect(fileName == "loremipsum-\(suffix).tar.gz.gif.jpeg.html") + #expect(fileName == "loremipsum-\(suffix).tgz.gif.jpeg.html") try compare(attachableValue, toContentsOfFileAtPath: filePath) } #if os(Windows) static let maximumNameCount = Int(_MAX_FNAME) - static let reservedNames: [String] = { - // Return the list of COM ports that are NOT configured (and so will fail - // to open for writing.) - (0...9).lazy - .map { "COM\($0)" } - .filter { !PathFileExistsA($0) } - }() #else static let maximumNameCount = Int(NAME_MAX) - static let reservedNames: [String] = [] #endif @Test(arguments: [ @@ -117,7 +113,7 @@ struct AttachmentTests { String(repeating: "a", count: maximumNameCount), String(repeating: "a", count: maximumNameCount + 1), String(repeating: "a", count: maximumNameCount + 2), - ] + reservedNames) func writeAttachmentWithBadName(name: String) throws { + ]) func writeAttachmentWithBadName(name: String) throws { let attachableValue = MySendableAttachable(string: "") let attachment = Attachment(attachableValue, named: name) @@ -226,6 +222,182 @@ struct AttachmentTests { } } } + +#if canImport(Foundation) +#if !SWT_NO_FILE_IO + @Test func attachContentsOfFileURL() async throws { + let data = try #require("".data(using: .utf8)) + let temporaryFileName = "\(UUID().uuidString).html" + let temporaryPath = try appendPathComponent(temporaryFileName, to: temporaryDirectory()) + let temporaryURL = URL(fileURLWithPath: temporaryPath, isDirectory: false) + try data.write(to: temporaryURL) + defer { + try? FileManager.default.removeItem(at: temporaryURL) + } + + await confirmation("Attachment detected") { valueAttached in + var configuration = Configuration() + configuration.eventHandler = { event, _ in + guard case let .valueAttached(attachment) = event.kind else { + return + } + + #expect(attachment.preferredName == temporaryFileName) + #expect(throws: Never.self) { + try attachment.withUnsafeBufferPointer { buffer in + #expect(buffer.count == data.count) + } + } + valueAttached() + } + + await Test { + let attachment = try await Test.Attachment(contentsOf: temporaryURL) + attachment.attach() + }.run(configuration: configuration) + } + } + +#if !SWT_NO_PROCESS_SPAWNING + @Test func attachContentsOfDirectoryURL() async throws { + let temporaryDirectoryName = UUID().uuidString + let temporaryPath = try appendPathComponent(temporaryDirectoryName, to: temporaryDirectory()) + let temporaryURL = URL(fileURLWithPath: temporaryPath, isDirectory: false) + try FileManager.default.createDirectory(at: temporaryURL, withIntermediateDirectories: true) + + let fileData = try #require("Hello world".data(using: .utf8)) + try fileData.write(to: temporaryURL.appendingPathComponent("loremipsum.txt"), options: [.atomic]) + + await confirmation("Attachment detected") { valueAttached in + var configuration = Configuration() + configuration.eventHandler = { event, _ in + guard case let .valueAttached(attachment) = event.kind else { + return + } + + #expect(attachment.preferredName == "\(temporaryDirectoryName).tgz") + valueAttached() + } + + await Test { + let attachment = try await Test.Attachment(contentsOf: temporaryURL) + attachment.attach() + }.run(configuration: configuration) + } + } +#endif + + @Test func attachUnsupportedContentsOfURL() async throws { + let url = try #require(URL(string: "https://www.example.com")) + await #expect(throws: CocoaError.self) { + _ = try await Test.Attachment(contentsOf: url) + } + } +#endif + + struct CodableAttachmentArguments: Sendable, CustomTestArgumentEncodable, CustomTestStringConvertible { + var forSecureCoding: Bool + var pathExtension: String? + var firstCharacter: Character + var decode: @Sendable (Data) throws -> String + + @Sendable static func decodeWithJSONDecoder(_ data: Data) throws -> String { + try JSONDecoder().decode(MyCodableAttachable.self, from: data).string + } + + @Sendable static func decodeWithPropertyListDecoder(_ data: Data) throws -> String { + try PropertyListDecoder().decode(MyCodableAttachable.self, from: data).string + } + + @Sendable static func decodeWithNSKeyedUnarchiver(_ data: Data) throws -> String { + let result = try NSKeyedUnarchiver.unarchivedObject(ofClass: MySecureCodingAttachable.self, from: data) + return try #require(result).string + } + + static func all() -> [Self] { + var result = [Self]() + + for forSecureCoding in [false, true] { + let decode = forSecureCoding ? decodeWithNSKeyedUnarchiver : decodeWithPropertyListDecoder + result += [ + Self( + forSecureCoding: forSecureCoding, + firstCharacter: forSecureCoding ? "b" : "{", + decode: forSecureCoding ? decodeWithNSKeyedUnarchiver : decodeWithJSONDecoder + ) + ] + + result += [ + Self(forSecureCoding: forSecureCoding, pathExtension: "xml", firstCharacter: "<", decode: decode), + Self(forSecureCoding: forSecureCoding, pathExtension: "plist", firstCharacter: "b", decode: decode), + ] + + if !forSecureCoding { + result += [ + Self(forSecureCoding: forSecureCoding, pathExtension: "json", firstCharacter: "{", decode: decodeWithJSONDecoder), + ] + } + } + + return result + } + + func encodeTestArgument(to encoder: some Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(pathExtension) + try container.encode(forSecureCoding) + try container.encode(firstCharacter.asciiValue!) + } + + var testDescription: String { + "(forSecureCoding: \(forSecureCoding), extension: \(String(describingForTest: pathExtension)))" + } + } + + @Test("Attach Codable- and NSSecureCoding-conformant values", .serialized, arguments: CodableAttachmentArguments.all()) + func attachCodable(args: CodableAttachmentArguments) async throws { + var name = "loremipsum" + if let ext = args.pathExtension { + name = "\(name).\(ext)" + } + + func open(_ attachment: borrowing Test.Attachment) throws where T: Test.Attachable { + try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { bytes in + #expect(bytes.first == args.firstCharacter.asciiValue) + let decodedStringValue = try args.decode(Data(bytes)) + #expect(decodedStringValue == "stringly speaking") + } + } + + if args.forSecureCoding { + let attachableValue = MySecureCodingAttachable(string: "stringly speaking") + let attachment = Test.Attachment(attachableValue, named: name) + try open(attachment) + } else { + let attachableValue = MyCodableAttachable(string: "stringly speaking") + let attachment = Test.Attachment(attachableValue, named: name) + try open(attachment) + } + } + + @Test("Attach NSSecureCoding-conformant value but with a JSON type") + func attachNSSecureCodingAsJSON() async throws { + let attachableValue = MySecureCodingAttachable(string: "stringly speaking") + let attachment = Test.Attachment(attachableValue, named: "loremipsum.json") + #expect(throws: CocoaError.self) { + try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { _ in } + } + } + + @Test("Attach NSSecureCoding-conformant value but with a nonsensical type") + func attachNSSecureCodingAsNonsensical() async throws { + let attachableValue = MySecureCodingAttachable(string: "stringly speaking") + let attachment = Test.Attachment(attachableValue, named: "loremipsum.gif") + #expect(throws: CocoaError.self) { + try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { _ in } + } + } +#endif } extension AttachmentTests { @@ -264,6 +436,13 @@ extension AttachmentTests { let value: Substring = "abc123"[...] try test(value) } + +#if canImport(Foundation) + @Test func data() throws { + let value = try #require("abc123".data(using: .utf8)) + try test(value) + } +#endif } } @@ -310,3 +489,45 @@ struct MySendableAttachableWithDefaultByteCount: Attachable, Sendable { } } } + +#if canImport(Foundation) +struct MyCodableAttachable: Codable, Test.Attachable, Sendable { + var string: String +} + +final class MySecureCodingAttachable: NSObject, NSSecureCoding, Test.Attachable, Sendable { + let string: String + + init(string: String) { + self.string = string + } + + static var supportsSecureCoding: Bool { + true + } + + func encode(with coder: NSCoder) { + coder.encode(string, forKey: "string") + } + + required init?(coder: NSCoder) { + string = (coder.decodeObject(of: NSString.self, forKey: "string") as? String) ?? "" + } +} + +final class MyCodableAndSecureCodingAttachable: NSObject, Codable, NSSecureCoding, Test.Attachable, Sendable { + let string: String + + static var supportsSecureCoding: Bool { + true + } + + func encode(with coder: NSCoder) { + coder.encode(string, forKey: "string") + } + + required init?(coder: NSCoder) { + string = (coder.decodeObject(of: NSString.self, forKey: "string") as? String) ?? "" + } +} +#endif diff --git a/cmake/modules/shared/AvailabilityDefinitions.cmake b/cmake/modules/shared/AvailabilityDefinitions.cmake index 0685fcecd..2124a32be 100644 --- a/cmake/modules/shared/AvailabilityDefinitions.cmake +++ b/cmake/modules/shared/AvailabilityDefinitions.cmake @@ -9,6 +9,7 @@ # Settings which define commonly-used OS availability macros. add_compile_options( "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_mangledTypeNameAPI:macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0\">" + "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_uttypesAPI:macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_backtraceAsyncAPI:macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_clockAPI:macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_regexAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0\">" From 6e75bbe969b65694891dbe9eaf4e5ddc04884454 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 12 Nov 2024 07:35:57 -0500 Subject: [PATCH 2/8] URL inits need source location --- .../Attachments/Test.Attachment+URL.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift index 5f56ad727..fa96e7613 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift @@ -50,12 +50,15 @@ extension Test.Attachment where AttachableValue == Data { /// - preferredName: The preferred name of the attachment when writing it /// to a test report or to disk. If `nil`, the name of the attachment is /// derived from the last path component of `url`. - /// - sourceLocation: The source location of the attachment. + /// - 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 attempting to read from `url`. public init( contentsOf url: URL, - named preferredName: String? = nil + named preferredName: String? = nil, + sourceLocation: SourceLocation = #_sourceLocation ) async throws { guard url.isFileURL else { // TODO: network URLs? @@ -87,10 +90,10 @@ extension Test.Attachment where AttachableValue == Data { return (preferredName as NSString).appendingPathExtension("tgz") ?? preferredName }() - try await self.init(Data(compressedContentsOfDirectoryAt: url), named: preferredName) + try await self.init(Data(compressedContentsOfDirectoryAt: url), named: preferredName, sourceLocation: sourceLocation) } else { // Load the file. - try self.init(Data(contentsOf: url, options: [.mappedIfSafe]), named: preferredName) + try self.init(Data(contentsOf: url, options: [.mappedIfSafe]), named: preferredName, sourceLocation: sourceLocation) } } } From 142197006c99f95efb29c4216174bbdfad1d8217 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 13 Nov 2024 09:26:36 -0500 Subject: [PATCH 3/8] Rebase after type rename --- ...Attachable+Encodable&NSSecureCoding.swift} | 4 +- ...dable.swift => Attachable+Encodable.swift} | 6 +- ....swift => Attachable+NSSecureCoding.swift} | 4 +- ...achment+URL.swift => Attachment+URL.swift} | 6 +- ...Attachable.swift => Data+Attachable.swift} | 4 +- .../Attachments/EncodingFormat.swift | 4 +- Sources/Testing/Attachments/Attachable.swift | 6 +- .../Testing/Attachments/Test.Attachable.swift | 143 ------------------ Tests/TestingTests/AttachmentTests.swift | 22 +-- 9 files changed, 26 insertions(+), 173 deletions(-) rename Sources/Overlays/_Testing_Foundation/Attachments/{Test.Attachable+Encodable&NSSecureCoding.swift => Attachable+Encodable&NSSecureCoding.swift} (81%) rename Sources/Overlays/_Testing_Foundation/Attachments/{Test.Attachable+Encodable.swift => Attachable+Encodable.swift} (94%) rename Sources/Overlays/_Testing_Foundation/Attachments/{Test.Attachable+NSSecureCoding.swift => Attachable+NSSecureCoding.swift} (96%) rename Sources/Overlays/_Testing_Foundation/Attachments/{Test.Attachment+URL.swift => Attachment+URL.swift} (97%) rename Sources/Overlays/_Testing_Foundation/Attachments/{Data+Test.Attachable.swift => Data+Attachable.swift} (82%) delete mode 100644 Sources/Testing/Attachments/Test.Attachable.swift diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable&NSSecureCoding.swift similarity index 81% rename from Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift rename to Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable&NSSecureCoding.swift index 4b1b95f22..21797109a 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable&NSSecureCoding.swift @@ -13,9 +13,9 @@ public import Foundation @_spi(Experimental) -extension Test.Attachable where Self: Encodable & NSSecureCoding { +extension Attachable where Self: Encodable & NSSecureCoding { @_documentation(visibility: private) - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + 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/Test.Attachable+Encodable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift similarity index 94% rename from Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable.swift rename to Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift index 0bafb011e..d00f32110 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift @@ -26,7 +26,7 @@ private import Foundation /// /// - Throws: Whatever is thrown by `body`, or any error that prevented the /// creation of the buffer. -func withUnsafeBufferPointer(encoding attachableValue: borrowing E, for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R where E: Test.Attachable & Encodable { +func withUnsafeBufferPointer(encoding attachableValue: borrowing E, for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R where E: Attachable & Encodable { let format = try EncodingFormat(for: attachment) let data: Data @@ -53,7 +53,7 @@ func withUnsafeBufferPointer(encoding attachableValue: borrowing E, for at // encoding to JSON. This lets developers provide trivial conformance to the // protocol for types that already support Codable. @_spi(Experimental) -extension Test.Attachable where Self: Encodable { +extension Attachable where Self: Encodable { /// Encode this value into a buffer using either [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder) /// or [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder), /// then call a function and pass that buffer to it. @@ -90,7 +90,7 @@ extension Test.Attachable where Self: Encodable { /// 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 Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + 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/Test.Attachable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift similarity index 96% rename from Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+NSSecureCoding.swift rename to Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift index 6a79a37ff..db3f02edf 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift @@ -16,7 +16,7 @@ public import Foundation // NSSecureCoding-conformant classes by default. The implementation uses // NSKeyedArchiver for encoding. @_spi(Experimental) -extension Test.Attachable where Self: NSSecureCoding { +extension Attachable where Self: NSSecureCoding { /// Encode this object using [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver) /// into a buffer, then call a function and pass that buffer to it. /// @@ -50,7 +50,7 @@ extension Test.Attachable where Self: NSSecureCoding { /// - 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 Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + public func withUnsafeBufferPointer(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { let format = try EncodingFormat(for: attachment) var data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift similarity index 97% rename from Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift rename to Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index fa96e7613..bdd419c73 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -42,13 +42,13 @@ extension UTType { #endif @_spi(Experimental) -extension Test.Attachment where AttachableValue == Data { +extension Attachment where AttachableValue == Data { /// Initialize an instance of this type with the contents of the given URL. /// /// - Parameters: /// - url: The URL containing the attachment's data. - /// - preferredName: The preferred name of the attachment when writing it - /// to a test report or to disk. If `nil`, the name of the attachment is + /// - preferredName: The preferred name of the attachment when writing it to + /// a test report or to disk. If `nil`, the name of the attachment is /// derived from the last path component of `url`. /// - sourceLocation: The source location of the call to this initializer. /// This value is used when recording issues associated with the diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Data+Test.Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift similarity index 82% rename from Sources/Overlays/_Testing_Foundation/Attachments/Data+Test.Attachable.swift rename to Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift index 4c771562e..f931e5824 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Data+Test.Attachable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift @@ -13,8 +13,8 @@ public import Foundation @_spi(Experimental) -extension Data: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { +extension Data: Attachable { + public func withUnsafeBufferPointer(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } } diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift b/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift index fb3a85413..ccb10c754 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift @@ -17,7 +17,7 @@ private import UniformTypeIdentifiers #endif /// An enumeration describing the encoding formats we support for `Encodable` -/// and `NSSecureCoding` types that conform to `Test.Attachable`. +/// and `NSSecureCoding` types that conform to `Attachable`. enum EncodingFormat { /// A property list format. /// @@ -41,7 +41,7 @@ enum EncodingFormat { /// - attachment: The attachment that will be encoded. /// /// - Throws: If the attachment's content type or media type is unsupported. - init(for attachment: borrowing Test.Attachment) throws { + init(for attachment: borrowing Attachment) throws { let ext = (attachment.preferredName as NSString).pathExtension #if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers) diff --git a/Sources/Testing/Attachments/Attachable.swift b/Sources/Testing/Attachments/Attachable.swift index 200421240..71740c444 100644 --- a/Sources/Testing/Attachments/Attachable.swift +++ b/Sources/Testing/Attachments/Attachable.swift @@ -82,12 +82,8 @@ extension Attachable where Self: Collection, Element == UInt8 { // We do not provide an implementation of withUnsafeBufferPointer(for:_:) here // because there is no way in the standard library to statically detect if a // collection can provide contiguous storage (_HasContiguousBytes is not API.) - // If withContiguousBytesIfAvailable(_:) fails, we don't want to make a + // If withContiguousStorageIfAvailable(_:) fails, we don't want to make a // (potentially expensive!) copy of the collection. - // - // The planned Foundation cross-import overlay can provide a default - // implementation for collection types that conform to Foundation's - // ContiguousBytes protocol. } extension Attachable where Self: StringProtocol { diff --git a/Sources/Testing/Attachments/Test.Attachable.swift b/Sources/Testing/Attachments/Test.Attachable.swift deleted file mode 100644 index 3bd4e88af..000000000 --- a/Sources/Testing/Attachments/Test.Attachable.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// 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 -// - -@_spi(Experimental) -extension Test { - /// A protocol describing a type that can be attached to a test report or - /// written to disk when a test is run. - /// - /// To attach an attachable value to a test report or test run output, use it - /// to initialize a new instance of ``Test/Attachment``, then call - /// ``Test/Attachment/attach(sourceLocation:)``. An attachment can only be - /// attached once. - /// - /// The testing library provides default conformances to this protocol for a - /// variety of standard library types. Most user-defined types do not need to - /// conform to this protocol. - /// - /// A type should conform to this protocol if it can be represented as a - /// sequence of bytes that would be diagnostically useful if a test fails. If - /// a type cannot conform directly to this protocol (such as a non-final class - /// or a type declared in a third-party module), you can create a container - /// type that conforms to ``Test/AttachableContainer`` to act as a proxy. - public protocol Attachable: ~Copyable { - /// An estimate of the number of bytes of memory needed to store this value - /// as an attachment. - /// - /// The testing library uses this property to determine if an attachment - /// should be held in memory or should be immediately persisted to storage. - /// Larger attachments are more likely to be persisted, but the algorithm - /// the testing library uses is an implementation detail and is subject to - /// change. - /// - /// The value of this property is approximately equal to the number of bytes - /// that will actually be needed, or `nil` if the value cannot be computed - /// efficiently. The default implementation of this property returns `nil`. - /// - /// - Complexity: O(1) unless `Self` conforms to `Collection`, in which case - /// up to O(_n_) where _n_ is the length of the collection. - var estimatedAttachmentByteCount: Int? { get } - - /// Call a function and pass a buffer representing this instance to it. - /// - /// - Parameters: - /// - attachment: The attachment that is requesting a buffer (that is, the - /// attachment containing this instance.) - /// - body: A function to call. A temporary buffer containing a data - /// representation of this instance is passed to it. - /// - /// - Returns: Whatever is returned by `body`. - /// - /// - Throws: Whatever is thrown by `body`, or any error that prevented the - /// creation of the buffer. - /// - /// The testing library uses this function when writing an attachment to a - /// test report or to a file on disk. The format of the buffer is - /// implementation-defined, but should be "idiomatic" for this type: for - /// example, if this type represents an image, it would be appropriate for - /// the buffer to contain an image in PNG format, JPEG format, etc., but it - /// would not be idiomatic for the buffer to contain a textual description - /// of the image. - borrowing func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R - } -} - -// MARK: - Default implementations - -extension Test.Attachable where Self: ~Copyable { - public var estimatedAttachmentByteCount: Int? { - nil - } -} - -extension Test.Attachable where Self: Collection, Element == UInt8 { - public var estimatedAttachmentByteCount: Int? { - count - } - - // We do not provide an implementation of withUnsafeBufferPointer(for:_:) here - // because there is no way in the standard library to statically detect if a - // collection can provide contiguous storage (_HasContiguousBytes is not API.) - // If withContiguousStorageIfAvailable(_:) fails, we don't want to make a - // (potentially expensive!) copy of the collection. -} - -extension Test.Attachable where Self: StringProtocol { - public var estimatedAttachmentByteCount: Int? { - // NOTE: utf8.count may be O(n) for foreign strings. - // SEE: https://github.com/swiftlang/swift/blob/main/stdlib/public/core/StringUTF8View.swift - utf8.count - } -} - -// MARK: - Default conformances - -// Implement the protocol requirements for byte arrays and buffers so that -// developers can attach raw data when needed. -@_spi(Experimental) -extension Array: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try withUnsafeBytes(body) - } -} - -@_spi(Experimental) -extension ContiguousArray: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try withUnsafeBytes(body) - } -} - -@_spi(Experimental) -extension ArraySlice: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try withUnsafeBytes(body) - } -} - -@_spi(Experimental) -extension String: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - var selfCopy = self - return try selfCopy.withUTF8 { utf8 in - try body(UnsafeRawBufferPointer(utf8)) - } - } -} - -@_spi(Experimental) -extension Substring: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - var selfCopy = self - return try selfCopy.withUTF8 { utf8 in - try body(UnsafeRawBufferPointer(utf8)) - } - } -} diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 111c70c4c..4905e90a1 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -252,7 +252,7 @@ struct AttachmentTests { } await Test { - let attachment = try await Test.Attachment(contentsOf: temporaryURL) + let attachment = try await Attachment(contentsOf: temporaryURL) attachment.attach() }.run(configuration: configuration) } @@ -280,7 +280,7 @@ struct AttachmentTests { } await Test { - let attachment = try await Test.Attachment(contentsOf: temporaryURL) + let attachment = try await Attachment(contentsOf: temporaryURL) attachment.attach() }.run(configuration: configuration) } @@ -290,7 +290,7 @@ struct AttachmentTests { @Test func attachUnsupportedContentsOfURL() async throws { let url = try #require(URL(string: "https://www.example.com")) await #expect(throws: CocoaError.self) { - _ = try await Test.Attachment(contentsOf: url) + _ = try await Attachment(contentsOf: url) } } #endif @@ -361,7 +361,7 @@ struct AttachmentTests { name = "\(name).\(ext)" } - func open(_ attachment: borrowing Test.Attachment) throws where T: Test.Attachable { + func open(_ attachment: borrowing Attachment) throws where T: Attachable { try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { bytes in #expect(bytes.first == args.firstCharacter.asciiValue) let decodedStringValue = try args.decode(Data(bytes)) @@ -371,11 +371,11 @@ struct AttachmentTests { if args.forSecureCoding { let attachableValue = MySecureCodingAttachable(string: "stringly speaking") - let attachment = Test.Attachment(attachableValue, named: name) + let attachment = Attachment(attachableValue, named: name) try open(attachment) } else { let attachableValue = MyCodableAttachable(string: "stringly speaking") - let attachment = Test.Attachment(attachableValue, named: name) + let attachment = Attachment(attachableValue, named: name) try open(attachment) } } @@ -383,7 +383,7 @@ struct AttachmentTests { @Test("Attach NSSecureCoding-conformant value but with a JSON type") func attachNSSecureCodingAsJSON() async throws { let attachableValue = MySecureCodingAttachable(string: "stringly speaking") - let attachment = Test.Attachment(attachableValue, named: "loremipsum.json") + let attachment = Attachment(attachableValue, named: "loremipsum.json") #expect(throws: CocoaError.self) { try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { _ in } } @@ -392,7 +392,7 @@ struct AttachmentTests { @Test("Attach NSSecureCoding-conformant value but with a nonsensical type") func attachNSSecureCodingAsNonsensical() async throws { let attachableValue = MySecureCodingAttachable(string: "stringly speaking") - let attachment = Test.Attachment(attachableValue, named: "loremipsum.gif") + let attachment = Attachment(attachableValue, named: "loremipsum.gif") #expect(throws: CocoaError.self) { try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { _ in } } @@ -491,11 +491,11 @@ struct MySendableAttachableWithDefaultByteCount: Attachable, Sendable { } #if canImport(Foundation) -struct MyCodableAttachable: Codable, Test.Attachable, Sendable { +struct MyCodableAttachable: Codable, Attachable, Sendable { var string: String } -final class MySecureCodingAttachable: NSObject, NSSecureCoding, Test.Attachable, Sendable { +final class MySecureCodingAttachable: NSObject, NSSecureCoding, Attachable, Sendable { let string: String init(string: String) { @@ -515,7 +515,7 @@ final class MySecureCodingAttachable: NSObject, NSSecureCoding, Test.Attachable, } } -final class MyCodableAndSecureCodingAttachable: NSObject, Codable, NSSecureCoding, Test.Attachable, Sendable { +final class MyCodableAndSecureCodingAttachable: NSObject, Codable, NSSecureCoding, Attachable, Sendable { let string: String static var supportsSecureCoding: Bool { From 06cc43d4c3a07574c37e045a1a1d1bcf56aa0c8c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 13 Nov 2024 09:50:26 -0500 Subject: [PATCH 4/8] Update DocC references --- .../_Testing_Foundation/Attachments/Attachable+Encodable.swift | 2 +- .../Attachments/Attachable+NSSecureCoding.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift index d00f32110..b6e8e3f14 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift @@ -71,7 +71,7 @@ extension Attachable where Self: Encodable { /// /// The testing library uses this function when writing an attachment to a /// test report or to a file on disk. The encoding used depends on the path - /// extension specified by the value of `attachment`'s ``Testing/Test/Attachment/preferredName`` + /// extension specified by the value of `attachment`'s ``Testing/Attachment/preferredName`` /// property: /// /// | Extension | Encoding Used | Encoder Used | diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift index db3f02edf..622787384 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift @@ -33,7 +33,7 @@ extension Attachable where Self: NSSecureCoding { /// /// The testing library uses this function when writing an attachment to a /// test report or to a file on disk. The encoding used depends on the path - /// extension specified by the value of `attachment`'s ``Testing/Test/Attachment/preferredName`` + /// extension specified by the value of `attachment`'s ``Testing/Attachment/preferredName`` /// property: /// /// | Extension | Encoding Used | Encoder Used | From 9b24b943bfe31870f4a2f883e1afbcdd5cfa0f3e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 13 Nov 2024 10:20:26 -0500 Subject: [PATCH 5/8] Fix test --- Tests/TestingTests/AttachmentTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 4905e90a1..431e08d24 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -82,7 +82,7 @@ struct AttachmentTests { @Test func writeAttachmentWithMultiplePathExtensions() throws { let attachableValue = MySendableAttachable(string: "") - let attachment = Attachment(attachableValue, named: "loremipsum.tar.gz.gif.jpeg.html") + let attachment = Attachment(attachableValue, named: "loremipsum.tgz.gif.jpeg.html") // Write the attachment to disk once to ensure the original filename is not // available and we add a suffix. From f0b15bf6c50c1315f2b516a56c69943846279cae Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 13 Nov 2024 13:08:36 -0500 Subject: [PATCH 6/8] Don't use package access level, it won't work correctly when building separate library targets --- Documentation/SPI.md | 22 +++++++++++++++---- .../Attachments/Attachment+URL.swift | 2 +- Sources/Testing/Attachments/Attachment.swift | 3 ++- cmake/modules/shared/CompilerSettings.cmake | 2 -- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Documentation/SPI.md b/Documentation/SPI.md index 9d44710bf..edfb7c6a8 100644 --- a/Documentation/SPI.md +++ b/Documentation/SPI.md @@ -13,14 +13,16 @@ See https://swift.org/CONTRIBUTORS.txt for Swift project authors -This post describes the set of SPI groups used in Swift Testing. In general, two -groups of SPI exist in the testing library: +This post describes the set of SPI groups used in Swift Testing. In general, +three groups of SPI exist in the testing library: 1. Interfaces that aren't needed by test authors, but which may be needed by - tools that use the testing library such as Swift Package Manager; and + tools that use the testing library such as Swift Package Manager; 1. Interfaces that are available for test authors to use, but which are experimental or under active development and which may be modified or removed - in the future. + in the future; and +1. Interfaces that are private to the testing library but need to be shared + across targets, but which for technical reasons cannot use `package`. For interfaces used to integrate with external tools, the SPI group `@_spi(ForToolsIntegrationOnly)` is used. The name is a hint to adopters that @@ -37,6 +39,13 @@ external tools, _both_ groups are specified. Such SPI is not generally meant to be promoted to public API, but is still experimental until tools authors have a chance to evaluate it. +For interfaces internal to Swift Testing that must be available across targets, +the SPI group `@_spi(ForSwiftTestingOnly)` is used. They _should_ be marked +`package` and may be in the future, but are currently exported due to technical +constraints when Swift Testing is built using CMake. + +- Warning: Never use symbols marked `@_spi(ForSwiftTestingOnly)`. + ## SPI stability The testing library does **not** guarantee SPI stability for either group of @@ -49,6 +58,11 @@ to newer interfaces. SPI marked `@_spi(Experimental)` should be assumed to be unstable. It may be modified or removed at any time. +SPI marked `@_spi(ForSwiftTestingOnly)` is unstable and subject to change at any +time. + +- Warning: Never use symbols marked `@_spi(ForSwiftTestingOnly)`. + ## API and ABI stability When Swift Testing reaches its 1.0 release, API changes will follow the same diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index bdd419c73..d70641c69 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -9,7 +9,7 @@ // #if canImport(Foundation) -@_spi(Experimental) public import Testing +@_spi(Experimental) @_spi(ForSwiftTestingOnly) public import Testing public import Foundation #if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers) diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index 4e230a292..d79a43fcb 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -37,7 +37,8 @@ public struct Attachment: ~Copyable where AttachableValue: Atta public var fileSystemPath: String? /// The default preferred name to use if the developer does not supply one. - package static var defaultPreferredName: String { + @_spi(ForSwiftTestingOnly) + public static var defaultPreferredName: String { "untitled" } diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index d38c36c7e..49e2579fe 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -16,8 +16,6 @@ add_compile_options( add_compile_options( "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend ExistentialAny>" "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend InternalImportsByDefault>") -add_compile_options( - "SHELL:$<$:-package-name swift_testing>") # Platform-specific definitions. if(APPLE) From 6f204f56452cabf69e2c759f6083d8d9ea76632b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 13 Nov 2024 13:13:34 -0500 Subject: [PATCH 7/8] Use GitHub-style warning callouts --- Documentation/SPI.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Documentation/SPI.md b/Documentation/SPI.md index edfb7c6a8..534d6e51f 100644 --- a/Documentation/SPI.md +++ b/Documentation/SPI.md @@ -44,7 +44,8 @@ the SPI group `@_spi(ForSwiftTestingOnly)` is used. They _should_ be marked `package` and may be in the future, but are currently exported due to technical constraints when Swift Testing is built using CMake. -- Warning: Never use symbols marked `@_spi(ForSwiftTestingOnly)`. +> [!WARNING] +> Never use symbols marked `@_spi(ForSwiftTestingOnly)`. ## SPI stability @@ -61,7 +62,8 @@ modified or removed at any time. SPI marked `@_spi(ForSwiftTestingOnly)` is unstable and subject to change at any time. -- Warning: Never use symbols marked `@_spi(ForSwiftTestingOnly)`. +> [!WARNING] +> Never use symbols marked `@_spi(ForSwiftTestingOnly)`. ## API and ABI stability From 939e578073e1e2c8cb27252126b091d9f575debc Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 13 Nov 2024 16:05:39 -0500 Subject: [PATCH 8/8] Incorporate feedback --- Package.swift | 2 +- ...ift => Attachable+Encodable+NSSecureCoding.swift} | 6 ++++++ .../Attachments/Attachable+Encodable.swift | 1 + .../Attachments/EncodingFormat.swift | 12 ++++++------ 4 files changed, 14 insertions(+), 7 deletions(-) rename Sources/Overlays/_Testing_Foundation/Attachments/{Attachable+Encodable&NSSecureCoding.swift => Attachable+Encodable+NSSecureCoding.swift} (71%) diff --git a/Package.swift b/Package.swift index d42d570c0..f840a21cb 100644 --- a/Package.swift +++ b/Package.swift @@ -90,7 +90,7 @@ let package = Package( cxxSettings: .packageSettings ), - // Cross-module overlays (unsupported) + // Cross-import overlays (not supported by Swift Package Manager) .target( name: "_Testing_Foundation", dependencies: [ diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable&NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift similarity index 71% rename from Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable&NSSecureCoding.swift rename to Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift index 21797109a..80c75b5e9 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable&NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift @@ -12,6 +12,12 @@ @_spi(Experimental) public import Testing public import Foundation +// This implementation is necessary to let the compiler disambiguate when a type +// conforms to both Encodable and NSSecureCoding. It is hidden from the DocC +// compiler because it appears redundant next to the other two implementations +// (which explicitly document what happens when a type conforms to both +// protocols.) + @_spi(Experimental) extension Attachable where Self: Encodable & NSSecureCoding { @_documentation(visibility: private) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift index b6e8e3f14..3e26f7ead 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift @@ -17,6 +17,7 @@ private import Foundation /// to `NSSecureCoding`. /// /// - Parameters: +/// - attachableValue: The value to encode. /// - attachment: The attachment that is requesting a buffer (that is, the /// attachment containing this instance.) /// - body: A function to call. A temporary buffer containing a data diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift b/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift index ccb10c754..b60a54882 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift @@ -19,6 +19,12 @@ private import UniformTypeIdentifiers /// An enumeration describing the encoding formats we support for `Encodable` /// and `NSSecureCoding` types that conform to `Attachable`. enum EncodingFormat { + /// The encoding format to use by default. + /// + /// The specific format this case corresponds to depends on if we are encoding + /// an `Encodable` value or an `NSSecureCoding` value. + case `default` + /// A property list format. /// /// - Parameters: @@ -28,12 +34,6 @@ enum EncodingFormat { /// The JSON format. case json - /// The encoding format to use by default. - /// - /// The specific format this case corresponds to depends on if we are encoding - /// an `Encodable` value or an `NSSecureCoding` value. - case `default` - /// Initialize an instance of this type representing the content type or media /// type of the specified attachment. ///