Răsfoiți Sursa

Add tests to verify the behavior of canceling client and server calls.

Also changes a few method signatures where passing a `CallResult` does not make sense.
Daniel Alm 7 ani în urmă
părinte
comite
5d322044ed

+ 4 - 4
Sources/Examples/Echo/Generated/echo.grpc.swift

@@ -212,7 +212,7 @@ internal protocol Echo_EchoExpandSession: ServerSessionServerStreaming {
 
   /// Close the connection and send the status. Non-blocking.
   /// You MUST call this method once you are done processing the request.
-  func close(withStatus status: ServerStatus, completion: ((CallResult) -> Void)?) throws
+  func close(withStatus status: ServerStatus, completion: (() -> Void)?) throws
 }
 
 fileprivate final class Echo_EchoExpandSessionBase: ServerSessionServerStreamingBase<Echo_EchoRequest, Echo_EchoResponse>, Echo_EchoExpandSession {}
@@ -227,11 +227,11 @@ internal protocol Echo_EchoCollectSession: ServerSessionClientStreaming {
 
   /// You MUST call one of these two methods once you are done processing the request.
   /// Close the connection and send a single result. Non-blocking.
-  func sendAndClose(response: Echo_EchoResponse, status: ServerStatus, completion: ((CallResult) -> Void)?) throws
+  func sendAndClose(response: Echo_EchoResponse, status: ServerStatus, completion: (() -> Void)?) throws
   /// Close the connection and send an error. Non-blocking.
   /// Use this method if you encountered an error that makes it impossible to send a response.
   /// Accordingly, it does not make sense to call this method with a status of `.ok`.
-  func sendErrorAndClose(status: ServerStatus, completion: ((CallResult) -> Void)?) throws
+  func sendErrorAndClose(status: ServerStatus, completion: (() -> Void)?) throws
 }
 
 fileprivate final class Echo_EchoCollectSessionBase: ServerSessionClientStreamingBase<Echo_EchoRequest, Echo_EchoResponse>, Echo_EchoCollectSession {}
@@ -251,7 +251,7 @@ internal protocol Echo_EchoUpdateSession: ServerSessionBidirectionalStreaming {
 
   /// Close the connection and send the status. Non-blocking.
   /// You MUST call this method once you are done processing the request.
-  func close(withStatus status: ServerStatus, completion: ((CallResult) -> Void)?) throws
+  func close(withStatus status: ServerStatus, completion: (() -> Void)?) throws
 }
 
 fileprivate final class Echo_EchoUpdateSessionBase: ServerSessionBidirectionalStreamingBase<Echo_EchoRequest, Echo_EchoResponse>, Echo_EchoUpdateSession {}

+ 2 - 0
Sources/SwiftGRPC/Core/Call.swift

@@ -99,6 +99,8 @@ public class Call {
   /// - Parameter metadata: metadata to send with the call
   /// - Parameter message: data containing the message to send (.unary and .serverStreaming only)
   /// - Parameter completion: a block to call with call results
+  ///     The argument to `completion` will always have `.success = true`
+  ///     because operations containing `.receiveCloseOnClient` always succeed.
   /// - Throws: `CallError` if fails to call.
   public func start(_ style: CallStyle,
                     metadata: Metadata,

+ 10 - 8
Sources/SwiftGRPC/Core/Handler.swift

@@ -121,9 +121,10 @@ public class Handler {
     })
   }
 
-  /// Sends the response to a request
+  /// Sends the response to a request.
+  /// The completion handler does not take an argument because operations containing `.receiveCloseOnServer` always succeed.
   public func sendResponse(message: Data, status: ServerStatus,
-                           completion: ((CallResult) -> Void)? = nil) throws {
+                           completion: (() -> Void)? = nil) throws {
     let messageBuffer = ByteBuffer(data: message)
     try call.perform(OperationGroup(
       call: call,
@@ -131,21 +132,22 @@ public class Handler {
         .sendMessage(messageBuffer),
         .receiveCloseOnServer,
         .sendStatusFromServer(status.code, status.message, status.trailingMetadata)
-    ]) { operationGroup in
-      completion?(CallResult(operationGroup))
+    ]) { _ in
+      completion?()
       self.shutdown()
     })
   }
 
