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];

XCTAssertEqual([operation.completionHandlerDidRunCondition wait:4 * NSEC_PER_SEC], 0, "Completion block should fire in a reasonable amount of time");
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%20)+1)*NSEC_PER_MSEC];

[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 */