Skip to content

Commit

Permalink
Merge pull request ProcedureKit#256 from danthorpe/feature/OPR-256_gr…
Browse files Browse the repository at this point in the history
…oup_operation_child_cancel_errors

Group operation children are cancelled with parent error
  • Loading branch information
danthorpe committed Apr 1, 2016
2 parents fd4b036 + eabea5c commit 968fcef
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 11 deletions.
7 changes: 6 additions & 1 deletion Sources/Core/Shared/ComposedOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,15 @@ public class ComposedOperation<T: NSOperation>: Operation {
}

public override func cancel() {
operation.cancel()
target.cancel()
super.cancel()
}

public override func cancelWithErrors(errors: [ErrorType]) {
target.cancelWithError(OperationError.ParentOperationCancelledWithErrors(errors))
super.cancelWithErrors(errors)
}

public override func execute() {
target.log.severity = log.severity
produceOperation(target)
Expand Down
14 changes: 10 additions & 4 deletions Sources/Core/Shared/GroupOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@

import Foundation




/**
An `Operation` subclass which enables the grouping
of other operations. Use `GroupOperation`s to associate
Expand Down Expand Up @@ -55,12 +52,21 @@ public class GroupOperation: Operation {

/// Override of public method
public override func cancel() {
queue.cancelAllOperations()
queue.suspended = false
queue.cancelAllOperations()
operations.forEach { $0.cancel() }
super.cancel()
}

/// Override of public method
public override func cancelWithErrors(errors: [ErrorType]) {
queue.suspended = false
let (nsops, ops) = operations.splitNSOperationsAndOperations
nsops.forEach { $0.cancel() }
ops.forEach { $0.cancelWithError(OperationError.ParentOperationCancelledWithErrors(errors)) }
super.cancelWithErrors(errors)
}

/**
Executes the group by adding the operations to the queue. Then
starting the queue, and adding the finishing operation.
Expand Down
31 changes: 25 additions & 6 deletions Sources/Core/Shared/Operation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ public class Operation: NSOperation {
// No op.
}

// MARK: - Cancellation

/**
Cancel the operation with an error.

Expand All @@ -256,8 +258,6 @@ public class Operation: NSOperation {
cancel()
}

// MARK: - Cancellation

public override func cancel() {
if !finished {

Expand Down Expand Up @@ -358,8 +358,6 @@ public extension Operation {

public extension Operation {

// MARK: - Dependencies

private func createDidFinishDependenciesOperation() -> NSOperation {
assert(waitForDependenciesOperation == nil, "Should only ever create the finishing dependency once.")
let __op = NSBlockOperation { }
Expand All @@ -381,8 +379,7 @@ public extension Operation {
get {
var _dependencies = super.dependencies
guard let
waiter = waitForDependenciesOperation,
index = _dependencies.indexOf(waiter) else {
waiter = waitForDependenciesOperation, index = _dependencies.indexOf(waiter) else {
return _dependencies
}

Expand Down Expand Up @@ -606,6 +603,9 @@ public enum OperationError: ErrorType, Equatable {

/// Indicates that the operation timed out.
case OperationTimedOut(NSTimeInterval)

/// Indicates that a parent operation was cancelled (with errors).
case ParentOperationCancelledWithErrors([ErrorType])
}

/// OperationError is Equatable.
Expand All @@ -615,6 +615,9 @@ public func == (lhs: OperationError, rhs: OperationError) -> Bool {
return true
case let (.OperationTimedOut(aTimeout), .OperationTimedOut(bTimeout)):
return aTimeout == bTimeout
case let (.ParentOperationCancelledWithErrors(aErrors), .ParentOperationCancelledWithErrors(bErrors)):
// Not possible to do a real equality check here.
return aErrors.count == bErrors.count
default:
return false
}
Expand Down Expand Up @@ -668,4 +671,20 @@ extension NSRecursiveLock {
}
}

extension Array where Element: NSOperation {

internal var splitNSOperationsAndOperations: ([NSOperation], [Operation]) {
return reduce(([], [])) { result, element in
var (ns, op) = result
if let operation = element as? Operation {
op.append(operation)
}
else {
ns.append(element)
}
return (ns, op)
}
}
}

// swiftlint:enable file_length
35 changes: 35 additions & 0 deletions Tests/Core/ComposedOperationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,39 @@ class ComposedOperationTests: OperationTests {
XCTAssertTrue(composed.finished)
XCTAssertTrue(composed.operation.didExecute)
}

func test__composed_operation_which_cancels_propagates_error_to_target() {
let target = TestOperation()

var targetErrors: [ErrorType] = []
target.addObserver(CancelledObserver { op in
targetErrors = op.errors
})

let operation = ComposedOperation(target)

let operationError = TestOperation.Error.SimulatedError
operation.cancelWithError(operationError)

addCompletionBlockToTestOperation(operation)
runOperation(operation)
waitForExpectationsWithTimeout(3, handler: nil)

XCTAssertEqual(targetErrors.count, 1)

guard let error = targetErrors.first as? OperationError else {
XCTFail("Incorrect error received"); return
}

switch error {
case let .ParentOperationCancelledWithErrors(parentErrors):
guard let parentError = parentErrors.first as? TestOperation.Error else {
XCTFail("Incorrect error received"); return
}
XCTAssertEqual(parentError, operationError)
default:
XCTFail("Incorrect error received"); return
}

}
}
36 changes: 36 additions & 0 deletions Tests/Core/GroupOperationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,40 @@ class GroupOperationTests: OperationTests {
XCTAssertEqual(group, observedGroup)
XCTAssertEqual(child1, observedChild)
}

func test__group_operation_which_cancels_propagates_error_to_children() {

let child = TestOperation()

var childErrors: [ErrorType] = []
child.addObserver(CancelledObserver { op in
childErrors = op.errors
})

let group = GroupOperation(operations: [child])

let groupError = TestOperation.Error.SimulatedError
group.cancelWithError(groupError)

addCompletionBlockToTestOperation(group)
runOperation(group)
waitForExpectationsWithTimeout(3, handler: nil)

XCTAssertEqual(childErrors.count, 1)

guard let error = childErrors.first as? OperationError else {
XCTFail("Incorrect error received"); return
}

switch error {
case let .ParentOperationCancelledWithErrors(parentErrors):
guard let parentError = parentErrors.first as? TestOperation.Error else {
XCTFail("Incorrect error received"); return
}
XCTAssertEqual(parentError, groupError)
default:
XCTFail("Incorrect error received"); return
}
}
}

0 comments on commit 968fcef

Please sign in to comment.