Skip to content

Commit

Permalink
Make AssertionHandler Sendable (#1141)
Browse files Browse the repository at this point in the history
  • Loading branch information
younata committed May 13, 2024
1 parent f4b472a commit ed2eba5
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 15 deletions.
4 changes: 4 additions & 0 deletions Nimble.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
89D8AC852B3211C600410644 /* CwlCatchException in Frameworks */ = {isa = PBXBuildFile; productRef = 89D8AC842B3211C600410644 /* CwlCatchException */; };
89D8AC872B3211EA00410644 /* CwlPosixPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; platformFilters = (tvos, watchos, ); productRef = 89D8AC862B3211EA00410644 /* CwlPosixPreconditionTesting */; };
89D8AC892B3211EA00410644 /* CwlPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; platformFilters = (driverkit, ios, maccatalyst, macos, xros, ); productRef = 89D8AC882B3211EA00410644 /* CwlPreconditionTesting */; };
89E5E1682BC78724002D54ED /* LockedContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89E5E1672BC78724002D54ED /* LockedContainer.swift */; };
89EEF5A52A03293100988224 /* AsyncMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89EEF5A42A03293100988224 /* AsyncMatcher.swift */; };
89EEF5B72A032C3200988224 /* AsyncPredicateTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89EEF5B22A032C2500988224 /* AsyncPredicateTest.swift */; };
89EEF5C02A06211C00988224 /* AsyncHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89EEF5BB2A06210D00988224 /* AsyncHelpers.swift */; };
Expand Down Expand Up @@ -331,6 +332,7 @@
899441F32902EF0900C1FAF9 /* DSL+AsyncAwait.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DSL+AsyncAwait.swift"; sourceTree = "<group>"; };
89C297CB2A911CDA002A143F /* AsyncTimerSequenceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncTimerSequenceTest.swift; sourceTree = "<group>"; };
89C297CD2A92AB34002A143F /* AsyncPromiseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncPromiseTest.swift; sourceTree = "<group>"; };
89E5E1672BC78724002D54ED /* LockedContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedContainer.swift; sourceTree = "<group>"; };
89EEF5A42A03293100988224 /* AsyncMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncMatcher.swift; sourceTree = "<group>"; };
89EEF5B22A032C2500988224 /* AsyncPredicateTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncPredicateTest.swift; sourceTree = "<group>"; };
89EEF5BB2A06210D00988224 /* AsyncHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHelpers.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -621,6 +623,7 @@
1FD8CD281968AB07008ED995 /* Stringers.swift */,
AE4BA9AC1C88DDB500B73906 /* Errors.swift */,
0477153423B740AD00402D4E /* NimbleTimeInterval.swift */,
89E5E1672BC78724002D54ED /* LockedContainer.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -855,6 +858,7 @@
1F1871D91CA89EF100A34BF2 /* NMBExpectation.swift in Sources */,
DA9E8C831A414BB9002633C2 /* DSL+Wait.swift in Sources */,
DDB1BC7A1A92235600F743C3 /* AllPass.swift in Sources */,
89E5E1682BC78724002D54ED /* LockedContainer.swift in Sources */,
1FD8CD3F1968AB07008ED995 /* BeAKindOf.swift in Sources */,
1FD8CD2F1968AB07008ED995 /* AssertionRecorder.swift in Sources */,
7B13BA061DD360AA00C9098C /* ContainElementSatisfying.swift in Sources */,
Expand Down
21 changes: 17 additions & 4 deletions Sources/Nimble/Adapters/AdapterProtocols.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// Protocol for the assertion handler that Nimble uses for all expectations.
public protocol AssertionHandler {
public protocol AssertionHandler: Sendable {
func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation)
}

