2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
26 #import <XCTest/XCTest.h>
28 #import "keychain/ckks/CKKSGroupOperation.h"
29 #import "keychain/ckks/CKKSCondition.h"
32 @interface CKKSResultCancelOperation : CKKSResultOperation
33 - (instancetype) init;
36 @implementation CKKSResultCancelOperation
37 - (instancetype)init {
38 if(self = [super init]) {
39 __weak __typeof(self) weakSelf = self;
40 [self addExecutionBlock:^{
50 @interface CKKSResultErrorOperation : CKKSResultOperation
51 - (instancetype) init;
54 @implementation CKKSResultErrorOperation
55 - (instancetype)init {
56 if(self = [super init]) {
57 __weak __typeof(self) weakSelf = self;
58 [self addExecutionBlock:^{
59 weakSelf.error = [NSError errorWithDomain:@"test domain" code:5 userInfo:nil];
67 @interface CKKSOperationTests : XCTestCase
68 @property NSOperationQueue* queue;
71 // Remaining tests to write:
72 // TODO: subclass of CKKSResultOperation implementing main() respects addSuccessDependency without any special code
73 // TODO: chain of automatic dependencies
74 // TODO: test showing that CKKSGroupOperations don't start if they success-depend on a failed CKKSResultOperation
76 @implementation CKKSOperationTests
81 self.queue = [[NSOperationQueue alloc] init];
85 [self.queue cancelAllOperations];
91 - (void)testIsPending {
92 NSBlockOperation* run = [NSBlockOperation blockOperationWithBlock:^{}];
93 NSBlockOperation* cancel = [NSBlockOperation blockOperationWithBlock:^{}];
94 NSBlockOperation* pending = [NSBlockOperation blockOperationWithBlock:^{}];
96 [self.queue addOperation: run];
98 [self.queue waitUntilAllOperationsAreFinished];
100 XCTAssertTrue( [pending isPending], @"Pending operation should be pending");
101 XCTAssertFalse([run isPending], @"run operation should not be pending");
102 XCTAssertFalse([cancel isPending], @"Cancelled operation should not be pending");
105 - (void)testResultOperation {
106 CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
107 __weak __typeof(op) weakOp = op;
109 [op addExecutionBlock:^{
110 weakOp.error = [NSError errorWithDomain:@"test domain" code:0 userInfo:nil];
113 [self.queue addOperation: op];
115 [op waitUntilFinished];
117 XCTAssertNotNil(op.error, "errors can persist");
120 - (void)testResultSuccessDependency {
121 __block bool firstRun = false;
122 __block bool secondRun = false;
124 CKKSResultOperation* first = [[CKKSResultOperation alloc] init];
125 [first addExecutionBlock:^{
129 CKKSResultOperation* second = [[CKKSResultOperation alloc] init];
130 [second addExecutionBlock:^{
131 XCTAssertTrue(firstRun);
134 [second addSuccessDependency: first];
136 [self.queue addOperation: second];
137 [self.queue addOperation: first];
139 [self.queue waitUntilAllOperationsAreFinished];
141 XCTAssertTrue(firstRun);
142 XCTAssertTrue(secondRun);
144 XCTAssertTrue(first.finished, "First operation finished");
145 XCTAssertFalse(first.cancelled, "First operation not cancelled");
146 XCTAssertTrue(second.finished, "Second operation finished");
147 XCTAssertFalse(second.cancelled, "Second operation not cancelled");
150 - (void)testResultSuccessDependencyCancel {
151 CKKSResultCancelOperation* first = [[CKKSResultCancelOperation alloc] init];
153 CKKSResultOperation* second = [[CKKSResultOperation alloc] init];
154 [second addExecutionBlock:^{
155 XCTFail("Second operation should never run");
157 [second addSuccessDependency: first];
159 [self.queue addOperation: second];
160 [self.queue addOperation: first];
162 [self.queue waitUntilAllOperationsAreFinished];
164 XCTAssertTrue(first.finished, "First operation finished");
165 XCTAssertTrue(first.cancelled, "First operation is canceled (as requested)");
167 XCTAssertTrue(second.cancelled, "Second operation is canceled");
168 XCTAssertTrue(second.finished, "Second operation finished");
169 XCTAssertNotNil(second.error, "Error is generated when CKKSResultOperation is cancelled");
170 XCTAssertEqual(second.error.code, CKKSResultSubresultCancelled, "Error code is CKKSResultSubresultCancelled");
173 - (void)testResultSuccessDependencyError {
174 CKKSResultErrorOperation* first = [[CKKSResultErrorOperation alloc] init];
176 CKKSResultOperation* second = [[CKKSResultOperation alloc] init];
177 [second addExecutionBlock:^{
178 XCTFail("Second operation should never run");
180 [second addSuccessDependency: first];
182 [self.queue addOperation: second];
183 [self.queue addOperation: first];
185 [self.queue waitUntilAllOperationsAreFinished];
187 XCTAssertTrue(first.finished, "First operation finished");
188 XCTAssertFalse(first.cancelled, "First operation is not canceled");
189 XCTAssertNotNil(first.error, "First operation has an error");
191 XCTAssertTrue(second.cancelled, "Second operation is canceled");
192 XCTAssertTrue(second.finished, "Second operation finished");
193 XCTAssertNotNil(second.error, "Error is generated when dependent CKKSResultOperation has an error");
194 XCTAssertEqual(second.error.code, CKKSResultSubresultError, "Error code is CKKSResultSubresultError");
196 XCTAssertNotNil(second.error.userInfo[NSUnderlyingErrorKey], "Passed up the error from the first operation");
197 XCTAssertEqual([second.error.userInfo[NSUnderlyingErrorKey] code], 5, "Passed up the right error from the first operation");
200 - (void)testResultTimeout {
201 __block bool firstRun = false;
202 __block bool secondRun = false;
204 CKKSResultOperation* first = [[CKKSResultOperation alloc] init];
205 [first addExecutionBlock:^{
209 CKKSResultOperation* second = [[CKKSResultOperation alloc] init];
210 [second addExecutionBlock:^{
211 XCTAssertTrue(firstRun);
214 [second addDependency: first];
216 [self.queue addOperation: [second timeout:(50)* NSEC_PER_MSEC]];
217 [self.queue waitUntilAllOperationsAreFinished];
219 XCTAssertFalse(firstRun);
220 XCTAssertFalse(secondRun);
222 XCTAssertFalse(first.finished, "First operation not finished");
223 XCTAssertFalse(first.cancelled, "First operation not cancelled");
224 XCTAssertTrue(second.finished, "Second operation finished");
225 XCTAssertTrue(second.cancelled, "Second operation cancelled");
226 XCTAssertNotNil(second.error, "Second operation has an error");
227 XCTAssertEqual(second.error.code, CKKSResultTimedOut, "Second operation error is good");
228 NSError* underlying = second.error.userInfo[NSUnderlyingErrorKey];
229 XCTAssertNil(underlying, "Second operation's error doesn't have an underlying explanation");
232 - (void)testResultTimeoutWithUnderlyingError {
233 __block bool firstRun = false;
234 __block bool secondRun = false;
236 CKKSResultOperation* first = [[CKKSResultOperation alloc] init];
237 [first addExecutionBlock:^{
240 first.descriptionErrorCode = 604;
242 CKKSResultOperation* second = [[CKKSResultOperation alloc] init];
243 [second addExecutionBlock:^{
244 XCTAssertTrue(firstRun);
247 [second addDependency: first];
249 [self.queue addOperation: [second timeout:(50)* NSEC_PER_MSEC]];
250 [self.queue waitUntilAllOperationsAreFinished];
252 XCTAssertFalse(firstRun);
253 XCTAssertFalse(secondRun);
255 XCTAssertFalse(first.finished, "First operation not finished");
256 XCTAssertFalse(first.cancelled, "First operation not cancelled");
257 XCTAssertTrue(second.finished, "Second operation finished");
258 XCTAssertTrue(second.cancelled, "Second operation cancelled");
259 XCTAssertNotNil(second.error, "Second operation has an error");
260 XCTAssertEqual(second.error.code, CKKSResultTimedOut, "Second operation error is good");
261 NSError* underlying = second.error.userInfo[NSUnderlyingErrorKey];
262 XCTAssertNotNil(underlying, "second operation's error has an underlying reason");
263 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "second operation's underlying error's domain should be CKKSResultDescriptionErrorDomain");
264 XCTAssertEqual(underlying.code, 604, "second operation's underlying error's domain should be first's description");
268 - (void)testResultNoTimeout {
269 __block bool firstRun = false;
270 __block bool secondRun = false;
272 CKKSResultOperation* first = [[CKKSResultOperation alloc] init];
273 [first addExecutionBlock:^{
277 CKKSResultOperation* second = [[CKKSResultOperation alloc] init];
278 [second addExecutionBlock:^{
279 XCTAssertTrue(firstRun);
282 [second addDependency: first];
284 [self.queue addOperation: [second timeout:(100)* NSEC_PER_MSEC]];
285 [self.queue addOperation: first];
286 [self.queue waitUntilAllOperationsAreFinished];
288 XCTAssertTrue(firstRun);
289 XCTAssertTrue(secondRun);
291 XCTAssertTrue(first.finished, "First operation finished");
292 XCTAssertFalse(first.cancelled, "First operation not cancelled");
293 XCTAssertTrue(second.finished, "Second operation finished");
294 XCTAssertFalse(second.cancelled, "Second operation not cancelled");
295 XCTAssertNil(second.error, "Second operation has no error");
298 - (void)testResultFinishDate
300 CKKSResultOperation* operation = [[CKKSResultOperation alloc] init];
301 XCTAssertNil(operation.finishDate, "Result operation does not have a finish date before it is run");
303 [operation addExecutionBlock:^{}];
305 [self.queue addOperation:operation];
306 [self.queue waitUntilAllOperationsAreFinished];
308 XCTAssertEqual([operation.completionHandlerDidRunCondition wait:4 * NSEC_PER_SEC], 0, "Completion block should fire in a reasonable amount of time");
309 XCTAssertNotNil(operation.finishDate, "Result operation has a finish date after everything is done");
310 NSTimeInterval timeIntervalSinceFinishDate = [[NSDate date] timeIntervalSinceDate:operation.finishDate];
311 XCTAssertTrue(timeIntervalSinceFinishDate >= 0.0 && timeIntervalSinceFinishDate <= 10.0, "Result operation finish datelooks reasonable");
314 - (void)testGroupOperation {
315 CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
317 CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
318 [group runBeforeGroupFinished: op1];
320 CKKSResultOperation* op2 = [[CKKSResultOperation alloc] init];
321 [group runBeforeGroupFinished: op2];
323 [self.queue addOperation: group];
325 [self.queue waitUntilAllOperationsAreFinished];
327 XCTAssertEqual(op1.finished, YES, "First operation finished");
328 XCTAssertEqual(op2.finished, YES, "Second operation finished");
329 XCTAssertEqual(group.finished, YES, "Group operation finished");
331 XCTAssertEqual(op1.cancelled, NO, "First operation not cancelled");
332 XCTAssertEqual(op2.cancelled, NO, "Second operation cancelled");
333 XCTAssertEqual(group.cancelled, NO, "Group operation not cancelled");
335 XCTAssertNil(op1.error, "First operation: no error");
336 XCTAssertNil(op2.error, "Second operation: no error");
337 XCTAssertNil(group.error, "Group operation: no error");
340 - (void)testGroupOperationRunBlock {
341 XCTestExpectation* operationRun = [self expectationWithDescription:@"operation run with named:withBlock:"];
342 CKKSGroupOperation* group = [CKKSGroupOperation named:@"asdf" withBlock: ^{
343 [operationRun fulfill];
345 [self.queue addOperation:group];
346 [self waitForExpectations: @[operationRun] timeout:5];
348 operationRun = [self expectationWithDescription:@"operation run with named:withBlockTakingSelf:"];
349 group = [CKKSGroupOperation named:@"asdf" withBlockTakingSelf:^(CKKSGroupOperation *strongOp) {
350 [operationRun fulfill];
352 [self.queue addOperation:group];
353 [self waitForExpectations: @[operationRun] timeout:5];
356 - (void)testGroupOperationSubOperationCancel {
357 CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
359 CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
360 [group runBeforeGroupFinished: op1];
362 CKKSResultCancelOperation* op2 = [[CKKSResultCancelOperation alloc] init];
363 [group runBeforeGroupFinished: op2];
365 [self.queue addOperation: group];
367 [self.queue waitUntilAllOperationsAreFinished];
369 XCTAssertEqual(op1.finished, YES, "First operation finished");
370 XCTAssertEqual(op2.finished, YES, "Second operation finished");
371 XCTAssertEqual(group.finished, YES, "Group operation finished");
373 XCTAssertEqual(op1.cancelled, NO, "First operation not cancelled");
374 XCTAssertEqual(op2.cancelled, YES, "Second operation not cancelled");
375 XCTAssertEqual(group.cancelled, NO, "Group operation not cancelled");
377 XCTAssertNil(op1.error, "First operation: no error");
378 XCTAssertNil(op2.error, "Second operation: no error");
379 XCTAssertNotNil(group.error, "Group operation: has an error");
380 XCTAssertEqual(group.error.code, CKKSResultSubresultCancelled, "Error code is CKKSResultSubresultCancelled");
383 - (void)testGroupOperationCancel {
384 CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
386 CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
387 [group runBeforeGroupFinished: op1];
390 [self.queue addOperation: group];
392 [self.queue waitUntilAllOperationsAreFinished];
394 XCTAssertEqual(op1.finished, YES, "First operation finished");
395 XCTAssertEqual(group.finished, YES, "Group operation finished");
397 XCTAssertEqual(op1.cancelled, YES, "First operation cancelled");
398 XCTAssertEqual(group.cancelled, YES, "Group operation cancelled");
400 XCTAssertNil(op1.error, "First operation: no error");
401 XCTAssertNil(group.error, "Group operation: no error");
404 - (void)testGroupOperationCancelAfterAdd {
405 CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
407 CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
408 [group runBeforeGroupFinished: op1];
410 CKKSResultOperation* never = [[CKKSResultOperation alloc] init];
411 [group addDependency: never];
413 [self.queue addOperation: group];
417 // Both of these should finish. Wait for that.
418 [op1 waitUntilFinished];
419 [group waitUntilFinished];
421 XCTAssertEqual(op1.finished, YES, "First operation finished");
422 XCTAssertEqual(group.finished, YES, "Group operation finished");
424 XCTAssertEqual(op1.cancelled, YES, "First operation cancelled");
425 XCTAssertEqual(group.cancelled, YES, "Group operation cancelled");
427 XCTAssertNil(op1.error, "First operation: no error");
428 XCTAssertNil(group.error, "Group operation: no error");
431 - (void)testGroupOperationCancelWhileRunning {
432 CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
433 group.name = @"operation-under-test";
435 XCTestExpectation* groupStarted = [self expectationWithDescription: @"group started"];
436 XCTestExpectation* cancelOccurs = [self expectationWithDescription: @"cancel occurs"];
438 CKKSCondition* everythingFinished = [[CKKSCondition alloc] init];
439 CKKSResultOperation* op1 = [CKKSResultOperation named:@"op1" withBlock:^{
440 [groupStarted fulfill];
442 [self waitForExpectations:@[cancelOccurs] timeout:8.0];
444 // 'do some work'. Will wait 200msec.
445 [everythingFinished wait:200*NSEC_PER_MSEC];
447 [group runBeforeGroupFinished: op1];
448 [self.queue addOperation: group];
450 [self waitForExpectations:@[groupStarted] timeout:8.0];
452 [cancelOccurs fulfill];
454 [group waitUntilFinished];
456 XCTAssertEqual(op1.finished, YES, "First operation finished");
457 XCTAssertEqual(group.finished, YES, "Group operation finished");
459 [everythingFinished fulfill];
461 XCTAssertEqual(op1.cancelled, YES, "First operation cancelled");
462 XCTAssertEqual(group.cancelled, YES, "Group operation cancelled");
464 XCTAssertNil(op1.error, "First operation: no error");
465 XCTAssertNotNil(group.error, "Group operation: has an error");
466 XCTAssertEqual(group.error.code, CKKSResultSubresultCancelled, "Error code is CKKSResultSubresultCancelled");
469 - (void)testGroupOperationWithDependsOn {
470 CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
472 CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
473 [group dependOnBeforeGroupFinished:op1];
476 [self.queue addOperation: group];
478 [self.queue waitUntilAllOperationsAreFinished];
480 XCTAssertEqual(op1.finished, NO, "First operation not finished");
481 XCTAssertEqual(group.finished, YES, "Group operation finished");
483 XCTAssertEqual(op1.cancelled, NO, "First operation not cancelled");
484 XCTAssertEqual(group.cancelled, YES, "Group operation cancelled");
486 XCTAssertNil(op1.error, "First operation: no error");
487 XCTAssertNil(group.error, "Group operation: no error");
490 - (void)testGroupOperationTimeout {
491 CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
493 __block bool run1 = false;
494 CKKSResultOperation* op1 = [CKKSResultOperation operationWithBlock: ^{
497 [group runBeforeGroupFinished: op1];
499 __block bool run2 = false;
500 CKKSResultOperation* op2 = [CKKSResultOperation operationWithBlock: ^{
503 [group runBeforeGroupFinished: op2];
505 CKKSResultOperation* never = [[CKKSResultOperation alloc] init];
506 [group addDependency: never];
508 [group timeout:10*NSEC_PER_MSEC];
509 [self.queue addOperation: group];
511 [self.queue waitUntilAllOperationsAreFinished];
513 // Shouldn't be necessary, but I'm not sure the NSOperation's finished property vs. dependency triggering is thread-safe
514 [op1 waitUntilFinished];
515 [op2 waitUntilFinished];
517 XCTAssertEqual(op1.finished, YES, "First operation finished");
518 XCTAssertEqual(op2.finished, YES, "Second operation finished");
519 XCTAssertEqual(group.finished, YES, "Group operation finished");
521 XCTAssertEqual(op1.cancelled, YES, "First operation cancelled");
522 XCTAssertEqual(op2.cancelled, YES, "Second operation cancelled");
523 XCTAssertEqual(group.cancelled, YES, "Group operation cancelled");
525 XCTAssertFalse(run1, "First operation did not run");
526 XCTAssertFalse(run2, "Second operation did not run");
528 XCTAssertNil(op1.error, "First operation: no error");
529 XCTAssertNil(op2.error, "Second operation: no error");
530 XCTAssertNotNil(group.error, "Group operation: error");
531 XCTAssertEqual(group.error.code, CKKSResultTimedOut, "Error code is CKKSResultTimedOut");
533 // Try a few more times, just in case
534 for(int i = 0; i < 100; i++) {
535 CKKSGroupOperation* g = [[CKKSGroupOperation alloc] init];
536 [g addDependency: never];
537 [g timeout:((i%20)+1)*NSEC_PER_MSEC];
539 [self.queue addOperation: g];
540 [self.queue waitUntilAllOperationsAreFinished];
544 - (void)testGroupOperationError {
545 CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
547 CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
548 [group runBeforeGroupFinished: op1];
550 CKKSResultErrorOperation* op2 = [[CKKSResultErrorOperation alloc] init];
551 [group runBeforeGroupFinished: op2];
553 [self.queue addOperation: group];
555 [self.queue waitUntilAllOperationsAreFinished];
557 XCTAssertEqual(op1.finished, YES, "First operation finished");
558 XCTAssertEqual(op2.finished, YES, "Second operation finished");
559 XCTAssertEqual(group.finished, YES, "Group operation finished");
561 XCTAssertEqual(op1.cancelled, NO, "First operation not cancelled");
562 XCTAssertEqual(op2.cancelled, NO, "Second operation cancelled");
563 XCTAssertEqual(group.cancelled, NO, "Group operation not cancelled");
565 XCTAssertNil(op1.error, "First operation: no error");
566 XCTAssertNotNil(op2.error, "Second operation: error (as expected)");
567 XCTAssertEqual(op2.error.code, 5, "Rght error from the erroring operation");
569 XCTAssertNotNil(group.error, "Error is generated when dependent CKKSResultOperation has an error");
570 XCTAssertEqual(group.error.code, CKKSResultSubresultError, "Error code is CKKSResultSubresultError");
571 XCTAssertNotNil(group.error.userInfo[NSUnderlyingErrorKey], "Passed up the error from the first operation");
572 XCTAssertEqual([group.error.userInfo[NSUnderlyingErrorKey] code], 5, "Passed up the right error from the first operation");
575 - (void)testGroupOperationPending {
576 CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
578 CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
579 [group runBeforeGroupFinished: op1];
581 CKKSResultOperation* op2 = [[CKKSResultOperation alloc] init];
582 [group addDependency: op2];
584 [self.queue addOperation: group];
586 XCTAssertTrue([group isPending], "group operation hasn't started yet");
588 [self.queue addOperation: op2];
589 [self.queue waitUntilAllOperationsAreFinished];
590 XCTAssertFalse([group isPending], "group operation has started");
592 XCTAssertEqual(op1.finished, YES, "First operation finished");
593 XCTAssertEqual(op2.finished, YES, "Second operation finished");
594 XCTAssertEqual(group.finished, YES, "Group operation finished");
596 XCTAssertEqual(op1.cancelled, NO, "First operation not cancelled");
597 XCTAssertEqual(op2.cancelled, NO, "Second operation cancelled");
598 XCTAssertEqual(group.cancelled, NO, "Group operation not cancelled");
600 XCTAssertNil(op1.error, "First operation: no error");
601 XCTAssertNil(op2.error, "Second operation: no error");
602 XCTAssertNil(group.error, "Group operation: no error");
605 - (void)testGroupOperationPendingAfterCancel {
606 CKKSGroupOperation* group = [[CKKSGroupOperation alloc] init];
608 CKKSResultOperation* op1 = [[CKKSResultOperation alloc] init];
609 [group runBeforeGroupFinished: op1];
611 CKKSResultOperation* op2 = [[CKKSResultOperation alloc] init];
612 [group addDependency: op2];
616 XCTAssertFalse([group isPending], "group operation isn't pending, as it's cancelled");