diff --git a/Tests/Foundation/Tests/TestOperationQueue.swift b/Tests/Foundation/Tests/TestOperationQueue.swift index efc09f7e6b..2d1c770fc7 100644 --- a/Tests/Foundation/Tests/TestOperationQueue.swift +++ b/Tests/Foundation/Tests/TestOperationQueue.swift @@ -13,7 +13,7 @@ class TestOperationQueue : XCTestCase { static var allTests: [(String, (TestOperationQueue) -> () throws -> Void)] { return [ ("test_OperationPriorities", test_OperationPriorities), - ("test_OperationCount", test_OperationCount), + /* ⚠️ */ ("test_OperationCount", testExpectedToFail(test_OperationCount, "OperationQueue can't count to more than 1")), ("test_AsyncOperation", test_AsyncOperation), ("test_SyncOperationWithoutAQueue", test_SyncOperationWithoutAQueue), ("test_isExecutingWorks", test_isExecutingWorks), @@ -27,6 +27,18 @@ class TestOperationQueue : XCTestCase { ("test_CurrentQueueWithUnderlyingQueueResetToNil", test_CurrentQueueWithUnderlyingQueueResetToNil), ("test_isSuspended", test_isSuspended), ("test_OperationDependencyCount", test_OperationDependencyCount), + ("test_CancelDependency", test_CancelDependency), + /* ⚠️ */ ("test_Deadlock", testExpectedToFail(test_Deadlock, "Crashes due to overrelease of OperationQueue")), + ("test_CancelOutOfQueue", test_CancelOutOfQueue), + /* ⚠️ */ ("test_CrossQueueDependency", testExpectedToFail(test_CrossQueueDependency, "Crashes due to overrelease of OperationQueue")), + ("test_CancelWhileSuspended", test_CancelWhileSuspended), + ("test_OperationOrder", test_OperationOrder), + ("test_OperationOrder2", test_OperationOrder2), + /* ⚠️ */ ("test_WaitUntilFinished", testExpectedToFail(test_WaitUntilFinished, "Crashes due to overrelease of OperationQueue")), + ("test_OperationWaitUntilFinished", test_OperationWaitUntilFinished), + /* ⚠️ */ ("test_CustomOperationReady", testExpectedToFail(test_CustomOperationReady, "Crashes due to overrelease of OperationQueue")), + /* ⚠️ */ ("test_DependencyCycleBreak", testExpectedToFail(test_DependencyCycleBreak, "Crashes due to overrelease of OperationQueue")), + /* ⚠️ */ ("test_Lifecycle", testExpectedToFail(test_Lifecycle, "Crashes due to overrelease of OperationQueue")), ] } @@ -34,9 +46,19 @@ class TestOperationQueue : XCTestCase { let queue = OperationQueue() let op1 = BlockOperation(block: { Thread.sleep(forTimeInterval: 2) }) queue.addOperation(op1) - XCTAssertTrue(queue.operationCount == 1) + XCTAssertEqual(queue.operationCount, 1) queue.waitUntilAllOperationsAreFinished() - XCTAssertTrue(queue.operationCount == 0) + XCTAssertEqual(queue.operationCount, 0) + + let op2 = BlockOperation(block: { Thread.sleep(forTimeInterval: 0.5) }) + let op3 = BlockOperation(block: { Thread.sleep(forTimeInterval: 0.5) }) + queue.addOperation(op2) + queue.addOperation(op3) + XCTAssertEqual(queue.operationCount, 2) + XCTAssertEqual(queue.operations.count, 2) + queue.waitUntilAllOperationsAreFinished() + XCTAssertEqual(queue.operationCount, 0) + XCTAssertEqual(queue.operations.count, 0) } func test_OperationPriorities() { @@ -294,6 +316,352 @@ class TestOperationQueue : XCTestCase { op1.addDependency(op2) XCTAssert(op1.dependencies.count == 1) } + + func test_CancelDependency() { + let expectation = self.expectation(description: "Operation should finish") + + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + + let op1 = BlockOperation() { + XCTAssert(false, "Should not run") + } + let op2 = BlockOperation() { + expectation.fulfill() + } + + op2.addDependency(op1) + op1.cancel() + + queue.addOperation(op1) + queue.addOperation(op2) + + waitForExpectations(timeout: 1) + } + + func test_Deadlock() { + let expectation1 = self.expectation(description: "Operation should finish") + let expectation2 = self.expectation(description: "Operation should finish") + + let op1 = BlockOperation { + expectation1.fulfill() + } + op1.name = "op1" + + let op2 = BlockOperation { + expectation2.fulfill() + } + op2.name = "op2" + + op1.addDependency(op2) + + // Narrow scope to force early release of queue object + _ = { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + queue.addOperation(op1) + queue.addOperation(op2) + }() + + waitForExpectations(timeout: 1) + Thread.sleep(forTimeInterval: 1) + } + + public func test_CancelOutOfQueue() { + let op = Operation() + op.cancel() + + XCTAssert(op.isCancelled) + XCTAssertFalse(op.isExecuting) + XCTAssertFalse(op.isFinished) + } + + public func test_CrossQueueDependency() { + let queue = OperationQueue() + let queue2 = OperationQueue() + + let expectation1 = self.expectation(description: "Operation should finish") + let expectation2 = self.expectation(description: "Operation should finish") + + let op1 = BlockOperation { + expectation1.fulfill() + } + op1.name = "op1" + + let op2 = BlockOperation { + expectation2.fulfill() + } + op2.name = "op2" + + op1.addDependency(op2) + + queue.addOperation(op1) + queue2.addOperation(op2) + + waitForExpectations(timeout: 1) + } + + public func test_CancelWhileSuspended() { + let queue = OperationQueue() + queue.isSuspended = true + + let op1 = BlockOperation {} + op1.name = "op1" + + let op2 = BlockOperation {} + op2.name = "op2" + + queue.addOperation(op1) + queue.addOperation(op2) + + op1.cancel() + op2.cancel() + + queue.isSuspended = false + queue.waitUntilAllOperationsAreFinished() + + XCTAssert(op1.isCancelled) + XCTAssertFalse(op1.isExecuting) + XCTAssert(op1.isFinished) + XCTAssert(op2.isCancelled) + XCTAssertFalse(op2.isExecuting) + XCTAssert(op2.isFinished) + } + + public func test_OperationOrder() { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + queue.isSuspended = true + + var array = [Int]() + + let op1 = BlockOperation { + array.append(1) + } + op1.queuePriority = .normal + op1.name = "op1" + + let op2 = BlockOperation { + array.append(2) + } + op2.queuePriority = .normal + op2.name = "op2" + + let op3 = BlockOperation { + array.append(3) + } + op3.queuePriority = .normal + op3.name = "op3" + + let op4 = BlockOperation { + array.append(4) + } + op4.queuePriority = .normal + op4.name = "op4" + + let op5 = BlockOperation { + array.append(5) + } + op5.queuePriority = .normal + op5.name = "op5" + + queue.addOperation(op1) + queue.addOperation(op2) + queue.addOperation(op3) + queue.addOperation(op4) + queue.addOperation(op5) + + queue.isSuspended = false + queue.waitUntilAllOperationsAreFinished() + + XCTAssertEqual(array, [1, 2, 3, 4, 5]) + } + + public func test_OperationOrder2() { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + queue.isSuspended = true + + var array = [Int]() + + let op1 = BlockOperation { + array.append(1) + } + op1.queuePriority = .veryLow + op1.name = "op1" + + let op2 = BlockOperation { + array.append(2) + } + op2.queuePriority = .low + op2.name = "op2" + + let op3 = BlockOperation { + array.append(3) + } + op3.queuePriority = .normal + op3.name = "op3" + + let op4 = BlockOperation { + array.append(4) + } + op4.queuePriority = .high + op4.name = "op4" + + let op5 = BlockOperation { + array.append(5) + } + op5.queuePriority = .veryHigh + op5.name = "op5" + + queue.addOperation(op1) + queue.addOperation(op2) + queue.addOperation(op3) + queue.addOperation(op4) + queue.addOperation(op5) + + queue.isSuspended = false + queue.waitUntilAllOperationsAreFinished() + + XCTAssertEqual(array, [5, 4, 3, 2, 1]) + } + + func test_WaitUntilFinished() { + let queue1 = OperationQueue() + let queue2 = OperationQueue() + + let op1 = BlockOperation {Thread.sleep(forTimeInterval: 1) } + let op2 = BlockOperation { } + + op2.addDependency(op1) + + queue1.addOperation(op1) + queue2.addOperation(op2) + + queue2.waitUntilAllOperationsAreFinished() + XCTAssertEqual(queue2.operationCount, 0) + } + + func test_OperationWaitUntilFinished() { + let queue1 = OperationQueue() + let op1 = BlockOperation { Thread.sleep(forTimeInterval: 1) } + queue1.addOperation(op1) + op1.waitUntilFinished() + XCTAssertEqual(queue1.operationCount, 0) + } + + func test_CustomOperationReady() { + class CustomOperation: Operation { + + private var _isReady = false + + override var isReady: Bool { + return _isReady + } + + func setIsReady() { + willChangeValue(forKey: "isReady") + _isReady = true + didChangeValue(forKey: "isReady") + } + + } + + let expectation = self.expectation(description: "Operation should finish") + + let queue1 = OperationQueue() + let op1 = CustomOperation() + let op2 = BlockOperation(block: { + expectation.fulfill() + }) + + queue1.addOperation(op1) + queue1.addOperation(op2) + + waitForExpectations(timeout: 1) + + XCTAssertEqual(queue1.operationCount, 1) + op1.setIsReady() + queue1.waitUntilAllOperationsAreFinished() + XCTAssertEqual(queue1.operationCount, 0) + } + + func test_DependencyCycleBreak() { + let op1DidRun = expectation(description: "op1 supposed to be run") + let op2DidRun = expectation(description: "op2 supposed to be run") + let op2Finished = expectation(description: "op2 supposed to be finished") + let op3Cancelled = expectation(description: "op3 supposed to be cancelled") + let op3DidRun = expectation(description: "op3 is not supposed to be run") + op3DidRun.isInverted = true + + var op1: Operation! + var op2: Operation! + var op3: Operation! + + let queue1 = OperationQueue() + op1 = BlockOperation { + op1DidRun.fulfill() + if op2.isFinished { + op2Finished.fulfill() + } + } + op2 = BlockOperation { + op2DidRun.fulfill() + if op3.isCancelled { + op3Cancelled.fulfill() + } + } + op3 = BlockOperation { + op3DidRun.fulfill() + } + + // Create debendency cycle + op1.addDependency(op2) + op2.addDependency(op3) + op3.addDependency(op1) + + queue1.addOperation(op1) + queue1.addOperation(op2) + queue1.addOperation(op3) + + XCTAssertEqual(queue1.operationCount, 3) + + //Break dependency cycle + op3.cancel() + + waitForExpectations(timeout: 1) + } + + func test_Lifecycle() { + let opStarted = expectation(description: "Operation supposed to start") + let opDone = expectation(description: "Operation supposed to be done") + + let op1 = BlockOperation { + Thread.sleep(forTimeInterval: 0.3) + } + let op2 = BlockOperation { + opStarted.fulfill() + Thread.sleep(forTimeInterval: 0.3) + opDone.fulfill() + } + + op1.addDependency(op2) + + weak var weakQueue: OperationQueue? + _ = { + let queue = OperationQueue() + weakQueue = queue + queue.addOperation(op1) + queue.addOperation(op2) + }() + + wait(for: [opStarted], timeout: 1) + op2.cancel() // op2 + wait(for: [opDone], timeout: 1) + + Thread.sleep(forTimeInterval: 1) // Let queue to be deallocated + XCTAssertNil(weakQueue, "Queue should be deallocated at this point") + } } class AsyncOperation: Operation {