Skip to content
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

Enable concurrency checking #1040

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Nimble.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2304,6 +2304,7 @@
);
SDKROOT = macosx;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_SWIFT3_OBJC_INFERENCE = Off;
SWIFT_VERSION = 5.0;
TVOS_DEPLOYMENT_TARGET = 13.0;
Expand Down Expand Up @@ -2372,6 +2373,7 @@
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_SWIFT3_OBJC_INFERENCE = Off;
SWIFT_VERSION = 5.0;
TVOS_DEPLOYMENT_TARGET = 13.0;
Expand Down
2 changes: 1 addition & 1 deletion 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 Down
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
4 changes: 1 addition & 3 deletions Sources/Nimble/Adapters/AssertionRecorder+Async.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler,
closure: () async throws -> Void) async {
let environment = NimbleEnvironment.activeInstance
let oldRecorder = environment.assertionHandler
let capturer = NMBExceptionCapture(handler: nil, finally: ({
environment.assertionHandler = oldRecorder
}))
environment.assertionHandler = tempAssertionHandler

do {
Expand All @@ -26,6 +23,7 @@ public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler,
let location = SourceLocation(file: file, line: line)
tempAssertionHandler.assert(false, message: failureMessage, location: location)
}
environment.assertionHandler = oldRecorder
}

/// Captures expectations that occur in the given closure. Note that all
Expand Down
2 changes: 1 addition & 1 deletion Sources/Nimble/Adapters/AssertionRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ 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]()

