From f4e5b9e4d1499415a3c0fd163be86a4a8f78ba97 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Thu, 16 Oct 2025 15:23:10 -0700 Subject: [PATCH 1/2] Create fallback event handler library Minimal version of the changes in Jonathan's original PR here: https://github.com/swiftlang/swift-testing/pull/1280 The new library is renamed to be "_TestingInterop" to more closely match its intended (and limited) purpose, but that's open to further discussion. This library will eventually be visible in the toolchain, and either testing library (Swift Testing or XCTest) will be able to use it to pass along unhandled issues to a test runner from the other framework, enabling interoperability. --- Sources/CMakeLists.txt | 1 + Sources/_TestingInterop/CMakeLists.txt | 24 ++++ .../FallbackEventHandler.swift | 106 ++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 Sources/_TestingInterop/CMakeLists.txt create mode 100644 Sources/_TestingInterop/FallbackEventHandler.swift diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index 09e5e9fd6..4fc0847b7 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -104,6 +104,7 @@ endif() include(AvailabilityDefinitions) include(CompilerSettings) add_subdirectory(_TestDiscovery) +add_subdirectory(_TestingInterop) add_subdirectory(_TestingInternals) add_subdirectory(Overlays) add_subdirectory(Testing) diff --git a/Sources/_TestingInterop/CMakeLists.txt b/Sources/_TestingInterop/CMakeLists.txt new file mode 100644 index 000000000..adbf7037b --- /dev/null +++ b/Sources/_TestingInterop/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(_TestingInterop + FallbackEventHandler.swift) + +target_link_libraries(_TestingInterop PRIVATE + _TestingInternals) +if(NOT BUILD_SHARED_LIBS) + # When building a static library, tell clients to autolink the internal + # libraries. + target_compile_options(_TestingInterop PRIVATE + "SHELL:-Xfrontend -public-autolink-library -Xfrontend _TestingInternals") +endif() +target_compile_options(_TestingInterop PRIVATE + -enable-library-evolution + -emit-module-interface -emit-module-interface-path $/_TestingInterop.swiftinterface) + +_swift_testing_install_target(_TestingInterop) diff --git a/Sources/_TestingInterop/FallbackEventHandler.swift b/Sources/_TestingInterop/FallbackEventHandler.swift new file mode 100644 index 000000000..667a9d3d3 --- /dev/null +++ b/Sources/_TestingInterop/FallbackEventHandler.swift @@ -0,0 +1,106 @@ +// +// 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 +// + +#if !SWT_NO_INTEROP +#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 +/// The installed event handler. +private nonisolated(unsafe) let _fallbackEventHandler = { + let result = ManagedBuffer.create( + minimumCapacity: 1, + makingHeaderWith: { _ in nil } + ) + result.withUnsafeMutablePointerToHeader { $0.initialize(to: nil) } + return result +}() +#else +/// The installed event handler. +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. +/// +/// - 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 _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) + defer { + os_unfair_lock_unlock(lock) + } + return fallbackEventHandler.pointee + } +#else + return _fallbackEventHandler.load(ordering: .sequentiallyConsistent).flatMap { fallbackEventHandler in + unsafeBitCast(fallbackEventHandler, to: FallbackEventHandler?.self) + } +#endif +} + +/// 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 _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) + 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 +} +#endif From cff44ba243ecd7df8a637027a06b6ab8cdda5295 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Tue, 21 Oct 2025 13:59:33 -0700 Subject: [PATCH 2/2] Update docs --- Sources/_TestingInterop/FallbackEventHandler.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/_TestingInterop/FallbackEventHandler.swift b/Sources/_TestingInterop/FallbackEventHandler.swift index 667a9d3d3..2e33cd04d 100644 --- a/Sources/_TestingInterop/FallbackEventHandler.swift +++ b/Sources/_TestingInterop/FallbackEventHandler.swift @@ -30,8 +30,13 @@ private nonisolated(unsafe) let _fallbackEventHandler = { 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. +/// A type describing a fallback event handler that testing API can invoke as an +/// alternate method of reporting test events to the current test runner. +/// +/// For example, an `XCTAssert` failure in the body of a Swift Testing test +/// cannot record issues directly with the Swift Testing runner. Instead, the +/// framework packages the assertion failure as a JSON `Event` and invokes this +/// handler to report the failure. /// /// - Parameters: /// - recordJSONSchemaVersionNumber: The JSON schema version used to encode @@ -50,11 +55,6 @@ package typealias FallbackEventHandler = @Sendable @convention(c) ( /// 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 _swift_testing_getFallbackEventHandler() -> FallbackEventHandler? {