From 0bea15ff8610e27b52824ca57da366ddcd7b66db Mon Sep 17 00:00:00 2001 From: Yasuhiro Inami Date: Sun, 24 May 2015 16:09:47 +0900 Subject: [PATCH 1/7] [Test] Reproduce then() task-flow bug when chaining with different Error types. --- SwiftTask.xcodeproj/project.pbxproj | 6 ++ SwiftTaskTests/MultipleErrorTypesTests.swift | 86 ++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 SwiftTaskTests/MultipleErrorTypesTests.swift diff --git a/SwiftTask.xcodeproj/project.pbxproj b/SwiftTask.xcodeproj/project.pbxproj index 37639a7..fe45051 100644 --- a/SwiftTask.xcodeproj/project.pbxproj +++ b/SwiftTask.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 1F20250219ADA8FD00DE0495 /* BasicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F20250119ADA8FD00DE0495 /* BasicTests.swift */; }; 1F218D5B1AFC3FFD00C849FF /* RemoveHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F218D5A1AFC3FFD00C849FF /* RemoveHandlerTests.swift */; }; 1F218D5C1AFC3FFD00C849FF /* RemoveHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F218D5A1AFC3FFD00C849FF /* RemoveHandlerTests.swift */; }; + 1F29F6421B115EF500F476AF /* MultipleErrorTypesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F29F6411B115EF500F476AF /* MultipleErrorTypesTests.swift */; }; + 1F29F6431B115EF500F476AF /* MultipleErrorTypesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F29F6411B115EF500F476AF /* MultipleErrorTypesTests.swift */; }; 1F46DEDA199EDF1000F97868 /* SwiftTask.h in Headers */ = {isa = PBXBuildFile; fileRef = 1F46DED9199EDF1000F97868 /* SwiftTask.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1F46DEFB199EDF8100F97868 /* SwiftTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F46DEFA199EDF8100F97868 /* SwiftTask.swift */; }; 1F46DEFD199EE2C200F97868 /* _TestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F46DEFC199EE2C200F97868 /* _TestCase.swift */; }; @@ -46,6 +48,7 @@ /* Begin PBXFileReference section */ 1F20250119ADA8FD00DE0495 /* BasicTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicTests.swift; sourceTree = ""; }; 1F218D5A1AFC3FFD00C849FF /* RemoveHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoveHandlerTests.swift; sourceTree = ""; }; + 1F29F6411B115EF500F476AF /* MultipleErrorTypesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipleErrorTypesTests.swift; sourceTree = ""; }; 1F46DED4199EDF1000F97868 /* SwiftTask.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftTask.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1F46DED8199EDF1000F97868 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1F46DED9199EDF1000F97868 /* SwiftTask.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftTask.h; sourceTree = ""; }; @@ -161,6 +164,7 @@ 485C31F01A1D619A00040DA3 /* TypeInferenceTests.swift */, 1F218D5A1AFC3FFD00C849FF /* RemoveHandlerTests.swift */, 1F5FA35619A374E600975FB9 /* AlamofireTests.swift */, + 1F29F6411B115EF500F476AF /* MultipleErrorTypesTests.swift */, 1F46DEE1199EDF1000F97868 /* Supporting Files */, ); path = SwiftTaskTests; @@ -366,6 +370,7 @@ 1FF52EB41A4C395A00B4BA28 /* _InterruptableTask.swift in Sources */, 1F218D5B1AFC3FFD00C849FF /* RemoveHandlerTests.swift in Sources */, 1F6A8CA319A4E4F200369A5D /* SwiftTaskTests.swift in Sources */, + 1F29F6421B115EF500F476AF /* MultipleErrorTypesTests.swift in Sources */, 1F4C76A41AD8CF40004E47C1 /* AlamofireTests.swift in Sources */, 485C31F11A1D619A00040DA3 /* TypeInferenceTests.swift in Sources */, 48511C5B19C17563002FE03C /* RetainCycleTests.swift in Sources */, @@ -381,6 +386,7 @@ 4822F0DD19D00B2300F5F572 /* BasicTests.swift in Sources */, 1F218D5C1AFC3FFD00C849FF /* RemoveHandlerTests.swift in Sources */, 1FF52EB51A4C395A00B4BA28 /* _InterruptableTask.swift in Sources */, + 1F29F6431B115EF500F476AF /* MultipleErrorTypesTests.swift in Sources */, 1F4C76A51AD8CF41004E47C1 /* AlamofireTests.swift in Sources */, 485C31F21A1D619A00040DA3 /* TypeInferenceTests.swift in Sources */, 4822F0DC19D00B2300F5F572 /* _TestCase.swift in Sources */, diff --git a/SwiftTaskTests/MultipleErrorTypesTests.swift b/SwiftTaskTests/MultipleErrorTypesTests.swift new file mode 100644 index 0000000..2d59f53 --- /dev/null +++ b/SwiftTaskTests/MultipleErrorTypesTests.swift @@ -0,0 +1,86 @@ +// +// MultipleErrorTypesTests.swift +// SwiftTask +// +// Created by Yasuhiro Inami on 2015/05/24. +// Copyright (c) 2015年 Yasuhiro Inami. All rights reserved. +// + +import SwiftTask +import Async +import XCTest + +class MultipleErrorTypesTests: _TestCase +{ + enum MyEnum + { + case Default + } + + struct Dummy {} + + typealias Task1 = Task + typealias Task2 = Task + + var flow = [Int]() + + // delayed task + counting flow 1 & 2 + func _task1(#success: Bool) -> Task1 + { + return Task1 { progress, fulfill, reject, configure in + println("[task1] start") + self.flow += [1] + + Async.main(after: 0.1) { + println("[task1] end") + self.flow += [2] + + success ? fulfill("OK") : reject("NG") + } + return + } + } + + // delayed task + counting flow 4 & 5 + func _task2(#success: Bool) -> Task2 + { + return Task2 { progress, fulfill, reject, configure in + println("[task2] start") + self.flow += [4] + + Async.main(after: 0.1) { + println("[task2] end") + self.flow += [5] + + success ? fulfill(.Default) : reject(.Default) + } + return + } + } + + func testMultipleErrorTypes_then() + { + var expect = self.expectationWithDescription(__FUNCTION__) + + self._task1(success: true) + .then { value, errorInfo -> Task2 in + + println("task1.then") + self.flow += [3] + + return self._task2(success: true) + + } + .then { value, errorInfo -> Void in + + println("task1.then.then (task2 should end at this point)") + self.flow += [6] + + XCTAssertEqual(self.flow, Array(1...6), "Tasks should flow in order from 1 to 6.") + expect.fulfill() + + } + + self.wait() + } +} \ No newline at end of file From b3a7d3466a8a4f17c91ce6006a55286355d6e83b Mon Sep 17 00:00:00 2001 From: Yasuhiro Inami Date: Sun, 24 May 2015 16:14:41 +0900 Subject: [PATCH 2/7] Fix 0bea15f by allowing then() to handle any innerTask types including any generic Error types. --- SwiftTask/SwiftTask.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SwiftTask/SwiftTask.swift b/SwiftTask/SwiftTask.swift index 736f0c7..57fa0c0 100644 --- a/SwiftTask/SwiftTask.swift +++ b/SwiftTask/SwiftTask.swift @@ -398,7 +398,7 @@ public class Task: Cancellable, Printable /// /// - e.g. task.then { value, errorInfo -> NextTaskType in ... } /// - public func then(thenClosure: (Value?, ErrorInfo?) -> Task) -> Task + public func then(thenClosure: (Value?, ErrorInfo?) -> Task) -> Task { var dummyCanceller: Canceller? = nil return self.then(&dummyCanceller, thenClosure) @@ -410,9 +410,9 @@ public class Task: Cancellable, Printable // - `let canceller = Canceller(); task1.then(&canceller) {...}; canceller.cancel();` // - `let task2 = task1.then {...}; task2.cancel();` // - public func then(inout canceller: C?, _ thenClosure: (Value?, ErrorInfo?) -> Task) -> Task + public func then(inout canceller: C?, _ thenClosure: (Value?, ErrorInfo?) -> Task) -> Task { - return Task { [unowned self, weak canceller] newMachine, progress, fulfill, _reject, configure in + return Task { [unowned self, weak canceller] newMachine, progress, fulfill, _reject, configure in // // NOTE: From 4400589e1ec1bf8bb0205e5d21ce2f37b1299d14 Mon Sep 17 00:00:00 2001 From: Yasuhiro Inami Date: Sun, 24 May 2015 16:16:31 +0900 Subject: [PATCH 3/7] [Test] Reproduce success() task-flow bug when chaining with different Error types. --- SwiftTaskTests/MultipleErrorTypesTests.swift | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/SwiftTaskTests/MultipleErrorTypesTests.swift b/SwiftTaskTests/MultipleErrorTypesTests.swift index 2d59f53..beb26ec 100644 --- a/SwiftTaskTests/MultipleErrorTypesTests.swift +++ b/SwiftTaskTests/MultipleErrorTypesTests.swift @@ -83,4 +83,31 @@ class MultipleErrorTypesTests: _TestCase self.wait() } + + func testMultipleErrorTypes_success() + { + var expect = self.expectationWithDescription(__FUNCTION__) + + self._task1(success: true) + .success { value -> Task2 in + + println("task1.success") + self.flow += [3] + + return self._task2(success: true) + + } + .success { value -> Void in + + println("task1.success.success (task2 should end at this point)") + self.flow += [6] + + XCTAssertEqual(self.flow, Array(1...6), "Tasks should flow in order from 1 to 6.") + expect.fulfill() + + } + + self.wait() + } + } \ No newline at end of file From bd7828b02123b9cf7846bd8d060b1fef89616cfd Mon Sep 17 00:00:00 2001 From: Yasuhiro Inami Date: Sun, 24 May 2015 16:20:29 +0900 Subject: [PATCH 4/7] Fix 4400589 by allowing success() to handle any innerTask types and silence wrong Error type as nil whenever needed. --- SwiftTask/SwiftTask.swift | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/SwiftTask/SwiftTask.swift b/SwiftTask/SwiftTask.swift index 57fa0c0..fe4e921 100644 --- a/SwiftTask/SwiftTask.swift +++ b/SwiftTask/SwiftTask.swift @@ -470,13 +470,13 @@ public class Task: Cancellable, Printable /// /// - e.g. task.success { value -> NextTaskType in ... } /// - public func success(successClosure: Value -> Task) -> Task + public func success(successClosure: Value -> Task) -> Task { var dummyCanceller: Canceller? = nil return self.success(&dummyCanceller, successClosure) } - public func success(inout canceller: C?, _ successClosure: Value -> Task) -> Task + public func success(inout canceller: C?, _ successClosure: Value -> Task) -> Task { return Task { [unowned self] newMachine, progress, fulfill, _reject, configure in @@ -581,8 +581,8 @@ public class Task: Cancellable, Printable // MARK: - Helper -internal func _bindInnerTask( - innerTask: Task, +internal func _bindInnerTask( + innerTask: Task, newMachine: _StateMachine, progress: Task.ProgressHandler, fulfill: Task.FulfillHandler, @@ -595,7 +595,10 @@ internal func _bindInnerTask( fulfill(innerTask.value!) return case .Rejected, .Cancelled: - _reject(innerTask.errorInfo!) + let (error2, isCancelled) = innerTask.errorInfo! + + // NOTE: innerTask's `error2` will be treated as `nil` if not same type as outerTask's `Error` type + _reject((error2 as? Error, isCancelled)) return default: break @@ -603,12 +606,15 @@ internal func _bindInnerTask( innerTask.progress { _, progressValue in progress(progressValue) - }.then { (value: Value2?, errorInfo: Task.ErrorInfo?) -> Void in + }.then { (value: Value2?, errorInfo2: Task.ErrorInfo?) -> Void in if let value = value { fulfill(value) } - else if let errorInfo = errorInfo { - _reject(errorInfo) + else if let errorInfo2 = errorInfo2 { + let (error2, isCancelled) = errorInfo2 + + // NOTE: innerTask's `error2` will be treated as `nil` if not same type as outerTask's `Error` type + _reject((error2 as? Error, isCancelled)) } } From 7ca1f73c05ef597ae4c952396b328aad4778efb5 Mon Sep 17 00:00:00 2001 From: Yasuhiro Inami Date: Sun, 24 May 2015 16:23:03 +0900 Subject: [PATCH 5/7] [Test] Reproduce failure() compiler error when chaining with different Error types. --- SwiftTaskTests/MultipleErrorTypesTests.swift | 34 +++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/SwiftTaskTests/MultipleErrorTypesTests.swift b/SwiftTaskTests/MultipleErrorTypesTests.swift index beb26ec..19cf469 100644 --- a/SwiftTaskTests/MultipleErrorTypesTests.swift +++ b/SwiftTaskTests/MultipleErrorTypesTests.swift @@ -109,5 +109,37 @@ class MultipleErrorTypesTests: _TestCase self.wait() } - + + func testMultipleErrorTypes_failure() + { + var expect = self.expectationWithDescription(__FUNCTION__) + + self._task1(success: false) + .failure { errorInfo -> Task in + + println("task1.failure") + self.flow += [3] + + // + // NOTE: + // Returning `Task2` won't work since same Value type as `task1` is required inside `task1.failure()`, + // so use `then()` to promote `Task2` to `Task<..., Task1.Value, ...>`. + // + return self._task2(success: false).then { value, errorInfo in + return Task(error: Dummy()) // error task + } + } + .failure { errorInfo -> String /* must be task1's value type to recover */ in + + println("task1.failure.failure (task2 should end at this point)") + self.flow += [6] + + XCTAssertEqual(self.flow, Array(1...6), "Tasks should flow in order from 1 to 6.") + expect.fulfill() + + return "DUMMY" + } + + self.wait() + } } \ No newline at end of file From dd698b9c5e4dc581c6b07e892b0bfdb563a809f3 Mon Sep 17 00:00:00 2001 From: Yasuhiro Inami Date: Sun, 24 May 2015 16:24:53 +0900 Subject: [PATCH 6/7] Fix 7ca1f73 by allowing failure() to handle innerTask with different generic Error type to outerTask. --- SwiftTask/SwiftTask.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SwiftTask/SwiftTask.swift b/SwiftTask/SwiftTask.swift index fe4e921..710d7d2 100644 --- a/SwiftTask/SwiftTask.swift +++ b/SwiftTask/SwiftTask.swift @@ -521,15 +521,15 @@ public class Task: Cancellable, Printable /// - e.g. task.failure { errorInfo -> NextTaskType in ... } /// - e.g. task.failure { error, isCancelled -> NextTaskType in ... } /// - public func failure(failureClosure: ErrorInfo -> Task) -> Task + public func failure(failureClosure: ErrorInfo -> Task) -> Task { var dummyCanceller: Canceller? = nil return self.failure(&dummyCanceller, failureClosure) } - public func failure(inout canceller: C?, _ failureClosure: ErrorInfo -> Task) -> Task + public func failure(inout canceller: C?, _ failureClosure: ErrorInfo -> Task) -> Task { - return Task { [unowned self] newMachine, progress, fulfill, _reject, configure in + return Task { [unowned self] newMachine, progress, fulfill, _reject, configure in let selfMachine = self._machine From fac9ff68ef2caf8027ddf1c68e4acbf6d7e2d25a Mon Sep 17 00:00:00 2001 From: Yasuhiro Inami Date: Mon, 25 May 2015 10:46:26 +0900 Subject: [PATCH 7/7] [Test] Add more examples for different Error type handling. --- SwiftTaskTests/MultipleErrorTypesTests.swift | 101 +++++++++++++++++-- 1 file changed, 90 insertions(+), 11 deletions(-) diff --git a/SwiftTaskTests/MultipleErrorTypesTests.swift b/SwiftTaskTests/MultipleErrorTypesTests.swift index 19cf469..3339d8d 100644 --- a/SwiftTaskTests/MultipleErrorTypesTests.swift +++ b/SwiftTaskTests/MultipleErrorTypesTests.swift @@ -12,9 +12,10 @@ import XCTest class MultipleErrorTypesTests: _TestCase { - enum MyEnum + enum MyEnum: Printable { case Default + var description: String { return "MyEnumDefault" } } struct Dummy {} @@ -25,7 +26,7 @@ class MultipleErrorTypesTests: _TestCase var flow = [Int]() // delayed task + counting flow 1 & 2 - func _task1(#success: Bool) -> Task1 + func _task1(#ok: Bool) -> Task1 { return Task1 { progress, fulfill, reject, configure in println("[task1] start") @@ -35,14 +36,14 @@ class MultipleErrorTypesTests: _TestCase println("[task1] end") self.flow += [2] - success ? fulfill("OK") : reject("NG") + ok ? fulfill("OK") : reject("NG") } return } } // delayed task + counting flow 4 & 5 - func _task2(#success: Bool) -> Task2 + func _task2(#ok: Bool) -> Task2 { return Task2 { progress, fulfill, reject, configure in println("[task2] start") @@ -52,7 +53,7 @@ class MultipleErrorTypesTests: _TestCase println("[task2] end") self.flow += [5] - success ? fulfill(.Default) : reject(.Default) + ok ? fulfill(.Default) : reject(.Default) } return } @@ -62,13 +63,13 @@ class MultipleErrorTypesTests: _TestCase { var expect = self.expectationWithDescription(__FUNCTION__) - self._task1(success: true) + self._task1(ok: true) .then { value, errorInfo -> Task2 in println("task1.then") self.flow += [3] - return self._task2(success: true) + return self._task2(ok: true) } .then { value, errorInfo -> Void in @@ -88,13 +89,13 @@ class MultipleErrorTypesTests: _TestCase { var expect = self.expectationWithDescription(__FUNCTION__) - self._task1(success: true) + self._task1(ok: true) .success { value -> Task2 in println("task1.success") self.flow += [3] - return self._task2(success: true) + return self._task2(ok: true) } .success { value -> Void in @@ -110,11 +111,89 @@ class MultipleErrorTypesTests: _TestCase self.wait() } + func testMultipleErrorTypes_success_differentErrorType() + { + var expect = self.expectationWithDescription(__FUNCTION__) + + self._task1(ok: true) + .success { value -> Task2 in + + println("task1.success") + self.flow += [3] + + // + // NOTE: + // If Task1 and Task2 have different Error types, + // returning `self._task2(ok: false)` inside `self._task1.success()` will fail error conversion + // (Task2.Error -> Task1.Error). + // + return self._task2(ok: false) // inner rejection with different Error type + + } + .then { value, errorInfo -> Void in + + println("task1.success.success (task2 should end at this point)") + self.flow += [6] + + XCTAssertEqual(self.flow, Array(1...6), "Tasks should flow in order from 1 to 6.") + + XCTAssertTrue(value == nil) + XCTAssertTrue(errorInfo != nil) + XCTAssertTrue(errorInfo!.error == nil, "Though `errorInfo` will still be non-nil, `errorInfo!.error` will become as `nil` if Task1 and Task2 have different Error types.") + XCTAssertTrue(errorInfo!.isCancelled == false) + + expect.fulfill() + + } + + self.wait() + } + + func testMultipleErrorTypes_success_differentErrorType_conversion() + { + var expect = self.expectationWithDescription(__FUNCTION__) + + self._task1(ok: true) + .success { value -> Task in + + println("task1.success") + self.flow += [3] + + // + // NOTE: + // Since returning `self._task2(ok: false)` inside `self._task1.success()` will fail error conversion + // (Task2.Error -> Task1.Error) as seen in above test case, + // it is **user's responsibility** to add conversion logic to maintain same Error type throughout task-flow. + // + return self._task2(ok: false) + .failure { Task(error: "Mapping errorInfo=\($0.error!) to String") } // error-conversion + + } + .then { value, errorInfo -> Void in + + println("task1.success.success (task2 should end at this point)") + self.flow += [6] + + XCTAssertEqual(self.flow, Array(1...6), "Tasks should flow in order from 1 to 6.") + + XCTAssertTrue(value == nil) + XCTAssertTrue(errorInfo != nil) + XCTAssertEqual(errorInfo!.error!, "Mapping errorInfo=MyEnumDefault to String", + "Now `self._task2()`'s error is catched by this scope by adding manual error-conversion logic by user side.") + XCTAssertTrue(errorInfo!.isCancelled == false) + + expect.fulfill() + + } + + self.wait() + } + func testMultipleErrorTypes_failure() { var expect = self.expectationWithDescription(__FUNCTION__) - self._task1(success: false) + self._task1(ok: false) .failure { errorInfo -> Task in println("task1.failure") @@ -125,7 +204,7 @@ class MultipleErrorTypesTests: _TestCase // Returning `Task2` won't work since same Value type as `task1` is required inside `task1.failure()`, // so use `then()` to promote `Task2` to `Task<..., Task1.Value, ...>`. // - return self._task2(success: false).then { value, errorInfo in + return self._task2(ok: false).then { value, errorInfo in return Task(error: Dummy()) // error task } }