diff --git a/CHANGELOG.md b/CHANGELOG.md index ce8af62..e9708d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -614,4 +614,11 @@ autorelease pool. ### Changes - Class method `all:` and `allSettled:` now fulfill the returned promise with an empty `NSArray` if no promise are given, instead of rejecting the promise. + + +## Version 0.13.2 beta (2016-03-09) + +### Changes + +- Progress reporting for RXPromise is implemented \ No newline at end of file diff --git a/RXPromise Libraries/Source/RXPromise.h b/RXPromise Libraries/Source/RXPromise.h index 803d92c..ec3a554 100644 --- a/RXPromise Libraries/Source/RXPromise.h +++ b/RXPromise Libraries/Source/RXPromise.h @@ -215,6 +215,22 @@ typedef id (^promise_completionHandler_t)(id result) /*NS_RETURNS_RETAINED*/; */ typedef id (^promise_errorHandler_t)(NSError* error) /*NS_RETURNS_RETAINED*/; +/*! + @brief Type definition for the promise progress block. + + @discussion The progress handler will be invoked when the associated promise progress will be changed. + + @par The block will report a progress of an operation to client + + @note The execution context is either the execution context specified + when the handlers have been registered via property \p progressOn or it is + unspecified when registred via \p progress. + + @param progress The value set by the "asynchronous result provider" when it progressed. + + */ +typedef void (^promise_progressHandler_t)(float progress); + /*! @brief Type definition of the "then block". The "then block" is the return value of the property \p then. @@ -294,6 +310,51 @@ typedef RXPromise *(^catch_on_block_t)(id, promise_errorHandler_t errorHandler); */ typedef RXPromise* (^catch_on_main_block_t)(promise_errorHandler_t); +/*! + @brief Type definition of the "progress_block_t". The "progress_block_t" is the return + value of the property \p progress. + + @discussion The "progress_block_t" has one parameter, the progress handler block. + The handler may be \c nil. + + @par The "progress_block_t" returns a promise, the "returned promise". When the parent + promise will be resolved the error handler (if not \c nil) will be invoked if the promise + is rejected and its return value resolves the "returned promise" (if it exists). Otherwise, + if the handler block is \c nil the "returned promise" (if it exists) will be resolved + with the parent promise' result value. + */ +typedef RXPromise* (^progress_block_t)(promise_progressHandler_t onProgress); + +/*! + @brief Type definition of the "progress_on_block_t". The "progress_on_block_t" is the return + value of the property \p progressOn. + + @discussion The "progress_on_block_t" has two parameters, the execution context and the progress handler block. + The handler may be \c nil. + + @par The "progress_on_block_t" returns a promise, the "returned promise". When the parent + promise will be resolved the error handler (if not \c nil) will be invoked if the promise + is rejected and its return value resolves the "returned promise" (if it exists). Otherwise, + if the handler block is \c nil the "returned promise" (if it exists) will be resolved + with the parent promise' result value. + */ +typedef RXPromise* (^progress_on_block_t)(id, promise_progressHandler_t onProgress); + +/*! + @brief Type definition of the "progress_on_main_block_t". The "progress_on_main_block_t" is the return + value of the property \p progressOnMain. + + @discussion The "progress_on_main_block_t" has one parameter, the progress handler block. + The handler may be \c nil. + + @par The "progress_on_main_block_t" returns a promise, the "returned promise". When the parent + promise will be resolved the error handler (if not \c nil) will be invoked if the promise + is rejected and its return value resolves the "returned promise" (if it exists). Otherwise, + if the handler block is \c nil the "returned promise" (if it exists) will be resolved + with the parent promise' result value. + */ +typedef RXPromise* (^progress_on_main_block_t)(promise_progressHandler_t onProgress); + /*! @brief A \p RXPromise object represents the eventual result of an asynchronous @@ -475,7 +536,83 @@ typedef RXPromise* (^catch_on_main_block_t)(promise_errorHandler_t); */ @property (nonatomic, readonly) catch_on_main_block_t catchOnMain; +/*! + @brief Property \p progress returns a block whose signature is + @code + RXPromise* (^)(promise_progressHandler_t onProgress) + @endcode + + When the block is called it will register the progress handler \p onProgress. + When the receiver will progress the progress handler will be called. + + @par The receiver won't be retained + + @par The block returns a new \c RXPromise, the "returned promise", whose result will become + the return value of the handler if it is called. + + @par When the block is invoked and the receiver is already rejected, the error handler will + be immediately asynchronously scheduled for execution on the main thread. + + @par Parameter \p onProgress may be \c nil. + + @par The receiver can register zero or more handlers through clients calling + the block multiple times. + + @return Returns a block of type \c progress_block_t. + */ +@property (nonatomic, readonly) progress_block_t progress; + +/*! + @brief Property \p progressOn returns a block whose signature is + @code + RXPromise* (^)(id, promise_progressHandler_t onProgress) + @endcode + + When the block is called it will register the progress handler \p onProgress. + When the receiver will progress the progress handler will be called and executed in the specified execution context. + + @par The receiver won't be retained + + @par The block returns a new \c RXPromise, the "returned promise", whose result will become + the return value of the handler if it is called. + + @par When the block is invoked and the receiver is already rejected, the error handler will + be immediately asynchronously scheduled for execution on the main thread. + + @par Parameter \p onProgress may be \c nil. + + @par The receiver can register zero or more handlers through clients calling + the block multiple times. + + @return Returns a block of type \c progress_on_block_t. + */ +@property (nonatomic, readonly) progress_on_block_t progressOn; +/*! + @brief Property \p progressOnMain returns a block whose signature is + @code + RXPromise* (^)(promise_progressHandler_t onProgress) + @endcode + + When the block is called it will register the progress handler \p onProgress. + When the receiver will progress the progress handler will be called and executed on the main thread. + + @par The receiver won't be retained + + @par The block returns a new \c RXPromise, the "returned promise", whose result will become + the return value of the handler if it is called. + + @par When the block is invoked and the receiver is already rejected, the error handler will + be immediately asynchronously scheduled for execution on the main thread. + + @par Parameter \p onProgress may be \c nil. + + @par The receiver can register zero or more handlers through clients calling + the block multiple times. + + @return Returns a block of type \c progress_on_main_block_t. + */ +@property (nonatomic, readonly) progress_on_main_block_t progressOnMain; /*! Returns \c YES if the receiveer is pending. @@ -497,14 +634,12 @@ typedef RXPromise* (^catch_on_main_block_t)(promise_errorHandler_t); */ @property (nonatomic, readonly) BOOL isCancelled; - /*! Returns the parent promise - the promise which created the receiver. */ @property (nonatomic, readonly) RXPromise* parent; - /*! Returns the root promise. */ @@ -756,7 +891,15 @@ typedef RXPromise* (^catch_on_main_block_t)(promise_errorHandler_t); */ - (void) resolveWithResult:(id)result; - +/*! + @brief Updates the promise with the specified progress value and fires the notification to subscribers. + + This won't call any subsequent subscribers and won't trigger progress reporting chain, which could be expected, however isn't way yet implemented + + @param progress The relative value of progress set by the asynchronous result provider which should + represent a value in range from 0.0 to 1.0 + */ +- (void) updateWithProgress:(float)progress; @end diff --git a/RXPromise Libraries/Source/RXPromise.mm b/RXPromise Libraries/Source/RXPromise.mm index fc649a1..78208e8 100644 --- a/RXPromise Libraries/Source/RXPromise.mm +++ b/RXPromise Libraries/Source/RXPromise.mm @@ -24,9 +24,11 @@ #import "RXPromise+Private.h" #import #import +#import #include #include #include +#include // Set default logger serverity to "Error" (logs only errors) #if !defined (DEBUG_LOG) @@ -143,12 +145,14 @@ @implementation RXPromise { dispatch_queue_t _handler_queue; // a serial queue, uses target queue: s_sync_queue id _result; RXPromise_State _state; + + std::list> _progressHandlers; + std::list _children; } + @synthesize result = _result; @synthesize parent = _parent; - - - (void) dealloc { DLogInfo(@"dealloc: %p", (__bridge void*)self); if (_handler_queue) { @@ -259,7 +263,7 @@ - (RXPromise*) setTimeout:(NSTimeInterval)timeout { } onFailure:^id(NSError *error) { dispatch_source_cancel(timer); return nil; - } + } onProgress:nil returnPromise:NO]; return self; @@ -344,6 +348,39 @@ - (void) synced_cancelWithReason:(id)reason { } } +- (void)synced_updateWithProgress:(float)progress { + assert(dispatch_get_specific(rxpromise::shared::QueueID) == rxpromise::shared::sync_queue_id); + if (_state != Pending) { + return; + } + if (_progressHandlers.size()) { + for (const std::pair handlerPair : _progressHandlers) { + id executionContext = handlerPair.first; + promise_progressHandler_t handlerBlock = handlerPair.second; + if (executionContext == Shared.default_concurrent_queue) { + // If the continuation has been registered with `progress`, we run + // the handler is parallel: + dispatch_async(executionContext, ^(){handlerBlock(progress);}); + } + else if ([executionContext conformsToProtocol:@protocol(OS_dispatch_queue)]) { + // If the continuation has been registered with `progressOn:` and when the + // execution context is a dispatch queue, we run the handler serially: + dispatch_barrier_async(executionContext, ^(){handlerBlock(progress);}); + } + else { + // Otherwise, the execution context is not a dispatch_queue. Dispatch + // to the corresponding execution context: + [executionContext rxp_dispatchBlock:^(){handlerBlock(progress);}]; + } + } + } + if (_children.size()) { + for(const RXPromise * __weak child : _children) { + [child updateWithProgress:progress]; + } + } +} + // Registers success and failure handlers. // The receiver will be retained and only released when the receiver will be @@ -353,6 +390,7 @@ - (void) synced_cancelWithReason:(id)reason { - (instancetype) registerWithExecutionContext:(id)executionContext onSuccess:(promise_completionHandler_t)onSuccess onFailure:(promise_errorHandler_t)onFailure + onProgress:(promise_progressHandler_t)onProgress returnPromise:(BOOL)returnPromise NS_RETURNS_RETAINED { RXPromise* returnedPromise = returnPromise ? ([[[self class] alloc] init]) : nil; @@ -367,6 +405,13 @@ - (instancetype) registerWithExecutionContext:(id)executionContext if (_handler_queue == nil) { _handler_queue = createHandlerQueue(_state == Pending, (__bridge void*)self); } + if (onProgress) { + _progressHandlers.push_back({executionContext, onProgress}); + } + if (weakReturnedPromise) { + _children.push_back(weakReturnedPromise); + } + // Finally, *enqueue* a wrapper block which eventually gets invoked when the // promise will be resolved: dispatch_async(_handler_queue, ^{ @@ -470,33 +515,51 @@ - (instancetype) registerWithExecutionContext:(id)executionContext - (then_block_t) then { return ^RXPromise*(promise_completionHandler_t onSuccess, promise_errorHandler_t onFailure) { - return [self registerWithExecutionContext:nil onSuccess:onSuccess onFailure:onFailure returnPromise:YES]; + return [self registerWithExecutionContext:nil onSuccess:onSuccess onFailure:onFailure onProgress:nil returnPromise:YES]; }; } - (then_on_block_t) thenOn { return ^RXPromise*(id executionContext, promise_completionHandler_t onSuccess, promise_errorHandler_t onFailure) { - return [self registerWithExecutionContext:executionContext onSuccess:onSuccess onFailure:onFailure returnPromise:YES]; + return [self registerWithExecutionContext:executionContext onSuccess:onSuccess onFailure:onFailure onProgress:nil returnPromise:YES]; }; } - (then_on_main_block_t) thenOnMain { return ^RXPromise*(promise_completionHandler_t onSuccess, promise_errorHandler_t onFailure) { - return [self registerWithExecutionContext:dispatch_get_main_queue() onSuccess:onSuccess onFailure:onFailure returnPromise:YES]; + return [self registerWithExecutionContext:dispatch_get_main_queue() onSuccess:onSuccess onFailure:onFailure onProgress:nil returnPromise:YES]; }; } - (catch_on_block_t) catchOn { return ^RXPromise*(id executionContext, promise_errorHandler_t onFailure) { - return [self registerWithExecutionContext:executionContext onSuccess:nil onFailure:onFailure returnPromise:YES]; + return [self registerWithExecutionContext:executionContext onSuccess:nil onFailure:onFailure onProgress:nil returnPromise:YES]; }; } - (catch_on_main_block_t) catchOnMain { return ^RXPromise*(promise_errorHandler_t onFailure) { - return [self registerWithExecutionContext:dispatch_get_main_queue() onSuccess:nil onFailure:onFailure returnPromise:YES]; + return [self registerWithExecutionContext:dispatch_get_main_queue() onSuccess:nil onFailure:onFailure onProgress:nil returnPromise:YES]; + }; +} + +- (progress_block_t) progress { + return ^RXPromise*(promise_progressHandler_t onProgress) { + return [self registerWithExecutionContext:nil onSuccess:nil onFailure:nil onProgress:onProgress returnPromise:YES]; + }; +} + +- (progress_on_block_t) progressOn { + return ^RXPromise*(id executionContext, promise_progressHandler_t onProgress) { + return [self registerWithExecutionContext:executionContext onSuccess:nil onFailure:nil onProgress:onProgress returnPromise:YES]; + }; +} + +- (progress_on_main_block_t) progressOnMain { + return ^RXPromise*(promise_progressHandler_t onProgress) { + return [self registerWithExecutionContext:dispatch_get_main_queue() onSuccess:nil onFailure:nil onProgress:onProgress returnPromise:YES]; }; } @@ -643,7 +706,9 @@ - (void) synced_bind:(RXPromise*) other { [strongSelf synced_rejectWithReason:error]; } return nil; - } returnPromise:NO]; + } + onProgress:nil + returnPromise:NO]; } } __weak RXPromise* weakSelf = self; @@ -658,8 +723,9 @@ - (void) synced_bind:(RXPromise*) other { } } return error; - } returnPromise:NO]; - + } + onProgress:nil + returnPromise:NO]; } @@ -882,8 +948,16 @@ - (void) rejectWithReason:(id)reason { } } - - +- (void) updateWithProgress:(float)progress { + if (dispatch_get_specific(rxpromise::shared::QueueID) == rxpromise::shared::sync_queue_id) { + [self synced_updateWithProgress:progress]; + } + else { + dispatch_barrier_async(Shared.sync_queue, ^{ + [self synced_updateWithProgress:progress]; + }); + } +} @end \ No newline at end of file diff --git a/RXPromise.podspec b/RXPromise.podspec index 02087d1..b54eaba 100644 --- a/RXPromise.podspec +++ b/RXPromise.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "RXPromise" - s.version = "0.13.1" + s.version = "0.13.2" s.summary = "A thread safe implementation of the Promises/A+ specification in Objective-C with extensions." s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE.md'} s.author = { "Andreas Grosam" => "agrosam@onlinehome.de" } diff --git a/Test/Tests/RXPromiseTest.mm b/Test/Tests/RXPromiseTest.mm index f9d549b..c9e2e2a 100644 --- a/Test/Tests/RXPromiseTest.mm +++ b/Test/Tests/RXPromiseTest.mm @@ -3132,6 +3132,254 @@ -(void) testTreeCancel2WithBoundPromise } } +#pragma mark - progress + +- (void) testSimpleProgressReporting { + XCTestExpectation *expectation = [self expectationWithDescription:@"Progress should be equal to sum of progresses"]; + NSArray *progressValues = @[@0.0, @0.25, @0.5, @0.75, @1.0]; + RXPromise *promise = [[RXPromise alloc] init]; + // For thread-safety, access variable progress on the main thread only! + __block float progress = 0; + promise.progressOnMain(^(float reportedProgress) { + progress += reportedProgress; + if (fabs(reportedProgress - 1.0) < 0.01) { + [expectation fulfill]; + } + }); + for (NSNumber *progressValue in progressValues) { + [promise updateWithProgress:progressValue.floatValue]; + } + [self waitForExpectationsWithTimeout:0.1 handler:nil]; + XCTAssertEqualWithAccuracy(progress, + [[progressValues valueForKeyPath:@"@sum.self"] floatValue], + 0.01); +} + +- (void) testTwoSimultaneousProgressReporting { + XCTestExpectation *expectation = [self expectationWithDescription:@"Progress should be equal to sum of progresses"]; + NSArray *progressValues = @[@0.0, @0.25, @0.5, @0.75, @1.0]; + RXPromise *promise = [[RXPromise alloc] init]; + // For thread-safety, access variable progress on the main thread only! + __block float progress = 0; + promise.progressOnMain(^(float reportedProgress) { + progress += reportedProgress; + if (fabs(reportedProgress - 1.0) < 0.01) { + [expectation fulfill]; + } + }); + promise.progressOnMain(^(float reportedProgress) { + progress += reportedProgress; + }); + for (NSNumber *progressValue in progressValues) { + [promise updateWithProgress:progressValue.floatValue]; + } + [self waitForExpectationsWithTimeout:0.1 handler:nil]; + XCTAssertEqualWithAccuracy(progress, + [[progressValues valueForKeyPath:@"@sum.self"] floatValue] * 2, + 0.01); +} + +- (void) testSimultaneousProgressReportingAndFullfill { + XCTestExpectation *expectation = [self expectationWithDescription:@"Progress should be equal to sum of progresses"]; + NSArray *progressValues = @[@0.0, @0.25, @0.5, @0.75, @1.0]; + RXPromise *promise = [[RXPromise alloc] init]; + // For thread-safety, access variable progress on the main thread only! + __block float progress = 0; + promise.progressOnMain(^(float reportedProgress) { + progress += reportedProgress; + if (fabs(reportedProgress - 1.0) < 0.01) { + [promise fulfillWithValue:nil]; + } + }); + promise.then(^id(id result) { + [expectation fulfill]; + return nil; + }, nil); + for (NSNumber *progressValue in progressValues) { + [promise updateWithProgress:progressValue.floatValue]; + } + [self waitForExpectationsWithTimeout:0.1 handler:nil]; + XCTAssertEqualWithAccuracy(progress, + [[progressValues valueForKeyPath:@"@sum.self"] floatValue], + 0.01); +} + +- (void) testSimultaneousProgressReportingAndReject { + XCTestExpectation *expectation = [self expectationWithDescription:@"Progress should be equal to sum of progresses"]; + NSArray *progressValues = @[@0.0, @0.25, @0.5, @0.75, @1.0]; + RXPromise *promise = [[RXPromise alloc] init]; + // For thread-safety, ensure progress will be accessed on the sync_queue only: + dispatch_queue_t sync_queue = dispatch_queue_create("sync_queue", DISPATCH_QUEUE_SERIAL); + __block float progress = 0; + promise.progressOn(sync_queue, ^(float reportedProgress) { + progress += reportedProgress; + if (fabs(reportedProgress - 1.0) < 0.01) { + [promise rejectWithReason:nil]; + } + }); + promise.then(nil, ^id(id result) { + [expectation fulfill]; + return nil; + }); + for (NSNumber *progressValue in progressValues) { + [promise updateWithProgress:progressValue.floatValue]; + } + [self waitForExpectationsWithTimeout:0.1 handler:nil]; + + __block float progress2 = 0; + dispatch_sync(sync_queue, ^{ + progress2 = progress; + }); + XCTAssertEqualWithAccuracy(progress2, + [[progressValues valueForKeyPath:@"@sum.self"] floatValue], + 0.01); +} + +- (void) testProgressReportingAfterFullfill { + XCTestExpectation *expectation = [self expectationWithDescription:@"Progress should be equal to sum of progresses"]; + NSArray *progressValues = @[@0.0, @0.25, @0.5, @0.75, @1.0]; + RXPromise *promise = [[RXPromise alloc] init]; + // For thread-safety, ensure progress will be accessed on the sync_queue only: + dispatch_queue_t sync_queue = dispatch_queue_create("sync_queue", DISPATCH_QUEUE_SERIAL); + __block float progress = 0; + promise.progress(^(float reportedProgress) { + progress += reportedProgress; + }); + promise.then(^id(id result) { + [expectation fulfill]; + return nil; + }, nil); + [promise fulfillWithValue:nil]; + for (NSNumber *progressValue in progressValues) { + [promise updateWithProgress:progressValue.floatValue]; + } + [self waitForExpectationsWithTimeout:0.1 handler:nil]; + __block float progress2 = 0; + dispatch_sync(sync_queue, ^{ + progress2 = progress; + }); + XCTAssertEqualWithAccuracy(progress2, + 0.0, + 0.01); +} + +- (void) testProgressReportingAfterReject { + XCTestExpectation *expectation = [self expectationWithDescription:@"Progress should be equal to sum of progresses"]; + NSArray *progressValues = @[@0.0, @0.25, @0.5, @0.75, @1.0]; + RXPromise *promise = [[RXPromise alloc] init]; + // For thread-safety, ensure progress will be accessed on the sync_queue only: + dispatch_queue_t sync_queue = dispatch_queue_create("sync_queue", DISPATCH_QUEUE_SERIAL); + __block float progress = 0; + promise.progress(^(float reportedProgress) { + progress += reportedProgress; + }); + promise.then(nil, ^id(id result) { + [expectation fulfill]; + return nil; + }); + [promise rejectWithReason:nil]; + for (NSNumber *progressValue in progressValues) { + [promise updateWithProgress:progressValue.floatValue]; + } + [self waitForExpectationsWithTimeout:0.1 handler:nil]; + + __block float progress2 = 0; + dispatch_sync(sync_queue, ^{ + progress2 = progress; + }); + XCTAssertEqualWithAccuracy(progress2, + 0.0, + 0.01); +} + +- (void)testProgressPropagationByPromiseChain { + XCTestExpectation *expectation = [self expectationWithDescription:@"Progress should be equal to sum of progresses"]; + NSArray *progressValues = @[@0.0, @0.25, @0.5, @0.75, @1.0]; + RXPromise *promise = [[RXPromise alloc] init]; + // For thread-safety, access variable progress on the main thread only! + __block float progress = 0; + RXPromise *promise2 = promise.then(nil, ^id(id result) { + [expectation fulfill]; + return nil; + }); + promise2.progressOnMain(^(float reportedProgress) { + progress += reportedProgress; + if (fabs(reportedProgress - 1.0) < 0.01) { + [expectation fulfill]; + } + }); + for (NSNumber *progressValue in progressValues) { + [promise updateWithProgress:progressValue.floatValue]; + } + [self waitForExpectationsWithTimeout:0.1 handler:nil]; + XCTAssertEqualWithAccuracy(progress, + [[progressValues valueForKeyPath:@"@sum.self"] floatValue], + 0.01); +} + +- (void)testProgressPropagationByPromiseChainOfThreePromises { + XCTestExpectation *expectation = [self expectationWithDescription:@"Progress should be equal to sum of progresses"]; + NSArray *progressValues = @[@0.0, @0.25, @0.5, @0.75, @1.0]; + RXPromise *promise = [[RXPromise alloc] init]; + // For thread-safety, access variable progress on the main thread only! + __block float progress = 0; + RXPromise *promise2 = promise.then(nil, ^id(id result) { + return nil; + }); + RXPromise *promise3 = promise2.then(nil, ^id(id result) { + return nil; + }); + promise3.progressOnMain(^(float reportedProgress) { + progress += reportedProgress; + if (fabs(reportedProgress - 1.0) < 0.01) { + [expectation fulfill]; + } + }); + for (NSNumber *progressValue in progressValues) { + [promise updateWithProgress:progressValue.floatValue]; + } + [self waitForExpectationsWithTimeout:0.1 handler:nil]; + XCTAssertEqualWithAccuracy(progress, + [[progressValues valueForKeyPath:@"@sum.self"] floatValue], + 0.01); +} + +- (void)testProgressPropagationByPromiseChainOfTreeOfPromises { + XCTestExpectation *expectation = [self expectationWithDescription:@"Progress should be equal to sum of progresses"]; + NSArray *progressValues = @[@0.0, @0.25, @0.5, @0.75, @1.0]; + RXPromise *promise = [[RXPromise alloc] init]; + // Ensure progress will be accessed on the sync_queue only to ensure concurrent + // access is correct: + dispatch_queue_t sync_queue = dispatch_queue_create("sync_queue", DISPATCH_QUEUE_SERIAL); + __block float progress = 0; + RXPromise *promise2 = promise.then(nil, ^id(id result) { + return nil; + }); + RXPromise *promise3 = promise.then(nil, ^id(id result) { + return nil; + }); + promise2.progressOn(sync_queue, ^(float reportedProgress) { + progress += reportedProgress; + if (fabs(reportedProgress - 1.0) < 0.01) { + [expectation fulfill]; + } + }); + promise3.progressOn(sync_queue, ^(float reportedProgress) { + progress += reportedProgress; + }); + for (NSNumber *progressValue in progressValues) { + [promise updateWithProgress:progressValue.floatValue]; + } + [self waitForExpectationsWithTimeout:0.1 handler:nil]; + + __block float progress2 = 0; + dispatch_sync(sync_queue, ^{ + progress2 = progress; + }); + XCTAssertEqualWithAccuracy(progress2, + [[progressValues valueForKeyPath:@"@sum.self"] floatValue] * 2.0, + 0.01); +} #pragma mark - all