diff --git a/Sources/NIO/EventLoopFuture.swift b/Sources/NIO/EventLoopFuture.swift index a3c17477f4..35b661fbef 100644 --- a/Sources/NIO/EventLoopFuture.swift +++ b/Sources/NIO/EventLoopFuture.swift @@ -1267,3 +1267,17 @@ func executeAndComplete(_ promise: EventLoopPromise?, _ body: () t promise?.fail(e) } } + + +extension EventLoopFuture { + /// Adds an observer callback to this `EventLoopFuture` that is called when the + /// `EventLoopFuture` has any result. + /// + /// - parameters: + /// - callback: the callback that is called when the `EventLoopFuture` is fulfilled. + /// - returns: the current `EventLoopFuture` + public func always(_ callback: @escaping (Result) -> Void) -> EventLoopFuture { + self.whenComplete { result in callback(result) } + return self + } +} diff --git a/Tests/NIOTests/EventLoopFutureTest+XCTest.swift b/Tests/NIOTests/EventLoopFutureTest+XCTest.swift index 1db1a5b39d..73102ebc1f 100644 --- a/Tests/NIOTests/EventLoopFutureTest+XCTest.swift +++ b/Tests/NIOTests/EventLoopFutureTest+XCTest.swift @@ -70,6 +70,8 @@ extension EventLoopFutureTest { ("testWhenAllCompleteResultsWithFailuresStillSucceed", testWhenAllCompleteResultsWithFailuresStillSucceed), ("testWhenAllCompleteResults", testWhenAllCompleteResults), ("testWhenAllCompleteResolvesAfterFutures", testWhenAllCompleteResolvesAfterFutures), + ("testAlways", testAlways), + ("testAlwaysWithFailingPromise", testAlwaysWithFailingPromise), ] } } diff --git a/Tests/NIOTests/EventLoopFutureTest.swift b/Tests/NIOTests/EventLoopFutureTest.swift index 5a3258b1d6..c9b3ab5b1e 100644 --- a/Tests/NIOTests/EventLoopFutureTest.swift +++ b/Tests/NIOTests/EventLoopFutureTest.swift @@ -1015,4 +1015,49 @@ class EventLoopFutureTest : XCTestCase { let results = try assertNoThrowWithValue(mainFuture.wait().map { try $0.get() }) XCTAssertEqual(results, [0, 1, 2, 3, 4]) } + + struct DatabaseError: Error {} + struct Database { + let query: () -> EventLoopFuture<[String]> + + var closed = false + + init(query: @escaping () -> EventLoopFuture<[String]>) { + self.query = query + } + + func runQuery() -> EventLoopFuture<[String]> { + return query() + } + + mutating func close() { + self.closed = true + } + } + + func testAlways() throws { + let group = EmbeddedEventLoop() + let loop = group.next() + var db = Database { loop.makeSucceededFuture(["Item 1", "Item 2", "Item 3"]) } + + XCTAssertFalse(db.closed) + let _ = try assertNoThrowWithValue(db.runQuery().always { result in + assertSuccess(result) + db.close() + }.map { $0.map { $0.uppercased() }}.wait()) + XCTAssertTrue(db.closed) + } + + func testAlwaysWithFailingPromise() throws { + let group = EmbeddedEventLoop() + let loop = group.next() + var db = Database { loop.makeFailedFuture(DatabaseError()) } + + XCTAssertFalse(db.closed) + let _ = try XCTAssertThrowsError(db.runQuery().always { result in + assertFailure(result) + db.close() + }.map { $0.map { $0.uppercased() }}.wait()) { XCTAssertTrue($0 is DatabaseError) } + XCTAssertTrue(db.closed) + } } diff --git a/Tests/NIOTests/TestUtils.swift b/Tests/NIOTests/TestUtils.swift index 3b7de71993..6530595bcc 100644 --- a/Tests/NIOTests/TestUtils.swift +++ b/Tests/NIOTests/TestUtils.swift @@ -250,3 +250,11 @@ func getBoolSocketOption(channel: Channel, level: IntTyp file: file, line: line).wait() != 0 } + +func assertSuccess(_ result: Result, file: StaticString = #file, line: UInt = #line) { + guard case .success = result else { return XCTFail("Expected result to be successful", file: file, line: line) } +} + +func assertFailure(_ result: Result, file: StaticString = #file, line: UInt = #line) { + guard case .failure = result else { return XCTFail("Expected result to be a failure", file: file, line: line) } +}