From d84d85812f4d11fe2dc8bdd4982852d60d1f1c1c Mon Sep 17 00:00:00 2001 From: Roman Podymov Date: Wed, 29 Jan 2020 00:20:40 +0100 Subject: [PATCH 1/6] Added PMKError.noWinner --- Sources/Error.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/Error.swift b/Sources/Error.swift index 7229e6f49..9e995f027 100644 --- a/Sources/Error.swift +++ b/Sources/Error.swift @@ -32,6 +32,9 @@ public enum PMKError: Error { Also used if all values of this collection failed the test passed to `firstValue(where:)`. */ case emptySequence + + /// no winner in `race(fulfilled:)` + case noWinner } extension PMKError: CustomDebugStringConvertible { @@ -51,6 +54,8 @@ extension PMKError: CustomDebugStringConvertible { return "The asynchronous sequence was cancelled" case .emptySequence: return "The first or last element was requested for an empty sequence" + case .noWinner: + return "All thenables passed to race(fulfilled:) were rejected" } } } From 21fda3d4608c60e5ee6a7aca0ad22a95a79a835c Mon Sep 17 00:00:00 2001 From: Roman Podymov Date: Wed, 29 Jan 2020 00:46:51 +0100 Subject: [PATCH 2/6] Added func race(fulfilled:) --- Sources/race.swift | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Sources/race.swift b/Sources/race.swift index 2b817de26..859d818f1 100644 --- a/Sources/race.swift +++ b/Sources/race.swift @@ -55,3 +55,46 @@ public func race(_ guarantees: Guarantee...) -> Guarantee { } return rg } + +/** + Waits for one promise to fulfill + + race(fulfilled: [promise1, promise2, promise3]).then { winner in + //… + } + + - Returns: The promise that was fulfilled first. + - Warning: Skips all rejected promises. + - Remark: If the provided array is empty, the returned promise is rejected with `PMKError.badInput`. If there are no fulfilled promises, the returned promise is rejected with `PMKError.noWinner`. +*/ +public func race(fulfilled thenables: [U]) -> Promise { + var countdown = thenables.count + guard countdown > 0 else { + return Promise(error: PMKError.badInput) + } + + let rp = Promise(.pending) + + let barrier = DispatchQueue(label: "org.promisekit.barrier.race", attributes: .concurrent) + + for promise in thenables { + promise.pipe { result in + barrier.sync(flags: .barrier) { + switch result { + case .rejected: + guard rp.isPending else { return } + countdown -= 1 + if countdown == 0 { + rp.box.seal(.rejected(PMKError.noWinner)) + } + case .fulfilled(let value): + guard rp.isPending else { return } + countdown = 0 + rp.box.seal(.fulfilled(value)) + } + } + } + } + + return rp +} From 409ebb4cd5683da354ba820e157a0e86d18dd726 Mon Sep 17 00:00:00 2001 From: Roman Podymov Date: Wed, 29 Jan 2020 00:50:13 +0100 Subject: [PATCH 3/6] added race(fulfilled:) tests --- Tests/CorePromise/RaceTests.swift | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Tests/CorePromise/RaceTests.swift b/Tests/CorePromise/RaceTests.swift index c3676a11e..6237072e1 100644 --- a/Tests/CorePromise/RaceTests.swift +++ b/Tests/CorePromise/RaceTests.swift @@ -48,4 +48,42 @@ class RaceTests: XCTestCase { } wait(for: [ex], timeout: 10) } + + func testFulfilled() { + enum Error: Swift.Error { case test1, test2, test3 } + let ex = expectation(description: "") + let promises: [Promise] = [after(seconds: 1).map { _ in throw Error.test1 }, after(seconds: 2).map { _ in throw Error.test2 }, after(seconds: 5).map { 1 }, after(seconds: 4).map { 2 }, after(seconds: 3).map { _ in throw Error.test3 }] + race(fulfilled: promises).done { + XCTAssertEqual($0, 2) + ex.fulfill() + }.catch { _ in + XCTFail() + ex.fulfill() + } + wait(for: [ex], timeout: 10) + } + + func testFulfilledEmptyArray() { + let ex = expectation(description: "") + let empty = [Promise]() + race(fulfilled: empty).catch { + guard case PMKError.badInput = $0 else { return XCTFail() } + ex.fulfill() + } + wait(for: [ex], timeout: 10) + } + + func testFulfilledWithNoWinner() { + enum Error: Swift.Error { case test1, test2 } + let ex = expectation(description: "") + let promises: [Promise] = [after(seconds: 1).map { _ in throw Error.test1 }, after(seconds: 2).map { _ in throw Error.test2 }] + race(fulfilled: promises).done { _ in + XCTFail() + ex.fulfill() + }.catch { + guard case PMKError.noWinner = $0 else { return XCTFail() } + ex.fulfill() + } + wait(for: [ex], timeout: 10) + } } From 723a285b6aed2be3221a4f2ed356bf19d344c37d Mon Sep 17 00:00:00 2001 From: Roman Podymov Date: Wed, 29 Jan 2020 01:59:26 +0100 Subject: [PATCH 4/6] import Dispatch --- Sources/race.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/race.swift b/Sources/race.swift index 859d818f1..76ae96d06 100644 --- a/Sources/race.swift +++ b/Sources/race.swift @@ -1,3 +1,5 @@ +import Dispatch + @inline(__always) private func _race(_ thenables: [U]) -> Promise { let rp = Promise(.pending) From ee85b0f9ad04653ea8217eed93b7abf1a7ee21f8 Mon Sep 17 00:00:00 2001 From: Roman Podymov Date: Wed, 29 Jan 2020 02:04:55 +0100 Subject: [PATCH 5/6] Added missing RaceTests --- Tests/CorePromise/XCTestManifests.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/CorePromise/XCTestManifests.swift b/Tests/CorePromise/XCTestManifests.swift index 9fbe50243..f7def6bb5 100644 --- a/Tests/CorePromise/XCTestManifests.swift +++ b/Tests/CorePromise/XCTestManifests.swift @@ -161,6 +161,9 @@ extension RaceTests { ("test2", test2), ("test2Array", test2Array), ("testEmptyArray", testEmptyArray), + ("testFulfilled", testFulfilled), + ("testFulfilledEmptyArray", testFulfilledEmptyArray), + ("testFulfilledWithNoWinner", testFulfilledWithNoWinner), ] } From 050ef04e795dd2e0dd9c3ce56c783ddd85d8b51f Mon Sep 17 00:00:00 2001 From: Roman Podymov Date: Mon, 16 Nov 2020 12:34:18 +0100 Subject: [PATCH 6/6] Fix Added line #L58 was not covered by tests --- Tests/CorePromise/RaceTests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/CorePromise/RaceTests.swift b/Tests/CorePromise/RaceTests.swift index 6237072e1..45d7ef0b7 100644 --- a/Tests/CorePromise/RaceTests.swift +++ b/Tests/CorePromise/RaceTests.swift @@ -81,7 +81,9 @@ class RaceTests: XCTestCase { XCTFail() ex.fulfill() }.catch { - guard case PMKError.noWinner = $0 else { return XCTFail() } + guard let pmkError = $0 as? PMKError else { return XCTFail() } + guard case .noWinner = pmkError else { return XCTFail() } + guard pmkError.debugDescription == "All thenables passed to race(fulfilled:) were rejected" else { return XCTFail() } ex.fulfill() } wait(for: [ex], timeout: 10)