diff --git a/Documentation/Proposals/0005-ranged-confirmations.md b/Documentation/Proposals/0005-ranged-confirmations.md index 7ba133c9f..fef9d7675 100644 --- a/Documentation/Proposals/0005-ranged-confirmations.md +++ b/Documentation/Proposals/0005-ranged-confirmations.md @@ -65,7 +65,7 @@ A new overload of `confirmation()` is added: /// - comment: An optional comment to apply to any issues generated by this /// function. /// - expectedCount: A range of integers indicating the number of times the -/// expected event should occur when `body` is invoked. +/// expected event should occur when `body` is invoked. /// - isolation: The actor to which `body` is isolated, if any. /// - sourceLocation: The source location to which any recorded issues should /// be attributed. @@ -93,7 +93,7 @@ A new overload of `confirmation()` is added: /// let minBuns = 5 /// let maxBuns = 10 /// await confirmation( -/// "Baked between \(minBuns) and \(maxBuns) buns", +/// "Baked between \(minBuns) and \(maxBuns) buns", /// expectedCount: minBuns ... maxBuns /// ) { bunBaked in /// foodTruck.eventHandler = { event in diff --git a/Package.swift b/Package.swift index f7fb5dc5f..202b09e1f 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/ABI/v0/Encoded/ABIv0.EncodedAttachment.swift b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedAttachment.swift index 525f8718f..29ea5e414 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/Attachments/Test.Attachable.swift b/Sources/Testing/Attachments/Test.Attachable.swift index 0053bec62..83a7dc66c 100644 --- a/Sources/Testing/Attachments/Test.Attachable.swift +++ b/Sources/Testing/Attachments/Test.Attachable.swift @@ -15,14 +15,18 @@ 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 /// 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. @@ -61,7 +65,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 +107,28 @@ 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 { - try body(.init(self)) - } -} - -@_spi(Experimental) -extension UnsafeMutableBufferPointer: Test.Attachable { - 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 { - try body(self) - } -} - -@_spi(Experimental) -extension UnsafeMutableRawBufferPointer: Test.Attachable { - 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 +138,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.AttachableContainer.swift b/Sources/Testing/Attachments/Test.AttachableContainer.swift new file mode 100644 index 000000000..7eb2de3ba --- /dev/null +++ b/Sources/Testing/Attachments/Test.AttachableContainer.swift @@ -0,0 +1,37 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +@_spi(Experimental) +extension Test { + /// A protocol describing a type that can be attached to a test report or + /// written to disk when a test is run 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 } + } +} diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 36da9f8c6..f4817a211 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -20,30 +20,9 @@ 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 - - /// 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 struct Attachment: ~Copyable where AttachableValue: Test.Attachable & ~Copyable { + /// Storage for ``attachableValue-7dyjv``. + fileprivate var _attachableValue: AttachableValue /// The path to which the this attachment was written, if any. /// @@ -72,18 +51,25 @@ extension Test { /// will attempt to generate its own value. public var preferredName: String - /// The source location where the attachment was initialized. + /// The source location of this instance. + /// + /// This property is not part of the public interface of the testing + /// library. It is initially set when the attachment is created and is + /// updated later when the attachment is attached to something. /// /// The value of this property is used when recording issues associated with /// the attachment. - public var sourceLocation: SourceLocation + 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: ~Copyable { /// Initialize an instance of this type that encloses the given attachable /// value. /// @@ -96,95 +82,182 @@ extension Test.Attachment { /// - 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, sourceLocation: SourceLocation = #_sourceLocation) { + self._attachableValue = attachableValue + self.preferredName = preferredName ?? Self.defaultPreferredName + self.sourceLocation = sourceLocation } -#endif +} - /// Attach this instance to the current test. +@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +extension Test.Attachment where AttachableValue == Test.AnyAttachable { + /// 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: Test.AnyAttachable(attachableValue: attachment.attachableValue), + fileSystemPath: attachment.fileSystemPath, + preferredName: attachment.preferredName, + sourceLocation: attachment.sourceLocation + ) } } +#endif + +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(_:)``. 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(_:)`, otherwise it would be declared private. + /// } + @_spi(Experimental) @_spi(ForToolsIntegrationOnly) + public struct AnyAttachable: Test.AttachableContainer, Copyable, Sendable { +#if !SWT_NO_LAZY_ATTACHMENTS + public typealias AttachableValue = any Test.Attachable & Sendable /* & Copyable rdar://137614425 */ +#else + public typealias AttachableValue = [UInt8] +#endif -// MARK: - Non-sendable and move-only attachments + public var attachableValue: AttachableValue -/// 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]() + init(attachableValue: AttachableValue) { + self.attachableValue = attachableValue + } - var estimatedAttachmentByteCount: Int? + public var estimatedAttachmentByteCount: Int? { + attachableValue.estimatedAttachmentByteCount + } - func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try encodedValue.withUnsafeBufferPointer(for: attachment, 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, + sourceLocation: attachment.sourceLocation + ) + return try temporaryAttachment.withUnsafeBufferPointer(body) + } + return try open(attachableValue, for: attachment) + } } } -extension Test.Attachment { - /// Initialize an instance of this type that encloses the given attachable - /// value. +// 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. /// - /// - 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. + /// 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: /// - /// 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. + /// ```swift + /// let attachableValue = attachment.attachableValue as T + /// ``` + public var attachableValue: AttachableValue.AttachableValue { + _read { + yield attachableValue.attachableValue + } + } +} + +// MARK: - Attaching an attachment to a test (etc.) + #if !SWT_NO_LAZY_ATTACHMENTS - @_disfavoredOverload +extension Test.Attachment where AttachableValue: 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) { + var attachmentCopy = Test.Attachment(self) + attachmentCopy.sourceLocation = sourceLocation + Event.post(.valueAttached(attachmentCopy)) + } +} #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. + +extension Test.Attachment where AttachableValue: ~Copyable { + /// Attach this instance to the current test. + /// + /// - Parameters: + /// - sourceLocation: The source location of the call to this function. + /// + /// 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 withUnsafeBufferPointer { buffer in + let attachableContainer = Test.AnyAttachable(attachableValue: Array(buffer)) + return Test.Attachment(_attachableValue: attachableContainer, fileSystemPath: fileSystemPath, preferredName: preferredName, sourceLocation: sourceLocation) } - proxyAttachable.estimatedAttachmentByteCount = proxyAttachable.encodedValue.count + Event.post(.valueAttached(attachmentCopy)) } 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() } + } +} + +// MARK: - Getting the serialized form of an attachable value (generically) - self.init(_attachableValue: proxyAttachable, preferredName: preferredName, sourceLocation: sourceLocation) +extension Test.Attachment where AttachableValue: ~Copyable { + /// Call a function and pass a buffer representing the value of this + /// instance's ``attachableValue-7dyjv`` 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-7dyjv`` property. + @inlinable public borrowing func withUnsafeBufferPointer(_ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try attachableValue.withUnsafeBufferPointer(for: self, body) } } #if !SWT_NO_FILE_IO // MARK: - Writing -extension Test.Attachment { +extension Test.Attachment where AttachableValue: ~Copyable { /// Write the attachment's contents to a file in the specified directory. /// /// - Parameters: @@ -210,8 +283,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 +311,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,7 +351,9 @@ extension Test.Attachment { } } - try attachableValue.withUnsafeBufferPointer(for: self) { buffer in + // There should be no code path that leads to this call where the attachable + // value is nil. + try withUnsafeBufferPointer { buffer in try file!.write(buffer) } @@ -296,15 +371,17 @@ 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 { @@ -313,16 +390,23 @@ extension Configuration { 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) + + // Update the event before returning and continuing to handle it. event.kind = .valueAttached(attachment) + return true + } catch { + // Record the error as an issue and suppress the event. + let sourceContext = SourceContext(backtrace: .current(), sourceLocation: attachment.sourceLocation) + Issue(kind: .valueAttachmentFailed(error), comments: [], sourceContext: sourceContext).record(configuration: self) + return false } } } diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 12efce2b2..696a3d9d0 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -22,6 +22,7 @@ add_library(Testing ABI/v0/Encoded/ABIv0.EncodedMessage.swift ABI/v0/Encoded/ABIv0.EncodedTest.swift Attachments/Test.Attachable.swift + Attachments/Test.AttachableContainer.swift Attachments/Test.Attachment.swift Events/Clock.swift Events/Event.swift diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index 54f4eea31..d7813e1c6 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -103,7 +103,7 @@ public struct Event: Sendable { /// - Parameters: /// - attachment: The attachment that was created. @_spi(Experimental) - indirect case valueAttached(_ attachment: Test.Attachment) + indirect case valueAttached(_ attachment: Test.Attachment) /// A test ended. /// diff --git a/Sources/Testing/Issues/Confirmation.swift b/Sources/Testing/Issues/Confirmation.swift index 28621e404..04fbfa766 100644 --- a/Sources/Testing/Issues/Confirmation.swift +++ b/Sources/Testing/Issues/Confirmation.swift @@ -116,7 +116,7 @@ public func confirmation( /// - comment: An optional comment to apply to any issues generated by this /// function. /// - expectedCount: A range of integers indicating the number of times the -/// expected event should occur when `body` is invoked. +/// expected event should occur when `body` is invoked. /// - isolation: The actor to which `body` is isolated, if any. /// - sourceLocation: The source location to which any recorded issues should /// be attributed. @@ -144,7 +144,7 @@ public func confirmation( /// let minBuns = 5 /// let maxBuns = 10 /// await confirmation( -/// "Baked between \(minBuns) and \(maxBuns) buns", +/// "Baked between \(minBuns) and \(maxBuns) buns", /// expectedCount: minBuns ... maxBuns /// ) { bunBaked in /// foodTruck.eventHandler = { event in diff --git a/Sources/Testing/Issues/Issue.swift b/Sources/Testing/Issues/Issue.swift index 44943d1b3..68f9bb7b4 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..cd8a87493 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -170,6 +170,7 @@ struct AttachmentTests { } #expect(attachment.preferredName == "loremipsum") + #expect(attachment.sourceLocation.fileID == #fileID) valueAttached() } @@ -189,7 +190,9 @@ struct AttachmentTests { } #expect(attachment.preferredName == "loremipsum") - valueAttached() + #expect(attachment.attachableValue is MySendableAttachable) + #expect(attachment.sourceLocation.fileID == #fileID) + valueAttached() } await Test { @@ -200,15 +203,17 @@ 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 { + if case let .valueAttached(attachment) = event.kind { + #expect(attachment.sourceLocation.fileID == #fileID) valueAttached() } else if case let .issueRecorded(issue) = event.kind, - case let .errorCaught(error) = issue.kind, + case let .valueAttachmentFailed(error) = issue.kind, error is MyError { + #expect(issue.sourceLocation?.fileID == #fileID) issueRecorded() } } @@ -226,10 +231,10 @@ 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 + try attachment.withUnsafeBufferPointer { buffer in #expect(buffer.elementsEqual("abc123".utf8)) #expect(buffer.count == 6) } @@ -250,34 +255,6 @@ extension AttachmentTests { try test(value) } - @Test func uint8UnsafeBufferPointer() throws { - let value: [UInt8] = Array("abc123".utf8) - try value.withUnsafeBufferPointer { value in - try test(value) - } - } - - @Test func uint8UnsafeMutableBufferPointer() throws { - var value: [UInt8] = Array("abc123".utf8) - try value.withUnsafeMutableBufferPointer { value in - try test(value) - } - } - - @Test func unsafeRawBufferPointer() throws { - let value: [UInt8] = Array("abc123".utf8) - try value.withUnsafeBytes { value in - try test(value) - } - } - - @Test func unsafeMutableRawBufferPointer() throws { - var value: [UInt8] = Array("abc123".utf8) - try value.withUnsafeMutableBytes { value in - try test(value) - } - } - @Test func string() throws { let value = "abc123" try test(value) @@ -296,7 +273,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 +291,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 +303,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)) diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index 5963d10bb..d38c36c7e 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -11,7 +11,8 @@ add_compile_options( "SHELL:$<$:-Xfrontend -require-explicit-sendable>") add_compile_options( - "SHELL:$<$:-Xfrontend -enable-experimental-feature -Xfrontend AccessLevelOnImport>") + "SHELL:$<$:-Xfrontend -enable-experimental-feature -Xfrontend AccessLevelOnImport>" + "SHELL:$<$:-Xfrontend -enable-experimental-feature -Xfrontend SuppressedAssociatedTypes>") add_compile_options( "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend ExistentialAny>" "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend InternalImportsByDefault>")