Skip to content

Commit ed2eba5

Browse files
authored
Make AssertionHandler Sendable (#1141)
1 parent f4b472a commit ed2eba5

6 files changed

+85
-15
lines changed

Nimble.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
89D8AC852B3211C600410644 /* CwlCatchException in Frameworks */ = {isa = PBXBuildFile; productRef = 89D8AC842B3211C600410644 /* CwlCatchException */; };
147147
89D8AC872B3211EA00410644 /* CwlPosixPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; platformFilters = (tvos, watchos, ); productRef = 89D8AC862B3211EA00410644 /* CwlPosixPreconditionTesting */; };
148148
89D8AC892B3211EA00410644 /* CwlPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; platformFilters = (driverkit, ios, maccatalyst, macos, xros, ); productRef = 89D8AC882B3211EA00410644 /* CwlPreconditionTesting */; };
149+
89E5E1682BC78724002D54ED /* LockedContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89E5E1672BC78724002D54ED /* LockedContainer.swift */; };
149150
89EEF5A52A03293100988224 /* AsyncMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89EEF5A42A03293100988224 /* AsyncMatcher.swift */; };
150151
89EEF5B72A032C3200988224 /* AsyncPredicateTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89EEF5B22A032C2500988224 /* AsyncPredicateTest.swift */; };
151152
89EEF5C02A06211C00988224 /* AsyncHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89EEF5BB2A06210D00988224 /* AsyncHelpers.swift */; };
@@ -331,6 +332,7 @@
331332
899441F32902EF0900C1FAF9 /* DSL+AsyncAwait.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DSL+AsyncAwait.swift"; sourceTree = "<group>"; };
332333
89C297CB2A911CDA002A143F /* AsyncTimerSequenceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncTimerSequenceTest.swift; sourceTree = "<group>"; };
333334
89C297CD2A92AB34002A143F /* AsyncPromiseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncPromiseTest.swift; sourceTree = "<group>"; };
335+
89E5E1672BC78724002D54ED /* LockedContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedContainer.swift; sourceTree = "<group>"; };
334336
89EEF5A42A03293100988224 /* AsyncMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncMatcher.swift; sourceTree = "<group>"; };
335337
89EEF5B22A032C2500988224 /* AsyncPredicateTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncPredicateTest.swift; sourceTree = "<group>"; };
336338
89EEF5BB2A06210D00988224 /* AsyncHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHelpers.swift; sourceTree = "<group>"; };
@@ -621,6 +623,7 @@
621623
1FD8CD281968AB07008ED995 /* Stringers.swift */,
622624
AE4BA9AC1C88DDB500B73906 /* Errors.swift */,
623625
0477153423B740AD00402D4E /* NimbleTimeInterval.swift */,
626+
89E5E1672BC78724002D54ED /* LockedContainer.swift */,
624627
);
625628
path = Utils;
626629
sourceTree = "<group>";
@@ -855,6 +858,7 @@
855858
1F1871D91CA89EF100A34BF2 /* NMBExpectation.swift in Sources */,
856859
DA9E8C831A414BB9002633C2 /* DSL+Wait.swift in Sources */,
857860
DDB1BC7A1A92235600F743C3 /* AllPass.swift in Sources */,
861+
89E5E1682BC78724002D54ED /* LockedContainer.swift in Sources */,
858862
1FD8CD3F1968AB07008ED995 /* BeAKindOf.swift in Sources */,
859863
1FD8CD2F1968AB07008ED995 /* AssertionRecorder.swift in Sources */,
860864
7B13BA061DD360AA00C9098C /* ContainElementSatisfying.swift in Sources */,
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/// Protocol for the assertion handler that Nimble uses for all expectations.
2-
public protocol AssertionHandler {
2+
public protocol AssertionHandler: Sendable {
33
func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation)
44
}
55

