diff --git a/Nimble.xcodeproj/project.pbxproj b/Nimble.xcodeproj/project.pbxproj index 78e685c35..17fa52ecb 100644 --- a/Nimble.xcodeproj/project.pbxproj +++ b/Nimble.xcodeproj/project.pbxproj @@ -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 */; }; @@ -331,6 +332,7 @@ 899441F32902EF0900C1FAF9 /* DSL+AsyncAwait.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DSL+AsyncAwait.swift"; sourceTree = ""; }; 89C297CB2A911CDA002A143F /* AsyncTimerSequenceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncTimerSequenceTest.swift; sourceTree = ""; }; 89C297CD2A92AB34002A143F /* AsyncPromiseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncPromiseTest.swift; sourceTree = ""; }; + 89E5E1672BC78724002D54ED /* LockedContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedContainer.swift; sourceTree = ""; }; 89EEF5A42A03293100988224 /* AsyncMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncMatcher.swift; sourceTree = ""; }; 89EEF5B22A032C2500988224 /* AsyncPredicateTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncPredicateTest.swift; sourceTree = ""; }; 89EEF5BB2A06210D00988224 /* AsyncHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHelpers.swift; sourceTree = ""; }; @@ -621,6 +623,7 @@ 1FD8CD281968AB07008ED995 /* Stringers.swift */, AE4BA9AC1C88DDB500B73906 /* Errors.swift */, 0477153423B740AD00402D4E /* NimbleTimeInterval.swift */, + 89E5E1672BC78724002D54ED /* LockedContainer.swift */, ); path = Utils; sourceTree = ""; @@ -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 */, diff --git a/Sources/Nimble/Adapters/AdapterProtocols.swift b/Sources/Nimble/Adapters/AdapterProtocols.swift index a28bf2be5..42e96538f 100644 --- a/Sources/Nimble/Adapters/AdapterProtocols.swift +++ b/Sources/Nimble/Adapters/AdapterProtocols.swift @@ -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) } @@ -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 { + // swiftlint:disable:previous identifier_name + if isXCTestAvailable() { + return NimbleXCTestHandler() as AssertionHandler + } else { + return NimbleXCTestUnavailableHandler() as AssertionHandler + } +} diff --git a/Sources/Nimble/Adapters/AssertionDispatcher.swift b/Sources/Nimble/Adapters/AssertionDispatcher.swift index 94a9030eb..cf0b8ac77 100644 --- a/Sources/Nimble/Adapters/AssertionDispatcher.swift +++ b/Sources/Nimble/Adapters/AssertionDispatcher.swift @@ -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]) { diff --git a/Sources/Nimble/Adapters/AssertionRecorder.swift b/Sources/Nimble/Adapters/AssertionRecorder.swift index 239393eda..874593ba7 100644 --- a/Sources/Nimble/Adapters/AssertionRecorder.swift +++ b/Sources/Nimble/Adapters/AssertionRecorder.swift @@ -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. @@ -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() {} diff --git a/Sources/Nimble/Adapters/NimbleXCTestHandler.swift b/Sources/Nimble/Adapters/NimbleXCTestHandler.swift index 286ae7ea6..a6a43d82b 100644 --- a/Sources/Nimble/Adapters/NimbleXCTestHandler.swift +++ b/Sources/Nimble/Adapters/NimbleXCTestHandler.swift @@ -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) @@ -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 @@ -29,7 +29,7 @@ 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.") } @@ -37,24 +37,37 @@ class NimbleXCTestUnavailableHandler: AssertionHandler { #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 diff --git a/Sources/Nimble/Utils/LockedContainer.swift b/Sources/Nimble/Utils/LockedContainer.swift new file mode 100644 index 000000000..fcc8455ba --- /dev/null +++ b/Sources/Nimble/Utils/LockedContainer.swift @@ -0,0 +1,32 @@ +import Foundation + +final class LockedContainer: @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 + } +}