From 573a5c172d7dc001a491d21c5213505db0ed8990 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 6 Nov 2024 15:12:05 -0500 Subject: [PATCH 01/23] 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 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..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 a40bc31e8c7ef662f1a63f69bdfe8125db2ff352 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Nov 2024 16:36:30 -0500 Subject: [PATCH 02/23] 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 7df01e911c73bcd5f48f8aafde660a6eca7e33aa Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Nov 2024 16:48:32 -0500 Subject: [PATCH 03/23] 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 2a100209b70967966ac81e469c63d2c0aba7bc97 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Nov 2024 17:07:13 -0500 Subject: [PATCH 04/23] 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 e71c2ada4464c37f277a6d49a779360bc76ede08 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Nov 2024 17:19:22 -0500 Subject: [PATCH 05/23] 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 5ef993dbd6962046887d3446c530904582a94c30 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sat, 9 Nov 2024 19:50:38 -0500 Subject: [PATCH 06/23] 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 98c96a1766f049b6fd4bd322a8531ef5c33ce5f5 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sat, 9 Nov 2024 19:55:02 -0500 Subject: [PATCH 07/23] 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 e714477aec98e8795853158298853f019a1236eb Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 10 Nov 2024 11:09:37 -0500 Subject: [PATCH 08/23] 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 ffbe17a32b15ac31e788a49205fef9c9932f9ae8 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 10 Nov 2024 14:55:27 -0500 Subject: [PATCH 09/23] 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 33bcb0329e698d35a324c14d6ae484c24fffb12b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 10 Nov 2024 14:57:04 -0500 Subject: [PATCH 10/23] 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 6c90061509ccbd1d7683868c73bfc04a39b4fb62 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 10 Nov 2024 15:27:51 -0500 Subject: [PATCH 11/23] Enable SuppressedAssociatedTypes feature --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 471bbbe95..202b09e1f 100644 --- a/Package.swift +++ b/Package.swift @@ -124,7 +124,7 @@ extension Array where Element == PackageDescription.SwiftSetting { availabilityMacroSettings + [ .unsafeFlags(["-require-explicit-sendable"]), .enableUpcomingFeature("ExistentialAny"), - //.enableExperimentalFeature("SuppressedAssociatedTypes"), + .enableExperimentalFeature("SuppressedAssociatedTypes"), .enableExperimentalFeature("AccessLevelOnImport"), .enableUpcomingFeature("InternalImportsByDefault"), From 1e06f223ac44e84052bba47cff0aaa34cd959b00 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 10 Nov 2024 15:41:38 -0500 Subject: [PATCH 12/23] Move AttachableContainer to its own file --- .../Testing/Attachments/Test.Attachable.swift | 28 ------------- .../Test.AttachableContainer.swift | 40 +++++++++++++++++++ .../Testing/Attachments/Test.Attachment.swift | 12 +++--- Sources/Testing/CMakeLists.txt | 1 + 4 files changed, 47 insertions(+), 34 deletions(-) create mode 100644 Sources/Testing/Attachments/Test.AttachableContainer.swift diff --git a/Sources/Testing/Attachments/Test.Attachable.swift b/Sources/Testing/Attachments/Test.Attachable.swift index 40891bc07..d20c7ffca 100644 --- a/Sources/Testing/Attachments/Test.Attachable.swift +++ b/Sources/Testing/Attachments/Test.Attachable.swift @@ -67,34 +67,6 @@ 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. 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, ~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.AttachableContainer.swift b/Sources/Testing/Attachments/Test.AttachableContainer.swift new file mode 100644 index 000000000..72d1d8424 --- /dev/null +++ b/Sources/Testing/Attachments/Test.AttachableContainer.swift @@ -0,0 +1,40 @@ +// +// 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. 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, ~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 5f49f4333..5315cce9f 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -25,7 +25,7 @@ extension Test { /// type can only be created with attachable values that conform to /// ``Test/Attachable``. public struct Attachment: ~Copyable where AttachableValue: Test.Attachable & ~Copyable { - /// Storage for ``attachableValue-29ppv``. + /// Storage for ``attachableValue-7dyjv``. fileprivate var _attachableValue: AttachableValue /// The path to which the this attachment was written, if any. @@ -118,7 +118,7 @@ extension Test { public var attachableValue: AttachableValue - fileprivate init(attachableValue: AttachableValue) { + init(attachableValue: AttachableValue) { self.attachableValue = attachableValue } @@ -207,8 +207,8 @@ 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) - return Test.Attachment(attachmentCopy) + let attachableContainer = Test.AnyAttachable(attachableValue: Array(buffer)) + return Test.Attachment(_attachableValue: attachableContainer, fileSystemPath: fileSystemPath, preferredName: preferredName) } Event.post(.valueAttached(attachmentCopy, sourceLocation: sourceLocation)) } catch { @@ -222,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-29ppv`` property to it. + /// instance's ``attachableValue-7dyjv`` property to it. /// /// - Parameters: /// - body: A function to call. A temporary buffer containing a data @@ -236,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-29ppv`` property. + /// attachment's ``attachableValue-7dyjv`` property. @inlinable public borrowing func withUnsafeBufferPointer(_ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try attachableValue.withUnsafeBufferPointer(for: self, body) } 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 From 77e5be49914a9f215161776387fccab9e7722d3e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Nov 2024 08:16:07 -0500 Subject: [PATCH 13/23] Delete incorrect comment --- Sources/Testing/Attachments/Test.AttachableContainer.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/Testing/Attachments/Test.AttachableContainer.swift b/Sources/Testing/Attachments/Test.AttachableContainer.swift index 72d1d8424..7eb2de3ba 100644 --- a/Sources/Testing/Attachments/Test.AttachableContainer.swift +++ b/Sources/Testing/Attachments/Test.AttachableContainer.swift @@ -21,10 +21,7 @@ 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. 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). + /// 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. From ae8fc5421edbbb6b73b6642f949c07c515b10993 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Nov 2024 08:28:47 -0500 Subject: [PATCH 14/23] Hoist source location up to Event --- .../ABI/v0/Encoded/ABIv0.EncodedEvent.swift | 2 +- .../Testing/Attachments/Test.Attachment.swift | 10 ++--- Sources/Testing/Events/Event.swift | 38 +++++++++++++++++-- .../Event.HumanReadableOutputRecorder.swift | 2 +- .../ExpectationChecking+Macro.swift | 2 +- Tests/TestingTests/AttachmentTests.swift | 12 ++++-- 6 files changed, 51 insertions(+), 15 deletions(-) diff --git a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedEvent.swift b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedEvent.swift index 5b67d350f..fd9dc464a 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.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 5315cce9f..f5438c405 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -184,7 +184,7 @@ extension Test.Attachment where AttachableValue: Sendable & Copyable { @_documentation(visibility: private) public consuming func attach(sourceLocation: SourceLocation = #_sourceLocation) { let attachmentCopy = Test.Attachment(self) - Event.post(.valueAttached(attachmentCopy, sourceLocation: sourceLocation)) + Event.post(.valueAttached(attachmentCopy), sourceLocation: sourceLocation) } } #endif @@ -210,7 +210,7 @@ extension Test.Attachment where AttachableValue: ~Copyable { let attachableContainer = Test.AnyAttachable(attachableValue: Array(buffer)) return Test.Attachment(_attachableValue: attachableContainer, fileSystemPath: fileSystemPath, preferredName: preferredName) } - Event.post(.valueAttached(attachmentCopy, sourceLocation: sourceLocation)) + Event.post(.valueAttached(attachmentCopy), sourceLocation: sourceLocation) } catch { let sourceContext = SourceContext(backtrace: .current(), sourceLocation: sourceLocation) Issue(kind: .valueAttachmentFailed(error), comments: [], sourceContext: sourceContext).record() @@ -372,7 +372,7 @@ extension Configuration { return true } - guard case let .valueAttached(attachment, sourceLocation) = event.kind else { + guard case let .valueAttached(attachment) = 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 { @@ -388,11 +388,11 @@ extension Configuration { attachment.fileSystemPath = try attachment.write(toFileInDirectoryAtPath: attachmentsPath) // Update the event before returning and continuing to handle it. - event.kind = .valueAttached(attachment, sourceLocation: sourceLocation) + event.kind = .valueAttached(attachment) return true } catch { // Record the error as an issue and suppress the event. - let sourceContext = SourceContext(backtrace: .current(), sourceLocation: sourceLocation) + let sourceContext = SourceContext(backtrace: .current(), sourceLocation: event.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 3fe1b6b87..99c1c65da 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) /// A test ended. /// @@ -170,6 +170,28 @@ public struct Event: Sendable { /// The instant at which the event occurred. public var instant: Test.Clock.Instant + /// Storage for ``sourceLocation``. + private var _sourceLocation: SourceLocation? + + /// The source location where this event occurred, if available. + /// + /// Not all events have associated source location information. In particular, + /// source location information is available for events with the following + /// ``kind-swift.property``: + /// + /// - ``Kind/expectationChecked(_:)`` + /// - ``Kind/issueRecorded(_:)`` + /// - ``Kind/valueAttached(_:)`` + public var sourceLocation: SourceLocation? { + if let _sourceLocation { + return _sourceLocation + } + if case let .issueRecorded(issue) = kind { + return issue.sourceLocation + } + return nil + } + /// Initialize an instance of this type. /// /// - Parameters: @@ -179,16 +201,19 @@ public struct Event: Sendable { /// any. /// - instant: The instant at which the event occurred. The default value /// of this argument is `.now`. + /// - sourceLocation: The source location at which the event occurred, if + /// known, or `nil` if that information is not applicable. /// /// When creating an event to be posted, use /// ``post(_:for:testCase:instant:configuration)`` instead since that ensures /// any task local-derived values in the associated ``Event/Context`` match /// the event. - init(_ kind: Kind, testID: Test.ID?, testCaseID: Test.Case.ID?, instant: Test.Clock.Instant = .now) { + init(_ kind: Kind, testID: Test.ID?, testCaseID: Test.Case.ID?, instant: Test.Clock.Instant = .now, sourceLocation: SourceLocation? = nil) { self.kind = kind self.testID = testID self.testCaseID = testCaseID self.instant = instant + self._sourceLocation = sourceLocation } /// Post an ``Event`` with the specified values. @@ -200,6 +225,8 @@ public struct Event: Sendable { /// ``Test/Case/current``. /// - instant: The instant at which the event occurred. The default value /// of this argument is `.now`. + /// - sourceLocation: The source location at which the event occurred, if + /// known, or `nil` if that information is not applicable. /// - configuration: The configuration whose event handler should handle /// this event. If `nil` is passed, the current task's configuration is /// used, if known. @@ -207,6 +234,7 @@ public struct Event: Sendable { _ kind: Kind, for testAndTestCase: (Test?, Test.Case?) = currentTestAndTestCase(), instant: Test.Clock.Instant = .now, + sourceLocation: SourceLocation? = nil, configuration: Configuration? = nil ) { // Create both the event and its associated context here at same point, to @@ -215,7 +243,7 @@ public struct Event: Sendable { // reset it to the actual configuration that handles the event when we call // handleEvent() later, so there's no need to make a copy of it yet. let (test, testCase) = testAndTestCase - let event = Event(kind, testID: test?.id, testCaseID: testCase?.id, instant: instant) + let event = Event(kind, testID: test?.id, testCaseID: testCase?.id, instant: instant, sourceLocation: sourceLocation) let context = Event.Context(test: test, testCase: testCase, configuration: nil) event._post(in: context, configuration: configuration) } @@ -334,6 +362,9 @@ extension Event { /// The instant at which the event occurred. public var instant: Test.Clock.Instant + /// The source location where this event occurred, if available. + public var sourceLocation: SourceLocation? + /// Snapshots an ``Event``. /// /// - Parameters: @@ -343,6 +374,7 @@ extension Event { testID = event.testID testCaseID = event.testCaseID instant = event.instant + sourceLocation = event.sourceLocation } } } diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index 91671da57..ab1f56702 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/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index eff01e5bf..cc3cd3122 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -97,7 +97,7 @@ public func __checkValue( // kind, this event is discarded. lazy var expectation = Expectation(evaluatedExpression: expression, isPassing: condition, isRequired: isRequired, sourceLocation: sourceLocation) if Configuration.deliverExpectationCheckedEvents { - Event.post(.expectationChecked(expectation)) + Event.post(.expectationChecked(expectation), sourceLocation: sourceLocation) } // Early exit if the expectation passed. diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 8ae9c4996..466b80f44 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,11 +165,12 @@ 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 == "loremipsum") + #expect(event.sourceLocation?.fileID == #fileID) valueAttached() } @@ -184,13 +185,14 @@ 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 == "loremipsum") #expect(attachment.attachableValue is MySendableAttachable) - valueAttached() + #expect(event.sourceLocation?.fileID == #fileID) + valueAttached() } await Test { @@ -206,10 +208,12 @@ struct AttachmentTests { var configuration = Configuration() configuration.eventHandler = { event, _ in if case .valueAttached = event.kind { + #expect(event.sourceLocation?.fileID == #fileID) valueAttached() } else if case let .issueRecorded(issue) = event.kind, case let .valueAttachmentFailed(error) = issue.kind, error is MyError { + #expect(event.sourceLocation?.fileID == #fileID) issueRecorded() } } From ec9542d1d24e52758977096463fcb2b32b6bbc9b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Nov 2024 08:41:00 -0500 Subject: [PATCH 15/23] Post issue events to the current configuration --- Sources/Testing/Attachments/Test.Attachment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index f5438c405..9e9386d2a 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -393,7 +393,7 @@ extension Configuration { } catch { // Record the error as an issue and suppress the event. let sourceContext = SourceContext(backtrace: .current(), sourceLocation: event.sourceLocation) - Issue(kind: .valueAttachmentFailed(error), comments: [], sourceContext: sourceContext).record() + Issue(kind: .valueAttachmentFailed(error), comments: [], sourceContext: sourceContext).record(configuration: self) return false } } From 0967acc2151da6338d3474b46e11a4a862defb14 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Nov 2024 10:59:52 -0500 Subject: [PATCH 16/23] Use the proxied withUnsafeBufferPointer call more --- Sources/Testing/Attachments/Test.Attachment.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 9e9386d2a..81354cf66 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -133,7 +133,7 @@ extension Test { fileSystemPath: attachment.fileSystemPath, preferredName: attachment.preferredName ) - return try attachableValue.withUnsafeBufferPointer(for: temporaryAttachment, body) + return try temporaryAttachment.withUnsafeBufferPointer(body) } return try open(attachableValue, for: attachment) } @@ -206,7 +206,7 @@ extension Test.Attachment where AttachableValue: ~Copyable { /// An attachment can only be attached once. public consuming func attach(sourceLocation: SourceLocation = #_sourceLocation) { do { - let attachmentCopy = try attachableValue.withUnsafeBufferPointer(for: self) { buffer in + let attachmentCopy = try withUnsafeBufferPointer { buffer in let attachableContainer = Test.AnyAttachable(attachableValue: Array(buffer)) return Test.Attachment(_attachableValue: attachableContainer, fileSystemPath: fileSystemPath, preferredName: preferredName) } @@ -341,7 +341,7 @@ extension Test.Attachment where AttachableValue: ~Copyable { // 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 withUnsafeBufferPointer { buffer in try file!.write(buffer) } From 064222d36ddf8fd0bf78e71e265070e0c1b06039 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Nov 2024 11:04:07 -0500 Subject: [PATCH 17/23] Update doc comment for updated .valueAttached case --- Sources/Testing/Attachments/Test.Attachment.swift | 9 ++++----- Sources/Testing/Events/Event.swift | 2 -- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 81354cf66..46124fba1 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -99,14 +99,13 @@ 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. + /// 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(_:sourceLocation:)`, otherwise it would be - /// declared as `private`. + /// `Event.Kind.valueAttached(_:)`, otherwise it would be declared private. /// } @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public struct AnyAttachable: Test.AttachableContainer, Copyable, Sendable { diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index 99c1c65da..b179b620f 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -102,8 +102,6 @@ public struct Event: Sendable { /// /// - 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) From 8660c4defa3b095ebb5b529178e2d48fd53079d5 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Nov 2024 11:08:43 -0500 Subject: [PATCH 18/23] Fix documentation links on Event.sourceLocation --- Sources/Testing/Events/Event.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index b179b620f..e50499ab8 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -177,9 +177,9 @@ public struct Event: Sendable { /// source location information is available for events with the following /// ``kind-swift.property``: /// - /// - ``Kind/expectationChecked(_:)`` - /// - ``Kind/issueRecorded(_:)`` - /// - ``Kind/valueAttached(_:)`` + /// - ``Kind-swift.enum/expectationChecked(_:)`` + /// - ``Kind-swift.enum/issueRecorded(_:)`` + /// - ``Kind-swift.enum/valueAttached(_:)`` public var sourceLocation: SourceLocation? { if let _sourceLocation { return _sourceLocation From 6bb233518f80aecd5e58da39c74ebba9e2f3ec46 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Nov 2024 13:48:54 -0500 Subject: [PATCH 19/23] Correct comment --- Sources/Testing/Attachments/Test.Attachment.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index 46124fba1..eb9ed58df 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -20,10 +20,6 @@ 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. - /// - /// 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: Test.Attachable & ~Copyable { /// Storage for ``attachableValue-7dyjv``. fileprivate var _attachableValue: AttachableValue From da4ea7c6ca861a9fa8cb716fa33dfac3d322d7e5 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Nov 2024 15:10:11 -0500 Subject: [PATCH 20/23] Add SuppressedAssociatedTypes to CMake; remove buffer pointer conformances as unsafe now --- .../Testing/Attachments/Test.Attachable.swift | 28 ------------------- Tests/TestingTests/AttachmentTests.swift | 28 ------------------- cmake/modules/shared/CompilerSettings.cmake | 3 +- 3 files changed, 2 insertions(+), 57 deletions(-) diff --git a/Sources/Testing/Attachments/Test.Attachable.swift b/Sources/Testing/Attachments/Test.Attachable.swift index d20c7ffca..83a7dc66c 100644 --- a/Sources/Testing/Attachments/Test.Attachable.swift +++ b/Sources/Testing/Attachments/Test.Attachable.swift @@ -126,34 +126,6 @@ extension ArraySlice: Test.Attachable { } } -@_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 { diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 466b80f44..9b324652a 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -255,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) 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>") From 839766e10c9baf8a4f3552773de23ced59ff5d5c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Nov 2024 15:12:56 -0500 Subject: [PATCH 21/23] Remove generic parameter on ABIv0 structure --- Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedAttachment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedAttachment.swift b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedAttachment.swift index 7f52e0059..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 } } From 5ed70397806ee7079dbd47101e3d95a60e0897f1 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Nov 2024 15:22:46 -0500 Subject: [PATCH 22/23] Replace tabs with spaces --- Documentation/Proposals/0005-ranged-confirmations.md | 4 ++-- Sources/Testing/Events/Event.swift | 2 +- Sources/Testing/Issues/Confirmation.swift | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) 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/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index e50499ab8..aea634607 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -224,7 +224,7 @@ public struct Event: Sendable { /// - instant: The instant at which the event occurred. The default value /// of this argument is `.now`. /// - sourceLocation: The source location at which the event occurred, if - /// known, or `nil` if that information is not applicable. + /// known, or `nil` if that information is not applicable. /// - configuration: The configuration whose event handler should handle /// this event. If `nil` is passed, the current task's configuration is /// used, if known. 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 From b9b94365d2c882cf474cb55a4c6bcef6e78cb12b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Nov 2024 16:09:53 -0500 Subject: [PATCH 23/23] Move sourceLocation back to Attachment --- .../Testing/Attachments/Test.Attachment.swift | 33 ++++++++++++----- Sources/Testing/Events/Event.swift | 36 ++----------------- .../ExpectationChecking+Macro.swift | 2 +- Tests/TestingTests/AttachmentTests.swift | 10 +++--- 4 files changed, 33 insertions(+), 48 deletions(-) diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift index eb9ed58df..f4817a211 100644 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ b/Sources/Testing/Attachments/Test.Attachment.swift @@ -50,6 +50,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 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. + var sourceLocation: SourceLocation } } @@ -69,9 +79,13 @@ extension Test.Attachment where AttachableValue: ~Copyable { /// - 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. - public init(_ attachableValue: consuming AttachableValue, named preferredName: String? = nil) { + /// - sourceLocation: The source location of the call to this initializer. + /// This value is used when recording issues associated with the + /// attachment. + public init(_ attachableValue: consuming AttachableValue, named preferredName: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) { self._attachableValue = attachableValue self.preferredName = preferredName ?? Self.defaultPreferredName + self.sourceLocation = sourceLocation } } @@ -85,7 +99,8 @@ extension Test.Attachment where AttachableValue == Test.AnyAttachable { self.init( _attachableValue: Test.AnyAttachable(attachableValue: attachment.attachableValue), fileSystemPath: attachment.fileSystemPath, - preferredName: attachment.preferredName + preferredName: attachment.preferredName, + sourceLocation: attachment.sourceLocation ) } } @@ -126,7 +141,8 @@ extension Test { let temporaryAttachment = Test.Attachment( _attachableValue: attachableValue, fileSystemPath: attachment.fileSystemPath, - preferredName: attachment.preferredName + preferredName: attachment.preferredName, + sourceLocation: attachment.sourceLocation ) return try temporaryAttachment.withUnsafeBufferPointer(body) } @@ -178,8 +194,9 @@ 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) - Event.post(.valueAttached(attachmentCopy), sourceLocation: sourceLocation) + var attachmentCopy = Test.Attachment(self) + attachmentCopy.sourceLocation = sourceLocation + Event.post(.valueAttached(attachmentCopy)) } } #endif @@ -203,9 +220,9 @@ extension Test.Attachment where AttachableValue: ~Copyable { do { let attachmentCopy = try withUnsafeBufferPointer { buffer in let attachableContainer = Test.AnyAttachable(attachableValue: Array(buffer)) - return Test.Attachment(_attachableValue: attachableContainer, fileSystemPath: fileSystemPath, preferredName: preferredName) + return Test.Attachment(_attachableValue: attachableContainer, fileSystemPath: fileSystemPath, preferredName: preferredName, sourceLocation: sourceLocation) } - Event.post(.valueAttached(attachmentCopy), sourceLocation: sourceLocation) + Event.post(.valueAttached(attachmentCopy)) } catch { let sourceContext = SourceContext(backtrace: .current(), sourceLocation: sourceLocation) Issue(kind: .valueAttachmentFailed(error), comments: [], sourceContext: sourceContext).record() @@ -387,7 +404,7 @@ extension Configuration { return true } catch { // Record the error as an issue and suppress the event. - let sourceContext = SourceContext(backtrace: .current(), sourceLocation: event.sourceLocation) + 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/Events/Event.swift b/Sources/Testing/Events/Event.swift index aea634607..d7813e1c6 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -168,28 +168,6 @@ public struct Event: Sendable { /// The instant at which the event occurred. public var instant: Test.Clock.Instant - /// Storage for ``sourceLocation``. - private var _sourceLocation: SourceLocation? - - /// The source location where this event occurred, if available. - /// - /// Not all events have associated source location information. In particular, - /// source location information is available for events with the following - /// ``kind-swift.property``: - /// - /// - ``Kind-swift.enum/expectationChecked(_:)`` - /// - ``Kind-swift.enum/issueRecorded(_:)`` - /// - ``Kind-swift.enum/valueAttached(_:)`` - public var sourceLocation: SourceLocation? { - if let _sourceLocation { - return _sourceLocation - } - if case let .issueRecorded(issue) = kind { - return issue.sourceLocation - } - return nil - } - /// Initialize an instance of this type. /// /// - Parameters: @@ -199,19 +177,16 @@ public struct Event: Sendable { /// any. /// - instant: The instant at which the event occurred. The default value /// of this argument is `.now`. - /// - sourceLocation: The source location at which the event occurred, if - /// known, or `nil` if that information is not applicable. /// /// When creating an event to be posted, use /// ``post(_:for:testCase:instant:configuration)`` instead since that ensures /// any task local-derived values in the associated ``Event/Context`` match /// the event. - init(_ kind: Kind, testID: Test.ID?, testCaseID: Test.Case.ID?, instant: Test.Clock.Instant = .now, sourceLocation: SourceLocation? = nil) { + init(_ kind: Kind, testID: Test.ID?, testCaseID: Test.Case.ID?, instant: Test.Clock.Instant = .now) { self.kind = kind self.testID = testID self.testCaseID = testCaseID self.instant = instant - self._sourceLocation = sourceLocation } /// Post an ``Event`` with the specified values. @@ -223,8 +198,6 @@ public struct Event: Sendable { /// ``Test/Case/current``. /// - instant: The instant at which the event occurred. The default value /// of this argument is `.now`. - /// - sourceLocation: The source location at which the event occurred, if - /// known, or `nil` if that information is not applicable. /// - configuration: The configuration whose event handler should handle /// this event. If `nil` is passed, the current task's configuration is /// used, if known. @@ -232,7 +205,6 @@ public struct Event: Sendable { _ kind: Kind, for testAndTestCase: (Test?, Test.Case?) = currentTestAndTestCase(), instant: Test.Clock.Instant = .now, - sourceLocation: SourceLocation? = nil, configuration: Configuration? = nil ) { // Create both the event and its associated context here at same point, to @@ -241,7 +213,7 @@ public struct Event: Sendable { // reset it to the actual configuration that handles the event when we call // handleEvent() later, so there's no need to make a copy of it yet. let (test, testCase) = testAndTestCase - let event = Event(kind, testID: test?.id, testCaseID: testCase?.id, instant: instant, sourceLocation: sourceLocation) + let event = Event(kind, testID: test?.id, testCaseID: testCase?.id, instant: instant) let context = Event.Context(test: test, testCase: testCase, configuration: nil) event._post(in: context, configuration: configuration) } @@ -360,9 +332,6 @@ extension Event { /// The instant at which the event occurred. public var instant: Test.Clock.Instant - /// The source location where this event occurred, if available. - public var sourceLocation: SourceLocation? - /// Snapshots an ``Event``. /// /// - Parameters: @@ -372,7 +341,6 @@ extension Event { testID = event.testID testCaseID = event.testCaseID instant = event.instant - sourceLocation = event.sourceLocation } } } diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index cc3cd3122..eff01e5bf 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -97,7 +97,7 @@ public func __checkValue( // kind, this event is discarded. lazy var expectation = Expectation(evaluatedExpression: expression, isPassing: condition, isRequired: isRequired, sourceLocation: sourceLocation) if Configuration.deliverExpectationCheckedEvents { - Event.post(.expectationChecked(expectation), sourceLocation: sourceLocation) + Event.post(.expectationChecked(expectation)) } // Early exit if the expectation passed. diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 9b324652a..cd8a87493 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -170,7 +170,7 @@ struct AttachmentTests { } #expect(attachment.preferredName == "loremipsum") - #expect(event.sourceLocation?.fileID == #fileID) + #expect(attachment.sourceLocation.fileID == #fileID) valueAttached() } @@ -191,7 +191,7 @@ struct AttachmentTests { #expect(attachment.preferredName == "loremipsum") #expect(attachment.attachableValue is MySendableAttachable) - #expect(event.sourceLocation?.fileID == #fileID) + #expect(attachment.sourceLocation.fileID == #fileID) valueAttached() } @@ -207,13 +207,13 @@ struct AttachmentTests { await confirmation("Issue recorded") { issueRecorded in var configuration = Configuration() configuration.eventHandler = { event, _ in - if case .valueAttached = event.kind { - #expect(event.sourceLocation?.fileID == #fileID) + if case let .valueAttached(attachment) = event.kind { + #expect(attachment.sourceLocation.fileID == #fileID) valueAttached() } else if case let .issueRecorded(issue) = event.kind, case let .valueAttachmentFailed(error) = issue.kind, error is MyError { - #expect(event.sourceLocation?.fileID == #fileID) + #expect(issue.sourceLocation?.fileID == #fileID) issueRecorded() } }