-
Notifications
You must be signed in to change notification settings - Fork 124
Create a separate library containing a fallback event handler. #1280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
grynspan
wants to merge
14
commits into
main
Choose a base branch
from
jgrynspan/fallback-event-handler
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
deee757
Create a separate library containing a fallback event handler.
grynspan 0fc58d1
Need to preserve attachment source location info
grynspan 8bc2bd4
Typo
grynspan aeceb6e
Add missing add_subdirectory
grynspan 71c5fa5
Ensure tests build without snapshot types enabled
grynspan 0b81a36
Merge branch 'main' into jgrynspan/fallback-event-handler
grynspan 3c4b16e
Factor out the new code to its own file
grynspan c4e3e1b
Remove C++ atomic layer, just use a lock on Darwin for now
grynspan 4ba3e1f
package symbols need to be exported
grynspan 4af44ac
Merge branch 'main' into jgrynspan/fallback-event-handler
grynspan 5536aa3
Common code between exit tests and the fallback handler
grynspan 690f3bb
bit more cheese
grynspan a38088b
Fix optionality
grynspan e01f7a8
Build the side target in CMake but not in the package; the package ou…
grynspan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
// | ||
// 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 canImport(_TestingInfrastructure) | ||
private import _TestingInfrastructure | ||
#endif | ||
|
||
extension Event { | ||
/// Attempt to handle an event encoded as JSON as if it had been generated in | ||
/// the current testing context. | ||
/// | ||
/// - Parameters: | ||
/// - 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. | ||
/// | ||
/// - Important: This function only handles a subset of event kinds. | ||
static func handle<V>(_ recordJSON: UnsafeRawBufferPointer, encodedWith abi: V.Type) throws where V: ABI.Version { | ||
let record = try JSON.decode(ABI.Record<V>.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 = 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 { | ||
_ = try? Test.cancel(with: skipInfo) | ||
} else if case .testCaseCancelled = event.kind { | ||
_ = try? Test.Case.cancel(with: skipInfo) | ||
} | ||
} | ||
|
||
#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 | ||
let abi = String(validatingCString: recordJSONSchemaVersionNumber) | ||
.flatMap(VersionNumber.init) | ||
.flatMap(ABI.version(forVersionNumber:)) | ||
if let abi { | ||
let recordJSON = UnsafeRawBufferPointer(start: recordJSONBaseAddress, count: recordJSONByteCount) | ||
try! Self.handle(recordJSON, encodedWith: abi) | ||
} | ||
} | ||
#endif | ||
|
||
/// The implementation of ``installFallbackEventHandler()``. | ||
private static let _installFallbackHandler: Bool = { | ||
#if canImport(_TestingInfrastructure) | ||
_swift_testing_installFallbackEventHandler(Self._fallbackEventHandler) | ||
#else | ||
false | ||
#endif | ||
}() | ||
|
||
/// 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 { | ||
#if canImport(_TestingInfrastructure) | ||
guard let fallbackEventHandler = _swift_testing_getFallbackEventHandler() 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 | ||
#else | ||
return false | ||
#endif | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(_TestingInfrastructure | ||
FallbackEventHandler.swift) | ||
|
||
target_link_libraries(_TestingInfrastructure 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(_TestingInfrastructure PRIVATE | ||
-enable-library-evolution | ||
-emit-module-interface -emit-module-interface-path $<TARGET_PROPERTY:_TestingInfrastructure,Swift_MODULE_DIRECTORY>/_TestingInfrastructure.swiftinterface) | ||
|
||
_swift_testing_install_target(_TestingInfrastructure) |
104 changes: 104 additions & 0 deletions
104
Sources/_TestingInfrastructure/FallbackEventHandler.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// | ||
// 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_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<FallbackEventHandler?, os_unfair_lock>.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<UnsafeRawPointer?>(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<CChar>, | ||
_ 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 | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This value is not set if an attachment wasn't saved to disk already, so it should be optional in the schema.