-  /// Send final status to the client
-  public func sendStatus(_ status: ServerStatus, completion: ((CallResult) -> Void)? = nil) throws {
+  /// Send final status to the client.
+  /// The completion handler does not take an argument because operations containing `.receiveCloseOnServer` always succeed.
+  public func sendStatus(_ status: ServerStatus, completion: (() -> Void)? = nil) throws {
     try call.perform(OperationGroup(
       call: call,
       operations: [
         .receiveCloseOnServer,
         .sendStatusFromServer(status.code, status.message, status.trailingMetadata)
-    ]) { operationGroup in
-      completion?(CallResult(operationGroup))
+    ]) { _ in
+      completion?()
       self.shutdown()
     })
   }

+ 2 - 3
Sources/SwiftGRPC/Runtime/ClientCallUnary.swift

@@ -47,9 +47,8 @@ open class ClientCallUnaryBase<InputType: Message, OutputType: Message>: ClientC
                     completion: @escaping ((OutputType?, CallResult) -> Void)) throws -> Self {
     let requestData = try request.serializedData()
     try call.start(.unary, metadata: metadata, message: requestData) { callResult in
-      if let responseData = callResult.resultData,
-        let response = try? OutputType(serializedData: responseData) {
-        completion(response, callResult)
+      if let responseData = callResult.resultData {
+        completion(try? OutputType(serializedData: responseData), callResult)
       } else {
         completion(nil, callResult)
       }

+ 8 - 0
Sources/SwiftGRPC/Runtime/ServerSession.swift

@@ -39,6 +39,8 @@ public protocol ServerSession: class {
   var requestMetadata: Metadata { get }
 
   var initialMetadata: Metadata { get set }
+  
+  func cancel()
 }
 
 open class ServerSessionBase: ServerSession {
@@ -52,6 +54,10 @@ open class ServerSessionBase: ServerSession {
   public init(handler: Handler) {
     self.handler = handler
   }
+  
+  public func cancel() {
+    call.cancel()
+  }
 }
 
 open class ServerSessionTestStub: ServerSession {
@@ -60,4 +66,6 @@ open class ServerSessionTestStub: ServerSession {
   open var initialMetadata = Metadata()
 
   public init() {}
+  
+  open func cancel() {}
 }

+ 2 - 2
Sources/SwiftGRPC/Runtime/ServerSessionBidirectionalStreaming.swift

@@ -86,9 +86,9 @@ open class ServerSessionBidirectionalStreamingTestStub<InputType: Message, Outpu
     outputs.append(message)
   }
 
-  open func close(withStatus status: ServerStatus, completion: ((CallResult) -> Void)?) throws {
+  open func close(withStatus status: ServerStatus, completion: (() -> Void)?) throws {
     self.status = status
-    completion?(.fakeOK)
+    completion?()
   }
 
   open func waitForSendOperationsToFinish() {}

+ 6 - 6
Sources/SwiftGRPC/Runtime/ServerSessionClientStreaming.swift

@@ -32,11 +32,11 @@ open class ServerSessionClientStreamingBase<InputType: Message, OutputType: Mess
   }
   
   public func sendAndClose(response: OutputType, status: ServerStatus = .ok,
-                           completion: ((CallResult) -> Void)? = nil) throws {
+                           completion: (() -> Void)? = nil) throws {
     try handler.sendResponse(message: response.serializedData(), status: status, completion: completion)
   }
 
-  public func sendErrorAndClose(status: ServerStatus, completion: ((CallResult) -> Void)? = nil) throws {
+  public func sendErrorAndClose(status: ServerStatus, completion: (() -> Void)? = nil) throws {
     try handler.sendStatus(status, completion: completion)
   }
   
@@ -84,15 +84,15 @@ open class ServerSessionClientStreamingTestStub<InputType: Message, OutputType:
     completion(.result(try self.receive()))
   }
 
-  open func sendAndClose(response: OutputType, status: ServerStatus, completion: ((CallResult) -> Void)?) throws {
+  open func sendAndClose(response: OutputType, status: ServerStatus, completion: (() -> Void)?) throws {
     self.output = response
     self.status = status
-    completion?(.fakeOK)
+    completion?()
   }
 
-  open func sendErrorAndClose(status: ServerStatus, completion: ((CallResult) -> Void)? = nil) throws {
+  open func sendErrorAndClose(status: ServerStatus, completion: (() -> Void)? = nil) throws {
     self.status = status
-    completion?(.fakeOK)
+    completion?()
   }
   
   open func close() throws {}

+ 2 - 2
Sources/SwiftGRPC/Runtime/ServerSessionServerStreaming.swift

@@ -76,9 +76,9 @@ open class ServerSessionServerStreamingTestStub<OutputType: Message>: ServerSess
     outputs.append(message)
   }
 
-  open func close(withStatus status: ServerStatus, completion: ((CallResult) -> Void)?) throws {
+  open func close(withStatus status: ServerStatus, completion: (() -> Void)?) throws {
     self.status = status
-    completion?(.fakeOK)
+    completion?()
   }
 
   open func waitForSendOperationsToFinish() {}

+ 1 - 1
Sources/SwiftGRPC/Runtime/StreamSending.swift

@@ -48,7 +48,7 @@ extension StreamSending {
 }
 
 extension StreamSending where Self: ServerSessionBase {
-  public func close(withStatus status: ServerStatus = .ok, completion: ((CallResult) -> Void)? = nil) throws {
+  public func close(withStatus status: ServerStatus = .ok, completion: (() -> Void)? = nil) throws {
     try handler.sendStatus(status, completion: completion)
   }
 }

+ 3 - 3
Sources/protoc-gen-swiftgrpc/Generator-Server.swift

@@ -145,11 +145,11 @@ extension Generator {
   private func printServerMethodSendAndClose(sentType: String) {
     println("/// You MUST call one of these two methods once you are done processing the request.")
     println("/// Close the connection and send a single result. Non-blocking.")
-    println("func sendAndClose(response: \(sentType), status: ServerStatus, completion: ((CallResult) -> Void)?) throws")
+    println("func sendAndClose(response: \(sentType), status: ServerStatus, completion: (() -> Void)?) throws")
     println("/// Close the connection and send an error. Non-blocking.")
     println("/// Use this method if you encountered an error that makes it impossible to send a response.")
     println("/// Accordingly, it does not make sense to call this method with a status of `.ok`.")
-    println("func sendErrorAndClose(status: ServerStatus, completion: ((CallResult) -> Void)?) throws")
+    println("func sendErrorAndClose(status: ServerStatus, completion: (() -> Void)?) throws")
   }
 
   private func printServerMethodClientStreaming() {
@@ -171,7 +171,7 @@ extension Generator {
   private func printServerMethodClose() {
     println("/// Close the connection and send the status. Non-blocking.")
     println("/// You MUST call this method once you are done processing the request.")
-    println("func close(withStatus status: ServerStatus, completion: ((CallResult) -> Void)?) throws")
+    println("func close(withStatus status: ServerStatus, completion: (() -> Void)?) throws")
   }
   
   private func printServerMethodServerStreaming() {

+ 2 - 0
Tests/LinuxMain.swift

@@ -18,10 +18,12 @@ import XCTest
 
 XCTMain([
   testCase(gRPCTests.allTests),
+  testCase(ClientCancellingTests.allTests),
   testCase(ClientTestExample.allTests),
   testCase(ClientTimeoutTests.allTests),
   testCase(ConnectionFailureTests.allTests),
   testCase(EchoTests.allTests),
+  testCase(ServerCancellingTests.allTests),
   testCase(ServerTestExample.allTests),
   testCase(ServerThrowingTests.allTests),
   testCase(ServerTimeoutTests.allTests)

+ 117 - 0
Tests/SwiftGRPCTests/ClientCancellingTests.swift

@@ -0,0 +1,117 @@
+/*
+ * Copyright 2018, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import Dispatch
+import Foundation
+@testable import SwiftGRPC
+import XCTest
+
+class ClientCancellingTests: BasicEchoTestCase {
+  static var allTests: [(String, (ClientCancellingTests) -> () throws -> Void)] {
+    return [
+      ("testUnary", testUnary),
+      ("testClientStreaming", testClientStreaming),
+      ("testServerStreaming", testServerStreaming),
+      ("testBidirectionalStreaming", testBidirectionalStreaming),
+    ]
+  }
+}
+
+extension ClientCancellingTests {
+  func testUnary() {
+    let completionHandlerExpectation = expectation(description: "final completion handler called")
+    let call = try! client.get(Echo_EchoRequest(text: "foo bar baz")) { response, callResult in
+      XCTAssertNil(response)
+      XCTAssertEqual(.cancelled, callResult.statusCode)
+      completionHandlerExpectation.fulfill()
+    }
+    
+    call.cancel()
+    
+    waitForExpectations(timeout: defaultTimeout)
+  }
+  
+  func testClientStreaming() {
+    let completionHandlerExpectation = expectation(description: "final completion handler called")
+    let call = try! client.collect { callResult in
+      XCTAssertEqual(.cancelled, callResult.statusCode)
+      completionHandlerExpectation.fulfill()
+    }
+    
+    call.cancel()
+    
+    let sendExpectation = expectation(description: "send completion handler 1 called")
+    try! call.send(Echo_EchoRequest(text: "foo")) { [sendExpectation] in XCTAssertEqual(.unknown, $0 as! CallError); sendExpectation.fulfill() }
+    call.waitForSendOperationsToFinish()
+    
+    do {
+      let result = try call.closeAndReceive()
+      XCTFail("should have thrown, received \(result) instead")
+    } catch let receiveError {
+      XCTAssertEqual(.unknown, (receiveError as! RPCError).callResult!.statusCode)
+    }
+    
+    waitForExpectations(timeout: defaultTimeout)
+  }
+  
+  func testServerStreaming() {
+    let completionHandlerExpectation = expectation(description: "completion handler called")
+    let call = try! client.expand(Echo_EchoRequest(text: "foo bar baz")) { callResult in
+      XCTAssertEqual(.cancelled, callResult.statusCode)
+      completionHandlerExpectation.fulfill()
+    }
+    
+    XCTAssertEqual("Swift echo expand (0): foo", try! call.receive()!.text)
+    
+    call.cancel()
+    
+    do {
+      let result = try call.receive()
+      XCTFail("should have thrown, received \(String(describing: result)) instead")
+    } catch let receiveError {
+      XCTAssertEqual(.unknown, (receiveError as! RPCError).callResult!.statusCode)
+    }
+    
+    waitForExpectations(timeout: defaultTimeout)
+  }
+  
+  func testBidirectionalStreaming() {
+    let finalCompletionHandlerExpectation = expectation(description: "final completion handler called")
+    let call = try! client.update { callResult in
+      XCTAssertEqual(.cancelled, callResult.statusCode)
+      finalCompletionHandlerExpectation.fulfill()
+    }
+    
+    var sendExpectation = expectation(description: "send completion handler 1 called")
+    try! call.send(Echo_EchoRequest(text: "foo")) { [sendExpectation] in XCTAssertNil($0); sendExpectation.fulfill() }
+    XCTAssertEqual("Swift echo update (0): foo", try! call.receive()!.text)
+    
+    call.cancel()
+    
+    sendExpectation = expectation(description: "send completion handler 2 called")
+    try! call.send(Echo_EchoRequest(text: "bar")) { [sendExpectation] in XCTAssertEqual(.unknown, $0 as! CallError); sendExpectation.fulfill() }
+    do {
+      let result = try call.receive()
+      XCTFail("should have thrown, received \(String(describing: result)) instead")
+    } catch let receiveError {
+      XCTAssertEqual(.unknown, (receiveError as! RPCError).callResult!.statusCode)
+    }
+    
+    let closeCompletionHandlerExpectation = expectation(description: "close completion handler called")
+    try! call.closeSend { closeCompletionHandlerExpectation.fulfill() }
+    
+    waitForExpectations(timeout: defaultTimeout)
+  }
+}

+ 4 - 4
Tests/SwiftGRPCTests/ClientTimeoutTests.swift

@@ -47,8 +47,8 @@ extension ClientTimeoutTests {
     call.waitForSendOperationsToFinish()
     
     do {
-      _ = try call.closeAndReceive()
-      XCTFail("should have thrown")
+      let result = try call.closeAndReceive()
+      XCTFail("should have thrown, received \(result) instead")
     } catch let receiveError {
       XCTAssertEqual(.unknown, (receiveError as! RPCError).callResult!.statusCode)
     }
@@ -70,8 +70,8 @@ extension ClientTimeoutTests {
     Thread.sleep(forTimeInterval: 0.2)
     
     do {
-      _ = try call.closeAndReceive()
-      XCTFail("should have thrown")
+      let result = try call.closeAndReceive()
+      XCTFail("should have thrown, received \(result) instead")
     } catch let receiveError {
       XCTAssertEqual(.unknown, (receiveError as! RPCError).callResult!.statusCode)
     }

+ 8 - 8
Tests/SwiftGRPCTests/ConnectionFailureTests.swift

@@ -40,8 +40,8 @@ extension ConnectionFailureTests {
     client.timeout = defaultTimeout
     
     do {
-      _ = try client.get(Echo_EchoRequest(text: "foo")).text
-      XCTFail("should have thrown")
+      let result = try client.get(Echo_EchoRequest(text: "foo")).text
+      XCTFail("should have thrown, received \(result) instead")
     } catch {
       guard case let .callError(callResult) = error as! RPCError
         else { XCTFail("unexpected error \(error)"); return }
@@ -68,8 +68,8 @@ extension ConnectionFailureTests {
     call.waitForSendOperationsToFinish()
     
     do {
-      _ = try call.closeAndReceive()
-      XCTFail("should have thrown")
+      let result = try call.closeAndReceive()
+      XCTFail("should have thrown, received \(result) instead")
     } catch let receiveError {
       XCTAssertEqual(.unknown, (receiveError as! RPCError).callResult!.statusCode)
     }
@@ -88,8 +88,8 @@ extension ConnectionFailureTests {
     }
     
     do {
-      _ = try call.receive()
-      XCTFail("should have thrown")
+      let result = try call.receive()
+      XCTFail("should have thrown, received \(String(describing: result)) instead")
     } catch let receiveError {
       XCTAssertEqual(.unknown, (receiveError as! RPCError).callResult!.statusCode)
     }
@@ -115,8 +115,8 @@ extension ConnectionFailureTests {
     call.waitForSendOperationsToFinish()
     
     do {
-      _ = try call.receive()
-      XCTFail("should have thrown")
+      let result = try call.receive()
+      XCTFail("should have thrown, received \(String(describing: result)) instead")
     } catch let receiveError {
       XCTAssertEqual(.unknown, (receiveError as! RPCError).callResult!.statusCode)
     }

+ 1 - 1
Tests/SwiftGRPCTests/GRPCTests.swift

@@ -404,6 +404,6 @@ func handleBiDiStream(requestHandler: Handler) throws {
     // to sleep for a few milliseconds before sending the non-OK status.
     code: .ok,
     message: "Custom Status Message BiDi",
-    trailingMetadata: trailingMetadataToSend)) { _ in sem.signal() }
+    trailingMetadata: trailingMetadataToSend)) { sem.signal() }
   _ = sem.wait()
 }

+ 130 - 0
Tests/SwiftGRPCTests/ServerCancellingTests.swift

@@ -0,0 +1,130 @@
+/*
+ * Copyright 2018, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import Dispatch
+import Foundation
+@testable import SwiftGRPC
+import XCTest
+
+fileprivate class CancellingProvider: Echo_EchoProvider {
+  func get(request: Echo_EchoRequest, session: Echo_EchoGetSession) throws -> Echo_EchoResponse {
+    session.cancel()
+    return Echo_EchoResponse()
+  }
+  
+  func expand(request: Echo_EchoRequest, session: Echo_EchoExpandSession) throws {
+    session.cancel()
+    XCTAssertThrowsError(try session.send(Echo_EchoResponse()))
+  }
+  
+  func collect(session: Echo_EchoCollectSession) throws {
+    session.cancel()
+    try! session.sendAndClose(response: Echo_EchoResponse(), status: .ok, completion: nil)
+  }
+  
+  func update(session: Echo_EchoUpdateSession) throws {
+    session.cancel()
+    XCTAssertThrowsError(try session.send(Echo_EchoResponse()))
+  }
+}
+
+class ServerCancellingTests: BasicEchoTestCase {
+  static var allTests: [(String, (ServerCancellingTests) -> () throws -> Void)] {
+    return [
+      ("testServerThrowsUnary", testServerThrowsUnary),
+      ("testServerThrowsClientStreaming", testServerThrowsClientStreaming),
+      ("testServerThrowsServerStreaming", testServerThrowsServerStreaming),
+      ("testServerThrowsBidirectionalStreaming", testServerThrowsBidirectionalStreaming)
+    ]
+  }
+  
+  override func makeProvider() -> Echo_EchoProvider { return CancellingProvider() }
+}
+
+extension ServerCancellingTests {
+  func testServerThrowsUnary() {
+    do {
+      let result = try client.get(Echo_EchoRequest(text: "foo")).text
+      XCTFail("should have thrown, received \(result) instead")
+    } catch {
+      guard case let .callError(callResult) = error as! RPCError
+        else { XCTFail("unexpected error \(error)"); return }
+      XCTAssertEqual(.cancelled, callResult.statusCode)
+      XCTAssertEqual("Cancelled", callResult.statusMessage)
+    }
+  }
+  
+  func testServerThrowsClientStreaming() {
+    let completionHandlerExpectation = expectation(description: "final completion handler called")
+    let call = try! client.collect { callResult in
+      XCTAssertEqual(.cancelled, callResult.statusCode)
+      XCTAssertEqual("Cancelled", callResult.statusMessage)
+      completionHandlerExpectation.fulfill()
+    }
+    
+    let sendExpectation = expectation(description: "send completion handler 1 called")
+    try! call.send(Echo_EchoRequest(text: "foo")) { [sendExpectation] in
+      // The server only times out later in its lifecycle, so we shouldn't get an error when trying to send a message.
+      XCTAssertNil($0)
+      sendExpectation.fulfill()
+    }
+    call.waitForSendOperationsToFinish()
+    
+    do {
+      let result = try call.closeAndReceive()
+      XCTFail("should have thrown, received \(result) instead")
+    } catch let receiveError {
+      XCTAssertEqual(.unknown, (receiveError as! RPCError).callResult!.statusCode)
+    }
+    
+    waitForExpectations(timeout: defaultTimeout)
+  }
+  
+  func testServerThrowsServerStreaming() {
+    let completionHandlerExpectation = expectation(description: "completion handler called")
+    let call = try! client.expand(Echo_EchoRequest(text: "foo bar baz")) { callResult in
+      XCTAssertEqual(.cancelled, callResult.statusCode)
+      XCTAssertEqual("Cancelled", callResult.statusMessage)
+      completionHandlerExpectation.fulfill()
+    }
+    
+    // FIXME(danielalm): Why does `call.receive()` essentially return "end of stream", rather than returning an error?
+    XCTAssertNil(try! call.receive())
+    
+    waitForExpectations(timeout: defaultTimeout)
+  }
+  
+  func testServerThrowsBidirectionalStreaming() {
+    let completionHandlerExpectation = expectation(description: "completion handler called")
+    let call = try! client.update { callResult in
+      XCTAssertEqual(.cancelled, callResult.statusCode)
+      XCTAssertEqual("Cancelled", callResult.statusMessage)
+      completionHandlerExpectation.fulfill()
+    }
+    
+    let sendExpectation = expectation(description: "send completion handler 1 called")
+    try! call.send(Echo_EchoRequest(text: "foo")) { [sendExpectation] in
+      // The server only times out later in its lifecycle, so we shouldn't get an error when trying to send a message.
+      XCTAssertNil($0)
+      sendExpectation.fulfill()
+    }
+    call.waitForSendOperationsToFinish()
+    
+    // FIXME(danielalm): Why does `call.receive()` essentially return "end of stream", rather than returning an error?
+    XCTAssertNil(try! call.receive())
+    
+    waitForExpectations(timeout: defaultTimeout)
+  }
+}

+ 4 - 4
Tests/SwiftGRPCTests/ServerThrowingTests.swift

@@ -54,8 +54,8 @@ class ServerThrowingTests: BasicEchoTestCase {
 extension ServerThrowingTests {
   func testServerThrowsUnary() {
     do {
-      _ = try client.get(Echo_EchoRequest(text: "foo")).text
-      XCTFail("should have thrown")
+      let result = try client.get(Echo_EchoRequest(text: "foo")).text
+      XCTFail("should have thrown, received \(result) instead")
     } catch {
       guard case let .callError(callResult) = error as! RPCError
         else { XCTFail("unexpected error \(error)"); return }
@@ -81,8 +81,8 @@ extension ServerThrowingTests {
     call.waitForSendOperationsToFinish()
     
     do {
-      _ = try call.closeAndReceive()
-      XCTFail("should have thrown")
+      let result = try call.closeAndReceive()
+      XCTFail("should have thrown, received \(result) instead")
     } catch let receiveError {
       XCTAssertEqual(.unknown, (receiveError as! RPCError).callResult!.statusCode)
     }

+ 2 - 2
Tests/SwiftGRPCTests/ServerTimeoutTests.swift

@@ -55,8 +55,8 @@ class ServerTimeoutTests: BasicEchoTestCase {
 extension ServerTimeoutTests {
   func testTimeoutUnary() {
     do {
-      _ = try client.get(Echo_EchoRequest(text: "foo")).text
-      XCTFail("should have thrown")
+      let result = try client.get(Echo_EchoRequest(text: "foo")).text
+      XCTFail("should have thrown, received \(result) instead")
     } catch {
       guard case let .callError(callResult) = error as! RPCError
         else { XCTFail("unexpected error \(error)"); return }