CKKSOperationTests.m [plain text]
/*
* Copyright (c) 2016 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/
#if OCTAGON
#import <XCTest/XCTest.h>
#import "keychain/ckks/CKKSGroupOperation.h"
#import "keychain/ckks/CKKSCondition.h"
// Helper Operations
@interface CKKSResultCancelOperation : CKKSResultOperation
- (instancetype) init;
@end
@implementation CKKSResultCancelOperation
- (instancetype)init {
if(self = [super init]) {
__weak __typeof(self) weakSelf = self;
[self addExecutionBlock:^{
[weakSelf cancel];
}];
}
return self;
}
@end
@interface CKKSResultErrorOperation : CKKSResultOperation
- (instancetype) init;
@end
@implementation CKKSResultErrorOperation
- (instancetype)init {
if(self = [super init]) {
__weak __typeof(self) weakSelf = self;
[self addExecutionBlock:^{
weakSelf.error = [NSError errorWithDomain:@"test domain" code:5 userInfo:nil];
}];
}
return self;
}
@end
@interface CKKSOperationTests : XCTestCase
@property NSOperationQueue* queue;
@end
// Remaining tests to write:
// TODO: subclass of CKKSResultOperation implementing main() respects addSuccessDependency without any special code
// TODO: chain of automatic dependencies
// TODO: test showing that CKKSGroupOperations don't start if they success-depend on a failed CKKSResultOperation
@implementation CKKSOperationTests
- (void)setUp {
[super setUp];
self.queue = [[NSOperationQueue alloc] init];
}
- (void)tearDown {
[self.queue cancelAllOperations];
self.queue = nil;
[super tearDown];
}
- (void)testIsPending {
NSBlockOperation* run = [NSBlockOperation blockOperationWithBlock:^{}];
NSBlockOperation* cancel = [NSBlockOperation blockOperationWithBlock:^{}];
NSBlockOperation* pending = [NSBlockOperation blockOperationWithBlock:^{}];
[self.queue addOperation: run];
[cancel cancel];
[self.queue waitUntilAllOperationsAreFinished];
XCTAssertTrue( [pending isPending], @"Pending operation should be pending");
XCTAssertFalse([run isPending], @"run operation should not be pending");
XCTAssertFalse([cancel isPending], @"Cancelled operation should not be pending");
}
- (void)testResultOperation {
CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
__weak __typeof(op) weakOp = op;
[op addExecutionBlock:^{
weakOp.error = [NSError errorWithDomain:@"test domain" code:0 userInfo:nil];
}];
[self.queue addOperation: op];
[op waitUntilFinished];
XCTAssertNotNil(op.error, "errors can persist");
}
- (void)testResultSuccessDependency {
__block bool firstRun = false;
__block bool secondRun = false;
CKKSResultOperation* first = [[CKKSResultOperation alloc] init];
[first addExecutionBlock:^{
firstRun = true;
}];
CKKSResultOperation* second = [[CKKSResultOperation alloc] init];
[second addExecutionBlock:^{
XCTAssertTrue(firstRun);
secondRun = true;
}];
[second addSuccessDependency: first];
[self.queue addOperation: second];
[self.queue addOperation: first];
[self.queue waitUntilAllOperationsAreFinished];
XCTAssertTrue(firstRun);
XCTAssertTrue(secondRun);
XCTAssertTrue(first.finished, "First operation finished");
XCTAssertFalse(first.cancelled, "First operation not cancelled");
XCTAssertTrue(second.finished, "Second operation finished");
XCTAssertFalse(second.cancelled, "Second operation not cancelled");
}
- (void)testResultSuccessDependencyCancel {
CKKSResultCancelOperation* first = [[CKKSResultCancelOperation alloc] init];
CKKSResultOperation* second = [[CKKSResultOperation alloc] init];
[second addExecutionBlock:^{
XCTFail("Second operation should never run");
}];
[second addSuccessDependency: first];
[self.queue addOperation: second];
[self.queue addOperation: first];
[self.queue waitUntilAllOperationsAreFinished];
XCTAssertTrue(first.finished, "First operation finished");
XCTAssertTrue(first.cancelled, "First operation is canceled (as requested)");
XCTAssertTrue(second.cancelled, "Second operation is canceled");
XCTAssertTrue(second.finished, "Second operation finished");
XCTAssertNotNil(second.error, "Error is generated when CKKSResultOperation is cancelled");
XCTAssertEqual(second.error.code, CKKSResultSubresultCancelled, "Error code is CKKSResultSubresultCancelled");
}
- (void)testResultSuccessDependencyError {
CKKSResultErrorOperation* first = [[CKKSResultErrorOperation alloc] init];
CKKSResultOperation* second = [[CKKSResultOperation alloc] init];
[second addExecutionBlock:^{
XCTFail("Second operation should never run");
}];
[second addSuccessDependency: first];
[self.queue addOperation: second];
[self.queue addOperation: first];
[self.queue waitUntilAllOperationsAreFinished];
XCTAssertTrue(first.finished, "First operation finished");
XCTAssertFalse(first.cancelled, "First operation is not canceled");
XCTAssertNotNil(first.error, "First operation has an error");
XCTAssertTrue(second.cancelled, "Second operation is canceled");
XCTAssertTrue(second.finished, "Second operation finished");
XCTAssertNotNil(second.error, "Error is generated when dependent CKKSResultOperation has an error");
XCTAssertEqual(second.error.code, CKKSResultSubresultError, "Error code is CKKSResultSubresultError");
XCTAssertNotNil(second.error.userInfo[NSUnderlyingErrorKey], "Passed up the error from the first operation");
XCTAssertEqual([second.error.userInfo[NSUnderlyingErrorKey] code], 5, "Passed up the right error from the first operation");
}
- (void)testResultTimeout {
__block bool firstRun = false;
__block bool secondRun = false;
CKKSResultOperation* first = [[CKKSResultOperation alloc] init];
[first addExecutionBlock:^{
firstRun = true;
}];
CKKSResultOperation* second = [[CKKSResultOperation alloc] init];
[second addExecutionBlock:^{
XCTAssertTrue(firstRun);
secondRun = true;
}];
[second addDependency: first];
[self.queue addOperation: [second timeout:(50)* NSEC_PER_MSEC]];
[self.queue waitUntilAllOperationsAreFinished];
XCTAssertFalse(firstRun);
XCTAssertFalse(secondRun);
XCTAssertFalse(first.finished, "First operation not finished");
XCTAssertFalse(first.cancelled, "First operation not cancelled");
XCTAssertTrue(second.finished, "Second operation finished");
XCTAssertTrue(second.cancelled, "Second operation cancelled");
XCTAssertNotNil(second.error, "Second operation has an error");
XCTAssertEqual(second.error.code, CKKSResultTimedOut, "Second operation error is good");
NSError* underlying = second.error.userInfo[NSUnderlyingErrorKey];
XCTAssertNil(underlying, "Second operation's error doesn't have an underlying explanation");
}
- (void)testResultTimeoutWithUnderlyingError {
__block bool firstRun = false;
__block bool secondRun = false;
CKKSResultOperation* first = [[CKKSResultOperation alloc] init];
[first addExecutionBlock:^{
firstRun = true;
}];
first.descriptionErrorCode = 604;
CKKSResultOperation* second = [[CKKSResultOperation alloc] init];
[second addExecutionBlock:^{
XCTAssertTrue(firstRun);
secondRun = true;
}];
[second addDependency: first];
[self.queue addOperation: [second timeout:(50)* NSEC_PER_MSEC]];
[self.queue waitUntilAllOperationsAreFinished];
XCTAssertFalse(firstRun);
XCTAssertFalse(secondRun);
XCTAssertFalse(first.finished, "First operation not finished");
XCTAssertFalse(first.cancelled, "First operation not cancelled");
XCTAssertTrue(second.finished, "Second operation finished");
XCTAssertTrue(second.cancelled, "Second operation cancelled");
XCTAssertNotNil(second.error, "Second operation has an error");
XCTAssertEqual(second.error.code, CKKSResultTimedOut, "Second operation error is good");
NSError* underlying = second.error.userInfo[NSUnderlyingErrorKey];
XCTAssertNotNil(underlying, "second operation's error has an underlying reason");
XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "second operation's underlying error's domain should be CKKSResultDescriptionErrorDomain");
XCTAssertEqual(underlying.code, 604, "second operation's underlying error's domain should be first's description");
}
- (void)testResultNoTimeout {
__block bool firstRun = false;
__block bool secondRun = false;
CKKSResultOperation* first = [[CKKSResultOperation alloc] init];
[first addExecutionBlock:^{
firstRun = true;
}];
CKKSResultOperation* second = [[CKKSResultOperation alloc] init];
[second addExecutionBlock:^{
XCTAssertTrue(firstRun);
secondRun = true;
}];
[second addDependency: first];
[self.queue addOperation: [second timeout:(100)* NSEC_PER_MSEC]];
[self.queue addOperation: first];
[self.queue waitUntilAllOperationsAreFinished];
XCTAssertTrue(firstRun);
XCTAssertTrue(secondRun);
XCTAssertTrue(first.finished, "First operation finished");
XCTAssertFalse(first.cancelled, "First operation not cancelled");
XCTAssertTrue(second.finished, "Second operation finished");
XCTAssertFalse(second.cancelled, "Second operation not cancelled");
XCTAssertNil(second.error, "Second operation has no error");
}
- (void)testResultFinishDate
{
CKKSResultOperation* operation = [[CKKSResultOperation alloc] init];
XCTAssertNil(operation.finishDate, "Result operation does not have a finish date before it is run");
[operation addExecutionBlock:^{}];
[self.queue addOperation:operation];
[self.queue waitUntilAllOperationsAreFinished];
sleep(0.1); // wait for the completion block to have time to fire
XCTAssertNotNil(operation.finishDate, "Result operation has a finish date after everything is done");
NSTimeInterval timeIntervalSinceFinishDate = [[NSDate date] timeIntervalSinceDate:operation.finishDate];
XCTAssertTrue(timeIntervalSinceFinishDate >= 0.0 && timeIntervalSinceFinishDate <= 10.0, "Result operation finish datelooks reasonable");
}
- (void)testGroupOperation {
CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
[group runBeforeGroupFinished: op1];
CKKSResultOperation* op2 = [[CKKSResultOperation alloc] init];
[group runBeforeGroupFinished: op2];
[self.queue addOperation: group];
[self.queue waitUntilAllOperationsAreFinished];
XCTAssertEqual(op1.finished, YES, "First operation finished");
XCTAssertEqual(op2.finished, YES, "Second operation finished");
XCTAssertEqual(group.finished, YES, "Group operation finished");
XCTAssertEqual(op1.cancelled, NO, "First operation not cancelled");
XCTAssertEqual(op2.cancelled, NO, "Second operation cancelled");
XCTAssertEqual(group.cancelled, NO, "Group operation not cancelled");
XCTAssertNil(op1.error, "First operation: no error");
XCTAssertNil(op2.error, "Second operation: no error");
XCTAssertNil(group.error, "Group operation: no error");
}
- (void)testGroupOperationRunBlock {
XCTestExpectation* operationRun = [self expectationWithDescription:@"operation run with named:withBlock:"];
CKKSGroupOperation* group = [CKKSGroupOperation named:@"asdf" withBlock: ^{
[operationRun fulfill];
}];
[self.queue addOperation:group];
[self waitForExpectations: @[operationRun] timeout:5];
operationRun = [self expectationWithDescription:@"operation run with named:withBlockTakingSelf:"];
group = [CKKSGroupOperation named:@"asdf" withBlockTakingSelf:^(CKKSGroupOperation *strongOp) {
[operationRun fulfill];
}];
[self.queue addOperation:group];
[self waitForExpectations: @[operationRun] timeout:5];
}
- (void)testGroupOperationSubOperationCancel {
CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
[group runBeforeGroupFinished: op1];
CKKSResultCancelOperation* op2 = [[CKKSResultCancelOperation alloc] init];
[group runBeforeGroupFinished: op2];
[self.queue addOperation: group];
[self.queue waitUntilAllOperationsAreFinished];
XCTAssertEqual(op1.finished, YES, "First operation finished");
XCTAssertEqual(op2.finished, YES, "Second operation finished");
XCTAssertEqual(group.finished, YES, "Group operation finished");
XCTAssertEqual(op1.cancelled, NO, "First operation not cancelled");
XCTAssertEqual(op2.cancelled, YES, "Second operation not cancelled");
XCTAssertEqual(group.cancelled, NO, "Group operation not cancelled");
XCTAssertNil(op1.error, "First operation: no error");
XCTAssertNil(op2.error, "Second operation: no error");
XCTAssertNotNil(group.error, "Group operation: has an error");
XCTAssertEqual(group.error.code, CKKSResultSubresultCancelled, "Error code is CKKSResultSubresultCancelled");
}
- (void)testGroupOperationCancel {
CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
[group runBeforeGroupFinished: op1];
[group cancel];
[self.queue addOperation: group];
[self.queue waitUntilAllOperationsAreFinished];
XCTAssertEqual(op1.finished, YES, "First operation finished");
XCTAssertEqual(group.finished, YES, "Group operation finished");
XCTAssertEqual(op1.cancelled, YES, "First operation cancelled");
XCTAssertEqual(group.cancelled, YES, "Group operation cancelled");
XCTAssertNil(op1.error, "First operation: no error");
XCTAssertNil(group.error, "Group operation: no error");
}
- (void)testGroupOperationCancelAfterAdd {
CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
[group runBeforeGroupFinished: op1];
CKKSResultOperation* never = [[CKKSResultOperation alloc] init];
[group addDependency: never];
[self.queue addOperation: group];
[group cancel];
// Both of these should finish. Wait for that.
[op1 waitUntilFinished];
[group waitUntilFinished];
XCTAssertEqual(op1.finished, YES, "First operation finished");
XCTAssertEqual(group.finished, YES, "Group operation finished");
XCTAssertEqual(op1.cancelled, YES, "First operation cancelled");
XCTAssertEqual(group.cancelled, YES, "Group operation cancelled");
XCTAssertNil(op1.error, "First operation: no error");
XCTAssertNil(group.error, "Group operation: no error");
}
- (void)testGroupOperationCancelWhileRunning {
CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
group.name = @"operation-under-test";
XCTestExpectation* groupStarted = [self expectationWithDescription: @"group started"];
XCTestExpectation* cancelOccurs = [self expectationWithDescription: @"cancel occurs"];
CKKSCondition* everythingFinished = [[CKKSCondition alloc] init];
CKKSResultOperation* op1 = [CKKSResultOperation named:@"op1" withBlock:^{
[groupStarted fulfill];
[self waitForExpectations:@[cancelOccurs] timeout:8.0];
// 'do some work'. Will wait 200msec.
[everythingFinished wait:200*NSEC_PER_MSEC];
}];
[group runBeforeGroupFinished: op1];
[self.queue addOperation: group];
[self waitForExpectations:@[groupStarted] timeout:8.0];
[group cancel];
[cancelOccurs fulfill];
[group waitUntilFinished];
XCTAssertEqual(op1.finished, YES, "First operation finished");
XCTAssertEqual(group.finished, YES, "Group operation finished");
[everythingFinished fulfill];
XCTAssertEqual(op1.cancelled, YES, "First operation cancelled");
XCTAssertEqual(group.cancelled, YES, "Group operation cancelled");
XCTAssertNil(op1.error, "First operation: no error");
XCTAssertNotNil(group.error, "Group operation: has an error");
XCTAssertEqual(group.error.code, CKKSResultSubresultCancelled, "Error code is CKKSResultSubresultCancelled");
}
- (void)testGroupOperationWithDependsOn {
CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
[group dependOnBeforeGroupFinished:op1];
[group cancel];
[self.queue addOperation: group];
[self.queue waitUntilAllOperationsAreFinished];
XCTAssertEqual(op1.finished, NO, "First operation not finished");
XCTAssertEqual(group.finished, YES, "Group operation finished");
XCTAssertEqual(op1.cancelled, NO, "First operation not cancelled");
XCTAssertEqual(group.cancelled, YES, "Group operation cancelled");
XCTAssertNil(op1.error, "First operation: no error");
XCTAssertNil(group.error, "Group operation: no error");
}
- (void)testGroupOperationTimeout {
CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
__block bool run1 = false;
CKKSResultOperation* op1 = [CKKSResultOperation operationWithBlock: ^{
run1 = true;
}];
[group runBeforeGroupFinished: op1];
__block bool run2 = false;
CKKSResultOperation* op2 = [CKKSResultOperation operationWithBlock: ^{
run2 = true;
}];
[group runBeforeGroupFinished: op2];
CKKSResultOperation* never = [[CKKSResultOperation alloc] init];
[group addDependency: never];
[group timeout:10*NSEC_PER_MSEC];
[self.queue addOperation: group];
[self.queue waitUntilAllOperationsAreFinished];
// Shouldn't be necessary, but I'm not sure the NSOperation's finished property vs. dependency triggering is thread-safe
[op1 waitUntilFinished];
[op2 waitUntilFinished];
XCTAssertEqual(op1.finished, YES, "First operation finished");
XCTAssertEqual(op2.finished, YES, "Second operation finished");
XCTAssertEqual(group.finished, YES, "Group operation finished");
XCTAssertEqual(op1.cancelled, YES, "First operation cancelled");
XCTAssertEqual(op2.cancelled, YES, "Second operation cancelled");
XCTAssertEqual(group.cancelled, YES, "Group operation cancelled");
XCTAssertFalse(run1, "First operation did not run");
XCTAssertFalse(run2, "Second operation did not run");
XCTAssertNil(op1.error, "First operation: no error");
XCTAssertNil(op2.error, "Second operation: no error");
XCTAssertNotNil(group.error, "Group operation: error");
XCTAssertEqual(group.error.code, CKKSResultTimedOut, "Error code is CKKSResultTimedOut");
// Try a few more times, just in case
for(int i = 0; i < 100; i++) {
CKKSGroupOperation* g = [[CKKSGroupOperation alloc] init];
[g addDependency: never];
[g timeout:((i
[self.queue addOperation: g];
[self.queue waitUntilAllOperationsAreFinished];
}
}
- (void)testGroupOperationError {
CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
[group runBeforeGroupFinished: op1];
CKKSResultErrorOperation* op2 = [[CKKSResultErrorOperation alloc] init];
[group runBeforeGroupFinished: op2];
[self.queue addOperation: group];
[self.queue waitUntilAllOperationsAreFinished];
XCTAssertEqual(op1.finished, YES, "First operation finished");
XCTAssertEqual(op2.finished, YES, "Second operation finished");
XCTAssertEqual(group.finished, YES, "Group operation finished");
XCTAssertEqual(op1.cancelled, NO, "First operation not cancelled");
XCTAssertEqual(op2.cancelled, NO, "Second operation cancelled");
XCTAssertEqual(group.cancelled, NO, "Group operation not cancelled");
XCTAssertNil(op1.error, "First operation: no error");
XCTAssertNotNil(op2.error, "Second operation: error (as expected)");
XCTAssertEqual(op2.error.code, 5, "Rght error from the erroring operation");
XCTAssertNotNil(group.error, "Error is generated when dependent CKKSResultOperation has an error");
XCTAssertEqual(group.error.code, CKKSResultSubresultError, "Error code is CKKSResultSubresultError");
XCTAssertNotNil(group.error.userInfo[NSUnderlyingErrorKey], "Passed up the error from the first operation");
XCTAssertEqual([group.error.userInfo[NSUnderlyingErrorKey] code], 5, "Passed up the right error from the first operation");
}
- (void)testGroupOperationPending {
CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
[group runBeforeGroupFinished: op1];
CKKSResultOperation* op2 = [[CKKSResultOperation alloc] init];
[group addDependency: op2];
[self.queue addOperation: group];
XCTAssertTrue([group isPending], "group operation hasn't started yet");
[self.queue addOperation: op2];
[self.queue waitUntilAllOperationsAreFinished];
XCTAssertFalse([group isPending], "group operation has started");
XCTAssertEqual(op1.finished, YES, "First operation finished");
XCTAssertEqual(op2.finished, YES, "Second operation finished");
XCTAssertEqual(group.finished, YES, "Group operation finished");
XCTAssertEqual(op1.cancelled, NO, "First operation not cancelled");
XCTAssertEqual(op2.cancelled, NO, "Second operation cancelled");
XCTAssertEqual(group.cancelled, NO, "Group operation not cancelled");
XCTAssertNil(op1.error, "First operation: no error");
XCTAssertNil(op2.error, "Second operation: no error");
XCTAssertNil(group.error, "Group operation: no error");
}
- (void)testGroupOperationPendingAfterCancel {
CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
[group runBeforeGroupFinished: op1];
CKKSResultOperation* op2 = [[CKKSResultOperation alloc] init];
[group addDependency: op2];
[group cancel];
XCTAssertFalse([group isPending], "group operation isn't pending, as it's cancelled");
}
@end
#endif /* OCTAGON */