Expand All @@ -10,7 +10,20 @@ public protocol AssertionHandler {
/// before using any matchers, otherwise Nimble will abort the program.
///
/// @see AssertionHandler
public var NimbleAssertionHandler: AssertionHandler = { () -> AssertionHandler in
public var NimbleAssertionHandler: AssertionHandler {
// swiftlint:disable:previous identifier_name
return isXCTestAvailable() ? NimbleXCTestHandler() : NimbleXCTestUnavailableHandler()
}()
get {
_NimbleAssertionHandler.value
}
set {
_NimbleAssertionHandler.set(newValue)
}
}
private let _NimbleAssertionHandler = LockedContainer<AssertionHandler> {
// swiftlint:disable:previous identifier_name
if isXCTestAvailable() {
return NimbleXCTestHandler() as AssertionHandler
} else {
return NimbleXCTestUnavailableHandler() as AssertionHandler
}
}
2 changes: 1 addition & 1 deletion Sources/Nimble/Adapters/AssertionDispatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/// @warning Does not fully dispatch if one of the handlers raises an exception.
/// This is possible with XCTest-based assertion handlers.
///
public class AssertionDispatcher: AssertionHandler {
public final class AssertionDispatcher: AssertionHandler {
let handlers: [AssertionHandler]

public init(handlers: [AssertionHandler]) {
Expand Down
14 changes: 11 additions & 3 deletions Sources/Nimble/Adapters/AssertionRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
///
/// @see AssertionRecorder
/// @see AssertionHandler
public struct AssertionRecord: CustomStringConvertible {
public struct AssertionRecord: CustomStringConvertible, Sendable {
/// Whether the assertion succeeded or failed
public let success: Bool
/// The failure message the assertion would display on failure.
Expand All @@ -20,9 +20,17 @@ public struct AssertionRecord: CustomStringConvertible {
/// This is useful for testing failure messages for matchers.
///
/// @see AssertionHandler
public class AssertionRecorder: AssertionHandler {
public final class AssertionRecorder: AssertionHandler {
/// All the assertions that were captured by this recorder
public var assertions = [AssertionRecord]()
public var assertions: [AssertionRecord] {
get {
_assertion.value
}
set {
_assertion.set(newValue)
}
}
private let _assertion = LockedContainer([AssertionRecord]())

public init() {}

Expand Down
27 changes: 20 additions & 7 deletions Sources/Nimble/Adapters/NimbleXCTestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import XCTest

/// Default handler for Nimble. This assertion handler passes failures along to
/// XCTest.
public class NimbleXCTestHandler: AssertionHandler {
public final class NimbleXCTestHandler: AssertionHandler {
public func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) {
if !assertion {
recordFailure("\(message.stringValue)\n", location: location)
Expand All @@ -13,7 +13,7 @@ public class NimbleXCTestHandler: AssertionHandler {

/// Alternative handler for Nimble. This assertion handler passes failures along
/// to XCTest by attempting to reduce the failure message size.
public class NimbleShortXCTestHandler: AssertionHandler {
public final class NimbleShortXCTestHandler: AssertionHandler {
public func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) {
if !assertion {
let msg: String
Expand All @@ -29,32 +29,45 @@ public class NimbleShortXCTestHandler: AssertionHandler {

/// Fallback handler in case XCTest is unavailable. This assertion handler will abort
/// the program if it is invoked.
class NimbleXCTestUnavailableHandler: AssertionHandler {
final class NimbleXCTestUnavailableHandler: AssertionHandler {
func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) {
fatalError("XCTest is not available and no custom assertion handler was configured. Aborting.")
}
}

#if canImport(Darwin)
/// Helper class providing access to the currently executing XCTestCase instance, if any
@objc final public class CurrentTestCaseTracker: NSObject, XCTestObservation {
@objc final public class CurrentTestCaseTracker: NSObject, XCTestObservation, @unchecked Sendable {
@objc public static let sharedInstance = CurrentTestCaseTracker()

private(set) var currentTestCase: XCTestCase?
private let lock = NSRecursiveLock()

private var _currentTestCase: XCTestCase?
var currentTestCase: XCTestCase? {
lock.lock()
defer { lock.unlock() }
return _currentTestCase
}

private var stashed_swift_reportFatalErrorsToDebugger: Bool = false

@objc public func testCaseWillStart(_ testCase: XCTestCase) {
lock.lock()
defer { lock.unlock() }

#if (os(macOS) || os(iOS) || os(visionOS)) && !SWIFT_PACKAGE
stashed_swift_reportFatalErrorsToDebugger = _swift_reportFatalErrorsToDebugger
_swift_reportFatalErrorsToDebugger = false
#endif

currentTestCase = testCase
_currentTestCase = testCase
}

@objc public func testCaseDidFinish(_ testCase: XCTestCase) {
currentTestCase = nil
lock.lock()
defer { lock.unlock() }

_currentTestCase = nil

#if (os(macOS) || os(iOS) || os(visionOS)) && !SWIFT_PACKAGE
_swift_reportFatalErrorsToDebugger = stashed_swift_reportFatalErrorsToDebugger
Expand Down
32 changes: 32 additions & 0 deletions Sources/Nimble/Utils/LockedContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation

final class LockedContainer<T: Sendable>: @unchecked Sendable {
private let lock = NSRecursiveLock()
private var _value: T

var value: T {
lock.lock()
defer { lock.unlock() }
return _value
}

init(_ value: T) {
_value = value
}

init(_ closure: () -> T) {
_value = closure()
}

func operate(_ closure: (T) -> T) {
lock.lock()
defer { lock.unlock() }
_value = closure(_value)
}

func set(_ newValue: T) {
lock.lock()
defer { lock.unlock() }
_value = newValue
}
}

0 comments on commit ed2eba5

Please sign in to comment.