diff --git a/Documentation/SPI.md b/Documentation/SPI.md index 9d44710bf..534d6e51f 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,14 @@ 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 +59,12 @@ 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/Package.swift b/Package.swift index 202b09e1f..f840a21cb 100644 --- a/Package.swift +++ b/Package.swift @@ -90,12 +90,13 @@ let package = Package( cxxSettings: .packageSettings ), - // Cross-module overlays (unsupported) + // Cross-import overlays (not supported by Swift Package Manager) .target( name: "_Testing_Foundation", 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/Attachable+Encodable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift new file mode 100644 index 000000000..80c75b5e9 --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift @@ -0,0 +1,28 @@ +// +// 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 + +// 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) + public func withUnsafeBufferPointer(for attachment: borrowing 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/Attachable+Encodable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift new file mode 100644 index 000000000..3e26f7ead --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift @@ -0,0 +1,98 @@ +// +// 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: +/// - 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 +/// 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 Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R where E: 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 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/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 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/Attachable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift new file mode 100644 index 000000000..622787384 --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/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 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/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 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/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift new file mode 100644 index 000000000..d70641c69 --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -0,0 +1,161 @@ +// +// 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) @_spi(ForSwiftTestingOnly) 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 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 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, + sourceLocation: SourceLocation = #_sourceLocation + ) 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, sourceLocation: sourceLocation) + } else { + // Load the file. + try self.init(Data(contentsOf: url, options: [.mappedIfSafe]), named: preferredName, sourceLocation: sourceLocation) + } + } +} + +// 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/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift new file mode 100644 index 000000000..f931e5824 --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Data+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: Attachable { + public func withUnsafeBufferPointer(for attachment: borrowing 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..b60a54882 --- /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 `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: + /// - format: The corresponding property list format. + case propertyListFormat(_ format: PropertyListSerialization.PropertyListFormat) + + /// The JSON format. + case json + + /// 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 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/_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/Attachable.swift b/Sources/Testing/Attachments/Attachable.swift index c1c3eb3e0..990f80dee 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/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/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 37430a6a4..431e08d24 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 { @@ -78,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. @@ -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 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 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 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 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)) + #expect(decodedStringValue == "stringly speaking") + } + } + + if args.forSecureCoding { + let attachableValue = MySecureCodingAttachable(string: "stringly speaking") + let attachment = Attachment(attachableValue, named: name) + try open(attachment) + } else { + let attachableValue = MyCodableAttachable(string: "stringly speaking") + let attachment = 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 = 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 = 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, Attachable, Sendable { + var string: String +} + +final class MySecureCodingAttachable: NSObject, NSSecureCoding, 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, 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\">" 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)