I have 2 performance test methods in test class. If i run them separately they pass. If i run hole class methods they fail with message:
**** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'API violation - multiple calls made to -[XCTestExpectation fulfill].'*
Is there any way to include couple performance tests in 1 class?
here is sample code:
- (void)testPerformanceAuthenticateWithLogin {
XCTestExpectation *authenticateExpectation = [self expectationWithDescription:#"Authenticate With Login"];
__block int userID = 0;
[self measureBlock:^{
[AuthenticationService authenticateWithLogin:email password:password success:^(AuthenticationResponse *result) {
XCTAssert(result.isAuthenticated);
userID = result.userID;
[authenticateExpectation fulfill];
} failure:^(NSError *error) {
XCTAssertNil(error);
}];
}];
[self waitForExpectationsWithTimeout:3 handler:^(NSError *error) {
XCTAssertNil(error);
[AuthenticationService logoutWithServicePersonID:userID success:nil failure:nil];
}];
}
- (void)testPerformanceGetServicePersonByID {
XCTestExpectation *getServicePersonExpectation = [self expectationWithDescription:#"get Service Person By ID"];
__block int userID = 0;
[AuthenticationService authenticateWithLogin:email password:password success:^(AuthenticationResponse *result) {
userID = result.userID;
[self loginSuccess:result];
[self measureBlock:^{
[ServicePersonService getServicePersonByIDWithServicePersonID:userID success:^(ServicePersonDTO *result) {
XCTAssertNotNil(result);
[getServicePersonExpectation fulfill];
} failure:^(NSError *error) {
XCTAssertNil(error);
}];
}];
} failure:^(NSError *error) {
XCTAssertNil(error);
}];
[self waitForExpectationsWithTimeout:3 handler:^(NSError *error) {
XCTAssertNil(error);
[AuthenticationService logoutWithServicePersonID:userID success:nil failure:nil];
}];
}
The measureBlock actually runs the code inside the block multiple times to determine an average value.
If I understand correctly you want to measure how long does the authentication take. If that's the case:
you could put the expectation inside the measure block so every time a measure iteration is done, the test iteration will wait for the expectation to be fulfilled before executing the block again
you could bump up the expectation timeout a bit just in case.
I've done something like this (which worked for me):
- (void)testPerformanceAuthenticateWithLogin {
[self measureBlock:^{
__block int userID = 0;
XCTestExpectation *authenticateExpectation = [self expectationWithDescription:#"Authenticate With Login"];
[AuthenticationService authenticateWithLogin:email password:password success:^(AuthenticationResponse *result) {
XCTAssert(result.isAuthenticated);
userID = result.userID;
[authenticateExpectation fulfill];
} failure:^(NSError *error) {
XCTAssertNil(error);
}];
[self waitForExpectationsWithTimeout:10 handler:^(NSError *error) {
XCTAssertNil(error);
[AuthenticationService logoutWithServicePersonID:userID success:nil failure:nil];
}];
}];
}
Related
So, when I trying to fetch some data, RACCommand return this error.
I have a picker for example and when user scroll it, app get data from server and show them, but if user scroll fast, (previous operation in progress) RACCommand get this error:
Error Domain=RACCommandErrorDomain Code=1 "The command is disabled and cannot be executed" UserInfo={RACUnderlyingCommandErrorKey=<RACCommand: 0x174280050>, NSLocalizedDescription=The command is disabled and cannot be executed}
I know, its related with some cancel mechanism, but I tried many examples and not working as well.
Its my piece of code:
#weakify(self);
[[[self.viewModel makeCommand] execute:nil]
subscribeError:^(NSError *error) {
#strongify(self);
[self showAlertWithError:error];
}];
and viewModel:
- (RACCommand*)makeCommand {
if (!_makeCommand) {
_makeCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [self getVehicleMake];
}];
}
return _makeCommand;
}
- (RACSignal*)getVehicleMake {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[self.forumService getForumMakesWithYear:#([self.selectedYear integerValue])
category:self.vehicleCategory]
subscribeNext:^(RACTuple *result) {
self.makes = result.first;
[subscriber sendNext:self.makes];
} error:^(NSError *error) {
[subscriber sendError:error];
} completed:^{
[subscriber sendCompleted];
}];
return [RACDisposable disposableWithBlock:^{
}];
}];
}
RACCommand doesn't allow concurrent execution by default. When it's executing, it becomes disabled. If you try to execute again, it will send that error.
But you can test for that error—RACCommand has RACCommandErrorDomain and RACCommandErrorNotEnabled constants available.
#weakify(self);
[[[self.viewModel makeCommand] execute:nil]
subscribeError:^(NSError *error) {
#strongify(self);
if ([error.domain isEqual:RACCommandErrorDomain] && error.code == RACCommandErrorNotEnabled) {
return;
}
[self showAlertWithError:error];
}];
I tried to use ReactiveCocoa to do network operation chaining, but I failed. I can't figure out what is wrong with my code.
- (RACSignal *)pg_findObjectsInBackground {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[self findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (error) {
[subscriber sendError:error];
return;
}
[subscriber sendNext:objects];
[subscriber sendCompleted];
}];
return [RACDisposable disposableWithBlock:^{
[self cancel];
}];
}];
}
- (RACSignal *)pg_countObjectsInBackground {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[self countObjectsInBackgroundWithBlock:^(int number, NSError *error) {
if (error) {
[subscriber sendError:error];
return;
}
[subscriber sendNext:#(number)];
[subscriber sendCompleted];
}];
return [RACDisposable disposableWithBlock:^{
[self cancel];
}];
}];
}
__block NSError *_error;
#weakify(self)
[[[self.query pg_countObjectsInBackground]flattenMap:^RACStream *(NSNumber *count) {
#strongify(self)
self.totalCount = [count integerValue];
// Second, fetch experiences
self.query.limit = self.pageSize;
self.query.skip = self.pageSize * self.currentPage;
return [self.query pg_findObjectsInBackground];
}]subscribeNext:^(NSArray *experiences) {
#strongify(self)
[self.experiences removeAllObjects];
[self.experiences addObjectsFromArray:experiences];
} error:^(NSError *error) {
_error = error;
} completed:^{
#strongify(self)
if (finishBlock) {
finishBlock(self, _error);
}
}];
The first request was successful. But as soon as I return [self.query pg_findObjectsInBackground], it went to disposableWithBlock directly.
Because you're using the same PFQuery object for both the count and the find object operation, the query gets canceled when you return from the flattenMap method. The flattenMap subscribes to the new signal (which is the same signal), which I believe causes the disposable to fire. A simple solution is to construct a new PFQuery and return it in the flattenMap block.
I assumed you're using Parse, and if you are, you should tag it.
Short version:
Is it possible to use RACMulticastConnection in the same way as NSNotificationCenter? I mean to keep the subscribed blocks valid even for another call [connection connect]?
Long version:
Among different subscribers I share a reference to RACMulticastConnection. Those who initiate the request pass loginRequest!=nil and those subscribers who want just listen use loginRequest==nil:
RACMulticastConnection *connection = [self.appModel loginRequest:loginRequest];
[connection connect]; //I initiate the request
[connection.signal subscribeNext:^(RACTuple* responseTuple) {
} error:^(NSError *error) {
} completed:^{
}];
In other modul I just subscribe and listen:
RACMulticastConnection *connection = [self.appModel loginRequest:nil];
[connection.signal subscribeNext:^(RACTuple* responseTuple) {
} error:^(NSError *error) {
} completed:^{
}];
When I call the [connection connect]; everything works fine. The subscribers are notified. But if I want to repeat the request to the server again with [connection connect]; I just receive successful signal with the old responses.
Basic idea is I want to create RACMulticastConnection once and share it for potential subscribers. Those who listen pass nil arguments, those who initiate the request pass not nil argument and call [connection connect]; But it does not trigger the block defined in RACSignal createSignal:.
The RACSignal is created just once when RACMulticastConnection does not exist. self.loginRequestConnection is property of model. The property is shared in the application to subscribers:
- (RACMulticastConnection*) loginRequest:(LoginRequest*)request {
self.loginRequest = request;
if(! self.loginRequestConnection) { // the instance of RACMulticastConnection shared among the subscribers
#weakify(self);
RACSignal* networkRequest = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
__block Response* blockResponse = nil;
#strongify(self);
[_serviceClient login:self.loginRequest success:^(TSNApiResponse *response, TSNError *error) {
blockResponse = response;
[subscriber sendNext:RACTuplePack(response, error)];
[subscriber sendCompleted];
} failure:^(TSNError *error) {
[self cleanUpRequestOnError:error subscriber:subscriber blockResponse:blockResponse blockRequest:self.loginRequest];
[subscriber sendError:error];
}];
return [RACDisposable disposableWithBlock:^{
[_serviceClient cancelRequest:self.loginRequest];
}];
}];
self.loginRequestConnection = [networkRequest multicast:[RACReplaySubject subject]];
}
return self.loginRequestConnection;
}
Is there any correct way how to make the connection trigger again the block in the RACSignal? Thank you.
I have used FBKVOController. The code done with FBKVOController is fraction of the implementation done with ReactiveCocoa:
- (void) loginRequest:(TSNLoginRequest*)request {
[_serviceClient login:request success:^(TSNApiResponse *response, TSNError *error) {
self.loginResponseTuple = [TSNResponseTuple responseTuple:response error:error];
} failure:^(TSNError *error) {
self.loginResponseTuple = [TSNResponseTuple responseTuple:nil error:error];
}];
}
Issuing the request:
[self.appModel loginRequest:loginRequest];
Observing the response:
[_KVOController observe:self.appModel keyPath:#"loginResponseTuple" options:NSKeyValueObservingOptionNew block:^(TSNStartupScreenViewModel* observer, TSNAppModel* observed, NSDictionary *change) {
TSNResponseTuple* responseTuple = change[NSKeyValueChangeNewKey];
#strongify(self);
if([responseTuple.error isError]) {
[TSNAppUtilities showError:(TSNError *)(responseTuple.error) completion:^(OHAlertView *alert, NSInteger buttonIndex) {
if(buttonIndex == alert.firstOtherButtonIndex) {
// ... process selection
}
}];
}
}];
I have the following snippet of code below that fetches data from Parse using PFQueues in the background and returns data and a status. This structure is based off of waiting for the dispatch_group_t to notify that's it's completed all entered groups. Unfortunately dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
is called before the completion blocks call dispatch_group_leave. By the time the dispatch_group_leave() is called on any of the completion blocks, an EXC_BAD_INSTRUCTION is thrown. I've attached an image below for the instruction error. Does anyone know if I'm doing something wrong or if Parse has some annoyances that prevent me from using this method?
- (void)downloadAndCacheObjectsWithCompletion:(void (^)(NSError *))callback
{
__block NSError *downloadError1;
__block NSError *downloadError2;
__block NSError *downloadError3;
__block NSError *downloadError4;
NSLog(#"%#", NSStringFromSelector(_cmd));
dispatch_group_t downloadGroup = dispatch_group_create();
dispatch_group_enter(downloadGroup);
[self fetchDataWithCompletion:^(NSArray *artwork, NSError *error) {
downloadError1 = error;
dispatch_group_leave(downloadGroup);
}];
dispatch_group_enter(downloadGroup);
[self fetchDataWithCompletion:^(NSArray *artworkPhotos, NSError *error) {
downloadError2 = error;
dispatch_group_leave(downloadGroup);
}];
dispatch_group_enter(downloadGroup);
[self fetchDataWithCompletion:^(NSArray *artists, NSError *error) {
downloadError3 = error;
dispatch_group_leave(downloadGroup);
}];
dispatch_group_enter(downloadGroup);
[self fetchDataWithCompletion:^(NSArray *badges, NSError *error) {
downloadError4 = error;
dispatch_group_leave(downloadGroup);
}];
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
NSError *returnError;
if (downloadError1 || downloadError2 || downloadError3 || downloadError4) {
returnError = [[NSError alloc] initWithDomain:#"ParseFactory" code:-1 userInfo:#{NSLocalizedDescriptionKey: #"There was an error retrieving the content"}];
}
if (callback) {
callback(returnError);
}
});
}
- (void)fetchDataWithCompletion:(void(^)(NSArray *data, NSError *error))callback
{
NSLog(#"Fetching Data");
if ([self.cachedData objectForKey:kDataClassName]) {
if (callback) {
callback([self.cachedData objectForKey:kDataClassName], nil);
}
return;
}
PFQuery *dataQueue = [PFQuery queryWithClassName:kDataClassName];
dataQueue.cachePolicy = kPFCachePolicyCacheThenNetwork;
[dataQueue findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
[self.cachedData setObject:objects forKey:kDataClassName];
} else {
NSLog(#"Fetching Data Error: %#", error);
}
if (callback) {
callback(objects, error);
}
}];
}
The download process listed above is called from AppDelegate as such
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//Register PFObject subclasses
[Data registerSubclass];
[Parse setApplicationId:#"appkey" clientKey:#"clientkey"];
[[ParseFactory sharedInstance] downloadAndCacheObjectsWithCompletion:^(NSError *error) {
}];
return YES;
}
Stack trace:
The error you're seeing indicates that your program calls dispatch_group_leave too many times. It's trivial to reproduce. I reproduced it with this program:
int main(int argc, const char * argv[])
{
#autoreleasepool {
dispatch_group_t group = dispatch_group_create();
dispatch_group_leave(group);
}
return 0;
}
Therefore I deduce that your fetchDataWithCompletion: method calls its completion block more than once. If you can't figure out why, edit your question to include the source code of that method (and any related methods or declarations).
I come in late, but it seems clear your issue comes from the kPFCachePolicyCacheThenNetwork.
Parse will call the completion block twice, one with cached data (even the 1st time), one with downloaded data... so your dispatch_group_leave will be called twice as much as your dispatch_group_enter.
Is it something wrong with the requestPanoramaNearCoordinate Google maps SDK method? cause it get´s stuck in the while loop. I´v written the loop cause I want to wait with executing the rest of the method until the asynchronous callback method has completed. But the the while loop loops infinitely. Is it my code that´s simply wrong?
__block GMSPanorama *panPhoto = nil;
__block BOOL finished = NO;
[self.panoService requestPanoramaNearCoordinate:ranLatLng callback:^(GMSPanorama *panorama, NSError *error) {
NSLog(#"panorama: %# error: %#", panorama, error);
panPhoto = panorama;
finished = YES;
}];
while (!finished) {
// Do nothing);
}
if (!panPhoto) return [self randomLatitudeLongitude];
return ranLatLng;
}
Why you launch async method and then doing loop? You must add block (with GMSPanorama argument) as parameter to your method and call this block inside callback:^(GMSPanorama *panorama, NSError *error){
Smth like that:
- (void) methodNameWithBlock:(BlockName)block;
__block GMSPanorama *panPhoto = nil;
__block BOOL finished = NO;
[self.panoService requestPanoramaNearCoordinate:ranLatLng callback:^(GMSPanorama *panorama, NSError *error) {
NSLog(#"panorama: %# error: %#", panorama, error);
panPhoto = panorama;
finished = YES;
BlockName handler = [block copy];
if (!ranLatLng){
handler([self randomLatitudeLongitude])
} else {
handler(ranLatLng)
}
}];
}