Skip to content

Commit

Permalink
Merge pull request Quick#224 from jeffh/stable-async
Browse files Browse the repository at this point in the history
Stabilize Asynchronous Expectations
  • Loading branch information
jeffh committed Jan 18, 2016
2 parents d3eb9be + 28393ae commit eb2bfc7
Show file tree
Hide file tree
Showing 15 changed files with 661 additions and 154 deletions.
24 changes: 16 additions & 8 deletions Nimble.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
1F5DF1891BDCA0F500C3A531 /* RaisesException.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD1E1968AB07008ED995 /* RaisesException.swift */; };
1F5DF18A1BDCA0F500C3A531 /* ThrowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EA59651B551EE6002D767E /* ThrowError.swift */; };
1F5DF18B1BDCA0F500C3A531 /* Functional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD251968AB07008ED995 /* Functional.swift */; };
1F5DF18C1BDCA0F500C3A531 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD261968AB07008ED995 /* Poll.swift */; };
1F5DF18C1BDCA0F500C3A531 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD261968AB07008ED995 /* Async.swift */; };
1F5DF18D1BDCA0F500C3A531 /* SourceLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD271968AB07008ED995 /* SourceLocation.swift */; };
1F5DF18E1BDCA0F500C3A531 /* Stringers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD281968AB07008ED995 /* Stringers.swift */; };
1F5DF18F1BDCA0F500C3A531 /* AsyncMatcherWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD2A1968AB07008ED995 /* AsyncMatcherWrapper.swift */; };
Expand Down Expand Up @@ -173,6 +173,9 @@
1F9DB8FC1A74E793002E96AD /* ObjCBeEmptyTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F9DB8FA1A74E793002E96AD /* ObjCBeEmptyTest.m */; };
1FB90098195EC4B8001D7FAE /* BeIdenticalToTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB90097195EC4B8001D7FAE /* BeIdenticalToTest.swift */; };
1FB90099195EC4B8001D7FAE /* BeIdenticalToTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB90097195EC4B8001D7FAE /* BeIdenticalToTest.swift */; };
1FC494AA1C29CBA40010975C /* NimbleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC494A91C29CBA40010975C /* NimbleEnvironment.swift */; };
1FC494AB1C29CBA40010975C /* NimbleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC494A91C29CBA40010975C /* NimbleEnvironment.swift */; };
1FC494AC1C29CBA40010975C /* NimbleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC494A91C29CBA40010975C /* NimbleEnvironment.swift */; };
1FD8CD2E1968AB07008ED995 /* AssertionRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD051968AB07008ED995 /* AssertionRecorder.swift */; };
1FD8CD2F1968AB07008ED995 /* AssertionRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD051968AB07008ED995 /* AssertionRecorder.swift */; };
1FD8CD301968AB07008ED995 /* AdapterProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD061968AB07008ED995 /* AdapterProtocols.swift */; };
Expand Down Expand Up @@ -229,8 +232,8 @@
1FD8CD651968AB07008ED995 /* NMBExceptionCapture.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FD8CD221968AB07008ED995 /* NMBExceptionCapture.h */; settings = {ATTRIBUTES = (Public, ); }; };
1FD8CD661968AB07008ED995 /* NMBExceptionCapture.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD231968AB07008ED995 /* NMBExceptionCapture.m */; };
1FD8CD671968AB07008ED995 /* NMBExceptionCapture.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD231968AB07008ED995 /* NMBExceptionCapture.m */; };
1FD8CD6A1968AB07008ED995 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD261968AB07008ED995 /* Poll.swift */; };
1FD8CD6B1968AB07008ED995 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD261968AB07008ED995 /* Poll.swift */; };
1FD8CD6A1968AB07008ED995 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD261968AB07008ED995 /* Async.swift */; };
1FD8CD6B1968AB07008ED995 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD261968AB07008ED995 /* Async.swift */; };
1FD8CD701968AB07008ED995 /* AsyncMatcherWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD2A1968AB07008ED995 /* AsyncMatcherWrapper.swift */; };
1FD8CD711968AB07008ED995 /* AsyncMatcherWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD2A1968AB07008ED995 /* AsyncMatcherWrapper.swift */; };
1FD8CD741968AB07008ED995 /* MatcherFunc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD8CD2C1968AB07008ED995 /* MatcherFunc.swift */; };
Expand Down Expand Up @@ -398,6 +401,7 @@
1F925F10195C190B00ED456B /* BeGreaterThanOrEqualToTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BeGreaterThanOrEqualToTest.swift; sourceTree = "<group>"; };
1F9DB8FA1A74E793002E96AD /* ObjCBeEmptyTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjCBeEmptyTest.m; sourceTree = "<group>"; };
1FB90097195EC4B8001D7FAE /* BeIdenticalToTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BeIdenticalToTest.swift; sourceTree = "<group>"; };
1FC494A91C29CBA40010975C /* NimbleEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NimbleEnvironment.swift; sourceTree = "<group>"; };
1FD8CD051968AB07008ED995 /* AssertionRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssertionRecorder.swift; sourceTree = "<group>"; };
1FD8CD061968AB07008ED995 /* AdapterProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterProtocols.swift; sourceTree = "<group>"; };
1FD8CD071968AB07008ED995 /* NimbleXCTestHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NimbleXCTestHandler.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -427,7 +431,7 @@
1FD8CD221968AB07008ED995 /* NMBExceptionCapture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NMBExceptionCapture.h; sourceTree = "<group>"; };
1FD8CD231968AB07008ED995 /* NMBExceptionCapture.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NMBExceptionCapture.m; sourceTree = "<group>"; };
1FD8CD251968AB07008ED995 /* Functional.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Functional.swift; sourceTree = "<group>"; };
1FD8CD261968AB07008ED995 /* Poll.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Poll.swift; sourceTree = "<group>"; };
1FD8CD261968AB07008ED995 /* Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Async.swift; sourceTree = "<group>"; };
1FD8CD271968AB07008ED995 /* SourceLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SourceLocation.swift; sourceTree = "<group>"; };
1FD8CD281968AB07008ED995 /* Stringers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stringers.swift; sourceTree = "<group>"; };
1FD8CD2A1968AB07008ED995 /* AsyncMatcherWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncMatcherWrapper.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -622,6 +626,7 @@
1FD8CD061968AB07008ED995 /* AdapterProtocols.swift */,
1FD8CD071968AB07008ED995 /* NimbleXCTestHandler.swift */,
1FDBD8661AF8A4FF0089F27B /* AssertionDispatcher.swift */,
1FC494A91C29CBA40010975C /* NimbleEnvironment.swift */,
);
path = Adapters;
sourceTree = "<group>";
Expand Down Expand Up @@ -670,7 +675,7 @@
isa = PBXGroup;
children = (
1FD8CD251968AB07008ED995 /* Functional.swift */,
1FD8CD261968AB07008ED995 /* Poll.swift */,
1FD8CD261968AB07008ED995 /* Async.swift */,
1FD8CD271968AB07008ED995 /* SourceLocation.swift */,
1FD8CD281968AB07008ED995 /* Stringers.swift */,
);
Expand Down Expand Up @@ -1000,6 +1005,7 @@
1FD8CD4C1968AB07008ED995 /* BeLessThan.swift in Sources */,
1FD8CD461968AB07008ED995 /* BeGreaterThan.swift in Sources */,
1FD8CD301968AB07008ED995 /* AdapterProtocols.swift in Sources */,
1FC494AA1C29CBA40010975C /* NimbleEnvironment.swift in Sources */,
1FD8CD5E1968AB07008ED995 /* RaisesException.swift in Sources */,
1FD8CD561968AB07008ED995 /* Contain.swift in Sources */,
1FD8CD481968AB07008ED995 /* BeGreaterThanOrEqualTo.swift in Sources */,
Expand All @@ -1009,7 +1015,7 @@
1FD8CD621968AB07008ED995 /* DSL.m in Sources */,
1FD8CD421968AB07008ED995 /* BeEmpty.swift in Sources */,
1FD8CD521968AB07008ED995 /* BeNil.swift in Sources */,
1FD8CD6A1968AB07008ED995 /* Poll.swift in Sources */,
1FD8CD6A1968AB07008ED995 /* Async.swift in Sources */,
1FD8CD581968AB07008ED995 /* EndWith.swift in Sources */,
1FD8CD5C1968AB07008ED995 /* MatcherProtocols.swift in Sources */,
1FD8CD761968AB07008ED995 /* ObjCMatcher.swift in Sources */,
Expand Down Expand Up @@ -1108,13 +1114,14 @@
1F5DF18F1BDCA0F500C3A531 /* AsyncMatcherWrapper.swift in Sources */,
1F5DF1711BDCA0F500C3A531 /* DSL+Wait.swift in Sources */,
1F5DF17D1BDCA0F500C3A531 /* BeGreaterThanOrEqualTo.swift in Sources */,
1FC494AC1C29CBA40010975C /* NimbleEnvironment.swift in Sources */,
1F5DF18E1BDCA0F500C3A531 /* Stringers.swift in Sources */,
1F5DF1AD1BDCA16E00C3A531 /* NMBExceptionCapture.m in Sources */,
1F5DF16D1BDCA0F500C3A531 /* AdapterProtocols.swift in Sources */,
1F5DF17B1BDCA0F500C3A531 /* BeginWith.swift in Sources */,
1F5DF17E1BDCA0F500C3A531 /* BeIdenticalTo.swift in Sources */,
1F5DF17A1BDCA0F500C3A531 /* BeEmpty.swift in Sources */,
1F5DF18C1BDCA0F500C3A531 /* Poll.swift in Sources */,
1F5DF18C1BDCA0F500C3A531 /* Async.swift in Sources */,
1F5DF1821BDCA0F500C3A531 /* BeNil.swift in Sources */,
1F5DF1AC1BDCA16E00C3A531 /* DSL.m in Sources */,
1F5DF16F1BDCA0F500C3A531 /* AssertionDispatcher.swift in Sources */,
Expand Down Expand Up @@ -1190,6 +1197,7 @@
1FD8CD4D1968AB07008ED995 /* BeLessThan.swift in Sources */,
1FD8CD471968AB07008ED995 /* BeGreaterThan.swift in Sources */,
1FD8CD311968AB07008ED995 /* AdapterProtocols.swift in Sources */,
1FC494AB1C29CBA40010975C /* NimbleEnvironment.swift in Sources */,
1FD8CD5F1968AB07008ED995 /* RaisesException.swift in Sources */,
1FD8CD571968AB07008ED995 /* Contain.swift in Sources */,
1FD8CD491968AB07008ED995 /* BeGreaterThanOrEqualTo.swift in Sources */,
Expand All @@ -1199,7 +1207,7 @@
1FD8CD631968AB07008ED995 /* DSL.m in Sources */,
1FD8CD431968AB07008ED995 /* BeEmpty.swift in Sources */,
1FD8CD531968AB07008ED995 /* BeNil.swift in Sources */,
1FD8CD6B1968AB07008ED995 /* Poll.swift in Sources */,
1FD8CD6B1968AB07008ED995 /* Async.swift in Sources */,
1FD8CD591968AB07008ED995 /* EndWith.swift in Sources */,
1FD8CD5D1968AB07008ED995 /* MatcherProtocols.swift in Sources */,
1FD8CD771968AB07008ED995 /* ObjCMatcher.swift in Sources */,
Expand Down
9 changes: 5 additions & 4 deletions Nimble/Adapters/AssertionRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ public class AssertionRecorder : AssertionHandler {
///
/// @see AssertionHandler
public func withAssertionHandler(tempAssertionHandler: AssertionHandler, closure: () throws -> Void) {
let oldRecorder = NimbleAssertionHandler
let environment = NimbleEnvironment.activeInstance
let oldRecorder = environment.assertionHandler
let capturer = NMBExceptionCapture(handler: nil, finally: ({
NimbleAssertionHandler = oldRecorder
environment.assertionHandler = oldRecorder
}))
NimbleAssertionHandler = tempAssertionHandler
environment.assertionHandler = tempAssertionHandler
capturer.tryBlock {
try! closure()
}
Expand All @@ -65,7 +66,7 @@ public func withAssertionHandler(tempAssertionHandler: AssertionHandler, closure
///
/// @see gatherFailingExpectations
public func gatherExpectations(silently silently: Bool = false, closure: () -> Void) -> [AssertionRecord] {
let previousRecorder = NimbleAssertionHandler
let previousRecorder = NimbleEnvironment.activeInstance.assertionHandler
let recorder = AssertionRecorder()
let handlers: [AssertionHandler]

Expand Down
35 changes: 35 additions & 0 deletions Nimble/Adapters/NimbleEnvironment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation

/// "Global" state of Nimble is stored here. Only DSL functions should access / be aware of this
/// class' existance
internal class NimbleEnvironment {
static var activeInstance: NimbleEnvironment {
get {
let env = NSThread.currentThread().threadDictionary["NimbleEnvironment"]
if let env = env as? NimbleEnvironment {
return env
} else {
let newEnv = NimbleEnvironment()
self.activeInstance = newEnv
return newEnv
}
}
set {
NSThread.currentThread().threadDictionary["NimbleEnvironment"] = newValue
}
}

// TODO: eventually migrate the global to this environment value
var assertionHandler: AssertionHandler {
get { return NimbleAssertionHandler }
set { NimbleAssertionHandler = newValue }
}
var awaiter: Awaiter

init() {
awaiter = Awaiter(
waitLock: AssertionWaitLock(),
asyncQueue: dispatch_get_main_queue(),
timeoutQueue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0))
}
}
92 changes: 70 additions & 22 deletions Nimble/DSL+Wait.swift
Original file line number Diff line number Diff line change
@@ -1,32 +1,72 @@
import Foundation

private enum ErrorResult {
case Exception(NSException)
case Error(ErrorType)
case None
}

/// Only classes, protocols, methods, properties, and subscript declarations can be
/// bridges to Objective-C via the @objc keyword. This class encapsulates callback-style
/// asynchronous waiting logic so that it may be called from Objective-C and Swift.
internal class NMBWait: NSObject {
internal class func until(timeout timeout: NSTimeInterval, file: String = __FILE__, line: UInt = __LINE__, action: (() -> Void) -> Void) -> Void {
var completed = false
var token: dispatch_once_t = 0
let result = pollBlock(pollInterval: 0.01, timeoutInterval: timeout) {
dispatch_once(&token) {
internal class func until(
timeout timeout: NSTimeInterval,
file: String = __FILE__,
line: UInt = __LINE__,
action: (() -> Void) -> Void) -> Void {
return throwableUntil(timeout: timeout, file: file, line: line) { (done: () -> Void) throws -> Void in
action() { done() }
}
}

// Using a throwable closure makes this method not objc compatible.
internal class func throwableUntil(
timeout timeout: NSTimeInterval,
file: String = __FILE__,
line: UInt = __LINE__,
action: (() -> Void) throws -> Void) -> Void {
let awaiter = NimbleEnvironment.activeInstance.awaiter
let leeway = timeout / 2.0
let result = awaiter.performBlock { (done: (ErrorResult) -> Void) throws -> Void in
dispatch_async(dispatch_get_main_queue()) {
action() { completed = true }
let capture = NMBExceptionCapture(
handler: ({ exception in
done(.Exception(exception))
}),
finally: ({ })
)
capture.tryBlock {
do {
try action() {
done(.None)
}
} catch let e {
done(.Error(e))
}
}
}
}.timeout(timeout, forcefullyAbortTimeout: leeway).wait("waitUntil(...)", file: file, line: line)

switch result {
case .Incomplete: internalError("Reached .Incomplete state for waitUntil(...).")
case .BlockedRunLoop:
fail(blockedRunLoopErrorMessageFor("-waitUntil()", leeway: leeway),
file: file, line: line)
case .TimedOut:
let pluralize = (timeout == 1 ? "" : "s")
fail("Waited more than \(timeout) second\(pluralize)", file: file, line: line)
case let .RaisedException(exception):
fail("Unexpected exception raised: \(exception)")
case let .ErrorThrown(error):
fail("Unexpected error thrown: \(error)")
case .Completed(.Exception(let exception)):
fail("Unexpected exception raised: \(exception)")
case .Completed(.Error(let error)):
fail("Unexpected error thrown: \(error)")
case .Completed(.None): // success
break
}
return completed
}
switch (result) {
case .Failure:
let pluralize = (timeout == 1 ? "" : "s")
fail("Waited more than \(timeout) second\(pluralize)", file: file, line: line)
case .Timeout:
fail("Stall on main thread - too much enqueued on main run loop before waitUntil executes.", file: file, line: line)
case let .ErrorThrown(error):
// Technically, we can never reach this via a public API call
fail("Unexpected error thrown: \(error)", file: file, line: line)
case .Success:
break
}
}

@objc(untilFile:line:action:)
Expand All @@ -35,9 +75,17 @@ internal class NMBWait: NSObject {
}
}

/// Wait asynchronously until the done closure is called.
internal func blockedRunLoopErrorMessageFor(fnName: String, leeway: NSTimeInterval) -> String {
return "\(fnName) timed out but was unable to run the timeout handler because the main thread is unresponsive (\(leeway) seconds is allow after the wait times out). Conditions that may cause this include processing blocking IO on the main thread, calls to sleep(), deadlocks, and synchronous IPC. Nimble forcefully stopped run loop which may cause future failures in test run."
}

/// Wait asynchronously until the done closure is called or the timeout has been reached.
///
/// This will advance the run loop.
/// @discussion
/// Call the done() closure to indicate the waiting has completed.
///
/// This function manages the main run loop (`NSRunLoop.mainRunLoop()`) while this function
/// is executing. Any attempts to touch the run loop may cause non-deterministic behavior.
public func waitUntil(timeout timeout: NSTimeInterval = 1, file: String = __FILE__, line: UInt = __LINE__, action: (() -> Void) -> Void) -> Void {
NMBWait.until(timeout: timeout, file: file, line: line, action: action)
}
Loading

0 comments on commit eb2bfc7

Please sign in to comment.