@@ -10,7 +10,20 @@ public protocol AssertionHandler {
1010
/// before using any matchers, otherwise Nimble will abort the program.
1111
///
1212
/// @see AssertionHandler
13-
public var NimbleAssertionHandler: AssertionHandler = { () -> AssertionHandler in
13+
public var NimbleAssertionHandler: AssertionHandler {
1414
// swiftlint:disable:previous identifier_name
15-
return isXCTestAvailable() ? NimbleXCTestHandler() : NimbleXCTestUnavailableHandler()
16-
}()
15+
get {
16+
_NimbleAssertionHandler.value
17+
}
18+
set {
19+
_NimbleAssertionHandler.set(newValue)
20+
}
21+
}
22+
private let _NimbleAssertionHandler = LockedContainer<AssertionHandler> {
23+
// swiftlint:disable:previous identifier_name
24+
if isXCTestAvailable() {
25+
return NimbleXCTestHandler() as AssertionHandler
26+
} else {
27+
return NimbleXCTestUnavailableHandler() as AssertionHandler
28+
}
29+
}

Sources/Nimble/Adapters/AssertionDispatcher.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
/// @warning Does not fully dispatch if one of the handlers raises an exception.
55
/// This is possible with XCTest-based assertion handlers.
66
///
7-
public class AssertionDispatcher: AssertionHandler {
7+
public final class AssertionDispatcher: AssertionHandler {
88
let handlers: [AssertionHandler]
99

1010
public init(handlers: [AssertionHandler]) {

Sources/Nimble/Adapters/AssertionRecorder.swift

+11-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
///
44
/// @see AssertionRecorder
55
/// @see AssertionHandler
6-
public struct AssertionRecord: CustomStringConvertible {
6+
public struct AssertionRecord: CustomStringConvertible, Sendable {
77
/// Whether the assertion succeeded or failed
88
public let success: Bool
99
/// The failure message the assertion would display on failure.
@@ -20,9 +20,17 @@ public struct AssertionRecord: CustomStringConvertible {
2020
/// This is useful for testing failure messages for matchers.
2121
///
2222
/// @see AssertionHandler
23-
public class AssertionRecorder: AssertionHandler {
23+
public final class AssertionRecorder: AssertionHandler {
2424
/// All the assertions that were captured by this recorder
25-
public var assertions = [AssertionRecord]()
25+
public var assertions: [AssertionRecord] {
26+
get {
27+
_assertion.value
28+
}
29+
set {
30+
_assertion.set(newValue)
31+
}
32+
}
33+
private let _assertion = LockedContainer([AssertionRecord]())
2634

2735
public init() {}
2836

Sources/Nimble/Adapters/NimbleXCTestHandler.swift

+20-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import XCTest
33

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

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

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

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

43-
private(set) var currentTestCase: XCTestCase?
43+
private let lock = NSRecursiveLock()
44+
45+
private var _currentTestCase: XCTestCase?
46+
var currentTestCase: XCTestCase? {
47+
lock.lock()
48+
defer { lock.unlock() }
49+
return _currentTestCase
50+
}
4451

4552
private var stashed_swift_reportFatalErrorsToDebugger: Bool = false
4653

4754
@objc public func testCaseWillStart(_ testCase: XCTestCase) {
55+
lock.lock()
56+
defer { lock.unlock() }
57+
4858
#if (os(macOS) || os(iOS) || os(visionOS)) && !SWIFT_PACKAGE
4959
stashed_swift_reportFatalErrorsToDebugger = _swift_reportFatalErrorsToDebugger
5060
_swift_reportFatalErrorsToDebugger = false
5161
#endif
5262

53-
currentTestCase = testCase
63+
_currentTestCase = testCase
5464
}
5565

5666
@objc public func testCaseDidFinish(_ testCase: XCTestCase) {
57-
currentTestCase = nil
67+
lock.lock()
68+
defer { lock.unlock() }
69+
70+
_currentTestCase = nil
5871

5972
#if (os(macOS) || os(iOS) || os(visionOS)) && !SWIFT_PACKAGE
6073
_swift_reportFatalErrorsToDebugger = stashed_swift_reportFatalErrorsToDebugger
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Foundation
2+
3+
final class LockedContainer<T: Sendable>: @unchecked Sendable {
4+
private let lock = NSRecursiveLock()
5+
private var _value: T
6+
7+
var value: T {
8+
lock.lock()
9+
defer { lock.unlock() }
10+
return _value
11+
}
12+
13+
init(_ value: T) {
14+
_value = value
15+
}
16+
17+
init(_ closure: () -> T) {
18+
_value = closure()
19+
}
20+
21+
func operate(_ closure: (T) -> T) {
22+
lock.lock()
23+
defer { lock.unlock() }
24+
_value = closure(_value)
25+
}
26+
27+
func set(_ newValue: T) {
28+
lock.lock()
29+
defer { lock.unlock() }
30+
_value = newValue
31+
}
32+
}

0 commit comments

Comments
 (0)