From e5332736973225aa278f9a31f2700a141b11e3ff Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 6 Nov 2024 15:12:05 -0500 Subject: [PATCH 01/27] Make `Test.Attachment` generic. This PR makes `Test.Attachment` generic over its attachable value type. It gains conditional conformance to `Copyable` and `Sendable` depending on the attachable value and if you call `attach()` on a move-only or non-sendable attachment, will eagerly serialize the attachable value at that point (rather than during initialization.) There are a few benefits here: 1. Callers can statically know the type of the attachable value in an attachment rather than needing to always deal with an existential box; 2. We can add associated types to `Test.Attachable` that will be readily accessible in `withUnsafeBufferPointer(for:_:)` again without needing an existential; and 3. When we eventually add support for image attachments, we won't need a bunch of additional initializers or intermediate box types or what-have-you; and 4. For Embedded Swift or other environments where existentials are problematic, we can eagerly serialize all attachments and pass a consistent type (`Test.Attachment<[UInt8]>`) to the event handler. There are also some drawbacks: 1. Because conformance to `Copyable` and `Sendable` is conditional, we lose a bit of flexibility if you have a non-sendable `Test.Attachment` instance or whatnot; 2. We still need a lazy, type-erased attachment type that can be passed to the event handler. I played around with `Test.Attachment` but that causes as many problems as it solves. We end up with `Test.Attachment` but, because that's an existential type that doesn't conform to itself, the generic parameter `AttachableValue` is not constrained to `Test.Attachable`. We only provide initializers for types that do conform though (plus the existential one internally) so in practice it's not a huge issue. 3. There is some code duplication necessary (i.e. multiple implementations of `attach()` and `write()`.) --- .../v0/Encoded/ABIv0.EncodedAttachment.swift | 2 +- .../ABI/v0/Encoded/ABIv0.EncodedEvent.swift | 2 +- .../Testing/Attachments/Test.Attachable.swift | 23 +- .../Testing/Attachments/Test.Attachment.swift | 239 +++++++++--------- Sources/Testing/Events/Event.swift | 15 +- .../Event.HumanReadableOutputRecorder.swift | 2 +- Sources/Testing/Issues/Issue.swift | 12 +- .../Running/Configuration+EventHandling.swift | 5 +- Tests/TestingTests/AttachmentTests.swift | 19 +- 9 files changed, 180 insertions(+), 139 deletions(-) diff --git a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedAttachment.swift b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedAttachment.swift index 525f8718f..7f52e0059 100644 --- a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedAttachment.swift +++ b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedAttachment.swift @@ -21,7 +21,7 @@ extension ABIv0 { /// The path where the attachment was written. var path: String? - init(encoding attachment: borrowing Test.Attachment, in eventContext: borrowing Event.Context) { + init(encoding attachment: borrowing Test.Attachment, in eventContext: borrowing Event.Context) { path = attachment.fileSystemPath } } diff --git a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedEvent.swift b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedEvent.swift index fd9dc464a..5b67d350f 100644 --- a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedEvent.swift +++ b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedEvent.swift @@ -80,7 +80,7 @@ extension ABIv0 { case let .issueRecorded(recordedIssue): kind = .issueRecorded issue = EncodedIssue(encoding: recordedIssue, in: eventContext) - case let .valueAttached(attachment): + case let .valueAttached(attachment, _): kind = .valueAttached _attachment = EncodedAttachment(encoding: attachment, in: eventContext) case .testCaseEnded: diff --git a/Sources/Testing/Attachments/Test.Attachable.swift b/Sources/Testing/Attachments/Test.Attachable.swift index 0053bec62..c1ad6c2e8 100644 --- a/Sources/Testing/Attachments/Test.Attachable.swift +++ b/Sources/Testing/Attachments/Test.Attachable.swift @@ -15,7 +15,8 @@ extension Test { /// /// 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()``. An attachment can only be attached once. + /// ``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 @@ -61,7 +62,7 @@ extension Test { /// 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 + borrowing func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R } } @@ -103,56 +104,56 @@ extension Test.Attachable where Self: StringProtocol { // 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 { + 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 { + 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 { + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } } @_spi(Experimental) extension UnsafeBufferPointer: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try body(.init(self)) } } @_spi(Experimental) extension UnsafeMutableBufferPointer: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try body(.init(self)) } } @_spi(Experimental) extension UnsafeRawBufferPointer: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try body(self) } } @_spi(Experimental) extension UnsafeMutableRawBufferPointer: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try body(.init(self)) } } @_spi(Experimental) extension String: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + 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)) @@ -162,7 +163,7 @@ extension String: Test.Attachable { @_spi(Experimental) extension Substring: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + 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/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 36da9f8c6..933596adf 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -20,30 +20,13 @@ extension Test { /// value of some type that conforms to ``Test/Attachable``. Initialize an /// instance of ``Test/Attachment`` with that value and, optionally, a /// preferred filename to use when writing to disk. - public struct Attachment: Sendable { -#if !SWT_NO_LAZY_ATTACHMENTS - /// Storage for ``attachableValue``. - private var _attachableValue: any Attachable & Sendable /* & Copyable rdar://137614425 */ - - /// The value of this attachment. - /// - /// The type of this property's value may not match the type of the value - /// originally used to create this attachment. - public var attachableValue: any Attachable & Sendable /* & Copyable rdar://137614425 */ { - _attachableValue - } -#else - /// Storage for ``attachableValue``. - private var _attachableValue: _AttachableProxy - + /// + /// Although it is not a constraint of `AttachableValue`, instances of this + /// type can only be created with attachable values that conform to + /// ``Test/Attachable``. + public struct Attachment: ~Copyable where AttachableValue: ~Copyable { /// The value of this attachment. - /// - /// The type of this property's value may not match the type of the value - /// originally used to create this attachment. - public var attachableValue: some Test.Attachable & Sendable & Copyable { - _attachableValue - } -#endif + public var attachableValue: AttachableValue /// The path to which the this attachment was written, if any. /// @@ -71,19 +54,16 @@ extension Test { /// value of this property has not been explicitly set, the testing library /// will attempt to generate its own value. public var preferredName: String - - /// The source location where the attachment was initialized. - /// - /// The value of this property is used when recording issues associated with - /// the attachment. - public var sourceLocation: SourceLocation } } -// MARK: - +extension Test.Attachment: Copyable where AttachableValue: Copyable {} +extension Test.Attachment: Sendable where AttachableValue: Sendable {} + +// MARK: - Initializing an attachment -extension Test.Attachment { #if !SWT_NO_LAZY_ATTACHMENTS +extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { /// Initialize an instance of this type that encloses the given attachable /// value. /// @@ -93,98 +73,80 @@ extension Test.Attachment { /// - preferredName: The preferred name of the attachment when writing it /// to a test report or to disk. If `nil`, the testing library attempts /// to derive a reasonable filename for the attached value. - /// - sourceLocation: The source location of the call to this initializer. - /// This value is used when recording issues associated with the - /// attachment. - public init( - _ attachableValue: some Test.Attachable & Sendable & Copyable, - named preferredName: String? = nil, - sourceLocation: SourceLocation = #_sourceLocation - ) { - let preferredName = preferredName ?? Self.defaultPreferredName - self.init(_attachableValue: attachableValue, preferredName: preferredName, sourceLocation: sourceLocation) + public init(_ attachableValue: consuming AttachableValue, named preferredName: String? = nil) { + self.attachableValue = attachableValue + self.preferredName = preferredName ?? Self.defaultPreferredName } -#endif +} - /// Attach this instance to the current test. +@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +extension Test.Attachment where AttachableValue == any Test.Attachable & Sendable & Copyable { + /// Create a type-erased attachment from an instance of ``Test/Attachment``. /// - /// An attachment can only be attached once. - public consuming func attach() { - Event.post(.valueAttached(self)) + /// - Parameters: + /// - attachment: The attachment to type-erase. + fileprivate init(_ attachment: Test.Attachment) { + self.init( + attachableValue: attachment.attachableValue, + fileSystemPath: attachment.fileSystemPath, + preferredName: attachment.preferredName + ) } } +#endif -// MARK: - Non-sendable and move-only attachments - -/// A type that stands in for an attachable type that is not also sendable. -private struct _AttachableProxy: Test.Attachable, Sendable { - /// The result of `withUnsafeBufferPointer(for:_:)` from the original - /// attachable value. - var encodedValue = [UInt8]() - - var estimatedAttachmentByteCount: Int? +// MARK: - Attaching an attachment to a test (etc.) - func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try encodedValue.withUnsafeBufferPointer(for: attachment, body) +extension Test.Attachment where AttachableValue: Test.Attachable & Sendable & Copyable { + /// Attach this instance to the current test. + /// + /// - Parameters: + /// - sourceLocation: The source location of the call to this function. + /// + /// An attachment can only be attached once. + @_documentation(visibility: private) + public consuming func attach(sourceLocation: SourceLocation = #_sourceLocation) { + let attachmentCopy = Test.Attachment(self) + Event.post(.valueAttached(attachmentCopy, sourceLocation: sourceLocation)) } } -extension Test.Attachment { - /// Initialize an instance of this type that encloses the given attachable - /// value. +extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { + /// Attach this instance to the current test. /// /// - Parameters: - /// - attachableValue: The value that will be attached to the output of - /// the test run. - /// - preferredName: The preferred name of the attachment when writing it - /// to a test report or to disk. If `nil`, the testing library attempts - /// to derive a reasonable filename for the attached value. - /// - sourceLocation: The source location of the call to this initializer. - /// This value is used when recording issues associated with the - /// attachment. + /// - sourceLocation: The source location of the call to this function. /// - /// When attaching a value of a type that does not conform to both `Sendable` - /// and `Copyable`, the testing library encodes it as data immediately. If the - /// value cannot be encoded and an error is thrown, that error is recorded as - /// an issue in the current test and the resulting instance of - /// ``Test/Attachment`` is empty. -#if !SWT_NO_LAZY_ATTACHMENTS - @_disfavoredOverload -#endif - public init( - _ attachableValue: borrowing some Test.Attachable & ~Copyable, - named preferredName: String? = nil, - sourceLocation: SourceLocation = #_sourceLocation - ) { - let preferredName = preferredName ?? Self.defaultPreferredName - var proxyAttachable = _AttachableProxy() - proxyAttachable.estimatedAttachmentByteCount = attachableValue.estimatedAttachmentByteCount - - // BUG: the borrow checker thinks that withErrorRecording() is consuming - // attachableValue, so get around it with an additional do/catch clause. + /// When attaching a value of a type that does not conform to both + /// [`Sendable`](https://developer.apple.com/documentation/swift/sendable) and + /// [`Copyable`](https://developer.apple.com/documentation/swift/copyable), + /// the testing library encodes it as data immediately. If the value cannot be + /// encoded and an error is thrown, that error is recorded as an issue in the + /// current test and the attachment is not written to the test report or to + /// disk. + /// + /// An attachment can only be attached once. + public consuming func attach(sourceLocation: SourceLocation = #_sourceLocation) { do { - let proxyAttachment = Self(_attachableValue: proxyAttachable, preferredName: preferredName, sourceLocation: sourceLocation) - proxyAttachable.encodedValue = try attachableValue.withUnsafeBufferPointer(for: proxyAttachment) { buffer in - [UInt8](buffer) + let attachmentCopy = try attachableValue.withUnsafeBufferPointer(for: self) { buffer in + Test.Attachment(attachableValue: Array(buffer), fileSystemPath: fileSystemPath, preferredName: preferredName) } - proxyAttachable.estimatedAttachmentByteCount = proxyAttachable.encodedValue.count +#if !SWT_NO_LAZY_ATTACHMENTS + attachmentCopy.attach(sourceLocation: sourceLocation) +#else + Event.post(.valueAttached(attachmentCopy, sourceLocation: sourceLocation)) +#endif } catch { - Issue.withErrorRecording(at: sourceLocation) { - // TODO: define new issue kind .valueAttachmentFailed(any Error) - // (but only use it if the caught error isn't ExpectationFailedError, - // SystemError, or APIMisuseError. We need a protocol for these things.) - throw error - } + let sourceContext = SourceContext(backtrace: .current(), sourceLocation: sourceLocation) + Issue(kind: .valueAttachmentFailed(error), comments: [], sourceContext: sourceContext).record() } - - self.init(_attachableValue: proxyAttachable, preferredName: preferredName, sourceLocation: sourceLocation) } } #if !SWT_NO_FILE_IO // MARK: - Writing -extension Test.Attachment { +extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { /// Write the attachment's contents to a file in the specified directory. /// /// - Parameters: @@ -210,8 +172,8 @@ extension Test.Attachment { /// This function is provided as a convenience to allow tools authors to write /// attachments to persistent storage the same way that Swift Package Manager /// does. You are not required to use this function. - @_spi(ForToolsIntegrationOnly) - public func write(toFileInDirectoryAtPath directoryPath: String) throws -> String { + @_spi(Experimental) @_spi(ForToolsIntegrationOnly) + public borrowing func write(toFileInDirectoryAtPath directoryPath: String) throws -> String { try write( toFileInDirectoryAtPath: directoryPath, appending: String(UInt64.random(in: 0 ..< .max), radix: 36) @@ -238,7 +200,7 @@ extension Test.Attachment { /// /// If the argument `suffix` always produces the same string, the result of /// this function is undefined. - func write(toFileInDirectoryAtPath directoryPath: String, usingPreferredName: Bool = true, appending suffix: @autoclosure () -> String) throws -> String { + borrowing func write(toFileInDirectoryAtPath directoryPath: String, usingPreferredName: Bool = true, appending suffix: @autoclosure () -> String) throws -> String { let result: String let preferredName = usingPreferredName ? preferredName : Self.defaultPreferredName @@ -278,6 +240,8 @@ extension Test.Attachment { } } + // There should be no code path that leads to this call where the attachable + // value is nil. try attachableValue.withUnsafeBufferPointer(for: self) { buffer in try file!.write(buffer) } @@ -286,6 +250,46 @@ extension Test.Attachment { } } +@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +extension Test.Attachment where AttachableValue == any Test.Attachable & Sendable & Copyable { + /// Write the attachment's contents to a file in the specified directory. + /// + /// - Parameters: + /// - directoryPath: The directory that should contain the attachment when + /// written. + /// + /// - Throws: Any error preventing writing the attachment. + /// + /// - Returns: The path to the file that was written. + /// + /// The attachment is written to a file _within_ `directoryPath`, whose name + /// is derived from the value of the ``Test/Attachment/preferredName`` + /// property. + /// + /// If you pass `--experimental-attachments-path` to `swift test`, the testing + /// library automatically uses this function to persist attachments to the + /// directory you specify. + /// + /// This function does not get or set the value of the attachment's + /// ``fileSystemPath`` property. The caller is responsible for setting the + /// value of this property if needed. + /// + /// This function is provided as a convenience to allow tools authors to write + /// attachments to persistent storage the same way that Swift Package Manager + /// does. You are not required to use this function. + public borrowing func write(toFileInDirectoryAtPath directoryPath: String) throws -> String { + func open(_ attachableValue: T) throws -> String where T: Test.Attachable & Copyable { + let temporaryAttachment = Test.Attachment( + attachableValue: attachableValue, + fileSystemPath: fileSystemPath, + preferredName: preferredName + ) + return try temporaryAttachment.write(toFileInDirectoryAtPath: directoryPath) + } + return try open(attachableValue) + } +} + extension Configuration { /// Handle the given "value attached" event. /// @@ -296,33 +300,42 @@ extension Configuration { /// function does nothing. /// - context: The context associated with the event. /// + /// - Returns: Whether or not to continue handling the event. + /// /// This function is called automatically by ``handleEvent(_:in:)``. You do /// not need to call it elsewhere. It automatically persists the attachment /// associated with `event` and modifies `event` to include the path where the /// attachment was stored. - func handleValueAttachedEvent(_ event: inout Event, in eventContext: borrowing Event.Context) { + func handleValueAttachedEvent(_ event: inout Event, in eventContext: borrowing Event.Context) -> Bool { guard let attachmentsPath else { // If there is no path to which attachments should be written, there's - // nothing to do. - return + // nothing to do here. The event handler may still want to handle it. + return true } - guard case let .valueAttached(attachment) = event.kind else { + guard case let .valueAttached(attachment, sourceLocation) = event.kind else { preconditionFailure("Passed the wrong kind of event to \(#function) (expected valueAttached, got \(event.kind)). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") } if attachment.fileSystemPath != nil { // Somebody already persisted this attachment. This isn't necessarily a // logic error in the testing library, but it probably means we shouldn't - // persist it again. - return + // persist it again. Suppress the event. + return false } - // Write the attachment. If an error occurs, record it as an issue in the - // current test. - Issue.withErrorRecording(at: attachment.sourceLocation, configuration: self) { + do { + // Write the attachment. var attachment = attachment attachment.fileSystemPath = try attachment.write(toFileInDirectoryAtPath: attachmentsPath) - event.kind = .valueAttached(attachment) + + // Update the event before returning and continuing to handle it. + event.kind = .valueAttached(attachment, sourceLocation: sourceLocation) + return true + } catch { + // Record the error as an issue and suppress the event. + let sourceContext = SourceContext(backtrace: .current(), sourceLocation: sourceLocation) + Issue(kind: .valueAttachmentFailed(error), comments: [], sourceContext: sourceContext).record() + return false } } } diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index 54f4eea31..1ec3f99d5 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -98,12 +98,25 @@ public struct Event: Sendable { /// - issue: The issue which was recorded. indirect case issueRecorded(_ issue: Issue) +#if !SWT_NO_LAZY_ATTACHMENTS /// An attachment was created. /// /// - Parameters: /// - attachment: The attachment that was created. + /// - sourceLocation: The source location of the function call that caused + /// this event. @_spi(Experimental) - indirect case valueAttached(_ attachment: Test.Attachment) + indirect case valueAttached(_ attachment: Test.Attachment, sourceLocation: SourceLocation) +#else + /// An attachment was created. + /// + /// - Parameters: + /// - attachment: The attachment that was created. + /// - sourceLocation: The source location of the function call that caused + /// this event. + @_spi(Experimental) + indirect case valueAttached(_ attachment: Test.Attachment<[UInt8]>, sourceLocation: SourceLocation) +#endif /// A test ended. /// diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index ab1f56702..91671da57 100644 --- a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift @@ -459,7 +459,7 @@ extension Event.HumanReadableOutputRecorder { } return CollectionOfOne(primaryMessage) + additionalMessages - case let .valueAttached(attachment): + case let .valueAttached(attachment, _): var result = [ Message( symbol: .attachment, diff --git a/Sources/Testing/Issues/Issue.swift b/Sources/Testing/Issues/Issue.swift index 9f9029459..69f92eaf6 100644 --- a/Sources/Testing/Issues/Issue.swift +++ b/Sources/Testing/Issues/Issue.swift @@ -60,6 +60,14 @@ public struct Issue: Sendable { /// A known issue was expected, but was not recorded. case knownIssueNotRecorded + /// An issue due to an `Error` being thrown while attempting to save an + /// attachment to a test report or to disk. + /// + /// - Parameters: + /// - error: The error which was associated with this issue. + @_spi(Experimental) + case valueAttachmentFailed(_ error: any Error) + /// An issue occurred due to misuse of the testing library. case apiMisused @@ -216,6 +224,8 @@ extension Issue.Kind: CustomStringConvertible { return "Time limit was exceeded: \(TimeValue(timeLimitComponents))" case .knownIssueNotRecorded: return "Known issue was not recorded" + case let .valueAttachmentFailed(error): + return "Caught error while saving attachment: \(error)" case .apiMisused: return "An API was misused" case .system: @@ -355,7 +365,7 @@ extension Issue.Kind { .expectationFailed(Expectation.Snapshot(snapshotting: expectation)) case .confirmationMiscounted: .unconditional - case let .errorCaught(error): + case let .errorCaught(error), let .valueAttachmentFailed(error): .errorCaught(ErrorSnapshot(snapshotting: error)) case let .timeLimitExceeded(timeLimitComponents: timeLimitComponents): .timeLimitExceeded(timeLimitComponents: timeLimitComponents) diff --git a/Sources/Testing/Running/Configuration+EventHandling.swift b/Sources/Testing/Running/Configuration+EventHandling.swift index 03931b790..025f07d2c 100644 --- a/Sources/Testing/Running/Configuration+EventHandling.swift +++ b/Sources/Testing/Running/Configuration+EventHandling.swift @@ -27,7 +27,10 @@ extension Configuration { #if !SWT_NO_FILE_IO if case .valueAttached = event.kind { var eventCopy = copy event - handleValueAttachedEvent(&eventCopy, in: context) + guard handleValueAttachedEvent(&eventCopy, in: contextCopy) else { + // The attachment could not be handled, so suppress this event. + return + } return eventHandler(eventCopy, contextCopy) } #endif diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 85299c588..d6371a5d9 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -135,7 +135,7 @@ struct AttachmentTests { var configuration = Configuration() configuration.attachmentsPath = try temporaryDirectory() configuration.eventHandler = { event, _ in - guard case let .valueAttached(attachment) = event.kind else { + guard case let .valueAttached(attachment, _) = event.kind else { return } valueAttached() @@ -165,7 +165,7 @@ struct AttachmentTests { await confirmation("Attachment detected") { valueAttached in var configuration = Configuration() configuration.eventHandler = { event, _ in - guard case let .valueAttached(attachment) = event.kind else { + guard case let .valueAttached(attachment, _) = event.kind else { return } @@ -184,7 +184,7 @@ struct AttachmentTests { await confirmation("Attachment detected") { valueAttached in var configuration = Configuration() configuration.eventHandler = { event, _ in - guard case let .valueAttached(attachment) = event.kind else { + guard case let .valueAttached(attachment, _) = event.kind else { return } @@ -200,14 +200,14 @@ struct AttachmentTests { } @Test func issueRecordedWhenAttachingNonSendableValueThatThrows() async { - await confirmation("Attachment detected") { valueAttached in + await confirmation("Attachment detected", expectedCount: 0) { valueAttached in await confirmation("Issue recorded") { issueRecorded in var configuration = Configuration() configuration.eventHandler = { event, _ in if case .valueAttached = event.kind { valueAttached() } else if case let .issueRecorded(issue) = event.kind, - case let .errorCaught(error) = issue.kind, + case let .valueAttachmentFailed(error) = issue.kind, error is MyError { issueRecorded() } @@ -226,7 +226,7 @@ struct AttachmentTests { extension AttachmentTests { @Suite("Built-in conformances") struct BuiltInConformances { - func test(_ value: borrowing some Test.Attachable & ~Copyable) throws { + func test(_ value: some Test.Attachable) throws { #expect(value.estimatedAttachmentByteCount == 6) let attachment = Test.Attachment(value) try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { buffer in @@ -296,7 +296,7 @@ struct MyAttachable: Test.Attachable, ~Copyable { var string: String var errorToThrow: (any Error)? - func withUnsafeBufferPointer(for attachment: borrowing Testing.Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { if let errorToThrow { throw errorToThrow } @@ -314,7 +314,8 @@ extension MyAttachable: Sendable {} struct MySendableAttachable: Test.Attachable, Sendable { var string: String - func withUnsafeBufferPointer(for attachment: borrowing Testing.Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + #expect(attachment.attachableValue.string == string) var string = string return try string.withUTF8 { buffer in try body(.init(buffer)) @@ -325,7 +326,7 @@ struct MySendableAttachable: Test.Attachable, Sendable { struct MySendableAttachableWithDefaultByteCount: Test.Attachable, Sendable { var string: String - func withUnsafeBufferPointer(for attachment: borrowing Testing.Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { var string = string return try string.withUTF8 { buffer in try body(.init(buffer)) From b9c95ef73af9253c4fe00922c523cd42477a0bb6 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Nov 2024 16:36:30 -0500 Subject: [PATCH 02/27] Add accessors through withUnsafeBufferPointer since the type-erased attachment is a pain to use with it --- .../Testing/Attachments/Test.Attachment.swift | 54 +++++++++++++++++++ Tests/TestingTests/AttachmentTests.swift | 2 +- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 933596adf..0f711f286 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -143,6 +143,60 @@ extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { } } +// MARK: - Getting the serialized form of an attachable value (generically) + +extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { + /// Call a function and pass a buffer representing the value of this + /// instance's ``attachableValue`` property to it. + /// + /// - Parameters: + /// - 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. This function calls the + /// ``Test/Attachable/withUnsafeBufferPointer(for:_:)`` function on this + /// attachment's ``attachableValue`` property. + @inlinable public borrowing func withUnsafeBufferPointer(_ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try attachableValue.withUnsafeBufferPointer(for: self, body) + } +} + +extension Test.Attachment where AttachableValue == any Test.Attachable & Sendable & Copyable { + /// Call a function and pass a buffer representing the value of this + /// instance's ``attachableValue`` property to it. + /// + /// - Parameters: + /// - 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. This function calls the + /// ``Test/Attachable/withUnsafeBufferPointer(for:_:)`` function on this + /// attachment's ``attachableValue`` property. + public borrowing func withUnsafeBufferPointer(_ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + func open(_ attachableValue: T) throws -> R where T: Test.Attachable & Copyable { + let temporaryAttachment = Test.Attachment( + attachableValue: attachableValue, + fileSystemPath: fileSystemPath, + preferredName: preferredName + ) + return try temporaryAttachment.withUnsafeBufferPointer(body) + } + return try open(attachableValue) + } +} + #if !SWT_NO_FILE_IO // MARK: - Writing diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index d6371a5d9..942bceefa 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -229,7 +229,7 @@ extension AttachmentTests { func test(_ value: some Test.Attachable) throws { #expect(value.estimatedAttachmentByteCount == 6) let attachment = Test.Attachment(value) - try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { buffer in + try attachment.withUnsafeBufferPointer { buffer in #expect(buffer.elementsEqual("abc123".utf8)) #expect(buffer.count == 6) } From 764cc6a1106b54b3e3dfb8c0206caca9706ecdad Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Nov 2024 16:48:32 -0500 Subject: [PATCH 03/27] Work around rdar://137614425 (again) --- Sources/Testing/Attachments/Test.Attachment.swift | 8 ++++---- Sources/Testing/Events/Event.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 0f711f286..2acd7b98e 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -80,7 +80,7 @@ extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { } @_spi(Experimental) @_spi(ForToolsIntegrationOnly) -extension Test.Attachment where AttachableValue == any Test.Attachable & Sendable & Copyable { +extension Test.Attachment where AttachableValue == any Test.Attachable & Sendable /* & Copyable rdar://137614425 */ { /// Create a type-erased attachment from an instance of ``Test/Attachment``. /// /// - Parameters: @@ -106,7 +106,7 @@ extension Test.Attachment where AttachableValue: Test.Attachable & Sendable & Co /// An attachment can only be attached once. @_documentation(visibility: private) public consuming func attach(sourceLocation: SourceLocation = #_sourceLocation) { - let attachmentCopy = Test.Attachment(self) + let attachmentCopy = Test.Attachment(self) Event.post(.valueAttached(attachmentCopy, sourceLocation: sourceLocation)) } } @@ -167,7 +167,7 @@ extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { } } -extension Test.Attachment where AttachableValue == any Test.Attachable & Sendable & Copyable { +extension Test.Attachment where AttachableValue == any Test.Attachable & Sendable /* & Copyable rdar://137614425 */ { /// Call a function and pass a buffer representing the value of this /// instance's ``attachableValue`` property to it. /// @@ -305,7 +305,7 @@ extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { } @_spi(Experimental) @_spi(ForToolsIntegrationOnly) -extension Test.Attachment where AttachableValue == any Test.Attachable & Sendable & Copyable { +extension Test.Attachment where AttachableValue == any Test.Attachable & Sendable /* & Copyable rdar://137614425 */ { /// Write the attachment's contents to a file in the specified directory. /// /// - Parameters: diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index 1ec3f99d5..aa472048c 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -106,7 +106,7 @@ public struct Event: Sendable { /// - sourceLocation: The source location of the function call that caused /// this event. @_spi(Experimental) - indirect case valueAttached(_ attachment: Test.Attachment, sourceLocation: SourceLocation) + indirect case valueAttached(_ attachment: Test.Attachment, sourceLocation: SourceLocation) #else /// An attachment was created. /// From 0f5252ec66873e37a8a12d772fe80b9a75c078be Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Nov 2024 17:07:13 -0500 Subject: [PATCH 04/27] Work around a compiler crash (again, yes) --- Sources/Testing/Attachments/Test.Attachment.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 2acd7b98e..71c96b578 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -185,15 +185,15 @@ extension Test.Attachment where AttachableValue == any Test.Attachable & Sendabl /// ``Test/Attachable/withUnsafeBufferPointer(for:_:)`` function on this /// attachment's ``attachableValue`` property. public borrowing func withUnsafeBufferPointer(_ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - func open(_ attachableValue: T) throws -> R where T: Test.Attachable & Copyable { + func open(_ attachableValue: T, for attachment: Self) throws -> R where T: Test.Attachable & Copyable { let temporaryAttachment = Test.Attachment( attachableValue: attachableValue, - fileSystemPath: fileSystemPath, - preferredName: preferredName + fileSystemPath: attachment.fileSystemPath, + preferredName: attachment.preferredName ) - return try temporaryAttachment.withUnsafeBufferPointer(body) + return try attachableValue.withUnsafeBufferPointer(for: temporaryAttachment, body) } - return try open(attachableValue) + return try open(attachableValue, for: self) } } From 78770a2ab4862f0a3623554dd3dd38adc9410177 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Nov 2024 17:19:22 -0500 Subject: [PATCH 05/27] Work around the compiler crash a second time --- Sources/Testing/Attachments/Test.Attachment.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 71c96b578..b4df254dc 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -332,15 +332,15 @@ extension Test.Attachment where AttachableValue == any Test.Attachable & Sendabl /// attachments to persistent storage the same way that Swift Package Manager /// does. You are not required to use this function. public borrowing func write(toFileInDirectoryAtPath directoryPath: String) throws -> String { - func open(_ attachableValue: T) throws -> String where T: Test.Attachable & Copyable { + func open(_ attachableValue: T, for attachment: Self) throws -> String where T: Test.Attachable & Copyable { let temporaryAttachment = Test.Attachment( attachableValue: attachableValue, - fileSystemPath: fileSystemPath, - preferredName: preferredName + fileSystemPath: attachment.fileSystemPath, + preferredName: attachment.preferredName ) return try temporaryAttachment.write(toFileInDirectoryAtPath: directoryPath) } - return try open(attachableValue) + return try open(attachableValue, for: self) } } From 06ab04f2ddddafc9e1e2b964a56fd4508ac9de14 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sat, 9 Nov 2024 19:50:38 -0500 Subject: [PATCH 06/27] Use a dedicated type to represent any attachable value rather than an existential --- .../Testing/Attachments/Test.Attachment.swift | 158 ++++++++---------- Sources/Testing/Events/Event.swift | 13 +- 2 files changed, 74 insertions(+), 97 deletions(-) diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index b4df254dc..38cf26bc0 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -24,9 +24,9 @@ extension Test { /// Although it is not a constraint of `AttachableValue`, instances of this /// type can only be created with attachable values that conform to /// ``Test/Attachable``. - public struct Attachment: ~Copyable where AttachableValue: ~Copyable { + public struct Attachment: ~Copyable where AttachableValue: Test.Attachable & ~Copyable { /// The value of this attachment. - public var attachableValue: AttachableValue + public fileprivate(set) var attachableValue: AttachableValue /// The path to which the this attachment was written, if any. /// @@ -63,7 +63,7 @@ extension Test.Attachment: Sendable where AttachableValue: Sendable {} // MARK: - Initializing an attachment #if !SWT_NO_LAZY_ATTACHMENTS -extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { +extension Test.Attachment where AttachableValue: ~Copyable { /// Initialize an instance of this type that encloses the given attachable /// value. /// @@ -80,14 +80,30 @@ extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { } @_spi(Experimental) @_spi(ForToolsIntegrationOnly) -extension Test.Attachment where AttachableValue == any Test.Attachable & Sendable /* & Copyable rdar://137614425 */ { +extension Test.Attachment where AttachableValue == AnyAttachable { + /// The value of this attachment. + /// + /// When working with a type-erased attachment, the value of this property + /// equals the underlying attachable value. To access the attachable value as + /// an instance of ``AnyAttachable``, specify the type explicitly: + /// + /// ```swift + /// let attachableValue = attachment.attachableValue as AnyAttachable + /// ``` + /// + /// In Embedded Swift, the value of this property is always an instance of + /// [`Array`](https://developer.apple.com/documentation/swift/array). + public var attachableValue: AnyAttachable.RawValue { + attachableValue.rawValue + } + /// Create a type-erased attachment from an instance of ``Test/Attachment``. /// /// - Parameters: /// - attachment: The attachment to type-erase. fileprivate init(_ attachment: Test.Attachment) { self.init( - attachableValue: attachment.attachableValue, + attachableValue: AnyAttachable(rawValue: attachment.attachableValue), fileSystemPath: attachment.fileSystemPath, preferredName: attachment.preferredName ) @@ -95,9 +111,53 @@ extension Test.Attachment where AttachableValue == any Test.Attachable & Sendabl } #endif +/// A type-erased container type that represents any attachable value. +/// +/// This type is not generally visible to developers. It is used when posting +/// events of kind ``Event/Kind/valueAttached(_:sourceLocation:)``. Test tools +/// authors who use `@_spi(ForToolsIntegrationOnly)` will see instances of this +/// type when handling those events. +/// +/// @Comment { +/// Swift's type system requires that this type be at least as visible as +/// `Event.Kind.valueAttached(_:sourceLocation:)`, otherwise it would be +/// declared as `private`. +/// } +@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +public struct AnyAttachable: RawRepresentable, Test.Attachable, Copyable, Sendable { +#if !SWT_NO_LAZY_ATTACHMENTS + public typealias RawValue = any Test.Attachable & Sendable /* & Copyable rdar://137614425 */ +#else + public typealias RawValue = [UInt8] +#endif + + public var rawValue: RawValue + + public init(rawValue: RawValue) { + self.rawValue = rawValue + } + + public var estimatedAttachmentByteCount: Int? { + rawValue.estimatedAttachmentByteCount + } + + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + func open(_ attachableValue: T, for attachment: borrowing Test.Attachment) throws -> R where T: Test.Attachable & Sendable & Copyable { + let temporaryAttachment = Test.Attachment( + attachableValue: attachableValue, + fileSystemPath: attachment.fileSystemPath, + preferredName: attachment.preferredName + ) + return try attachableValue.withUnsafeBufferPointer(for: temporaryAttachment, body) + } + return try open(rawValue, for: attachment) + } +} + // MARK: - Attaching an attachment to a test (etc.) -extension Test.Attachment where AttachableValue: Test.Attachable & Sendable & Copyable { +#if !SWT_NO_LAZY_ATTACHMENTS +extension Test.Attachment where AttachableValue: Sendable & Copyable { /// Attach this instance to the current test. /// /// - Parameters: @@ -106,12 +166,13 @@ extension Test.Attachment where AttachableValue: Test.Attachable & Sendable & Co /// An attachment can only be attached once. @_documentation(visibility: private) public consuming func attach(sourceLocation: SourceLocation = #_sourceLocation) { - let attachmentCopy = Test.Attachment(self) + let attachmentCopy = Test.Attachment(self) Event.post(.valueAttached(attachmentCopy, sourceLocation: sourceLocation)) } } +#endif -extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { +extension Test.Attachment where AttachableValue: ~Copyable { /// Attach this instance to the current test. /// /// - Parameters: @@ -129,13 +190,10 @@ extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { public consuming func attach(sourceLocation: SourceLocation = #_sourceLocation) { do { let attachmentCopy = try attachableValue.withUnsafeBufferPointer(for: self) { buffer in - Test.Attachment(attachableValue: Array(buffer), fileSystemPath: fileSystemPath, preferredName: preferredName) + let attachmentCopy = Test.Attachment(attachableValue: Array(buffer), fileSystemPath: fileSystemPath, preferredName: preferredName) + return Test.Attachment(attachmentCopy) } -#if !SWT_NO_LAZY_ATTACHMENTS - attachmentCopy.attach(sourceLocation: sourceLocation) -#else Event.post(.valueAttached(attachmentCopy, sourceLocation: sourceLocation)) -#endif } catch { let sourceContext = SourceContext(backtrace: .current(), sourceLocation: sourceLocation) Issue(kind: .valueAttachmentFailed(error), comments: [], sourceContext: sourceContext).record() @@ -145,7 +203,7 @@ extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { // MARK: - Getting the serialized form of an attachable value (generically) -extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { +extension Test.Attachment where AttachableValue: ~Copyable { /// Call a function and pass a buffer representing the value of this /// instance's ``attachableValue`` property to it. /// @@ -167,40 +225,10 @@ extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { } } -extension Test.Attachment where AttachableValue == any Test.Attachable & Sendable /* & Copyable rdar://137614425 */ { - /// Call a function and pass a buffer representing the value of this - /// instance's ``attachableValue`` property to it. - /// - /// - Parameters: - /// - 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. This function calls the - /// ``Test/Attachable/withUnsafeBufferPointer(for:_:)`` function on this - /// attachment's ``attachableValue`` property. - public borrowing func withUnsafeBufferPointer(_ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - func open(_ attachableValue: T, for attachment: Self) throws -> R where T: Test.Attachable & Copyable { - let temporaryAttachment = Test.Attachment( - attachableValue: attachableValue, - fileSystemPath: attachment.fileSystemPath, - preferredName: attachment.preferredName - ) - return try attachableValue.withUnsafeBufferPointer(for: temporaryAttachment, body) - } - return try open(attachableValue, for: self) - } -} - #if !SWT_NO_FILE_IO // MARK: - Writing -extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { +extension Test.Attachment where AttachableValue: ~Copyable { /// Write the attachment's contents to a file in the specified directory. /// /// - Parameters: @@ -304,46 +332,6 @@ extension Test.Attachment where AttachableValue: Test.Attachable & ~Copyable { } } -@_spi(Experimental) @_spi(ForToolsIntegrationOnly) -extension Test.Attachment where AttachableValue == any Test.Attachable & Sendable /* & Copyable rdar://137614425 */ { - /// Write the attachment's contents to a file in the specified directory. - /// - /// - Parameters: - /// - directoryPath: The directory that should contain the attachment when - /// written. - /// - /// - Throws: Any error preventing writing the attachment. - /// - /// - Returns: The path to the file that was written. - /// - /// The attachment is written to a file _within_ `directoryPath`, whose name - /// is derived from the value of the ``Test/Attachment/preferredName`` - /// property. - /// - /// If you pass `--experimental-attachments-path` to `swift test`, the testing - /// library automatically uses this function to persist attachments to the - /// directory you specify. - /// - /// This function does not get or set the value of the attachment's - /// ``fileSystemPath`` property. The caller is responsible for setting the - /// value of this property if needed. - /// - /// This function is provided as a convenience to allow tools authors to write - /// attachments to persistent storage the same way that Swift Package Manager - /// does. You are not required to use this function. - public borrowing func write(toFileInDirectoryAtPath directoryPath: String) throws -> String { - func open(_ attachableValue: T, for attachment: Self) throws -> String where T: Test.Attachable & Copyable { - let temporaryAttachment = Test.Attachment( - attachableValue: attachableValue, - fileSystemPath: attachment.fileSystemPath, - preferredName: attachment.preferredName - ) - return try temporaryAttachment.write(toFileInDirectoryAtPath: directoryPath) - } - return try open(attachableValue, for: self) - } -} - extension Configuration { /// Handle the given "value attached" event. /// diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index aa472048c..9579160e7 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -98,7 +98,6 @@ public struct Event: Sendable { /// - issue: The issue which was recorded. indirect case issueRecorded(_ issue: Issue) -#if !SWT_NO_LAZY_ATTACHMENTS /// An attachment was created. /// /// - Parameters: @@ -106,17 +105,7 @@ public struct Event: Sendable { /// - sourceLocation: The source location of the function call that caused /// this event. @_spi(Experimental) - indirect case valueAttached(_ attachment: Test.Attachment, sourceLocation: SourceLocation) -#else - /// An attachment was created. - /// - /// - Parameters: - /// - attachment: The attachment that was created. - /// - sourceLocation: The source location of the function call that caused - /// this event. - @_spi(Experimental) - indirect case valueAttached(_ attachment: Test.Attachment<[UInt8]>, sourceLocation: SourceLocation) -#endif + indirect case valueAttached(_ attachment: Test.Attachment, sourceLocation: SourceLocation) /// A test ended. /// From a31910c2868f002b02efb3114d959a998c095027 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sat, 9 Nov 2024 19:55:02 -0500 Subject: [PATCH 07/27] Nest AnyAttachable --- .../Testing/Attachments/Test.Attachment.swift | 80 ++++++++++--------- Sources/Testing/Events/Event.swift | 2 +- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 38cf26bc0..925ece1ce 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -80,20 +80,20 @@ extension Test.Attachment where AttachableValue: ~Copyable { } @_spi(Experimental) @_spi(ForToolsIntegrationOnly) -extension Test.Attachment where AttachableValue == AnyAttachable { +extension Test.Attachment where AttachableValue == Test.AnyAttachable { /// The value of this attachment. /// /// When working with a type-erased attachment, the value of this property /// equals the underlying attachable value. To access the attachable value as - /// an instance of ``AnyAttachable``, specify the type explicitly: + /// an instance of ``Test/AnyAttachable``, specify the type explicitly: /// /// ```swift - /// let attachableValue = attachment.attachableValue as AnyAttachable + /// let attachableValue = attachment.attachableValue as Test.AnyAttachable /// ``` /// /// In Embedded Swift, the value of this property is always an instance of /// [`Array`](https://developer.apple.com/documentation/swift/array). - public var attachableValue: AnyAttachable.RawValue { + public var attachableValue: Test.AnyAttachable.RawValue { attachableValue.rawValue } @@ -103,7 +103,7 @@ extension Test.Attachment where AttachableValue == AnyAttachable { /// - attachment: The attachment to type-erase. fileprivate init(_ attachment: Test.Attachment) { self.init( - attachableValue: AnyAttachable(rawValue: attachment.attachableValue), + attachableValue: Test.AnyAttachable(rawValue: attachment.attachableValue), fileSystemPath: attachment.fileSystemPath, preferredName: attachment.preferredName ) @@ -111,46 +111,48 @@ extension Test.Attachment where AttachableValue == AnyAttachable { } #endif -/// A type-erased container type that represents any attachable value. -/// -/// This type is not generally visible to developers. It is used when posting -/// events of kind ``Event/Kind/valueAttached(_:sourceLocation:)``. Test tools -/// authors who use `@_spi(ForToolsIntegrationOnly)` will see instances of this -/// type when handling those events. -/// -/// @Comment { -/// Swift's type system requires that this type be at least as visible as -/// `Event.Kind.valueAttached(_:sourceLocation:)`, otherwise it would be -/// declared as `private`. -/// } -@_spi(Experimental) @_spi(ForToolsIntegrationOnly) -public struct AnyAttachable: RawRepresentable, Test.Attachable, Copyable, Sendable { +extension Test { + /// A type-erased container type that represents any attachable value. + /// + /// This type is not generally visible to developers. It is used when posting + /// events of kind ``Event/Kind/valueAttached(_:sourceLocation:)``. Test tools + /// authors who use `@_spi(ForToolsIntegrationOnly)` will see instances of + /// this type when handling those events. + /// + /// @Comment { + /// Swift's type system requires that this type be at least as visible as + /// `Event.Kind.valueAttached(_:sourceLocation:)`, otherwise it would be + /// declared as `private`. + /// } + @_spi(Experimental) @_spi(ForToolsIntegrationOnly) + public struct AnyAttachable: RawRepresentable, Test.Attachable, Copyable, Sendable { #if !SWT_NO_LAZY_ATTACHMENTS - public typealias RawValue = any Test.Attachable & Sendable /* & Copyable rdar://137614425 */ + public typealias RawValue = any Test.Attachable & Sendable /* & Copyable rdar://137614425 */ #else - public typealias RawValue = [UInt8] + public typealias RawValue = [UInt8] #endif - public var rawValue: RawValue + public var rawValue: RawValue - public init(rawValue: RawValue) { - self.rawValue = rawValue - } + public init(rawValue: RawValue) { + self.rawValue = rawValue + } - public var estimatedAttachmentByteCount: Int? { - rawValue.estimatedAttachmentByteCount - } + public var estimatedAttachmentByteCount: Int? { + rawValue.estimatedAttachmentByteCount + } - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - func open(_ attachableValue: T, for attachment: borrowing Test.Attachment) throws -> R where T: Test.Attachable & Sendable & Copyable { - let temporaryAttachment = Test.Attachment( - attachableValue: attachableValue, - fileSystemPath: attachment.fileSystemPath, - preferredName: attachment.preferredName - ) - return try attachableValue.withUnsafeBufferPointer(for: temporaryAttachment, body) + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + func open(_ attachableValue: T, for attachment: borrowing Test.Attachment) throws -> R where T: Test.Attachable & Sendable & Copyable { + let temporaryAttachment = Test.Attachment( + attachableValue: attachableValue, + fileSystemPath: attachment.fileSystemPath, + preferredName: attachment.preferredName + ) + return try attachableValue.withUnsafeBufferPointer(for: temporaryAttachment, body) + } + return try open(rawValue, for: attachment) } - return try open(rawValue, for: attachment) } } @@ -166,7 +168,7 @@ extension Test.Attachment where AttachableValue: Sendable & Copyable { /// An attachment can only be attached once. @_documentation(visibility: private) public consuming func attach(sourceLocation: SourceLocation = #_sourceLocation) { - let attachmentCopy = Test.Attachment(self) + let attachmentCopy = Test.Attachment(self) Event.post(.valueAttached(attachmentCopy, sourceLocation: sourceLocation)) } } @@ -191,7 +193,7 @@ extension Test.Attachment where AttachableValue: ~Copyable { do { let attachmentCopy = try attachableValue.withUnsafeBufferPointer(for: self) { buffer in let attachmentCopy = Test.Attachment(attachableValue: Array(buffer), fileSystemPath: fileSystemPath, preferredName: preferredName) - return Test.Attachment(attachmentCopy) + return Test.Attachment(attachmentCopy) } Event.post(.valueAttached(attachmentCopy, sourceLocation: sourceLocation)) } catch { diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index 9579160e7..3fe1b6b87 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -105,7 +105,7 @@ public struct Event: Sendable { /// - sourceLocation: The source location of the function call that caused /// this event. @_spi(Experimental) - indirect case valueAttached(_ attachment: Test.Attachment, sourceLocation: SourceLocation) + indirect case valueAttached(_ attachment: Test.Attachment, sourceLocation: SourceLocation) /// A test ended. /// From 6862d64a9720f2e57b147d5eef5a1e2a5cbb977c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 10 Nov 2024 11:09:37 -0500 Subject: [PATCH 08/27] Add AttachableContainer protocol for abstracting away box/adapter types since we'll need them for image attachments --- Package.swift | 1 + .../Testing/Attachments/Test.Attachable.swift | 25 ++++++ .../Testing/Attachments/Test.Attachment.swift | 79 +++++++++++-------- Tests/TestingTests/AttachmentTests.swift | 1 + 4 files changed, 74 insertions(+), 32 deletions(-) diff --git a/Package.swift b/Package.swift index f7fb5dc5f..471bbbe95 100644 --- a/Package.swift +++ b/Package.swift @@ -124,6 +124,7 @@ extension Array where Element == PackageDescription.SwiftSetting { availabilityMacroSettings + [ .unsafeFlags(["-require-explicit-sendable"]), .enableUpcomingFeature("ExistentialAny"), + //.enableExperimentalFeature("SuppressedAssociatedTypes"), .enableExperimentalFeature("AccessLevelOnImport"), .enableUpcomingFeature("InternalImportsByDefault"), diff --git a/Sources/Testing/Attachments/Test.Attachable.swift b/Sources/Testing/Attachments/Test.Attachable.swift index c1ad6c2e8..6bc1c06be 100644 --- a/Sources/Testing/Attachments/Test.Attachable.swift +++ b/Sources/Testing/Attachments/Test.Attachable.swift @@ -64,6 +64,31 @@ extension Test { /// of the image. borrowing func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R } + + /// A protocol describing a type that can be attached to a test report or + /// written to disk when a test is run and which contains another value that + /// it stands in for. + /// + /// 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. + /// + /// A type can conform to this protocol if it represents another type that + /// cannot directly conform to ``Test/Attachable``, such as a non-final class + /// or a type declared in a third-party module. + public protocol AttachableContainer: Attachable, ~Copyable { +#if hasFeature(SuppressedAssociatedTypes) + /// The type of the attachable value represented by this type. + associatedtype AttachableValue: ~Copyable +#else + /// The type of the attachable value represented by this type. + associatedtype AttachableValue +#endif + + /// The attachable value represented by this instance. + var attachableValue: AttachableValue { get } + } } // MARK: - Default implementations diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 925ece1ce..5f49f4333 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -25,8 +25,8 @@ extension Test { /// type can only be created with attachable values that conform to /// ``Test/Attachable``. public struct Attachment: ~Copyable where AttachableValue: Test.Attachable & ~Copyable { - /// The value of this attachment. - public fileprivate(set) var attachableValue: AttachableValue + /// Storage for ``attachableValue-29ppv``. + fileprivate var _attachableValue: AttachableValue /// The path to which the this attachment was written, if any. /// @@ -74,36 +74,20 @@ extension Test.Attachment where AttachableValue: ~Copyable { /// to a test report or to disk. If `nil`, the testing library attempts /// to derive a reasonable filename for the attached value. public init(_ attachableValue: consuming AttachableValue, named preferredName: String? = nil) { - self.attachableValue = attachableValue + self._attachableValue = attachableValue self.preferredName = preferredName ?? Self.defaultPreferredName } } @_spi(Experimental) @_spi(ForToolsIntegrationOnly) extension Test.Attachment where AttachableValue == Test.AnyAttachable { - /// The value of this attachment. - /// - /// When working with a type-erased attachment, the value of this property - /// equals the underlying attachable value. To access the attachable value as - /// an instance of ``Test/AnyAttachable``, specify the type explicitly: - /// - /// ```swift - /// let attachableValue = attachment.attachableValue as Test.AnyAttachable - /// ``` - /// - /// In Embedded Swift, the value of this property is always an instance of - /// [`Array`](https://developer.apple.com/documentation/swift/array). - public var attachableValue: Test.AnyAttachable.RawValue { - attachableValue.rawValue - } - /// Create a type-erased attachment from an instance of ``Test/Attachment``. /// /// - Parameters: /// - attachment: The attachment to type-erase. fileprivate init(_ attachment: Test.Attachment) { self.init( - attachableValue: Test.AnyAttachable(rawValue: attachment.attachableValue), + _attachableValue: Test.AnyAttachable(attachableValue: attachment.attachableValue), fileSystemPath: attachment.fileSystemPath, preferredName: attachment.preferredName ) @@ -125,33 +109,64 @@ extension Test { /// declared as `private`. /// } @_spi(Experimental) @_spi(ForToolsIntegrationOnly) - public struct AnyAttachable: RawRepresentable, Test.Attachable, Copyable, Sendable { + public struct AnyAttachable: Test.AttachableContainer, Copyable, Sendable { #if !SWT_NO_LAZY_ATTACHMENTS - public typealias RawValue = any Test.Attachable & Sendable /* & Copyable rdar://137614425 */ + public typealias AttachableValue = any Test.Attachable & Sendable /* & Copyable rdar://137614425 */ #else - public typealias RawValue = [UInt8] + public typealias AttachableValue = [UInt8] #endif - public var rawValue: RawValue + public var attachableValue: AttachableValue - public init(rawValue: RawValue) { - self.rawValue = rawValue + fileprivate init(attachableValue: AttachableValue) { + self.attachableValue = attachableValue } public var estimatedAttachmentByteCount: Int? { - rawValue.estimatedAttachmentByteCount + attachableValue.estimatedAttachmentByteCount } public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { func open(_ attachableValue: T, for attachment: borrowing Test.Attachment) throws -> R where T: Test.Attachable & Sendable & Copyable { let temporaryAttachment = Test.Attachment( - attachableValue: attachableValue, + _attachableValue: attachableValue, fileSystemPath: attachment.fileSystemPath, preferredName: attachment.preferredName ) return try attachableValue.withUnsafeBufferPointer(for: temporaryAttachment, body) } - return try open(rawValue, for: attachment) + return try open(attachableValue, for: attachment) + } + } +} + +// MARK: - Getting an attachable value from an attachment + +@_spi(Experimental) +extension Test.Attachment where AttachableValue: ~Copyable { + /// The value of this attachment. + @_disfavoredOverload public var attachableValue: AttachableValue { + _read { + yield _attachableValue + } + } +} + +@_spi(Experimental) +extension Test.Attachment where AttachableValue: Test.AttachableContainer & ~Copyable { + /// The value of this attachment. + /// + /// When the attachable value's type conforms to ``Test/AttachableContainer``, + /// the value of this property equals the container's underlying attachable + /// value. To access the attachable value as an instance of `T` (where `T` + /// conforms to ``Test/AttachableContainer``), specify the type explicitly: + /// + /// ```swift + /// let attachableValue = attachment.attachableValue as T + /// ``` + public var attachableValue: AttachableValue.AttachableValue { + _read { + yield attachableValue.attachableValue } } } @@ -192,7 +207,7 @@ extension Test.Attachment where AttachableValue: ~Copyable { public consuming func attach(sourceLocation: SourceLocation = #_sourceLocation) { do { let attachmentCopy = try attachableValue.withUnsafeBufferPointer(for: self) { buffer in - let attachmentCopy = Test.Attachment(attachableValue: Array(buffer), fileSystemPath: fileSystemPath, preferredName: preferredName) + let attachmentCopy = Test.Attachment(_attachableValue: Array(buffer), fileSystemPath: fileSystemPath, preferredName: preferredName) return Test.Attachment(attachmentCopy) } Event.post(.valueAttached(attachmentCopy, sourceLocation: sourceLocation)) @@ -207,7 +222,7 @@ extension Test.Attachment where AttachableValue: ~Copyable { extension Test.Attachment where AttachableValue: ~Copyable { /// Call a function and pass a buffer representing the value of this - /// instance's ``attachableValue`` property to it. + /// instance's ``attachableValue-29ppv`` property to it. /// /// - Parameters: /// - body: A function to call. A temporary buffer containing a data @@ -221,7 +236,7 @@ extension Test.Attachment where AttachableValue: ~Copyable { /// The testing library uses this function when writing an attachment to a /// test report or to a file on disk. This function calls the /// ``Test/Attachable/withUnsafeBufferPointer(for:_:)`` function on this - /// attachment's ``attachableValue`` property. + /// attachment's ``attachableValue-29ppv`` property. @inlinable public borrowing func withUnsafeBufferPointer(_ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try attachableValue.withUnsafeBufferPointer(for: self, body) } diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 942bceefa..8ae9c4996 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -189,6 +189,7 @@ struct AttachmentTests { } #expect(attachment.preferredName == "loremipsum") + #expect(attachment.attachableValue is MySendableAttachable) valueAttached() } From 9aec954578872198477abc31fc1166c64bd80f04 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 10 Nov 2024 14:55:27 -0500 Subject: [PATCH 09/27] Update comments, make AttachableContainer always Copyable & Sendable --- Sources/Testing/Attachments/Test.Attachable.swift | 12 +++++++++--- Sources/Testing/Attachments/Test.Attachment.swift | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/Attachments/Test.Attachable.swift b/Sources/Testing/Attachments/Test.Attachable.swift index 6bc1c06be..c5704930b 100644 --- a/Sources/Testing/Attachments/Test.Attachable.swift +++ b/Sources/Testing/Attachments/Test.Attachable.swift @@ -23,7 +23,10 @@ extension Test { /// 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. + /// 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. @@ -76,8 +79,11 @@ extension Test { /// /// A type can conform to this protocol if it represents another type that /// cannot directly conform to ``Test/Attachable``, such as a non-final class - /// or a type declared in a third-party module. - public protocol AttachableContainer: Attachable, ~Copyable { + /// or a type declared in a third-party module. Unlike ``Test/Attachable``, + /// types that conform to ``Test/AttachableContainer`` must also conform to + /// both [`Sendable`](https://developer.apple.com/documentation/swift/sendable) + /// and [`Copyable`](https://developer.apple.com/documentation/swift/copyable). + public protocol AttachableContainer: Attachable, Sendable { #if hasFeature(SuppressedAssociatedTypes) /// The type of the attachable value represented by this type. associatedtype AttachableValue: ~Copyable diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 5f49f4333..973147086 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -153,7 +153,7 @@ extension Test.Attachment where AttachableValue: ~Copyable { } @_spi(Experimental) -extension Test.Attachment where AttachableValue: Test.AttachableContainer & ~Copyable { +extension Test.Attachment where AttachableValue: Test.AttachableContainer { /// The value of this attachment. /// /// When the attachable value's type conforms to ``Test/AttachableContainer``, From 173f0774a404bbc3a6e5c921f0e0c121474350a2 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 10 Nov 2024 14:57:04 -0500 Subject: [PATCH 10/27] Actually no, make Test.AttachableContainer optionally copyable/sendable, don't limit our options more than we have to --- Sources/Testing/Attachments/Test.Attachable.swift | 2 +- Sources/Testing/Attachments/Test.Attachment.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Attachments/Test.Attachable.swift b/Sources/Testing/Attachments/Test.Attachable.swift index c5704930b..40891bc07 100644 --- a/Sources/Testing/Attachments/Test.Attachable.swift +++ b/Sources/Testing/Attachments/Test.Attachable.swift @@ -83,7 +83,7 @@ extension Test { /// types that conform to ``Test/AttachableContainer`` must also conform to /// both [`Sendable`](https://developer.apple.com/documentation/swift/sendable) /// and [`Copyable`](https://developer.apple.com/documentation/swift/copyable). - public protocol AttachableContainer: Attachable, Sendable { + public protocol AttachableContainer: Attachable, ~Copyable { #if hasFeature(SuppressedAssociatedTypes) /// The type of the attachable value represented by this type. associatedtype AttachableValue: ~Copyable diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 973147086..5f49f4333 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -153,7 +153,7 @@ extension Test.Attachment where AttachableValue: ~Copyable { } @_spi(Experimental) -extension Test.Attachment where AttachableValue: Test.AttachableContainer { +extension Test.Attachment where AttachableValue: Test.AttachableContainer & ~Copyable { /// The value of this attachment. /// /// When the attachable value's type conforms to ``Test/AttachableContainer``, From b867258ea85067d4de75b377881cf58e90f4ed5d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 19 Sep 2024 14:25:02 -0400 Subject: [PATCH 11/27] 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 + .../ContiguousBytes+Test.Attachable.swift | 21 ++ .../Attachments/Data+Test.Attachable.swift | 21 ++ .../Encodable+Test.Attachable.swift | 43 +++ .../Attachments/EncodingFormat.swift | 84 ++++++ .../NSSecureCoding+Test.Attachable.swift | 45 ++++ .../Attachments/Test.Attachment+URL.swift | 154 +++++++++++ .../Events/Clock+Date.swift | 0 .../_Testing_Foundation/ReexportTesting.swift | 2 +- .../Testing/Attachments/Test.Attachable.swift | 5 +- Sources/Testing/Running/Configuration.swift | 2 + Tests/TestingTests/AttachmentTests.swift | 253 +++++++++++++++++- .../shared/AvailabilityDefinitions.cmake | 1 + 13 files changed, 628 insertions(+), 5 deletions(-) create mode 100644 Sources/Overlays/_Testing_Foundation/Attachments/ContiguousBytes+Test.Attachable.swift create mode 100644 Sources/Overlays/_Testing_Foundation/Attachments/Data+Test.Attachable.swift create mode 100644 Sources/Overlays/_Testing_Foundation/Attachments/Encodable+Test.Attachable.swift create mode 100644 Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift create mode 100644 Sources/Overlays/_Testing_Foundation/Attachments/NSSecureCoding+Test.Attachable.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%) diff --git a/Package.swift b/Package.swift index 471bbbe95..b5dc49d65 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/ContiguousBytes+Test.Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/ContiguousBytes+Test.Attachable.swift new file mode 100644 index 000000000..c908662b2 --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/ContiguousBytes+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 ContiguousBytes where Self: 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/Data+Test.Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Test.Attachable.swift new file mode 100644 index 000000000..e3e2c2651 --- /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/Encodable+Test.Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Encodable+Test.Attachable.swift new file mode 100644 index 000000000..39f3e39e5 --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Encodable+Test.Attachable.swift @@ -0,0 +1,43 @@ +// +// 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 + +// 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 Encodable where Self: Test.Attachable { + public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + 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(self) + 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(self) + } + + return try data.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..9b86734f9 --- /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/NSSecureCoding+Test.Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/NSSecureCoding+Test.Attachable.swift new file mode 100644 index 000000000..535319bc3 --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/NSSecureCoding+Test.Attachable.swift @@ -0,0 +1,45 @@ +// +// 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 NSSecureCoding where Self: Test.Attachable { + 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..586d59862 --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift @@ -0,0 +1,154 @@ +// +// 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_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 utf8 = path.utf8 + let array = Array(utf8) + if array.count > 4, array[0] == UInt8(ascii: "/"), Character(UnicodeScalar(array[1])).isLetter, array[2] == UInt8(ascii: ":"), array[3] == UInt8(ascii: "/") { + return String(Substring(utf8.dropFirst())) + } +#endif + return path + } +} + +// MARK: - Attaching files + +@_spi(Experimental) +extension Test.Attachment { + /// 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, + 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! + + let attachableValue: any Test.Attachable & Sendable + if isDirectory { + attachableValue = try await _DirectoryContentAttachableProxy(contentsOfDirectoryAt: url) + } else { + // Load the file. + attachableValue = try Data(contentsOf: url, options: [.mappedIfSafe]) + } + + // Determine the preferred name of the attachment if one was not provided. + var preferredName = preferredName + if preferredName == nil, case let lastPathComponent = url.lastPathComponent, !lastPathComponent.isEmpty { + if isDirectory { + preferredName = (lastPathComponent as NSString).appendingPathExtension("tar.gz") + } else { + preferredName = lastPathComponent + } + } + + self.init(attachableValue, named: preferredName, sourceLocation: sourceLocation) + } +} + +// MARK: - Attaching directories + +/// A type representing the content of a directory as an attachable value. +private struct _DirectoryContentAttachableProxy: Test.Attachable { + /// The URL of the directory. + /// + /// The contents of this directory may change after this instance is + /// initialized. Such changes are not tracked. + var url: URL + + /// The archived contents of the directory. + private let _directoryContent: Data + + /// Initialize an instance of this type. + /// + /// - 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 `.tar.gz` format, although this is subject + /// to change) and stores a mapped copy of that archive. + init(contentsOfDirectoryAt directoryURL: URL) async throws { + url = directoryURL + + let temporaryName = "\(UUID().uuidString).tar.gz" + 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 = url.fileSystemPath + let destinationPath = temporaryURL.fileSystemPath + + try await withCheckedThrowingContinuation { continuation in + do { + _ = try Process.run( + URL(fileURLWithPath: tarPath, isDirectory: false), + arguments: ["--create", "--gzip", "--file", destinationPath, sourcePath] + ) { 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) + } + } + _directoryContent = try Data(contentsOf: temporaryURL, options: [.mappedIfSafe]) +#else + throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "This platform does not support attaching directories to tests."]) +#endif + } + + func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try _directoryContent.withUnsafeBytes(body) + } +} +#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 index 40891bc07..73ef34a14 100644 --- a/Sources/Testing/Attachments/Test.Attachable.swift +++ b/Sources/Testing/Attachments/Test.Attachable.swift @@ -116,9 +116,8 @@ extension Test.Attachable where Self: Collection, Element == UInt8 { // If withContiguousBytesIfAvailable(_:) 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. + // The Foundation cross-import overlay provides a default implementation for + // collection types that conform to Foundation's ContiguousBytes protocol. } extension Test.Attachable where Self: StringProtocol { diff --git a/Sources/Testing/Running/Configuration.swift b/Sources/Testing/Running/Configuration.swift index 786856e10..1ae019c95 100644 --- a/Sources/Testing/Running/Configuration.swift +++ b/Sources/Testing/Running/Configuration.swift @@ -8,6 +8,8 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +private import _TestingInternals + /// A type containing settings for preparing and running tests. @_spi(ForToolsIntegrationOnly) public struct Configuration: Sendable { diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 8ae9c4996..d6c1496c1 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -9,7 +9,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 { @@ -222,6 +225,176 @@ 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.attachableValue.withUnsafeBufferPointer(for: attachment) { 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 temporaryFileName = UUID().uuidString + let temporaryPath = try appendPathComponent(temporaryFileName, to: temporaryDirectory()) + let temporaryURL = URL(fileURLWithPath: temporaryPath, isDirectory: false) + try FileManager.default.createDirectory(at: temporaryURL, withIntermediateDirectories: true) + + 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).tar.gz") + 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)" + } + + var attachment: Test.Attachment + if args.forSecureCoding { + let attachableValue = MySecureCodingAttachable(string: "stringly speaking") + attachment = Test.Attachment(attachableValue, named: name) + } else { + let attachableValue = MyCodableAttachable(string: "stringly speaking") + attachment = Test.Attachment(attachableValue, named: name) + } + + 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") + } + } + + @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 { @@ -288,6 +461,18 @@ 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) + } + + @Test func contiguousBytesCollection() throws { + let value = MyContiguousCollectionAttachable(string: "abc123") + try test(value) + } +#endif } } @@ -334,3 +519,69 @@ struct MySendableAttachableWithDefaultByteCount: Test.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) ?? "" + } +} + +struct MyContiguousCollectionAttachable: Collection, ContiguousBytes, Test.Attachable { + private var _utf8: String.UTF8View + + var string: String { + get { + String(_utf8) + } + set { + _utf8 = newValue.utf8 + } + } + + init(string: String) { + _utf8 = string.utf8 + } + + var startIndex: String.UTF8View.Index { + _utf8.startIndex + } + + var endIndex: String.UTF8View.Index { + _utf8.endIndex + } + + subscript(position: String.UTF8View.Index) -> String.UTF8View.Element { + _utf8[position] + } + + func index(after i: String.UTF8View.Index) -> String.UTF8View.Index { + _utf8.index(after: i) + } + + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + let result = try _utf8.withContiguousStorageIfAvailable { buffer in + try body(.init(buffer)) + } + return try result ?? Array(_utf8).withUnsafeBytes(body) + } +} +#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 32c487ab7d752b9b3ce99488a9f87905739b9d06 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sat, 2 Nov 2024 22:10:03 -0400 Subject: [PATCH 12/27] Minor fixes --- .../Attachments/Test.Attachment+URL.swift | 12 +++++++----- Sources/Testing/Running/Configuration.swift | 2 -- Tests/TestingTests/AttachmentTests.swift | 9 ++++++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift index 586d59862..08affc7aa 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift @@ -19,10 +19,9 @@ extension URL { #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 utf8 = path.utf8 - let array = Array(utf8) - if array.count > 4, array[0] == UInt8(ascii: "/"), Character(UnicodeScalar(array[1])).isLetter, array[2] == UInt8(ascii: ":"), array[3] == UInt8(ascii: "/") { - return String(Substring(utf8.dropFirst())) + let path = path + if path.starts(with: /\/[A-Za-z]:\//) { + return String(path.dropFirst()) } #endif return path @@ -118,12 +117,15 @@ private struct _DirectoryContentAttachableProxy: Test.Attachable { #endif let sourcePath = url.fileSystemPath let destinationPath = temporaryURL.fileSystemPath + defer { + remove(destinationPath) + } try await withCheckedThrowingContinuation { continuation in do { _ = try Process.run( URL(fileURLWithPath: tarPath, isDirectory: false), - arguments: ["--create", "--gzip", "--file", destinationPath, sourcePath] + arguments: ["--create", "--gzip", "--directory", sourcePath, "--file", destinationPath, "."] ) { process in let terminationReason = process.terminationReason let terminationStatus = process.terminationStatus diff --git a/Sources/Testing/Running/Configuration.swift b/Sources/Testing/Running/Configuration.swift index 1ae019c95..786856e10 100644 --- a/Sources/Testing/Running/Configuration.swift +++ b/Sources/Testing/Running/Configuration.swift @@ -8,8 +8,6 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -private import _TestingInternals - /// A type containing settings for preparing and running tests. @_spi(ForToolsIntegrationOnly) public struct Configuration: Sendable { diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index d6c1496c1..74713592b 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -263,11 +263,14 @@ struct AttachmentTests { #if !SWT_NO_PROCESS_SPAWNING @Test func attachContentsOfDirectoryURL() async throws { - let temporaryFileName = UUID().uuidString - let temporaryPath = try appendPathComponent(temporaryFileName, to: temporaryDirectory()) + 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 @@ -275,7 +278,7 @@ struct AttachmentTests { return } - #expect(attachment.preferredName == "\(temporaryFileName).tar.gz") + #expect(attachment.preferredName == "\(temporaryDirectoryName).tar.gz") valueAttached() } From a810dbf3230d3954c787fef0bcb8700aee43d92b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sat, 2 Nov 2024 22:29:42 -0400 Subject: [PATCH 13/27] Fix rebase glitch --- Tests/TestingTests/AttachmentTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 74713592b..eb611a831 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -9,6 +9,7 @@ // @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing +private import _TestingInternals #if canImport(Foundation) import Foundation @_spi(Experimental) import _Testing_Foundation From b3345297840d8a872a7aaa2dd2caf1bf425ad951 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 3 Nov 2024 11:25:26 -0500 Subject: [PATCH 14/27] Remove nonfunctional reserved-file check in a test --- Tests/TestingTests/AttachmentTests.swift | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index eb611a831..ff3d64124 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -104,16 +104,8 @@ struct AttachmentTests { #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: [ @@ -121,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 = Test.Attachment(attachableValue, named: name) From afdf317d96e8ecac427c846214176010b13b54af Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 3 Nov 2024 11:42:19 -0500 Subject: [PATCH 15/27] Consistently apply .tgz to the preferred name of a compressed directory --- .../Attachments/Test.Attachment+URL.swift | 57 +++++++++++++------ Tests/TestingTests/AttachmentTests.swift | 6 +- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift index 08affc7aa..aaa031c3e 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift @@ -12,6 +12,10 @@ @_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`. @@ -30,6 +34,15 @@ extension URL { // MARK: - Attaching files +#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 { /// Initialize an instance of this type with the contents of the given URL. @@ -57,25 +70,33 @@ extension Test.Attachment { let url = url.resolvingSymlinksInPath() let isDirectory = try url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory! - let attachableValue: any Test.Attachable & Sendable - if isDirectory { - attachableValue = try await _DirectoryContentAttachableProxy(contentsOfDirectoryAt: url) + // 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 { - // Load the file. - attachableValue = try Data(contentsOf: url, options: [.mappedIfSafe]) + Self.defaultPreferredName } - // Determine the preferred name of the attachment if one was not provided. - var preferredName = preferredName - if preferredName == nil, case let lastPathComponent = url.lastPathComponent, !lastPathComponent.isEmpty { - if isDirectory { - preferredName = (lastPathComponent as NSString).appendingPathExtension("tar.gz") - } else { - preferredName = lastPathComponent - } - } + 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 + }() - self.init(attachableValue, named: preferredName, sourceLocation: sourceLocation) + let attachableValue = try await _DirectoryContentAttachableProxy(contentsOfDirectoryAt: url) + self.init(attachableValue, named: preferredName, sourceLocation: sourceLocation) + } else { + // Load the file. + let attachableValue = try Data(contentsOf: url, options: [.mappedIfSafe]) + self.init(attachableValue, named: preferredName, sourceLocation: sourceLocation) + } } } @@ -101,12 +122,12 @@ private struct _DirectoryContentAttachableProxy: Test.Attachable { /// directories cannot be compressed on this platform. /// /// This initializer asynchronously compresses the contents of `directoryURL` - /// into an archive (currently of `.tar.gz` format, although this is subject - /// to change) and stores a mapped copy of that archive. + /// into an archive (currently of `.tgz` format, although this is subject to + /// change) and stores a mapped copy of that archive. init(contentsOfDirectoryAt directoryURL: URL) async throws { url = directoryURL - let temporaryName = "\(UUID().uuidString).tar.gz" + let temporaryName = "\(UUID().uuidString).tgz" let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(temporaryName) #if !SWT_NO_PROCESS_SPAWNING diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index ff3d64124..d038bf3c8 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 = Test.Attachment(attachableValue, named: "loremipsum.tar.gz.gif.jpeg.html") + let attachment = Test.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. @@ -98,7 +98,7 @@ 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) } @@ -271,7 +271,7 @@ struct AttachmentTests { return } - #expect(attachment.preferredName == "\(temporaryDirectoryName).tar.gz") + #expect(attachment.preferredName == "\(temporaryDirectoryName).tgz") valueAttached() } From 18d52d5a59d842a7862ddb8331350e0b23c106e2 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 4 Nov 2024 13:43:55 -0500 Subject: [PATCH 16/27] Customize documentation for protocol conformances of Encodable and NSSecureCoding --- .../Encodable+Test.Attachable.swift | 32 +++++++++++++++++++ .../NSSecureCoding+Test.Attachable.swift | 29 +++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Encodable+Test.Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Encodable+Test.Attachable.swift index 39f3e39e5..e8dce086a 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Encodable+Test.Attachable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Encodable+Test.Attachable.swift @@ -17,6 +17,38 @@ private import Foundation // protocol for types that already support Codable. @_spi(Experimental) extension Encodable where Self: Test.Attachable { + /// 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. + /// + /// - 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 { let format = try EncodingFormat(for: attachment) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/NSSecureCoding+Test.Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/NSSecureCoding+Test.Attachable.swift index 535319bc3..25aeb12df 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/NSSecureCoding+Test.Attachable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/NSSecureCoding+Test.Attachable.swift @@ -17,6 +17,35 @@ public import Foundation // NSKeyedArchiver for encoding. @_spi(Experimental) extension NSSecureCoding where Self: Test.Attachable { + /// 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. + /// + /// - 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) From c52b02de8585d6efeb0334a612cfa51ee666a8e0 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 5 Nov 2024 14:36:26 -0500 Subject: [PATCH 17/27] Make MyContiguousCollectionAttachable sendable --- 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 d038bf3c8..ac72789d2 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -541,7 +541,7 @@ final class MySecureCodingAttachable: NSObject, NSSecureCoding, Test.Attachable, } } -struct MyContiguousCollectionAttachable: Collection, ContiguousBytes, Test.Attachable { +struct MyContiguousCollectionAttachable: Collection, ContiguousBytes, Test.Attachable, Sendable { private var _utf8: String.UTF8View var string: String { From 3b68c328046402f34dc88ae36aab0923bdb20fa4 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 5 Nov 2024 14:50:44 -0500 Subject: [PATCH 18/27] Add default Attachable implementation for types that conform to BOTH coding protocols --- ... => Test.Attachable+ContiguousBytes.swift} | 2 +- ....Attachable+Encodable&NSSecureCoding.swift | 25 +++++++++++++++++++ ....swift => Test.Attachable+Encodable.swift} | 8 ++++-- ...t => Test.Attachable+NSSecureCoding.swift} | 8 ++++-- Tests/TestingTests/AttachmentTests.swift | 17 +++++++++++++ 5 files changed, 55 insertions(+), 5 deletions(-) rename Sources/Overlays/_Testing_Foundation/Attachments/{ContiguousBytes+Test.Attachable.swift => Test.Attachable+ContiguousBytes.swift} (91%) create mode 100644 Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift rename Sources/Overlays/_Testing_Foundation/Attachments/{Encodable+Test.Attachable.swift => Test.Attachable+Encodable.swift} (89%) rename Sources/Overlays/_Testing_Foundation/Attachments/{NSSecureCoding+Test.Attachable.swift => Test.Attachable+NSSecureCoding.swift} (89%) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/ContiguousBytes+Test.Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+ContiguousBytes.swift similarity index 91% rename from Sources/Overlays/_Testing_Foundation/Attachments/ContiguousBytes+Test.Attachable.swift rename to Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+ContiguousBytes.swift index c908662b2..2720cc5b0 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/ContiguousBytes+Test.Attachable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+ContiguousBytes.swift @@ -13,7 +13,7 @@ public import Foundation @_spi(Experimental) -extension ContiguousBytes where Self: Test.Attachable { +extension Test.Attachable where Self: ContiguousBytes { public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } 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..56e17ee3f --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift @@ -0,0 +1,25 @@ +// +// 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 { + func open(_ value: borrowing some Encodable & Test.Attachable) throws -> R { + return try value.withUnsafeBufferPointer(for: attachment, body) + } + return try open(self) + } +} +#endif diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Encodable+Test.Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable.swift similarity index 89% rename from Sources/Overlays/_Testing_Foundation/Attachments/Encodable+Test.Attachable.swift rename to Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable.swift index e8dce086a..4f8d71595 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Encodable+Test.Attachable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable.swift @@ -16,7 +16,7 @@ private import Foundation // encoding to JSON. This lets developers provide trivial conformance to the // protocol for types that already support Codable. @_spi(Experimental) -extension Encodable where Self: Test.Attachable { +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. @@ -43,7 +43,11 @@ extension Encodable where Self: Test.Attachable { /// | `".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. + /// 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 diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/NSSecureCoding+Test.Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+NSSecureCoding.swift similarity index 89% rename from Sources/Overlays/_Testing_Foundation/Attachments/NSSecureCoding+Test.Attachable.swift rename to Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+NSSecureCoding.swift index 25aeb12df..f927b67f1 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/NSSecureCoding+Test.Attachable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+NSSecureCoding.swift @@ -16,7 +16,7 @@ public import Foundation // NSSecureCoding-conformant classes by default. The implementation uses // NSKeyedArchiver for encoding. @_spi(Experimental) -extension NSSecureCoding where Self: Test.Attachable { +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. /// @@ -41,7 +41,11 @@ extension NSSecureCoding where Self: Test.Attachable { /// | `".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. + /// 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 diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index ac72789d2..7f4c8e061 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -541,6 +541,23 @@ final class MySecureCodingAttachable: NSObject, NSSecureCoding, Test.Attachable, } } +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) ?? "" + } +} + + struct MyContiguousCollectionAttachable: Collection, ContiguousBytes, Test.Attachable, Sendable { private var _utf8: String.UTF8View From 44439398eecaf60f6bf84eda7bacf0580752b5de Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 5 Nov 2024 14:51:10 -0500 Subject: [PATCH 19/27] Remove ContiguousBytes implementation for limited utility --- .../Test.Attachable+ContiguousBytes.swift | 21 ------------------- .../Testing/Attachments/Test.Attachable.swift | 3 --- 2 files changed, 24 deletions(-) delete mode 100644 Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+ContiguousBytes.swift diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+ContiguousBytes.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+ContiguousBytes.swift deleted file mode 100644 index 2720cc5b0..000000000 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+ContiguousBytes.swift +++ /dev/null @@ -1,21 +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 -// - -#if canImport(Foundation) -@_spi(Experimental) public import Testing -public import Foundation - -@_spi(Experimental) -extension Test.Attachable where Self: ContiguousBytes { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try withUnsafeBytes(body) - } -} -#endif diff --git a/Sources/Testing/Attachments/Test.Attachable.swift b/Sources/Testing/Attachments/Test.Attachable.swift index 73ef34a14..6a9c2597b 100644 --- a/Sources/Testing/Attachments/Test.Attachable.swift +++ b/Sources/Testing/Attachments/Test.Attachable.swift @@ -115,9 +115,6 @@ extension Test.Attachable where Self: Collection, Element == UInt8 { // collection can provide contiguous storage (_HasContiguousBytes is not API.) // If withContiguousBytesIfAvailable(_:) fails, we don't want to make a // (potentially expensive!) copy of the collection. - // - // The Foundation cross-import overlay provides a default implementation for - // collection types that conform to Foundation's ContiguousBytes protocol. } extension Test.Attachable where Self: StringProtocol { From 3feb34237a1ff08666a00d2d512cd0e627ee13e2 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 5 Nov 2024 14:52:12 -0500 Subject: [PATCH 20/27] Fix a typo --- Sources/Testing/Attachments/Test.Attachable.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/Attachments/Test.Attachable.swift b/Sources/Testing/Attachments/Test.Attachable.swift index 6a9c2597b..b7f6694eb 100644 --- a/Sources/Testing/Attachments/Test.Attachable.swift +++ b/Sources/Testing/Attachments/Test.Attachable.swift @@ -113,7 +113,7 @@ extension Test.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. } From 311c1c972c45b653d721a064e3bf6edc73d13f1c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 5 Nov 2024 15:01:20 -0500 Subject: [PATCH 21/27] Remove ContiguousBytes test --- Tests/TestingTests/AttachmentTests.swift | 46 ------------------------ 1 file changed, 46 deletions(-) diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 7f4c8e061..c3369313a 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -463,11 +463,6 @@ extension AttachmentTests { let value = try #require("abc123".data(using: .utf8)) try test(value) } - - @Test func contiguousBytesCollection() throws { - let value = MyContiguousCollectionAttachable(string: "abc123") - try test(value) - } #endif } } @@ -556,45 +551,4 @@ final class MyCodableAndSecureCodingAttachable: NSObject, Codable, NSSecureCodin string = (coder.decodeObject(of: NSString.self, forKey: "string") as? String) ?? "" } } - - -struct MyContiguousCollectionAttachable: Collection, ContiguousBytes, Test.Attachable, Sendable { - private var _utf8: String.UTF8View - - var string: String { - get { - String(_utf8) - } - set { - _utf8 = newValue.utf8 - } - } - - init(string: String) { - _utf8 = string.utf8 - } - - var startIndex: String.UTF8View.Index { - _utf8.startIndex - } - - var endIndex: String.UTF8View.Index { - _utf8.endIndex - } - - subscript(position: String.UTF8View.Index) -> String.UTF8View.Element { - _utf8[position] - } - - func index(after i: String.UTF8View.Index) -> String.UTF8View.Index { - _utf8.index(after: i) - } - - func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { - let result = try _utf8.withContiguousStorageIfAvailable { buffer in - try body(.init(buffer)) - } - return try result ?? Array(_utf8).withUnsafeBytes(body) - } -} #endif From 9ffc49a16a9acdc5c07d61c0ec0a9400e3fdc540 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 5 Nov 2024 15:36:02 -0500 Subject: [PATCH 22/27] Minor cleanup --- .../_Testing_Foundation/Attachments/Test.Attachment+URL.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift index aaa031c3e..2aa4c4f08 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift @@ -32,8 +32,6 @@ extension URL { } } -// MARK: - Attaching files - #if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers) @available(_uttypesAPI, *) extension UTType { From bb222803737dfefac0fc690394ccad81cc298031 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 5 Nov 2024 16:02:22 -0500 Subject: [PATCH 23/27] Work around compiler crash --- .../Attachments/Test.Attachable+Encodable&NSSecureCoding.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift index 56e17ee3f..d36a186f4 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift @@ -16,7 +16,7 @@ public import Foundation extension Test.Attachable where Self: Encodable & NSSecureCoding { @_documentation(visibility: private) public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - func open(_ value: borrowing some Encodable & Test.Attachable) throws -> R { + func open(_ value: some Encodable & Test.Attachable) throws -> R { return try value.withUnsafeBufferPointer(for: attachment, body) } return try open(self) From 184774b3c8a483849a341959a9dc7d15ced2b179 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 5 Nov 2024 16:07:08 -0500 Subject: [PATCH 24/27] Work around another way? --- .../Test.Attachable+Encodable&NSSecureCoding.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift index d36a186f4..5b43209cd 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift @@ -16,10 +16,10 @@ public import Foundation extension Test.Attachable where Self: Encodable & NSSecureCoding { @_documentation(visibility: private) public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - func open(_ value: some Encodable & Test.Attachable) throws -> R { + func open(_ value: some Encodable & Test.Attachable, for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { return try value.withUnsafeBufferPointer(for: attachment, body) } - return try open(self) + return try open(self, for: attachment, body) } } #endif From 950fa6e9215dcfef472f5bf4a3ca6d9b75017e67 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Nov 2024 16:34:54 -0500 Subject: [PATCH 25/27] Update to use generic Test.Attachment --- .../Attachments/Data+Test.Attachable.swift | 2 +- .../Attachments/EncodingFormat.swift | 2 +- ....Attachable+Encodable&NSSecureCoding.swift | 7 +-- .../Test.Attachable+Encodable.swift | 60 +++++++++++------- .../Test.Attachable+NSSecureCoding.swift | 2 +- .../Attachments/Test.Attachment+URL.swift | 62 +++++++++++-------- Tests/TestingTests/AttachmentTests.swift | 27 ++++---- 7 files changed, 96 insertions(+), 66 deletions(-) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Data+Test.Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Test.Attachable.swift index e3e2c2651..4c771562e 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Data+Test.Attachable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Test.Attachable.swift @@ -14,7 +14,7 @@ public import Foundation @_spi(Experimental) extension Data: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + public func withUnsafeBufferPointer(for attachment: borrowing Test.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 9b86734f9..fb3a85413 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift @@ -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 Test.Attachment) throws { let ext = (attachment.preferredName as NSString).pathExtension #if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift index 5b43209cd..4b1b95f22 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable&NSSecureCoding.swift @@ -15,11 +15,8 @@ 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 { - func open(_ value: some Encodable & Test.Attachable, for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - return try value.withUnsafeBufferPointer(for: attachment, body) - } - return try open(self, for: attachment, body) + 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 index 4f8d71595..0bafb011e 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+Encodable.swift @@ -12,6 +12,43 @@ @_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. @@ -53,27 +90,8 @@ 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 { - 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(self) - 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(self) - } - - return try data.withUnsafeBytes(body) + 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 index f927b67f1..6a79a37ff 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachable+NSSecureCoding.swift @@ -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 Test.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/Test.Attachment+URL.swift index 2aa4c4f08..fd9776b2d 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift @@ -42,7 +42,7 @@ extension UTType { #endif @_spi(Experimental) -extension Test.Attachment { +extension Test.Attachment where AttachableValue == FileAttachment { /// Initialize an instance of this type with the contents of the given URL. /// /// - Parameters: @@ -55,8 +55,7 @@ extension Test.Attachment { /// - Throws: Any error that occurs attempting to read from `url`. public init( contentsOf url: URL, - named preferredName: String? = nil, - sourceLocation: SourceLocation = #_sourceLocation + named preferredName: String? = nil ) async throws { guard url.isFileURL else { // TODO: network URLs? @@ -88,30 +87,39 @@ extension Test.Attachment { return (preferredName as NSString).appendingPathExtension("tgz") ?? preferredName }() - let attachableValue = try await _DirectoryContentAttachableProxy(contentsOfDirectoryAt: url) - self.init(attachableValue, named: preferredName, sourceLocation: sourceLocation) + try await self.init(FileAttachment(compressedContentsOfDirectoryAt: url), named: preferredName) } else { // Load the file. - let attachableValue = try Data(contentsOf: url, options: [.mappedIfSafe]) - self.init(attachableValue, named: preferredName, sourceLocation: sourceLocation) + try self.init(FileAttachment(contentsOfFileAt: url), named: preferredName) } } } -// MARK: - Attaching directories +// MARK: - Attaching files and directories -/// A type representing the content of a directory as an attachable value. -private struct _DirectoryContentAttachableProxy: Test.Attachable { - /// The URL of the directory. - /// - /// The contents of this directory may change after this instance is - /// initialized. Such changes are not tracked. - var url: URL +/// A type representing a file system object that has been added to an +/// attachment. +@_spi(Experimental) +public struct FileAttachment: Test.Attachable, Sendable { + /// The file URL where the represented file system object is located. + public private(set) var url: URL - /// The archived contents of the directory. - private let _directoryContent: Data + /// The contents of the file or directory at `url`. + private var _data: Data + + /// Initialize an instance of this type by reading the contents of a file. + /// + /// - Parameters: + /// - fileURL: A URL referring to the file to attach. + /// + /// - Throws: Any error encountered trying to read the file at `fileURL`. + init(contentsOfFileAt fileURL: URL) throws { + url = fileURL + _data = try Data(contentsOf: url, options: [.mappedIfSafe]) + } - /// Initialize an instance of this type. + /// Initialize an instance of this type by compressing the contents of a + /// directory. /// /// - Parameters: /// - directoryURL: A URL referring to the directory to attach. @@ -122,9 +130,7 @@ private struct _DirectoryContentAttachableProxy: Test.Attachable { /// 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(contentsOfDirectoryAt directoryURL: URL) async throws { - url = directoryURL - + init(compressedContentsOfDirectoryAt directoryURL: URL) async throws { let temporaryName = "\(UUID().uuidString).tgz" let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(temporaryName) @@ -134,7 +140,7 @@ private struct _DirectoryContentAttachableProxy: Test.Attachable { #else let tarPath = "/usr/bin/tar" #endif - let sourcePath = url.fileSystemPath + let sourcePath = directoryURL.fileSystemPath let destinationPath = temporaryURL.fileSystemPath defer { remove(destinationPath) @@ -161,14 +167,20 @@ private struct _DirectoryContentAttachableProxy: Test.Attachable { continuation.resume(throwing: error) } } - _directoryContent = try Data(contentsOf: temporaryURL, options: [.mappedIfSafe]) + + url = directoryURL + _data = try Data(contentsOf: temporaryURL, options: [.mappedIfSafe]) #else throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "This platform does not support attaching directories to tests."]) #endif } - func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try _directoryContent.withUnsafeBytes(body) + public var estimatedAttachmentByteCount: Int? { + _data.count + } + + public func withUnsafeBufferPointer(for attachment: borrowing Testing.Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try _data.withUnsafeBytes(body) } } #endif diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index c3369313a..86cdbea39 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -234,13 +234,13 @@ struct AttachmentTests { await confirmation("Attachment detected") { valueAttached in var configuration = Configuration() configuration.eventHandler = { event, _ in - guard case let .valueAttached(attachment) = event.kind else { + guard case let .valueAttached(attachment, _) = event.kind else { return } #expect(attachment.preferredName == temporaryFileName) #expect(throws: Never.self) { - try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { buffer in + try attachment.withUnsafeBufferPointer { buffer in #expect(buffer.count == data.count) } } @@ -267,7 +267,7 @@ struct AttachmentTests { await confirmation("Attachment detected") { valueAttached in var configuration = Configuration() configuration.eventHandler = { event, _ in - guard case let .valueAttached(attachment) = event.kind else { + guard case let .valueAttached(attachment, _) = event.kind else { return } @@ -357,19 +357,22 @@ struct AttachmentTests { name = "\(name).\(ext)" } - var attachment: Test.Attachment + 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") - attachment = Test.Attachment(attachableValue, named: name) + let attachment = Test.Attachment(attachableValue, named: name) + try open(attachment) } else { let attachableValue = MyCodableAttachable(string: "stringly speaking") - attachment = Test.Attachment(attachableValue, named: name) - } - - 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") + let attachment = Test.Attachment(attachableValue, named: name) + try open(attachment) } } From d039ea0824441b2b7da756b1c4ab0389a9eef35a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Nov 2024 17:04:03 -0500 Subject: [PATCH 26/27] Don't introduce a new public type for file attachments --- .../Attachments/Test.Attachment+URL.swift | 43 +++---------------- 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift index fd9776b2d..5f56ad727 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Test.Attachment+URL.swift @@ -42,7 +42,7 @@ extension UTType { #endif @_spi(Experimental) -extension Test.Attachment where AttachableValue == FileAttachment { +extension Test.Attachment where AttachableValue == Data { /// Initialize an instance of this type with the contents of the given URL. /// /// - Parameters: @@ -87,37 +87,17 @@ extension Test.Attachment where AttachableValue == FileAttachment { return (preferredName as NSString).appendingPathExtension("tgz") ?? preferredName }() - try await self.init(FileAttachment(compressedContentsOfDirectoryAt: url), named: preferredName) + try await self.init(Data(compressedContentsOfDirectoryAt: url), named: preferredName) } else { // Load the file. - try self.init(FileAttachment(contentsOfFileAt: url), named: preferredName) + try self.init(Data(contentsOf: url, options: [.mappedIfSafe]), named: preferredName) } } } -// MARK: - Attaching files and directories - -/// A type representing a file system object that has been added to an -/// attachment. -@_spi(Experimental) -public struct FileAttachment: Test.Attachable, Sendable { - /// The file URL where the represented file system object is located. - public private(set) var url: URL - - /// The contents of the file or directory at `url`. - private var _data: Data - - /// Initialize an instance of this type by reading the contents of a file. - /// - /// - Parameters: - /// - fileURL: A URL referring to the file to attach. - /// - /// - Throws: Any error encountered trying to read the file at `fileURL`. - init(contentsOfFileAt fileURL: URL) throws { - url = fileURL - _data = try Data(contentsOf: url, options: [.mappedIfSafe]) - } +// MARK: - Attaching directories +extension Data { /// Initialize an instance of this type by compressing the contents of a /// directory. /// @@ -143,7 +123,7 @@ public struct FileAttachment: Test.Attachable, Sendable { let sourcePath = directoryURL.fileSystemPath let destinationPath = temporaryURL.fileSystemPath defer { - remove(destinationPath) + try? FileManager().removeItem(at: temporaryURL) } try await withCheckedThrowingContinuation { continuation in @@ -168,20 +148,11 @@ public struct FileAttachment: Test.Attachable, Sendable { } } - url = directoryURL - _data = try Data(contentsOf: temporaryURL, options: [.mappedIfSafe]) + try self.init(contentsOf: temporaryURL, options: [.mappedIfSafe]) #else throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "This platform does not support attaching directories to tests."]) #endif } - - public var estimatedAttachmentByteCount: Int? { - _data.count - } - - public func withUnsafeBufferPointer(for attachment: borrowing Testing.Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try _data.withUnsafeBytes(body) - } } #endif #endif From df272e7f2f3419819f793cdb0d75661e29a47221 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 10 Nov 2024 15:19:27 -0500 Subject: [PATCH 27/27] Restore tests --- .../Testing/Attachments/Test.Attachment.swift | 2 +- Tests/TestingTests/AttachmentTests.swift | 43 ++++++++++--------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 5f49f4333..b3b6ada93 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -85,7 +85,7 @@ extension Test.Attachment where AttachableValue == Test.AnyAttachable { /// /// - Parameters: /// - attachment: The attachment to type-erase. - fileprivate init(_ attachment: Test.Attachment) { + package init(_ attachment: Test.Attachment) { self.init( _attachableValue: Test.AnyAttachable(attachableValue: attachment.attachableValue), fileSystemPath: attachment.fileSystemPath, diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 86cdbea39..24f0c6944 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -10,6 +10,7 @@ @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing private import _TestingInternals + #if canImport(Foundation) import Foundation @_spi(Experimental) import _Testing_Foundation @@ -82,7 +83,7 @@ struct AttachmentTests { @Test func writeAttachmentWithMultiplePathExtensions() throws { let attachableValue = MySendableAttachable(string: "") - let attachment = Test.Attachment(attachableValue, named: "loremipsum.tgz.gif.jpeg.html") + let attachment = Test.Attachment(attachableValue, named: "loremipsum.tar.gz.gif.jpeg.html") // Write the attachment to disk once to ensure the original filename is not // available and we add a suffix. @@ -98,14 +99,22 @@ struct AttachmentTests { remove(filePath) } let fileName = try #require(filePath.split { $0 == "/" || $0 == #"\"# }.last) - #expect(fileName == "loremipsum-\(suffix).tgz.gif.jpeg.html") + #expect(fileName == "loremipsum-\(suffix).tar.gz.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: [ @@ -113,7 +122,7 @@ struct AttachmentTests { String(repeating: "a", count: maximumNameCount), String(repeating: "a", count: maximumNameCount + 1), String(repeating: "a", count: maximumNameCount + 2), - ]) func writeAttachmentWithBadName(name: String) throws { + ] + reservedNames) func writeAttachmentWithBadName(name: String) throws { let attachableValue = MySendableAttachable(string: "") let attachment = Test.Attachment(attachableValue, named: name) @@ -240,7 +249,7 @@ struct AttachmentTests { #expect(attachment.preferredName == temporaryFileName) #expect(throws: Never.self) { - try attachment.withUnsafeBufferPointer { buffer in + try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { buffer in #expect(buffer.count == data.count) } } @@ -357,22 +366,21 @@ struct AttachmentTests { 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") - } - } - + let attachmentCopy: Test.Attachment if args.forSecureCoding { let attachableValue = MySecureCodingAttachable(string: "stringly speaking") let attachment = Test.Attachment(attachableValue, named: name) - try open(attachment) + attachmentCopy = Test.Attachment(attachment) } else { let attachableValue = MyCodableAttachable(string: "stringly speaking") let attachment = Test.Attachment(attachableValue, named: name) - try open(attachment) + attachmentCopy = Test.Attachment(attachment) + } + + try attachmentCopy.withUnsafeBufferPointer { bytes in + #expect(bytes.first == args.firstCharacter.asciiValue) + let decodedStringValue = try args.decode(Data(bytes)) + #expect(decodedStringValue == "stringly speaking") } } @@ -460,13 +468,6 @@ 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 } }