Expand Down
6 changes: 3 additions & 3 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, Sendable {
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, Sendable {
public func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) {
if !assertion {
let msg: String
Expand All @@ -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, Sendable {
func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) {
fatalError("XCTest is not available and no custom assertion handler was configured. Aborting.")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class NSException {}
/// A dummy reimplementation of the `NMBExceptionCapture` class to serve
/// as a stand-in for build and runtime environments that don't support
/// Objective C.
internal class ExceptionCapture {
internal final class ExceptionCapture: Sendable {
let finally: (() -> Void)?

init(handler: ((NSException) -> Void)?, finally: (() -> Void)?) {
Expand Down
36 changes: 24 additions & 12 deletions Sources/Nimble/AsyncExpression.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
// Memoizes the given closure, only calling the passed
// closure once; even if repeat calls to the returned closure
private func memoizedClosure<T>(_ closure: @escaping () async throws -> T) -> (Bool) async throws -> T {
public actor MemoizedClosure<T: Sendable> {
var cache: T?
return { withoutCaching in
if withoutCaching || cache == nil {
cache = try await closure()
private let closure: @Sendable (Bool) async throws -> T?

init(_ closure: @escaping @Sendable () async throws -> T?) {
self.closure = { _ in try await closure() }
}

init(prememoized: @escaping @Sendable (Bool) async throws -> T?) {
self.closure = prememoized
}

public func evaluate(_ withoutCaching: Bool) async throws -> T? {
if withoutCaching == false, let cache {
return cache
}
return cache!
let value = try await closure(withoutCaching)
cache = value
return value
}
}

Expand All @@ -21,8 +33,8 @@ private func memoizedClosure<T>(_ closure: @escaping () async throws -> T) -> (B
///
/// This provides a common consumable API for matchers to utilize to allow
/// Nimble to change internals to how the captured closure is managed.
public struct AsyncExpression<Value> {
internal let _expression: (Bool) async throws -> Value?
public struct AsyncExpression<Value: Sendable>: Sendable {
internal let _expression: MemoizedClosure<Value>
internal let _withoutCaching: Bool
public let location: SourceLocation
public let isClosure: Bool
Expand All @@ -38,8 +50,8 @@ public struct AsyncExpression<Value> {
/// requires an explicit closure. This gives Nimble
/// flexibility if @autoclosure behavior changes between
/// Swift versions. Nimble internals always sets this true.
public init(expression: @escaping () async throws -> Value?, location: SourceLocation, isClosure: Bool = true) {
self._expression = memoizedClosure(expression)
public init(expression: @escaping @Sendable () async throws -> Value?, location: SourceLocation, isClosure: Bool = true) {
self._expression = MemoizedClosure<Value>(expression)
self.location = location
self._withoutCaching = false
self.isClosure = isClosure
Expand All @@ -59,7 +71,7 @@ public struct AsyncExpression<Value> {
/// requires an explicit closure. This gives Nimble
/// flexibility if @autoclosure behavior changes between
/// Swift versions. Nimble internals always sets this true.
public init(memoizedExpression: @escaping (Bool) async throws -> Value?, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) {
public init(memoizedExpression: MemoizedClosure<Value>, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) {
self._expression = memoizedExpression
self.location = location
self._withoutCaching = withoutCaching
Expand All @@ -70,7 +82,7 @@ public struct AsyncExpression<Value> {
public func toSynchronousExpression() async -> Expression<Value> {
let value: Result<Value?, Error>
do {
value = .success(try await _expression(self._withoutCaching))
value = .success(try await _expression.evaluate(self._withoutCaching))
} catch {
value = .failure(error)
}
Expand Down Expand Up @@ -99,7 +111,7 @@ public struct AsyncExpression<Value> {
}

public func evaluate() async throws -> Value? {
return try await self._expression(_withoutCaching)
return try await self._expression.evaluate(_withoutCaching)
}

public func withoutCaching() -> AsyncExpression<Value> {
Expand Down
28 changes: 14 additions & 14 deletions Sources/Nimble/DSL+AsyncAwait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Dispatch
#endif

/// Make an ``AsyncExpectation`` on a given actual value. The value given is lazily evaluated.
public func expect<T>(file: FileString = #file, line: UInt = #line, _ expression: @escaping () async throws -> T?) -> AsyncExpectation<T> {
public func expect<T: Sendable>(file: FileString = #file, line: UInt = #line, _ expression: @escaping @Sendable () async throws -> T?) -> AsyncExpectation<T> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression,
Expand All @@ -12,7 +12,7 @@ public func expect<T>(file: FileString = #file, line: UInt = #line, _ expression
}

/// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked.
public func expect<T>(file: FileString = #file, line: UInt = #line, _ expression: () -> (() async throws -> T)) -> AsyncExpectation<T> {
public func expect<T: Sendable>(file: FileString = #file, line: UInt = #line, _ expression: () -> (@Sendable () async throws -> T)) -> AsyncExpectation<T> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression(),
Expand All @@ -21,7 +21,7 @@ public func expect<T>(file: FileString = #file, line: UInt = #line, _ expression
}

/// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked.
public func expect<T>(file: FileString = #file, line: UInt = #line, _ expression: () -> (() async throws -> T?)) -> AsyncExpectation<T> {
public func expect<T: Sendable>(file: FileString = #file, line: UInt = #line, _ expression: () -> (@Sendable () async throws -> T?)) -> AsyncExpectation<T> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression(),
Expand All @@ -30,7 +30,7 @@ public func expect<T>(file: FileString = #file, line: UInt = #line, _ expression
}

/// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked.
public func expect(file: FileString = #file, line: UInt = #line, _ expression: () -> (() async throws -> Void)) -> AsyncExpectation<Void> {
public func expect(file: FileString = #file, line: UInt = #line, _ expression: () -> (@Sendable () async throws -> Void)) -> AsyncExpectation<Void> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression(),
Expand All @@ -40,7 +40,7 @@ public func expect(file: FileString = #file, line: UInt = #line, _ expression: (

/// Make an ``AsyncExpectation`` on a given actual value. The value given is lazily evaluated.
/// This is provided to avoid confusion between `expect -> SyncExpectation` and `expect -> AsyncExpectation`.
public func expecta<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping () async throws -> T?) async -> AsyncExpectation<T> {
public func expecta<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async -> AsyncExpectation<T> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression,
Expand All @@ -50,7 +50,7 @@ public func expecta<T>(file: FileString = #file, line: UInt = #line, _ expressio

/// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked.
/// This is provided to avoid confusion between `expect -> SyncExpectation` and `expect -> AsyncExpectation`
public func expecta<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() async throws -> T)) async -> AsyncExpectation<T> {
public func expecta<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (@Sendable () async throws -> T)) async -> AsyncExpectation<T> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression(),
Expand All @@ -60,7 +60,7 @@ public func expecta<T>(file: FileString = #file, line: UInt = #line, _ expressio

/// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked.
/// This is provided to avoid confusion between `expect -> SyncExpectation` and `expect -> AsyncExpectation`
public func expecta<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() async throws -> T?)) async -> AsyncExpectation<T> {
public func expecta<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (@Sendable () async throws -> T?)) async -> AsyncExpectation<T> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression(),
Expand All @@ -70,7 +70,7 @@ public func expecta<T>(file: FileString = #file, line: UInt = #line, _ expressio

/// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked.
/// This is provided to avoid confusion between `expect -> SyncExpectation` and `expect -> AsyncExpectation`
public func expecta(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() async throws -> Void)) async -> AsyncExpectation<Void> {
public func expecta(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (@Sendable () async throws -> Void)) async -> AsyncExpectation<Void> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression(),
Expand All @@ -87,7 +87,7 @@ public func expecta(file: FileString = #file, line: UInt = #line, _ expression:
///
/// @warning
/// Unlike the synchronous version of this call, this does not support catching Objective-C exceptions.
public func waitUntil(timeout: DispatchTimeInterval = AsyncDefaults.timeout, file: FileString = #file, line: UInt = #line, action: @escaping (@escaping () -> Void) async -> Void) async {
public func waitUntil(timeout: DispatchTimeInterval = AsyncDefaults.timeout, file: FileString = #file, line: UInt = #line, action: @escaping @Sendable (@escaping () async -> Void) async -> Void) async {
await throwableUntil(timeout: timeout) { done in
await action(done)
}
Expand All @@ -100,7 +100,7 @@ public func waitUntil(timeout: DispatchTimeInterval = AsyncDefaults.timeout, fil
///
/// @warning
/// Unlike the synchronous version of this call, this does not support catching Objective-C exceptions.
public func waitUntil(timeout: DispatchTimeInterval = AsyncDefaults.timeout, file: FileString = #file, line: UInt = #line, action: @escaping (@escaping () -> Void) -> Void) async {
public func waitUntil(timeout: DispatchTimeInterval = AsyncDefaults.timeout, file: FileString = #file, line: UInt = #line, action: @escaping @Sendable (@escaping () async -> Void) -> Void) async {
await throwableUntil(timeout: timeout, file: file, line: line) { done in
action(done)
}
Expand All @@ -115,16 +115,16 @@ private func throwableUntil(
timeout: DispatchTimeInterval,
file: FileString = #file,
line: UInt = #line,
action: @escaping (@escaping () -> Void) async throws -> Void) async {
action: @escaping @Sendable (@escaping () async -> Void) async throws -> Void) async {
let awaiter = NimbleEnvironment.activeInstance.awaiter
let leeway = timeout.divided
let result = await awaiter.performBlock(file: file, line: line) { @MainActor (done: @escaping (ErrorResult) -> Void) async throws -> Void in
let result = await awaiter.performBlock(file: file, line: line) { @MainActor (done: @escaping (ErrorResult) async -> Void) async throws -> Void in
do {
try await action {
done(.none)
await done(.none)
}
} catch let e {
done(.error(e))
await done(.error(e))
}
}
.timeout(timeout, forcefullyAbortTimeout: leeway)
Expand Down
4 changes: 2 additions & 2 deletions Sources/Nimble/Expectation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ internal func execute<T>(_ expression: Expression<T>, _ style: ExpectationStyle,
return result
}

public enum ExpectationStatus: Equatable {
public enum ExpectationStatus: Equatable, Sendable {

/// No predicates have been performed.
case pending
Expand Down Expand Up @@ -197,7 +197,7 @@ public struct SyncExpectation<Value>: Expectation {
// - NMBExpectation for Objective-C interface
}

public struct AsyncExpectation<Value>: Expectation {
public struct AsyncExpectation<Value: Sendable>: Expectation, Sendable {
public let expression: AsyncExpression<Value>

/// The status of the test after predicates have been evaluated.
Expand Down
2 changes: 1 addition & 1 deletion Sources/Nimble/ExpectationMessage.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
public indirect enum ExpectationMessage {
public indirect enum ExpectationMessage: Sendable {
// --- Primary Expectations ---
/// includes actual value in output ("expected to <message>, got <actual>")
case expectedActualValueTo(/* message: */ String)
Expand Down
12 changes: 7 additions & 5 deletions Sources/Nimble/Matchers/PostNotification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,13 @@ private func _postNotifications<Out>(
actualValue = "<\(stringify(collector.observedNotifications))>"
}

var result = try predicate.satisfies(collectorNotificationsExpression)
result.message = result.message.replacedExpectation { message in
return .expectedCustomValueTo(message.expectedMessage, actual: actualValue)
}
return result
let predicateResult = try predicate.satisfies(collectorNotificationsExpression)
return PredicateResult(
status: predicateResult.status,
message: predicateResult.message.replacedExpectation { message in
return .expectedCustomValueTo(message.expectedMessage, actual: actualValue)
}
)
}
}

Expand Down
14 changes: 7 additions & 7 deletions Sources/Nimble/Matchers/Predicate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
/// The Predicate provide the heavy lifting on how to assert against a given value. Internally,
/// predicates are simple wrappers around closures to provide static type information and
/// allow composition and wrapping of existing behaviors.
public struct Predicate<T> {
fileprivate var matcher: (Expression<T>) throws -> PredicateResult
public struct Predicate<T>: Sendable {
fileprivate let matcher: (Expression<T>) throws -> PredicateResult

/// Constructs a predicate that knows how take a given value
public init(_ matcher: @escaping (Expression<T>) throws -> PredicateResult) {
Expand Down Expand Up @@ -82,17 +82,17 @@ extension Predicate {
}

// The Expectation style intended for comparison to a PredicateStatus.
public enum ExpectationStyle {
public enum ExpectationStyle: Sendable {
case toMatch, toNotMatch
}

/// The value that a Predicates return to describe if the given (actual) value matches the
/// predicate.
public struct PredicateResult {
public struct PredicateResult: Sendable {
/// Status indicates if the predicate matches, does not match, or fails.
public var status: PredicateStatus
public let status: PredicateStatus
/// The error message that can be displayed if it does not match
public var message: ExpectationMessage
public let message: ExpectationMessage

/// Constructs a new PredicateResult with a given status and error message
public init(status: PredicateStatus, message: ExpectationMessage) {
Expand All @@ -113,7 +113,7 @@ public struct PredicateResult {
}

/// PredicateStatus is a trinary that indicates if a Predicate matches a given value or not
public enum PredicateStatus {
public enum PredicateStatus: Sendable {
/// Matches indicates if the predicate / matcher passes with the given value
///
/// For example, `equals(1)` returns `.matches` for `expect(1).to(equal(1))`.
Expand Down
Loading