From deee757722f73b694e56d249a243e44ae3fc8158 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 20 Aug 2025 16:33:39 -0400 Subject: [PATCH 01/12] Create a separate library containing a fallback event handler. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds an experimental helper library that contains a hook function called the "fallback event handler". When Swift Testing posts an event such as "issue recorded", but Swift Testing itself isn't the running testing library (i.e. XCTest is actually running), it calls the fallback event handler and passes the event (JSON-encoded) to it. Because the hook function is exported from a separate library, XCTest can (in theory, anyway) set the fallback event handler to a function that decodes the JSON-encoded event and translates it into the corresponding XCTest event. Swift Testing sets the fallback event handler itself when it starts running so that, conversely, if a testing library such as XCTest were to generate an event and determine it isn't running, it could post that event to the same event handler and have Swift Testing pick it up and translate it into a Swift Testing event. So that if you have code that calls `#expect()` from within XCTest, or if you have code that calls `XCTAssert()`` from within Swift Testing, it'll "just work™". --- Documentation/ABI/JSON.md | 3 +- Package.swift | 16 +++ .../ABI/Encoded/ABI.EncodedAttachment.swift | 126 +++++++++++++++++- .../ABI/Encoded/ABI.EncodedIssue.swift | 49 ++++++- Sources/Testing/Events/Event.swift | 69 ++++++++++ Sources/Testing/ExitTests/ExitTest.swift | 31 +---- Sources/Testing/Running/Runner.Plan.swift | 3 + Sources/Testing/Running/Runner.swift | 3 + .../include/FallbackEventHandler.h | 71 ++++++++++ .../CMakeLists.txt | 24 ++++ .../FallbackEventHandler.swift | 58 ++++++++ Tests/TestingTests/EventTests.swift | 126 ++++++++++++++++++ 12 files changed, 544 insertions(+), 35 deletions(-) create mode 100644 Sources/_TestingInternals/include/FallbackEventHandler.h create mode 100644 Sources/_Testing_ExperimentalInfrastructure/CMakeLists.txt create mode 100644 Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift diff --git a/Documentation/ABI/JSON.md b/Documentation/ABI/JSON.md index e4ff24a4b..c0c5ead1d 100644 --- a/Documentation/ABI/JSON.md +++ b/Documentation/ABI/JSON.md @@ -203,7 +203,8 @@ sufficient information to display the event in a human-readable format. } ::= { - "path": , ; the absolute path to the attachment on disk + ["path": ,] ; the absolute path to the attachment on disk if it has + ; been saved as a file } ::= { diff --git a/Package.swift b/Package.swift index 80db6076c..c2183da1e 100644 --- a/Package.swift +++ b/Package.swift @@ -105,6 +105,14 @@ let package = Package( ) ) + result.append( + .library( + name: "_Testing_ExperimentalInfrastructure", + type: .dynamic, + targets: ["_Testing_ExperimentalInfrastructure"] + ) + ) + return result }(), @@ -118,6 +126,7 @@ let package = Package( dependencies: [ "_TestDiscovery", "_TestingInternals", + "_Testing_ExperimentalInfrastructure", "TestingMacros", ], exclude: ["CMakeLists.txt", "Testing.swiftcrossimport"], @@ -198,6 +207,13 @@ let package = Package( cxxSettings: .packageSettings, swiftSettings: .packageSettings + .enableLibraryEvolution() ), + .target( + name: "_Testing_ExperimentalInfrastructure", + dependencies: ["_TestingInternals",], + exclude: ["CMakeLists.txt"], + cxxSettings: .packageSettings, + swiftSettings: .packageSettings + .enableLibraryEvolution() + ), // Cross-import overlays (not supported by Swift Package Manager) .target( diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift index 7668f778a..125f3c344 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift @@ -8,6 +8,33 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +private import _TestingInternals + +#if canImport(Foundation) +private import Foundation +#endif + +/// The maximum size, in bytes, of an attachment that will be stored inline in +/// an encoded attachment. +private let _maximumInlineAttachmentByteCount: Int = { + let pageSize: Int +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) + pageSize = Int(clamping: sysconf(_SC_PAGESIZE)) +#elseif os(WASI) + // sysconf(_SC_PAGESIZE) is a complex macro in wasi-libc. + pageSize = Int(clamping: getpagesize()) +#elseif os(Windows) + var systemInfo = SYSTEM_INFO() + GetSystemInfo(&systemInfo) + pageSize = Int(clamping: systemInfo.dwPageSize) +#else +#warning("Platform-specific implementation missing: page size unknown (assuming 4KB)") + pageSize = 4 * 1024 +#endif + + return pageSize // i.e. we'll store up to a page-sized attachment +}() + extension ABI { /// A type implementing the JSON encoding of ``Attachment`` for the ABI entry /// point and event stream output. @@ -15,14 +42,42 @@ extension ABI { /// This type is not part of the public interface of the testing library. It /// assists in converting values to JSON; clients that consume this JSON are /// expected to write their own decoders. - /// - /// - Warning: Attachments are not yet part of the JSON schema. struct EncodedAttachment: Sendable where V: ABI.Version { /// The path where the attachment was written. var path: String? + /// The preferred name of the attachment. + /// + /// - Warning: Attachments' preferred names are not yet part of the JSON + /// schema. + var _preferredName: String? + + /// The raw content of the attachment, if available. + /// + /// If the value of this property is `nil`, the attachment can instead be + /// read from ``path``. + /// + /// - Warning: Inline attachment content is not yet part of the JSON schema. + var _bytes: Bytes? + init(encoding attachment: borrowing Attachment, in eventContext: borrowing Event.Context) { path = attachment.fileSystemPath + _preferredName = attachment.preferredName + + if let estimatedByteCount = attachment.attachableValue.estimatedAttachmentByteCount, + estimatedByteCount <= _maximumInlineAttachmentByteCount { + _bytes = try? attachment.withUnsafeBytes { bytes in + if bytes.count > 0 && bytes.count < _maximumInlineAttachmentByteCount { + return Bytes(rawValue: [UInt8](bytes)) + } + return nil + } + } + } + + /// A structure representing the bytes of an attachment. + struct Bytes: Sendable, RawRepresentable { + var rawValue: [UInt8] } } } @@ -30,3 +85,70 @@ extension ABI { // MARK: - Codable extension ABI.EncodedAttachment: Codable {} + +extension ABI.EncodedAttachment.Bytes: Codable { + func encode(to encoder: any Encoder) throws { +#if canImport(Foundation) + // If possible, encode this structure as Base64 data. + try rawValue.withUnsafeBytes { rawValue in + let data = Data(bytesNoCopy: .init(mutating: rawValue.baseAddress!), count: rawValue.count, deallocator: .none) + var container = encoder.singleValueContainer() + try container.encode(data) + } +#else + // Otherwise, it's an array of integers. + var container = encoder.singleValueContainer() + try container.encode(rawValue) +#endif + } + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + +#if canImport(Foundation) + // If possible, decode a whole Foundation Data object. + if let data = try? container.decode(Data.self) { + self.init(rawValue: [UInt8](data)) + return + } +#endif + + // Fall back to trying to decode an array of integers. + let bytes = try container.decode([UInt8].self) + self.init(rawValue: bytes) + } +} + +// MARK: - Attachable + +extension ABI.EncodedAttachment: Attachable { + var estimatedAttachmentByteCount: Int? { + _bytes?.rawValue.count + } + + fileprivate struct BytesUnavailableError: Error {} + + borrowing func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + if let bytes = _bytes?.rawValue { + return try bytes.withUnsafeBytes(body) + } + + guard let path else { + throw BytesUnavailableError() + } + let fileHandle = try FileHandle(forReadingAtPath: path) + // TODO: map the attachment back into memory + let bytes = try fileHandle.readToEnd() + return try bytes.withUnsafeBytes(body) + } + + borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + _preferredName ?? suggestedName + } +} + +extension ABI.EncodedAttachment.BytesUnavailableError: CustomStringConvertible { + var description: String { + "The attachment's content could not be deserialized." + } +} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift index 465be7aee..44ba5e13c 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift @@ -27,12 +27,12 @@ extension ABI { /// The severity of this issue. /// /// - Warning: Severity is not yet part of the JSON schema. - var _severity: Severity - + var _severity: Severity? + /// If the issue is a failing issue. /// /// - Warning: Non-failing issues are not yet part of the JSON schema. - var _isFailure: Bool + var _isFailure: Bool? /// Whether or not this issue is known to occur. var isKnown: Bool @@ -72,3 +72,46 @@ extension ABI { extension ABI.EncodedIssue: Codable {} extension ABI.EncodedIssue.Severity: Codable {} + +// MARK: - Converting back to an Issue + +extension Issue { + /// Attempt to reconstruct an instance of ``Issue`` from the given encoded + /// event. + /// + /// - Parameters: + /// - event: The event that may contain an encoded issue. + /// + /// If `event` does not represent an issue, this initializer returns `nil`. + init?(_ event: ABI.EncodedEvent) { + guard let issue = event.issue else { + return nil + } + // Translate the issue back into a "real" issue and record it + // in the parent process. This translation is, of course, lossy + // due to the process boundary, but we make a best effort. + let comments: [Comment] = event.messages.map(\.text).map(Comment.init(rawValue:)) + let issueKind: Issue.Kind = if let error = issue._error { + .errorCaught(error) + } else { + // TODO: improve fidelity of issue kind reporting (especially those without associated values) + .unconditional + } + let severity: Issue.Severity = switch issue._severity { + case .warning: + .warning + case nil, .error: + .error + } + let sourceContext = SourceContext( + backtrace: nil, // `issue._backtrace` will have the wrong address space. + sourceLocation: issue.sourceLocation + ) + self.init(kind: issueKind, severity: severity, comments: comments, sourceContext: sourceContext) + if issue.isKnown { + // The known issue comment, if there was one, is already included in + // the `comments` array above. + knownIssueContext = Issue.KnownIssueContext() + } + } +} diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index 0be14ae88..5957c69c8 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -8,6 +8,8 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +internal import _Testing_ExperimentalInfrastructure + /// An event that occurred during testing. @_spi(ForToolsIntegrationOnly) public struct Event: Sendable { @@ -270,6 +272,60 @@ extension Event { } } + /// The implementation of ``fallbackEventHandler``. + /// + /// - Parameters: + /// - abi: The ABI version to use for decoding `recordJSON`. + /// - recordJSON: The JSON encoding of an event record. + /// + /// - Throws: Any error that prevented handling the encoded record. + private static func _fallbackEventHandler(_ abi: V.Type, _ recordJSON: UnsafeRawBufferPointer) throws where V: ABI.Version { + let record = try JSON.decode(ABI.Record.self, from: recordJSON) + guard case let .event(event) = record.kind else { + return + } + switch event.kind { + case .issueRecorded: + Issue(event)?.record() + case .valueAttached: + if let attachment = event.attachment { + Attachment.record(attachment) + } + default: + // Not handled here. + break + } + } + + /// The fallback event handler to set when Swift Testing is the active testing + /// library. + /// + /// ## See Also + /// + /// - `swift_testing_getFallbackEventHandler()` + /// - `swift_testing_setFallbackEventHandler()` + static let fallbackEventHandler: FallbackEventHandler = { recordJSONSchemaVersionNumber, recordJSONBaseAddress, recordJSONByteCount, _ in + let abi = String(validatingCString: recordJSONSchemaVersionNumber) + .flatMap(VersionNumber.init) + .flatMap(ABI.version(forVersionNumber:)) + if let abi { + let recordJSON = UnsafeRawBufferPointer(start: recordJSONBaseAddress, count: recordJSONByteCount) + try! Self._fallbackEventHandler(abi, recordJSON) + } + } + + /// The implementation of ``installFallbackEventHandler()``. + private static let _installFallbackHandler: Bool = { + _Testing_ExperimentalInfrastructure.installFallbackEventHandler(Self.fallbackEventHandler) + }() + + /// Install the testing library's fallback event handler. + /// + /// - Returns: Whether or not the handler was installed. + static func installFallbackHandler() -> Bool { + _installFallbackHandler + } + /// Post this event to the currently-installed event handler. /// /// - Parameters: @@ -292,6 +348,19 @@ extension Event { if configuration.eventHandlingOptions.shouldHandleEvent(self) { configuration.handleEvent(self, in: context) } + } else if let fallbackEventHandler = _Testing_ExperimentalInfrastructure.fallbackEventHandler(), + castCFunction(fallbackEventHandler, to: UnsafeRawPointer.self) != castCFunction(Self.fallbackEventHandler, to: UnsafeRawPointer.self) { + // Some library other than Swift Testing has set a fallback event handler. + // Encode the event as JSON and call it. + let fallbackEventHandler = ABI.CurrentVersion.eventHandler(encodeAsJSONLines: false) { recordJSON in + fallbackEventHandler( + String(describing: ABI.CurrentVersion.versionNumber), + recordJSON.baseAddress!, + recordJSON.count, + nil + ) + } + fallbackEventHandler(self, context) } else { // The current task does NOT have an associated configuration. This event // will be lost! Post it to every registered event handler to avoid that. diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index c1d6e9fe1..92dce14c9 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -1034,35 +1034,8 @@ extension ExitTest { /// - Throws: Any error encountered attempting to decode or process the JSON. private static func _processRecord(_ recordJSON: UnsafeRawBufferPointer, fromBackChannel backChannel: borrowing FileHandle) throws { let record = try JSON.decode(ABI.Record.self, from: recordJSON) - - if case let .event(event) = record.kind, let issue = event.issue { - // Translate the issue back into a "real" issue and record it - // in the parent process. This translation is, of course, lossy - // due to the process boundary, but we make a best effort. - let comments: [Comment] = event.messages.map(\.text).map(Comment.init(rawValue:)) - let issueKind: Issue.Kind = if let error = issue._error { - .errorCaught(error) - } else { - // TODO: improve fidelity of issue kind reporting (especially those without associated values) - .unconditional - } - let severity: Issue.Severity = switch issue._severity { - case .warning: - .warning - case .error: - .error - } - let sourceContext = SourceContext( - backtrace: nil, // `issue._backtrace` will have the wrong address space. - sourceLocation: issue.sourceLocation - ) - var issueCopy = Issue(kind: issueKind, severity: severity, comments: comments, sourceContext: sourceContext) - if issue.isKnown { - // The known issue comment, if there was one, is already included in - // the `comments` array above. - issueCopy.knownIssueContext = Issue.KnownIssueContext() - } - issueCopy.record() + if case let .event(event) = record.kind, let issue = Issue(event) { + issue.record() } } diff --git a/Sources/Testing/Running/Runner.Plan.swift b/Sources/Testing/Running/Runner.Plan.swift index c89fdecb5..fbd94371b 100644 --- a/Sources/Testing/Running/Runner.Plan.swift +++ b/Sources/Testing/Running/Runner.Plan.swift @@ -208,6 +208,9 @@ extension Runner.Plan { Backtrace.flushThrownErrorCache() } + // Ensure events generated by e.g. XCTAssert() are handled. + _ = Event.installFallbackHandler() + // Convert the list of test into a graph of steps. The actions for these // steps will all be .run() *unless* an error was thrown while examining // them, in which case it will be .recordIssue(). diff --git a/Sources/Testing/Running/Runner.swift b/Sources/Testing/Running/Runner.swift index bd1167b8e..92f3d67fd 100644 --- a/Sources/Testing/Running/Runner.swift +++ b/Sources/Testing/Running/Runner.swift @@ -400,6 +400,9 @@ extension Runner { var runner = runner runner.configureEventHandlerRuntimeState() + // Ensure events generated by e.g. XCTAssert() are handled. + _ = Event.installFallbackHandler() + // Track whether or not any issues were recorded across the entire run. let issueRecorded = Locked(rawValue: false) runner.configuration.eventHandler = { [eventHandler = runner.configuration.eventHandler] event, context in diff --git a/Sources/_TestingInternals/include/FallbackEventHandler.h b/Sources/_TestingInternals/include/FallbackEventHandler.h new file mode 100644 index 000000000..241f8be21 --- /dev/null +++ b/Sources/_TestingInternals/include/FallbackEventHandler.h @@ -0,0 +1,71 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +#if !defined(SWT_FALLBACK_EVENT_HANDLER_H) +#define SWT_FALLBACK_EVENT_HANDLER_H + +/// - Important: `_Testing_ExperimentalInfrastructure` is the only target that +/// should directly access the symbols in this file! If another target uses +/// them, it will get its own copy of the fallback event handler instead of +/// using the shared copy. (The alternative is to create a separate C target +/// to hold the atomic variable and accessor functions. This is easier.) + +#include "Defines.h" + +#if !defined(__cplusplus) +#include +#include +#endif + +SWT_ASSUME_NONNULL_BEGIN + +/// A type describing a fallback event handler to invoke when testing API is +/// used while the testing library is not running. +/// +/// - Parameters: +/// - recordJSONSchemaVersionNumber: The JSON schema version used to encode +/// the event record. +/// - recordJSONBaseAddress: A pointer to the first byte of the encoded event. +/// - recordJSONByteCount: The size of the encoded event in bytes. +/// - reserved: Reserved for future use. +typedef void (* SWT_SENDABLE SWTFallbackEventHandler)( + const char *recordJSONSchemaVersionNumber, + const void *recordJSONBaseAddress, + size_t recordJSONByteCount, + const void *_Nullable reserved +); + +#if !defined(__cplusplus) +/// Storage for the fallback event handler. +/// +/// When Swift Testing is the active testing library, it calls +/// ``setFallbackEventHandler(_:)`` and passes it this function. Then, if +/// another library such as XCTest posts some event but is _not_ actively +/// running, it can call ``fallbackEventHandler()`` to get this callback and +/// invoke it, allowing Swift Testing to handle the event on its behalf. +/// +/// Conversely, If another testing library such as XCTest is active, it can call +/// ``setFallbackEventHandler(_:)`` and receive events generated by Swift +/// Testing API calls such as `#expect()`. +static _Atomic(SWTFallbackEventHandler SWT_SENDABLE) swt_fallbackEventHandler = 0; + +static inline SWTFallbackEventHandler SWT_SENDABLE _Nullable swt_loadFallbackEventHandler(void) { + return atomic_load(&swt_fallbackEventHandler); +} + +static inline bool swt_installFallbackEventHandler(SWTFallbackEventHandler SWT_SENDABLE handler) { + SWTFallbackEventHandler nullHandler = 0; + return atomic_compare_exchange_strong(&swt_fallbackEventHandler, &nullHandler, handler); +} +#endif + +SWT_ASSUME_NONNULL_END + +#endif diff --git a/Sources/_Testing_ExperimentalInfrastructure/CMakeLists.txt b/Sources/_Testing_ExperimentalInfrastructure/CMakeLists.txt new file mode 100644 index 000000000..819e730b4 --- /dev/null +++ b/Sources/_Testing_ExperimentalInfrastructure/CMakeLists.txt @@ -0,0 +1,24 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2025 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +add_library(_Testing_ExperimentalInfrastructure + FallbackEventHandler.swift) + +target_link_libraries(_Testing_ExperimentalInfrastructure PRIVATE + _TestingInternals) +if(NOT BUILD_SHARED_LIBS) + # When building a static library, tell clients to autolink the internal + # libraries. + target_compile_options(Testing PRIVATE + "SHELL:-Xfrontend -public-autolink-library -Xfrontend _TestingInternals") +endif() +target_compile_options(_Testing_ExperimentalInfrastructure PRIVATE + -enable-library-evolution + -emit-module-interface -emit-module-interface-path $/_Testing_ExperimentalInfrastructure.swiftinterface) + +_swift_testing_install_target(_Testing_ExperimentalInfrastructure) diff --git a/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift b/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift new file mode 100644 index 000000000..179026e88 --- /dev/null +++ b/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift @@ -0,0 +1,58 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 +// + +private import _TestingInternals + +/// A type describing a fallback event handler to invoke when testing API is +/// used while the testing library is not running. +/// +/// - Parameters: +/// - recordJSONSchemaVersionNumber: The JSON schema version used to encode +/// the event record. +/// - recordJSONBaseAddress: A pointer to the first byte of the encoded event. +/// - recordJSONByteCount: The size of the encoded event in bytes. +/// - reserved: Reserved for future use. +@usableFromInline +package typealias FallbackEventHandler = @Sendable @convention(c) ( + _ recordJSONSchemaVersionNumber: UnsafePointer, + _ recordJSONBaseAddress: UnsafeRawPointer, + _ recordJSONByteCount: Int, + _ reserved: UnsafeRawPointer? +) -> Void + +/// Get the current fallback event handler. +/// +/// - Returns: The currently-set handler function, if any. +/// +/// - Important: This operation is thread-safe, but is not atomic with respect +/// to calls to ``setFallbackEventHandler(_:)``. If you need to atomically +/// exchange the previous value with a new value, call +/// ``setFallbackEventHandler(_:)`` and store its returned value. +@_cdecl("swift_testing_getFallbackEventHandler") +@usableFromInline +package func fallbackEventHandler() -> FallbackEventHandler? { + swt_loadFallbackEventHandler() +} + +/// Set the current fallback event handler if one has not already been set. +/// +/// - Parameters: +/// - handler: The handler function to set. +/// +/// - Returns: Whether or not `handler` was installed. +/// +/// The fallback event handler can only be installed once per process, typically +/// by the first testing library to run. If this function has already been +/// called and the handler set, it does not replace the previous handler. +@_cdecl("swift_testing_installFallbackEventHandler") +@usableFromInline +package func installFallbackEventHandler(_ handler: FallbackEventHandler) -> CBool { + swt_installFallbackEventHandler(handler) +} diff --git a/Tests/TestingTests/EventTests.swift b/Tests/TestingTests/EventTests.swift index 653ad2a87..6e01207c7 100644 --- a/Tests/TestingTests/EventTests.swift +++ b/Tests/TestingTests/EventTests.swift @@ -78,3 +78,129 @@ struct EventTests { #endif } #endif + +// MARK: - + +#if canImport(Foundation) +private import _Testing_ExperimentalInfrastructure +import Foundation + +private func MockXCTAssert(_ condition: Bool, _ message: String, _ sourceLocation: SourceLocation = #_sourceLocation) { + #expect(throws: Never.self) { + if condition { + return + } + guard let fallbackEventHandler = fallbackEventHandler() else { + return + } + + let jsonObject: [String: Any] = [ + "version": 0, + "kind": "event", + "payload": [ + "kind": "issueRecorded", + "instant": [ + "absolute": 0.0, + "since1970": Date().timeIntervalSince1970, + ], + "issue": [ + "isKnown": false, + "sourceLocation": [ + "fileID": sourceLocation.fileID, + "_filePath": sourceLocation._filePath, + "line": sourceLocation.line, + "column": sourceLocation.column, + ] + ], + "messages": [ + [ + "symbol": "fail", + "text": message + ] + ], + ], + ] + + let json = try JSONSerialization.data(withJSONObject: jsonObject, options: []) + json.withUnsafeBytes { json in + fallbackEventHandler("0", json.baseAddress!, json.count, nil) + } + } +} + +private func MockXCTAttachmentAdd(_ string: String, named name: String) { + #expect(throws: Never.self) { + guard let fallbackEventHandler = fallbackEventHandler() else { + return + } + + let bytes = try #require(string.data(using: .utf8)?.base64EncodedString()) + + let jsonObject: [String: Any] = [ + "version": 0, + "kind": "event", + "payload": [ + "kind": "valueAttached", + "instant": [ + "absolute": 0.0, + "since1970": Date().timeIntervalSince1970, + ], + "attachment": [ + "_bytes": bytes, + "_preferredName": name + ], + "messages": [], + ], + ] + + let json = try JSONSerialization.data(withJSONObject: jsonObject, options: []) + json.withUnsafeBytes { json in + fallbackEventHandler("0", json.baseAddress!, json.count, nil) + } + } +} + +@Suite struct `Fallback event handler tests` { + @Test func `Fallback event handler is set`() { + #expect(fallbackEventHandler() != nil) + } + + @Test func `Fallback event handler is invoked for issue`() async { + await confirmation("Issue recorded") { issueRecorded in + var configuration = Configuration() + configuration.eventHandler = { event, _ in + guard case .issueRecorded = event.kind else { + return + } + issueRecorded() + } + + await Test { + MockXCTAssert(1 == 2, "I'm bad at math!") + }.run(configuration: configuration) + } + } + + @Test func `Attachment is passed to fallback event handler`() async { + await confirmation("Attachment recorded") { valueAttached in + var configuration = Configuration() + configuration.eventHandler = { event, _ in + guard case let .valueAttached(attachment) = event.kind else { + return + } + #expect(throws: Never.self) { + let estimatedByteCount = try #require(attachment.attachableValue.estimatedAttachmentByteCount) + #expect(estimatedByteCount == 10) + } + #expect(attachment.preferredName == "numbers.txt") + + valueAttached() + } + + await Test { + MockXCTAttachmentAdd("0123456789", named: "numbers.txt") + }.run(configuration: configuration) + } + } +} +#endif From 0fc58d10c346bbdcc9d3de1c540dc8aa8c7e2967 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 20 Aug 2025 17:02:30 -0400 Subject: [PATCH 02/12] Need to preserve attachment source location info --- Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift | 8 ++++++++ Sources/Testing/Events/Event.swift | 2 +- Tests/TestingTests/EventTests.swift | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift index 125f3c344..f039765eb 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift @@ -60,6 +60,12 @@ extension ABI { /// - Warning: Inline attachment content is not yet part of the JSON schema. var _bytes: Bytes? + /// The source location where this attachment was created. + /// + /// - Warning: Attachment source locations are not yet part of the JSON + /// schema. + var _sourceLocation: SourceLocation? + init(encoding attachment: borrowing Attachment, in eventContext: borrowing Event.Context) { path = attachment.fileSystemPath _preferredName = attachment.preferredName @@ -73,6 +79,8 @@ extension ABI { return nil } } + + _sourceLocation = attachment.sourceLocation } /// A structure representing the bytes of an attachment. diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index 5957c69c8..4488b5c32 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -289,7 +289,7 @@ extension Event { Issue(event)?.record() case .valueAttached: if let attachment = event.attachment { - Attachment.record(attachment) + Attachment.record(attachment, sourceLocation: attachment._sourceLocation ?? .__here()) } default: // Not handled here. diff --git a/Tests/TestingTests/EventTests.swift b/Tests/TestingTests/EventTests.swift index 6e01207c7..aa5782e88 100644 --- a/Tests/TestingTests/EventTests.swift +++ b/Tests/TestingTests/EventTests.swift @@ -182,6 +182,7 @@ private func MockXCTAttachmentAdd(_ string: String, named name: String) { } @Test func `Attachment is passed to fallback event handler`() async { + MockXCTAttachmentAdd("0123456789", named: "numbers.txt") await confirmation("Attachment recorded") { valueAttached in var configuration = Configuration() configuration.eventHandler = { event, _ in From 8bc2bd4ee04c7bfa242d13b1a28438861d2ad65a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 20 Aug 2025 17:02:50 -0400 Subject: [PATCH 03/12] Typo --- Tests/TestingTests/EventTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/TestingTests/EventTests.swift b/Tests/TestingTests/EventTests.swift index aa5782e88..6e01207c7 100644 --- a/Tests/TestingTests/EventTests.swift +++ b/Tests/TestingTests/EventTests.swift @@ -182,7 +182,6 @@ private func MockXCTAttachmentAdd(_ string: String, named name: String) { } @Test func `Attachment is passed to fallback event handler`() async { - MockXCTAttachmentAdd("0123456789", named: "numbers.txt") await confirmation("Attachment recorded") { valueAttached in var configuration = Configuration() configuration.eventHandler = { event, _ in From aeceb6ef5f2886caca2eb3978b220f43bfd6ed80 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 21 Aug 2025 10:45:53 -0400 Subject: [PATCH 04/12] Add missing add_subdirectory --- Sources/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index c6575a310..d2984afc6 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -104,6 +104,7 @@ endif() include(AvailabilityDefinitions) include(CompilerSettings) add_subdirectory(_TestDiscovery) +add_subdirectory(_Testing_ExperimentalInfrastructure) add_subdirectory(_TestingInternals) add_subdirectory(Overlays) add_subdirectory(Testing) From 71c5fa5e173e8138baa5b57b8f0a8695403b2103 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 21 Aug 2025 10:46:41 -0400 Subject: [PATCH 05/12] Ensure tests build without snapshot types enabled --- Tests/TestingTests/EventTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/TestingTests/EventTests.swift b/Tests/TestingTests/EventTests.swift index 6e01207c7..33395addd 100644 --- a/Tests/TestingTests/EventTests.swift +++ b/Tests/TestingTests/EventTests.swift @@ -8,10 +8,10 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if !SWT_NO_SNAPSHOT_TYPES @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing private import _TestingInternals +#if !SWT_NO_SNAPSHOT_TYPES @Suite("Event Tests") struct EventTests { #if canImport(Foundation) From 3c4b16eeb39b965382b1a2be67b1d2814b5fbab2 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 26 Aug 2025 17:29:07 -0400 Subject: [PATCH 06/12] Factor out the new code to its own file --- .../ABI/Encoded/ABI.EncodedIssue.swift | 2 +- Sources/Testing/CMakeLists.txt | 1 + .../Events/Event+FallbackHandler.swift | 94 +++++++++++++++++++ Sources/Testing/Events/Event.swift | 71 +------------- .../FallbackEventHandler.swift | 3 - 5 files changed, 98 insertions(+), 73 deletions(-) create mode 100644 Sources/Testing/Events/Event+FallbackHandler.swift diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift index 985ae8adb..e42bde1d4 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift @@ -112,7 +112,7 @@ extension Issue { // TODO: improve fidelity of issue kind reporting (especially those without associated values) .unconditional } - let severity: Issue.Severity = switch issue._severity { + let severity: Issue.Severity = switch issue.severity { case .warning: .warning case nil, .error: diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 531d27cfc..5168fbb0e 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -29,6 +29,7 @@ add_library(Testing Attachments/Attachment.swift Events/Clock.swift Events/Event.swift + Events/Event+FallbackHandler.swift Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift Events/Recorder/Event.ConsoleOutputRecorder.swift Events/Recorder/Event.HumanReadableOutputRecorder.swift diff --git a/Sources/Testing/Events/Event+FallbackHandler.swift b/Sources/Testing/Events/Event+FallbackHandler.swift new file mode 100644 index 000000000..3e59d9b79 --- /dev/null +++ b/Sources/Testing/Events/Event+FallbackHandler.swift @@ -0,0 +1,94 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 +// + +private import _Testing_ExperimentalInfrastructure + +extension Event { + /// The implementation of ``fallbackEventHandler``. + /// + /// - Parameters: + /// - abi: The ABI version to use for decoding `recordJSON`. + /// - recordJSON: The JSON encoding of an event record. + /// + /// - Throws: Any error that prevented handling the encoded record. + private static func _fallbackEventHandler(_ abi: V.Type, _ recordJSON: UnsafeRawBufferPointer) throws where V: ABI.Version { + let record = try JSON.decode(ABI.Record.self, from: recordJSON) + guard case let .event(event) = record.kind else { + return + } + switch event.kind { + case .issueRecorded: + Issue(event)?.record() + case .valueAttached: + if let attachment = event.attachment { + Attachment.record(attachment, sourceLocation: attachment._sourceLocation ?? .__here()) + } + default: + // Not handled here. + break + } + } + + /// The fallback event handler to set when Swift Testing is the active testing + /// library. + private static let _fallbackEventHandler: FallbackEventHandler = { recordJSONSchemaVersionNumber, recordJSONBaseAddress, recordJSONByteCount, _ in + let abi = String(validatingCString: recordJSONSchemaVersionNumber) + .flatMap(VersionNumber.init) + .flatMap(ABI.version(forVersionNumber:)) + if let abi { + let recordJSON = UnsafeRawBufferPointer(start: recordJSONBaseAddress, count: recordJSONByteCount) + try! Self._fallbackEventHandler(abi, recordJSON) + } + } + + /// The implementation of ``installFallbackEventHandler()``. + private static let _installFallbackHandler: Bool = { + _Testing_ExperimentalInfrastructure.installFallbackEventHandler(Self._fallbackEventHandler) + }() + + /// Install the testing library's fallback event handler. + /// + /// - Returns: Whether or not the handler was installed. + static func installFallbackHandler() -> Bool { + _installFallbackHandler + } + + /// Post this event to the currently-installed fallback event handler. + /// + /// - Parameters: + /// - context: The context associated with this event. + /// + /// - Returns: Whether or not the fallback event handler was invoked. If the + /// currently-installed handler belongs to the testing library, returns + /// `false`. + borrowing func postToFallbackHandler(in context: borrowing Context) -> Bool { + guard let fallbackEventHandler = _Testing_ExperimentalInfrastructure.fallbackEventHandler() else { + // No fallback event handler is installed. + return false + } + if castCFunction(fallbackEventHandler, to: UnsafeRawPointer.self) == castCFunction(Self._fallbackEventHandler, to: UnsafeRawPointer.self) { + // The fallback event handler belongs to Swift Testing, so we don't want + // to call it on our own behalf. + return false + } + + // Encode the event as JSON and pass it to the handler. + let encodeAndInvoke = ABI.CurrentVersion.eventHandler(encodeAsJSONLines: false) { recordJSON in + fallbackEventHandler( + String(describing: ABI.CurrentVersion.versionNumber), + recordJSON.baseAddress!, + recordJSON.count, + nil + ) + } + encodeAndInvoke(self, context) + return true + } +} diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index 4488b5c32..023c727fa 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -8,8 +8,6 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -internal import _Testing_ExperimentalInfrastructure - /// An event that occurred during testing. @_spi(ForToolsIntegrationOnly) public struct Event: Sendable { @@ -272,60 +270,6 @@ extension Event { } } - /// The implementation of ``fallbackEventHandler``. - /// - /// - Parameters: - /// - abi: The ABI version to use for decoding `recordJSON`. - /// - recordJSON: The JSON encoding of an event record. - /// - /// - Throws: Any error that prevented handling the encoded record. - private static func _fallbackEventHandler(_ abi: V.Type, _ recordJSON: UnsafeRawBufferPointer) throws where V: ABI.Version { - let record = try JSON.decode(ABI.Record.self, from: recordJSON) - guard case let .event(event) = record.kind else { - return - } - switch event.kind { - case .issueRecorded: - Issue(event)?.record() - case .valueAttached: - if let attachment = event.attachment { - Attachment.record(attachment, sourceLocation: attachment._sourceLocation ?? .__here()) - } - default: - // Not handled here. - break - } - } - - /// The fallback event handler to set when Swift Testing is the active testing - /// library. - /// - /// ## See Also - /// - /// - `swift_testing_getFallbackEventHandler()` - /// - `swift_testing_setFallbackEventHandler()` - static let fallbackEventHandler: FallbackEventHandler = { recordJSONSchemaVersionNumber, recordJSONBaseAddress, recordJSONByteCount, _ in - let abi = String(validatingCString: recordJSONSchemaVersionNumber) - .flatMap(VersionNumber.init) - .flatMap(ABI.version(forVersionNumber:)) - if let abi { - let recordJSON = UnsafeRawBufferPointer(start: recordJSONBaseAddress, count: recordJSONByteCount) - try! Self._fallbackEventHandler(abi, recordJSON) - } - } - - /// The implementation of ``installFallbackEventHandler()``. - private static let _installFallbackHandler: Bool = { - _Testing_ExperimentalInfrastructure.installFallbackEventHandler(Self.fallbackEventHandler) - }() - - /// Install the testing library's fallback event handler. - /// - /// - Returns: Whether or not the handler was installed. - static func installFallbackHandler() -> Bool { - _installFallbackHandler - } - /// Post this event to the currently-installed event handler. /// /// - Parameters: @@ -348,19 +292,8 @@ extension Event { if configuration.eventHandlingOptions.shouldHandleEvent(self) { configuration.handleEvent(self, in: context) } - } else if let fallbackEventHandler = _Testing_ExperimentalInfrastructure.fallbackEventHandler(), - castCFunction(fallbackEventHandler, to: UnsafeRawPointer.self) != castCFunction(Self.fallbackEventHandler, to: UnsafeRawPointer.self) { - // Some library other than Swift Testing has set a fallback event handler. - // Encode the event as JSON and call it. - let fallbackEventHandler = ABI.CurrentVersion.eventHandler(encodeAsJSONLines: false) { recordJSON in - fallbackEventHandler( - String(describing: ABI.CurrentVersion.versionNumber), - recordJSON.baseAddress!, - recordJSON.count, - nil - ) - } - fallbackEventHandler(self, context) + } else if postToFallbackHandler(in: context) { + // The fallback event handler handled this event. } else { // The current task does NOT have an associated configuration. This event // will be lost! Post it to every registered event handler to avoid that. diff --git a/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift b/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift index 179026e88..e8c56d3c2 100644 --- a/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift +++ b/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift @@ -19,7 +19,6 @@ private import _TestingInternals /// - recordJSONBaseAddress: A pointer to the first byte of the encoded event. /// - recordJSONByteCount: The size of the encoded event in bytes. /// - reserved: Reserved for future use. -@usableFromInline package typealias FallbackEventHandler = @Sendable @convention(c) ( _ recordJSONSchemaVersionNumber: UnsafePointer, _ recordJSONBaseAddress: UnsafeRawPointer, @@ -36,7 +35,6 @@ package typealias FallbackEventHandler = @Sendable @convention(c) ( /// exchange the previous value with a new value, call /// ``setFallbackEventHandler(_:)`` and store its returned value. @_cdecl("swift_testing_getFallbackEventHandler") -@usableFromInline package func fallbackEventHandler() -> FallbackEventHandler? { swt_loadFallbackEventHandler() } @@ -52,7 +50,6 @@ package func fallbackEventHandler() -> FallbackEventHandler? { /// by the first testing library to run. If this function has already been /// called and the handler set, it does not replace the previous handler. @_cdecl("swift_testing_installFallbackEventHandler") -@usableFromInline package func installFallbackEventHandler(_ handler: FallbackEventHandler) -> CBool { swt_installFallbackEventHandler(handler) } From c4e3e1b06ce6c03d322036ef8e2758efc093510f Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 26 Aug 2025 18:07:54 -0400 Subject: [PATCH 07/12] Remove C++ atomic layer, just use a lock on Darwin for now --- .../include/FallbackEventHandler.h | 71 ------------------- .../FallbackEventHandler.swift | 48 ++++++++++++- 2 files changed, 46 insertions(+), 73 deletions(-) delete mode 100644 Sources/_TestingInternals/include/FallbackEventHandler.h diff --git a/Sources/_TestingInternals/include/FallbackEventHandler.h b/Sources/_TestingInternals/include/FallbackEventHandler.h deleted file mode 100644 index 241f8be21..000000000 --- a/Sources/_TestingInternals/include/FallbackEventHandler.h +++ /dev/null @@ -1,71 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2023 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for Swift project authors -// - -#if !defined(SWT_FALLBACK_EVENT_HANDLER_H) -#define SWT_FALLBACK_EVENT_HANDLER_H - -/// - Important: `_Testing_ExperimentalInfrastructure` is the only target that -/// should directly access the symbols in this file! If another target uses -/// them, it will get its own copy of the fallback event handler instead of -/// using the shared copy. (The alternative is to create a separate C target -/// to hold the atomic variable and accessor functions. This is easier.) - -#include "Defines.h" - -#if !defined(__cplusplus) -#include -#include -#endif - -SWT_ASSUME_NONNULL_BEGIN - -/// A type describing a fallback event handler to invoke when testing API is -/// used while the testing library is not running. -/// -/// - Parameters: -/// - recordJSONSchemaVersionNumber: The JSON schema version used to encode -/// the event record. -/// - recordJSONBaseAddress: A pointer to the first byte of the encoded event. -/// - recordJSONByteCount: The size of the encoded event in bytes. -/// - reserved: Reserved for future use. -typedef void (* SWT_SENDABLE SWTFallbackEventHandler)( - const char *recordJSONSchemaVersionNumber, - const void *recordJSONBaseAddress, - size_t recordJSONByteCount, - const void *_Nullable reserved -); - -#if !defined(__cplusplus) -/// Storage for the fallback event handler. -/// -/// When Swift Testing is the active testing library, it calls -/// ``setFallbackEventHandler(_:)`` and passes it this function. Then, if -/// another library such as XCTest posts some event but is _not_ actively -/// running, it can call ``fallbackEventHandler()`` to get this callback and -/// invoke it, allowing Swift Testing to handle the event on its behalf. -/// -/// Conversely, If another testing library such as XCTest is active, it can call -/// ``setFallbackEventHandler(_:)`` and receive events generated by Swift -/// Testing API calls such as `#expect()`. -static _Atomic(SWTFallbackEventHandler SWT_SENDABLE) swt_fallbackEventHandler = 0; - -static inline SWTFallbackEventHandler SWT_SENDABLE _Nullable swt_loadFallbackEventHandler(void) { - return atomic_load(&swt_fallbackEventHandler); -} - -static inline bool swt_installFallbackEventHandler(SWTFallbackEventHandler SWT_SENDABLE handler) { - SWTFallbackEventHandler nullHandler = 0; - return atomic_compare_exchange_strong(&swt_fallbackEventHandler, &nullHandler, handler); -} -#endif - -SWT_ASSUME_NONNULL_END - -#endif diff --git a/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift b/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift index e8c56d3c2..ab52cce4e 100644 --- a/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift +++ b/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift @@ -8,7 +8,24 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK private import _TestingInternals +#else +private import Synchronization +#endif + +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK +private nonisolated(unsafe) let _fallbackEventHandler = { + let result = ManagedBuffer.create( + minimumCapacity: 1, + makingHeaderWith: { _ in nil } + ) + result.withUnsafeMutablePointerToHeader { $0.initialize(to: nil) } + return result +}() +#else +private nonisolated(unsafe) let _fallbackEventHandler = Atomic(nil) +#endif /// A type describing a fallback event handler to invoke when testing API is /// used while the testing library is not running. @@ -36,7 +53,19 @@ package typealias FallbackEventHandler = @Sendable @convention(c) ( /// ``setFallbackEventHandler(_:)`` and store its returned value. @_cdecl("swift_testing_getFallbackEventHandler") package func fallbackEventHandler() -> FallbackEventHandler? { - swt_loadFallbackEventHandler() +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK + return _fallbackEventHandler.withUnsafeMutablePointers { fallbackEventHandler, lock in + os_unfair_lock_lock(lock) + defer { + os_unfair_lock_unlock(lock) + } + return fallbackEventHandler.pointee + } +#else + return _fallbackEventHandler.load(ordering: .sequentiallyConsistent).map { fallbackEventHandler in + unsafeBitCast(fallbackEventHandler, to: FallbackEventHandler?.self) + } +#endif } /// Set the current fallback event handler if one has not already been set. @@ -51,5 +80,20 @@ package func fallbackEventHandler() -> FallbackEventHandler? { /// called and the handler set, it does not replace the previous handler. @_cdecl("swift_testing_installFallbackEventHandler") package func installFallbackEventHandler(_ handler: FallbackEventHandler) -> CBool { - swt_installFallbackEventHandler(handler) +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK + return _fallbackEventHandler.withUnsafeMutablePointers { fallbackEventHandler, lock in + os_unfair_lock_lock(lock) + defer { + os_unfair_lock_unlock(lock) + } + guard fallbackEventHandler.pointee == nil else { + return false + } + fallbackEventHandler.pointee = handler + return true + } +#else + let handler = unsafeBitCast(handler, to: UnsafeRawPointer.self) + return _fallbackEventHandler.compareExchange(expected: nil, desired: handler, ordering: .sequentiallyConsistent).exchanged +#endif } From 4ba3e1f504f85fa44298119e0d8da8bfef119c3b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 26 Aug 2025 18:15:06 -0400 Subject: [PATCH 08/12] package symbols need to be exported --- .../FallbackEventHandler.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift b/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift index ab52cce4e..b53a0259e 100644 --- a/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift +++ b/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift @@ -15,6 +15,7 @@ private import Synchronization #endif #if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK +/// The installed event handler. private nonisolated(unsafe) let _fallbackEventHandler = { let result = ManagedBuffer.create( minimumCapacity: 1, @@ -24,6 +25,7 @@ private nonisolated(unsafe) let _fallbackEventHandler = { return result }() #else +/// The installed event handler. private nonisolated(unsafe) let _fallbackEventHandler = Atomic(nil) #endif @@ -36,6 +38,7 @@ private nonisolated(unsafe) let _fallbackEventHandler = Atomic, _ recordJSONBaseAddress: UnsafeRawPointer, @@ -52,6 +55,7 @@ package typealias FallbackEventHandler = @Sendable @convention(c) ( /// exchange the previous value with a new value, call /// ``setFallbackEventHandler(_:)`` and store its returned value. @_cdecl("swift_testing_getFallbackEventHandler") +@usableFromInline package func fallbackEventHandler() -> FallbackEventHandler? { #if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK return _fallbackEventHandler.withUnsafeMutablePointers { fallbackEventHandler, lock in @@ -79,6 +83,7 @@ package func fallbackEventHandler() -> FallbackEventHandler? { /// by the first testing library to run. If this function has already been /// called and the handler set, it does not replace the previous handler. @_cdecl("swift_testing_installFallbackEventHandler") +@usableFromInline package func installFallbackEventHandler(_ handler: FallbackEventHandler) -> CBool { #if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK return _fallbackEventHandler.withUnsafeMutablePointers { fallbackEventHandler, lock in From 5536aa36df9b15c5553a0da8ee1dadf651c0ac92 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 18 Sep 2025 13:03:01 -0400 Subject: [PATCH 09/12] Common code between exit tests and the fallback handler --- .../Events/Event+FallbackHandler.swift | 47 +++++++++++++------ Sources/Testing/ExitTests/ExitTest.swift | 21 +-------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Sources/Testing/Events/Event+FallbackHandler.swift b/Sources/Testing/Events/Event+FallbackHandler.swift index 3e59d9b79..132d0088f 100644 --- a/Sources/Testing/Events/Event+FallbackHandler.swift +++ b/Sources/Testing/Events/Event+FallbackHandler.swift @@ -11,31 +11,50 @@ private import _Testing_ExperimentalInfrastructure extension Event { - /// The implementation of ``fallbackEventHandler``. + /// Attempt to handle an event encoded as JSON as if it had been generated in + /// the current testing context. /// /// - Parameters: - /// - abi: The ABI version to use for decoding `recordJSON`. /// - recordJSON: The JSON encoding of an event record. + /// - abi: The ABI version to use for decoding `recordJSON`. /// /// - Throws: Any error that prevented handling the encoded record. - private static func _fallbackEventHandler(_ abi: V.Type, _ recordJSON: UnsafeRawBufferPointer) throws where V: ABI.Version { - let record = try JSON.decode(ABI.Record.self, from: recordJSON) + /// + /// - Important: This function only handles a subset of event kinds. + static func handle(_ recordJSON: UnsafeRawBufferPointer, encodedWith abi: V.Type) throws where V: ABI.Version { + let record = try JSON.decode(ABI.Record.self, from: recordJSON) guard case let .event(event) = record.kind else { return } - switch event.kind { - case .issueRecorded: - Issue(event)?.record() - case .valueAttached: - if let attachment = event.attachment { - Attachment.record(attachment, sourceLocation: attachment._sourceLocation ?? .__here()) - } - default: - // Not handled here. - break + + lazy var comments: [Comment] = event._comments?.map(Comment.init(rawValue:)) ?? [] + lazy var sourceContext = SourceContext( + backtrace: nil, // A backtrace from the child process will have the wrong address space. + sourceLocation: event._sourceLocation + ) + lazy var skipInfo = SkipInfo(comment: comments.first, sourceContext: sourceContext) + if let issue = Issue(event) { + issue.record() + } else if let attachment = event.attachment { + Attachment.record(attachment, sourceLocation: event._sourceLocation!) + } else if case .testCancelled = event.kind { + _ = try? Test.cancel(with: skipInfo) + } else if case .testCaseCancelled = event.kind { + _ = try? Test.Case.cancel(with: skipInfo) } } + /// The implementation of ``fallbackEventHandler``. + /// + /// - Parameters: + /// - abi: The ABI version to use for decoding `recordJSON`. + /// - recordJSON: The JSON encoding of an event record. + /// + /// - Throws: Any error that prevented handling the encoded record. + private static func _fallbackEventHandler(_ abi: V.Type, _ recordJSON: UnsafeRawBufferPointer) throws where V: ABI.Version { + try handle(recordJSON, encodedWith: abi) + } + /// The fallback event handler to set when Swift Testing is the active testing /// library. private static let _fallbackEventHandler: FallbackEventHandler = { recordJSONSchemaVersionNumber, recordJSONBaseAddress, recordJSONByteCount, _ in diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 924cf694e..6b692a6a2 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -1031,26 +1031,7 @@ extension ExitTest { /// /// - Throws: Any error encountered attempting to decode or process the JSON. private static func _processRecord(_ recordJSON: UnsafeRawBufferPointer, fromBackChannel backChannel: borrowing FileHandle) throws { - let record = try JSON.decode(ABI.Record.self, from: recordJSON) - guard case let .event(event) = record.kind else { - return - } - - lazy var comments: [Comment] = event._comments?.map(Comment.init(rawValue:)) ?? [] - lazy var sourceContext = SourceContext( - backtrace: nil, // A backtrace from the child process will have the wrong address space. - sourceLocation: event._sourceLocation - ) - lazy var skipInfo = SkipInfo(comment: comments.first, sourceContext: sourceContext) - if let issue = Issue(event) { - issue.record() - } else if let attachment = event.attachment { - Attachment.record(attachment, sourceLocation: event._sourceLocation!) - } else if case .testCancelled = event.kind { - _ = try? Test.cancel(with: skipInfo) - } else if case .testCaseCancelled = event.kind { - _ = try? Test.Case.cancel(with: skipInfo) - } + try Event.handle(recordJSON, encodedWith: ABI.BackChannelVersion.self) } /// Decode this exit test's captured values and update its ``capturedValues`` From 690f3bbbca856c93280ac46becfa7522e6edfbba Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 18 Sep 2025 13:18:47 -0400 Subject: [PATCH 10/12] bit more cheese --- .../ABI/Encoded/ABI.EncodedIssue.swift | 53 ++++--------------- .../Events/Event+FallbackHandler.swift | 35 +++++++----- Tests/TestingTests/EventTests.swift | 11 +++- 3 files changed, 40 insertions(+), 59 deletions(-) diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift index 0cb255292..1755da678 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift @@ -58,6 +58,12 @@ extension ABI { /// - Warning: Errors are not yet part of the JSON schema. var _error: EncodedError? + /// The comment associated with the call to `withKnownIssue()` that + /// generated this issue. + /// + /// - Warning: This field is not yet part of the JSON schema. + var _knownIssueComment: String? + init(encoding issue: borrowing Issue, in eventContext: borrowing Event.Context) { // >= v0 isKnown = issue.isKnown @@ -80,6 +86,9 @@ extension ABI { if let error = issue.error { _error = EncodedError(encoding: error, in: eventContext) } + if let knownIssueContext = issue.knownIssueContext { + _knownIssueComment = knownIssueContext.comment?.rawValue + } } } } @@ -89,47 +98,3 @@ extension ABI { extension ABI.EncodedIssue: Codable {} extension ABI.EncodedIssue.Severity: Codable {} - -// MARK: - Converting back to an Issue - -extension Issue { - /// Attempt to reconstruct an instance of ``Issue`` from the given encoded - /// event. - /// - /// - Parameters: - /// - event: The event that may contain an encoded issue. - /// - /// If `event` does not represent an issue, this initializer returns `nil`. - init?(_ event: ABI.EncodedEvent) { - guard let issue = event.issue else { - return nil - } - - // Translate the issue back into a "real" issue and record it - // in the parent process. This translation is, of course, lossy - // due to the process boundary, but we make a best effort. - let comments: [Comment] = event.messages.map(\.text).map(Comment.init(rawValue:)) - let issueKind: Issue.Kind = if let error = issue._error { - .errorCaught(error) - } else { - // TODO: improve fidelity of issue kind reporting (especially those without associated values) - .unconditional - } - let severity: Issue.Severity = switch issue.severity { - case .warning: - .warning - case nil, .error: - .error - } - let sourceContext = SourceContext( - backtrace: nil, // `issue._backtrace` will have the wrong address space. - sourceLocation: issue.sourceLocation - ) - self.init(kind: issueKind, severity: severity, comments: comments, sourceContext: sourceContext) - if issue.isKnown { - // The known issue comment, if there was one, is already included in - // the `comments` array above. - knownIssueContext = Issue.KnownIssueContext() - } - } -} diff --git a/Sources/Testing/Events/Event+FallbackHandler.swift b/Sources/Testing/Events/Event+FallbackHandler.swift index 132d0088f..50b6da216 100644 --- a/Sources/Testing/Events/Event+FallbackHandler.swift +++ b/Sources/Testing/Events/Event+FallbackHandler.swift @@ -33,8 +33,26 @@ extension Event { sourceLocation: event._sourceLocation ) lazy var skipInfo = SkipInfo(comment: comments.first, sourceContext: sourceContext) - if let issue = Issue(event) { - issue.record() + if let issue = event.issue { + // Translate the issue back into a "real" issue and record it in the + // parent process. This translation is, of course, lossy due to the ABI + // and/or process boundary, but we make a best effort. + let issueKind: Issue.Kind = if let error = issue._error { + .errorCaught(error) + } else { + // TODO: improve fidelity of issue kind reporting (especially those without associated values) + .unconditional + } + let severity: Issue.Severity = switch issue.severity { + case .warning: .warning + case nil, .error: .error + } + var issueCopy = Issue(kind: issueKind, severity: severity, comments: comments, sourceContext: sourceContext) + if issue.isKnown { + issueCopy.knownIssueContext = Issue.KnownIssueContext() + issueCopy.knownIssueContext?.comment = issue._knownIssueComment.map(Comment.init(rawValue:)) + } + issueCopy.record() } else if let attachment = event.attachment { Attachment.record(attachment, sourceLocation: event._sourceLocation!) } else if case .testCancelled = event.kind { @@ -44,17 +62,6 @@ extension Event { } } - /// The implementation of ``fallbackEventHandler``. - /// - /// - Parameters: - /// - abi: The ABI version to use for decoding `recordJSON`. - /// - recordJSON: The JSON encoding of an event record. - /// - /// - Throws: Any error that prevented handling the encoded record. - private static func _fallbackEventHandler(_ abi: V.Type, _ recordJSON: UnsafeRawBufferPointer) throws where V: ABI.Version { - try handle(recordJSON, encodedWith: abi) - } - /// The fallback event handler to set when Swift Testing is the active testing /// library. private static let _fallbackEventHandler: FallbackEventHandler = { recordJSONSchemaVersionNumber, recordJSONBaseAddress, recordJSONByteCount, _ in @@ -63,7 +70,7 @@ extension Event { .flatMap(ABI.version(forVersionNumber:)) if let abi { let recordJSON = UnsafeRawBufferPointer(start: recordJSONBaseAddress, count: recordJSONByteCount) - try! Self._fallbackEventHandler(abi, recordJSON) + try! Self.handle(recordJSON, encodedWith: abi) } } diff --git a/Tests/TestingTests/EventTests.swift b/Tests/TestingTests/EventTests.swift index 33395addd..f35041ac5 100644 --- a/Tests/TestingTests/EventTests.swift +++ b/Tests/TestingTests/EventTests.swift @@ -128,7 +128,7 @@ private func MockXCTAssert(_ condition: Bool, _ message: String, _ sourceLocatio } } -private func MockXCTAttachmentAdd(_ string: String, named name: String) { +private func MockXCTAttachmentAdd(_ string: String, named name: String, _ sourceLocation: SourceLocation = #_sourceLocation) { #expect(throws: Never.self) { guard let fallbackEventHandler = fallbackEventHandler() else { return @@ -150,6 +150,15 @@ private func MockXCTAttachmentAdd(_ string: String, named name: String) { "_preferredName": name ], "messages": [], + "_comments": [ + "comment #1", + ], + "_sourceLocation": [ + "fileID": sourceLocation.fileID, + "_filePath": sourceLocation._filePath, + "line": sourceLocation.line, + "column": sourceLocation.column, + ] ], ] From a38088b3ea3c449b002118d4353a3b0a6a104a1e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 18 Sep 2025 13:24:50 -0400 Subject: [PATCH 11/12] Fix optionality --- .../FallbackEventHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift b/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift index b53a0259e..f0c5e7dc6 100644 --- a/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift +++ b/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift @@ -66,7 +66,7 @@ package func fallbackEventHandler() -> FallbackEventHandler? { return fallbackEventHandler.pointee } #else - return _fallbackEventHandler.load(ordering: .sequentiallyConsistent).map { fallbackEventHandler in + return _fallbackEventHandler.load(ordering: .sequentiallyConsistent).flatMap { fallbackEventHandler in unsafeBitCast(fallbackEventHandler, to: FallbackEventHandler?.self) } #endif From e01f7a82b02187c429ccf9c123fdf6aef8b3017e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 18 Sep 2025 14:19:04 -0400 Subject: [PATCH 12/12] Build the side target in CMake but not in the package; the package ought to use the copy from the toolchain rather than rolling its own which other libraries won't know to link to --- Package.swift | 16 ---------------- Sources/CMakeLists.txt | 2 +- .../Testing/Events/Event+FallbackHandler.swift | 18 +++++++++++++++--- .../CMakeLists.txt | 10 +++++----- .../FallbackEventHandler.swift | 8 ++++---- Tests/TestingTests/EventTests.swift | 4 ++-- 6 files changed, 27 insertions(+), 31 deletions(-) rename Sources/{_Testing_ExperimentalInfrastructure => _TestingInfrastructure}/CMakeLists.txt (65%) rename Sources/{_Testing_ExperimentalInfrastructure => _TestingInfrastructure}/FallbackEventHandler.swift (92%) diff --git a/Package.swift b/Package.swift index 76c534e7b..0f4b1648b 100644 --- a/Package.swift +++ b/Package.swift @@ -105,14 +105,6 @@ let package = Package( ) ) - result.append( - .library( - name: "_Testing_ExperimentalInfrastructure", - type: .dynamic, - targets: ["_Testing_ExperimentalInfrastructure"] - ) - ) - return result }(), @@ -134,7 +126,6 @@ let package = Package( dependencies: [ "_TestDiscovery", "_TestingInternals", - "_Testing_ExperimentalInfrastructure", "TestingMacros", ], exclude: ["CMakeLists.txt", "Testing.swiftcrossimport"], @@ -218,13 +209,6 @@ let package = Package( cxxSettings: .packageSettings, swiftSettings: .packageSettings + .enableLibraryEvolution() ), - .target( - name: "_Testing_ExperimentalInfrastructure", - dependencies: ["_TestingInternals",], - exclude: ["CMakeLists.txt"], - cxxSettings: .packageSettings, - swiftSettings: .packageSettings + .enableLibraryEvolution() - ), // Cross-import overlays (not supported by Swift Package Manager) .target( diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index b23c2ead4..b0c714664 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -104,7 +104,7 @@ endif() include(AvailabilityDefinitions) include(CompilerSettings) add_subdirectory(_TestDiscovery) -add_subdirectory(_Testing_ExperimentalInfrastructure) +add_subdirectory(_TestingInfrastructure) add_subdirectory(_TestingInternals) add_subdirectory(Overlays) add_subdirectory(Testing) diff --git a/Sources/Testing/Events/Event+FallbackHandler.swift b/Sources/Testing/Events/Event+FallbackHandler.swift index 50b6da216..396052d48 100644 --- a/Sources/Testing/Events/Event+FallbackHandler.swift +++ b/Sources/Testing/Events/Event+FallbackHandler.swift @@ -8,7 +8,9 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -private import _Testing_ExperimentalInfrastructure +#if canImport(_TestingInfrastructure) +private import _TestingInfrastructure +#endif extension Event { /// Attempt to handle an event encoded as JSON as if it had been generated in @@ -62,6 +64,7 @@ extension Event { } } +#if canImport(_TestingInfrastructure) /// The fallback event handler to set when Swift Testing is the active testing /// library. private static let _fallbackEventHandler: FallbackEventHandler = { recordJSONSchemaVersionNumber, recordJSONBaseAddress, recordJSONByteCount, _ in @@ -73,10 +76,15 @@ extension Event { try! Self.handle(recordJSON, encodedWith: abi) } } +#endif /// The implementation of ``installFallbackEventHandler()``. private static let _installFallbackHandler: Bool = { - _Testing_ExperimentalInfrastructure.installFallbackEventHandler(Self._fallbackEventHandler) +#if canImport(_TestingInfrastructure) + _swift_testing_installFallbackEventHandler(Self._fallbackEventHandler) +#else + false +#endif }() /// Install the testing library's fallback event handler. @@ -95,7 +103,8 @@ extension Event { /// currently-installed handler belongs to the testing library, returns /// `false`. borrowing func postToFallbackHandler(in context: borrowing Context) -> Bool { - guard let fallbackEventHandler = _Testing_ExperimentalInfrastructure.fallbackEventHandler() else { +#if canImport(_TestingInfrastructure) + guard let fallbackEventHandler = _swift_testing_getFallbackEventHandler() else { // No fallback event handler is installed. return false } @@ -116,5 +125,8 @@ extension Event { } encodeAndInvoke(self, context) return true +#else + return false +#endif } } diff --git a/Sources/_Testing_ExperimentalInfrastructure/CMakeLists.txt b/Sources/_TestingInfrastructure/CMakeLists.txt similarity index 65% rename from Sources/_Testing_ExperimentalInfrastructure/CMakeLists.txt rename to Sources/_TestingInfrastructure/CMakeLists.txt index 819e730b4..f621b6dd5 100644 --- a/Sources/_Testing_ExperimentalInfrastructure/CMakeLists.txt +++ b/Sources/_TestingInfrastructure/CMakeLists.txt @@ -6,10 +6,10 @@ # See http://swift.org/LICENSE.txt for license information # See http://swift.org/CONTRIBUTORS.txt for Swift project authors -add_library(_Testing_ExperimentalInfrastructure +add_library(_TestingInfrastructure FallbackEventHandler.swift) -target_link_libraries(_Testing_ExperimentalInfrastructure PRIVATE +target_link_libraries(_TestingInfrastructure PRIVATE _TestingInternals) if(NOT BUILD_SHARED_LIBS) # When building a static library, tell clients to autolink the internal @@ -17,8 +17,8 @@ if(NOT BUILD_SHARED_LIBS) target_compile_options(Testing PRIVATE "SHELL:-Xfrontend -public-autolink-library -Xfrontend _TestingInternals") endif() -target_compile_options(_Testing_ExperimentalInfrastructure PRIVATE +target_compile_options(_TestingInfrastructure PRIVATE -enable-library-evolution - -emit-module-interface -emit-module-interface-path $/_Testing_ExperimentalInfrastructure.swiftinterface) + -emit-module-interface -emit-module-interface-path $/_TestingInfrastructure.swiftinterface) -_swift_testing_install_target(_Testing_ExperimentalInfrastructure) +_swift_testing_install_target(_TestingInfrastructure) diff --git a/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift b/Sources/_TestingInfrastructure/FallbackEventHandler.swift similarity index 92% rename from Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift rename to Sources/_TestingInfrastructure/FallbackEventHandler.swift index f0c5e7dc6..0cd1404ef 100644 --- a/Sources/_Testing_ExperimentalInfrastructure/FallbackEventHandler.swift +++ b/Sources/_TestingInfrastructure/FallbackEventHandler.swift @@ -54,9 +54,9 @@ package typealias FallbackEventHandler = @Sendable @convention(c) ( /// to calls to ``setFallbackEventHandler(_:)``. If you need to atomically /// exchange the previous value with a new value, call /// ``setFallbackEventHandler(_:)`` and store its returned value. -@_cdecl("swift_testing_getFallbackEventHandler") +@_cdecl("_swift_testing_getFallbackEventHandler") @usableFromInline -package func fallbackEventHandler() -> FallbackEventHandler? { +package func _swift_testing_getFallbackEventHandler() -> FallbackEventHandler? { #if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK return _fallbackEventHandler.withUnsafeMutablePointers { fallbackEventHandler, lock in os_unfair_lock_lock(lock) @@ -82,9 +82,9 @@ package func fallbackEventHandler() -> FallbackEventHandler? { /// The fallback event handler can only be installed once per process, typically /// by the first testing library to run. If this function has already been /// called and the handler set, it does not replace the previous handler. -@_cdecl("swift_testing_installFallbackEventHandler") +@_cdecl("_swift_testing_installFallbackEventHandler") @usableFromInline -package func installFallbackEventHandler(_ handler: FallbackEventHandler) -> CBool { +package func _swift_testing_installFallbackEventHandler(_ handler: FallbackEventHandler) -> CBool { #if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK return _fallbackEventHandler.withUnsafeMutablePointers { fallbackEventHandler, lock in os_unfair_lock_lock(lock) diff --git a/Tests/TestingTests/EventTests.swift b/Tests/TestingTests/EventTests.swift index f35041ac5..68fde094a 100644 --- a/Tests/TestingTests/EventTests.swift +++ b/Tests/TestingTests/EventTests.swift @@ -81,8 +81,8 @@ struct EventTests { // MARK: - -#if canImport(Foundation) -private import _Testing_ExperimentalInfrastructure +#if canImport(_TestingInfrastructure) && canImport(Foundation) +private import _TestingInfrastructure import Foundation private func MockXCTAssert(_ condition: Bool, _ message: String, _ sourceLocation: SourceLocation = #_sourceLocation) {