From 1cb4e3587e99877eb94714aed4f44113e48327e3 Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Tue, 5 Mar 2024 15:41:36 +0000 Subject: [PATCH 01/11] Interop Tests implementation Motivation: The test cases test certain functionalities of the new GRPC server and client. Modifications: - created the InteroperabilityTest protocol with the run method - created the InteroperabilityTestCase enum that will be used by main to run certain tests according to the input - implemented the interop tests for the features we support (no caching or compression tests) - added new assertions to the test framework Result: We will be able to implement the main() function that runs the interop tests using in process transport. --- Package.swift | 1 + .../GRPCCore/Call/Client/ClientResponse.swift | 4 +- .../AssertionFailure.swift | 24 + .../InteroperabilityTestCase.swift | 126 +++ .../InteroperabilityTestCases.swift | 715 ++++++++++++++++++ .../InteroperabilityTests/ServerFeature.swift | 75 ++ 6 files changed, 943 insertions(+), 2 deletions(-) create mode 100644 Sources/InteroperabilityTests/InteroperabilityTestCase.swift create mode 100644 Sources/InteroperabilityTests/InteroperabilityTestCases.swift create mode 100644 Sources/InteroperabilityTests/ServerFeature.swift diff --git a/Package.swift b/Package.swift index a4a223855..3705ece7b 100644 --- a/Package.swift +++ b/Package.swift @@ -383,6 +383,7 @@ extension Target { name: "InteroperabilityTests", dependencies: [ .grpcCore, + .grpcInProcessTransport, .grpcProtobuf ] ) diff --git a/Sources/GRPCCore/Call/Client/ClientResponse.swift b/Sources/GRPCCore/Call/Client/ClientResponse.swift index 1ccdcfd00..49ea3973c 100644 --- a/Sources/GRPCCore/Call/Client/ClientResponse.swift +++ b/Sources/GRPCCore/Call/Client/ClientResponse.swift @@ -344,9 +344,9 @@ extension ClientResponse.Stream { } } - /// Returns metadata received from the server at the end of the response. + /// Returns the messages received from the server. /// - /// Unlike ``metadata``, for rejected RPCs the metadata returned may contain values. + /// For rejected RPCs the `RPCAsyncSequence` throws a `RPCError``. public var messages: RPCAsyncSequence { switch self.accepted { case let .success(contents): diff --git a/Sources/InteroperabilityTests/AssertionFailure.swift b/Sources/InteroperabilityTests/AssertionFailure.swift index fea2d18c3..9706d9071 100644 --- a/Sources/InteroperabilityTests/AssertionFailure.swift +++ b/Sources/InteroperabilityTests/AssertionFailure.swift @@ -34,3 +34,27 @@ public func assertTrue( throw AssertionFailure(message: message, file: file, line: line) } } + +/// Asserts that the two given values are equal. +public func assertEqual( + _ value1: T, + _ value2: T, + file: String = #fileID, + line: Int = #line +) throws { + return try assertTrue( + value1 == value2, + "'\(value1)' is not equal to '\(value2)'", + file: file, + line: line + ) +} + +/// Asserts a failure on a certain conditional branch. +public func assertFailure( + _ message: String = "Failure.", + file: String = #fileID, + line: Int = #line +) throws { + throw AssertionFailure(message: message, file: file, line: line) +} diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift new file mode 100644 index 000000000..0225cb1e0 --- /dev/null +++ b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift @@ -0,0 +1,126 @@ +/* + * Copyright 2024, 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 GRPCCore + +@available(macOS 13.0, *) +public protocol InteroperabilityTest { + /// Run a test case using the given connection. + /// + /// The test case is considered unsuccessful if any exception is thrown, conversely if no + /// exceptions are thrown it is successful. + /// + /// - Parameter client: The client to use for the test. + /// - Throws: Any exception may be thrown to indicate an unsuccessful test. + func run(client: GRPCClient) async throws +} + +/// Test cases as listed by the [gRPC interoperability test description +/// specification](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). +/// +/// This is not a complete list, the following tests have not been implemented: +/// - cacheable_unary +/// - client-compressed-unary +/// - server-compressed-unary +/// - client_compressed_streaming +/// - server_compressed_streaming +/// - compute_engine_creds +/// - jwt_token_creds +/// - oauth2_auth_token +/// - per_rpc_creds +/// - google_default_credentials +/// - compute_engine_channel_credentials +/// - cancel_after_begin +/// - cancel_after_first_response +/// +/// Note: Tests for compression have not been implemented yet as compression is +/// not supported. Once the API which allows for compression will be implemented +/// these tests should be added. +public enum InteroperabilityTestCase: String, CaseIterable { + case emptyUnary = "empty_unary" + case largeUnary = "large_unary" + case clientStreaming = "client_streaming" + case serverStreaming = "server_streaming" + case pingPong = "ping_pong" + case emptyStream = "empty_stream" + case customMetadata = "custom_metadata" + case statusCodeAndMessage = "status_code_and_message" + case specialStatusMessage = "special_status_message" + case unimplementedMethod = "unimplemented_method" + case unimplementedService = "unimplemented_service" + + public var name: String { + return self.rawValue + } +} + +@available(macOS 13.0, *) +extension InteroperabilityTestCase { + /// Return a new instance of the test case. + public func makeTest() -> InteroperabilityTest { + switch self { + case .emptyUnary: + return EmptyUnary() + case .largeUnary: + return LargeUnary() + case .clientStreaming: + return ClientStreaming() + case .serverStreaming: + return ServerStreaming() + case .pingPong: + return PingPong() + case .emptyStream: + return EmptyStream() + case .customMetadata: + return CustomMetadata() + case .statusCodeAndMessage: + return StatusCodeAndMessage() + case .specialStatusMessage: + return SpecialStatusMessage() + case .unimplementedMethod: + return UnimplementedMethod() + case .unimplementedService: + return UnimplementedService() + } + } + + /// The set of server features required to run this test. + public var requiredServerFeatures: Set { + switch self { + case .emptyUnary: + return [.emptyCall] + case .largeUnary: + return [.unaryCall] + case .clientStreaming: + return [.streamingInputCall] + case .serverStreaming: + return [.streamingOutputCall] + case .pingPong: + return [.fullDuplexCall] + case .emptyStream: + return [.fullDuplexCall] + case .customMetadata: + return [.unaryCall, .fullDuplexCall, .echoMetadata] + case .statusCodeAndMessage: + return [.unaryCall, .fullDuplexCall, .echoStatus] + case .specialStatusMessage: + return [.unaryCall, .echoStatus] + case .unimplementedMethod: + return [] + case .unimplementedService: + return [] + } + } +} diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift new file mode 100644 index 000000000..be320d35f --- /dev/null +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -0,0 +1,715 @@ +import Dispatch +/* + * Copyright 2019, 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 GRPCCore +import GRPCInProcessTransport + +import struct Foundation.Data + +/// This test verifies that implementations support zero-size messages. Ideally, client +/// implementations would verify that the request and response were zero bytes serialized, but +/// this is generally prohibitive to perform, so is not required. +/// +/// Server features: +/// - EmptyCall +/// +/// Procedure: +/// 1. Client calls EmptyCall with the default Empty message +/// +/// Client asserts: +/// - call was successful +/// - response is non-null +@available(macOS 13.0, *) +struct EmptyUnary: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + try await testServiceClient.emptyCall( + request: ClientRequest.Single(message: Grpc_Testing_Empty()) + ) { response in + try assertEqual(response.message, Grpc_Testing_Empty()) + } + } +} + +/// This test verifies unary calls succeed in sending messages, and touches on flow control (even +/// if compression is enabled on the channel). +/// +/// Server features: +/// - UnaryCall +/// +/// Procedure: +/// 1. Client calls UnaryCall with: +/// ``` +/// { +/// response_size: 314159 +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// +/// Client asserts: +/// - call was successful +/// - response payload body is 314159 bytes in size +/// - clients are free to assert that the response payload body contents are zero and comparing +/// the entire response message against a golden response +@available(macOS 13.0, *) +struct LargeUnary: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let request = Grpc_Testing_SimpleRequest.with { request in + request.responseSize = 314_159 + request.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 271_828) + } + } + try await testServiceClient.unaryCall(request: ClientRequest.Single(message: request)) { + response in + try assertEqual( + response.message.payload, + Grpc_Testing_Payload.with { + $0.body = Data(count: 314_159) + } + ) + } + } +} + +/// This test verifies that client-only streaming succeeds. +/// +/// Server features: +/// - StreamingInputCall +/// +/// Procedure: +/// 1. Client calls StreamingInputCall +/// 2. Client sends: +/// ``` +/// { +/// payload:{ +/// body: 27182 bytes of zeros +/// } +/// } +/// ``` +/// 3. Client then sends: +/// ``` +/// { +/// payload:{ +/// body: 8 bytes of zeros +/// } +/// } +/// ``` +/// 4. Client then sends: +/// ``` +/// { +/// payload:{ +/// body: 1828 bytes of zeros +/// } +/// } +/// ``` +/// 5. Client then sends: +/// ``` +/// { +/// payload:{ +/// body: 45904 bytes of zeros +/// } +/// } +/// ``` +/// 6. Client half-closes +/// +/// Client asserts: +/// - call was successful +/// - response aggregated_payload_size is 74922 +@available(macOS 13.0, *) +struct ClientStreaming: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let request = ClientRequest.Stream { writer in + for bytes in [27182, 8, 1828, 45904] { + let message = Grpc_Testing_StreamingInputCallRequest.with { + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: bytes) + } + } + try await writer.write(message) + } + } + + try await testServiceClient.streamingInputCall(request: request) { response in + try assertEqual(response.message.aggregatedPayloadSize, 74922) + } + } +} + +/// This test verifies that server-only streaming succeeds. +/// +/// Server features: +/// - StreamingOutputCall +/// +/// Procedure: +/// 1. Client calls StreamingOutputCall with StreamingOutputCallRequest: +/// ``` +/// { +/// response_parameters:{ +/// size: 31415 +/// } +/// response_parameters:{ +/// size: 9 +/// } +/// response_parameters:{ +/// size: 2653 +/// } +/// response_parameters:{ +/// size: 58979 +/// } +/// } +/// ``` +/// +/// Client asserts: +/// - call was successful +/// - exactly four responses +/// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 +/// - clients are free to assert that the response payload body contents are zero and +/// comparing the entire response messages against golden responses +@available(macOS 13.0, *) +struct ServerStreaming: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let responseSizes = [31415, 9, 2653, 58979] + let request = Grpc_Testing_StreamingOutputCallRequest.with { request in + request.responseParameters = responseSizes.map { + var parameter = Grpc_Testing_ResponseParameters() + parameter.size = Int32($0) + return parameter + } + } + + try await testServiceClient.streamingOutputCall(request: ClientRequest.Single(message: request)) + { response in + var responseParts = response.messages.makeAsyncIterator() + // There are 4 response sizes, so if there isn't a message for each one, + // it means that the client didn't receive 4 messages back. + for responseSize in responseSizes { + if let message = try await responseParts.next() { + try assertEqual(message.payload.body.count, responseSize) + } else { + try assertFailure("There were less than four responses received.") + } + } + } + } +} + +/// This test verifies that full duplex bidi is supported. +/// +/// Server features: +/// - FullDuplexCall +/// +/// Procedure: +/// 1. Client calls FullDuplexCall with: +/// ``` +/// { +/// response_parameters:{ +/// size: 31415 +/// } +/// payload:{ +/// body: 27182 bytes of zeros +/// } +/// } +/// ``` +/// 2. After getting a reply, it sends: +/// ``` +/// { +/// response_parameters:{ +/// size: 9 +/// } +/// payload:{ +/// body: 8 bytes of zeros +/// } +/// } +/// ``` +/// 3. After getting a reply, it sends: +/// ``` +/// { +/// response_parameters:{ +/// size: 2653 +/// } +/// payload:{ +/// body: 1828 bytes of zeros +/// } +/// } +/// ``` +/// 4. After getting a reply, it sends: +/// ``` +/// { +/// response_parameters:{ +/// size: 58979 +/// } +/// payload:{ +/// body: 45904 bytes of zeros +/// } +/// } +/// ``` +/// 5. After getting a reply, client half-closes +/// +/// Client asserts: +/// - call was successful +/// - exactly four responses +/// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 +/// - clients are free to assert that the response payload body contents are zero and +/// comparing the entire response messages against golden responses +@available(macOS 13.0, *) +struct PingPong: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let asyncStream = AsyncStream.makeStream(of: Int.self) + let request = ClientRequest.Stream { writer in + for try await id in asyncStream.stream { + var message = Grpc_Testing_StreamingOutputCallRequest() + switch id { + case 1: + message.responseParameters = [ + Grpc_Testing_ResponseParameters.with { + $0.size = 31_415 + } + ] + message.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 27_182) + } + case 2: + message.responseParameters = [ + Grpc_Testing_ResponseParameters.with { + $0.size = 9 + } + ] + message.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 8) + } + case 3: + message.responseParameters = [ + Grpc_Testing_ResponseParameters.with { + $0.size = 2_653 + } + ] + message.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 1_828) + } + case 4: + message.responseParameters = [ + Grpc_Testing_ResponseParameters.with { + $0.size = 58_979 + } + ] + message.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 45_904) + } + default: + return + } + try await writer.write(message) + } + } + asyncStream.continuation.yield(1) + try await testServiceClient.fullDuplexCall(request: request) { response in + var id = 1 + for try await message in response.messages { + switch id { + case 1: + try assertEqual(message.payload.body, Data(count: 31_415)) + case 2: + try assertEqual(message.payload.body, Data(count: 9)) + case 3: + try assertEqual(message.payload.body, Data(count: 2_653)) + case 4: + try assertEqual(message.payload.body, Data(count: 58_979)) + default: + return + } + + // Add the next id to the continuation. + id += 1 + asyncStream.continuation.yield(id) + } + } + } +} + +/// This test verifies that streams support having zero-messages in both directions. +/// +/// Server features: +/// - FullDuplexCall +/// +/// Procedure: +/// 1. Client calls FullDuplexCall and then half-closes +/// +/// Client asserts: +/// - call was successful +/// - exactly zero responses +@available(macOS 13.0, *) +struct EmptyStream: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + + let request = ClientRequest.Stream { writer in + let message = Grpc_Testing_StreamingOutputCallRequest() + try await writer.write(message) + } + + try await testServiceClient.fullDuplexCall(request: request) { response in + var messages = response.messages.makeAsyncIterator() + try await assertEqual(messages.next(), nil) + } + } +} + +/// This test verifies that custom metadata in either binary or ascii format can be sent as +/// initial-metadata by the client and as both initial- and trailing-metadata by the server. +/// +/// Server features: +/// - UnaryCall +/// - FullDuplexCall +/// - Echo Metadata +/// +/// Procedure: +/// 1. The client attaches custom metadata with the following keys and values +/// to a UnaryCall with request: +/// - key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" +/// - key: "x-grpc-test-echo-trailing-bin", value: 0xababab +/// ``` +/// { +/// response_size: 314159 +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// 2. The client attaches custom metadata with the following keys and values +/// to a FullDuplexCall with request: +/// - key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" +/// - key: "x-grpc-test-echo-trailing-bin", value: 0xababab +/// ``` +/// { +/// response_parameters:{ +/// size: 314159 +/// } +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// and then half-closes +/// +/// Client asserts: +/// - call was successful +/// - metadata with key "x-grpc-test-echo-initial" and value "test_initial_metadata_value" is +/// received in the initial metadata for calls in Procedure steps 1 and 2. +/// - metadata with key "x-grpc-test-echo-trailing-bin" and value 0xababab is received in the +/// trailing metadata for calls in Procedure steps 1 and 2. +@available(macOS 13.0, *) +struct CustomMetadata: InteroperabilityTest { + let initialMetadataName = "x-grpc-test-echo-initial" + let initialMetadataValue = "test_initial_metadata_value" + + let trailingMetadataName = "x-grpc-test-echo-trailing-bin" + let trailingMetadataValue: [UInt8] = [0xAB, 0xAB, 0xAB] + + func checkInitialMetadata(_ metadata: Metadata) throws { + try assertEqual(metadata.count, 1) + var metadataIterator = metadata.makeIterator() + guard let initialMetadataPair = metadataIterator.next() else { + try assertFailure("The initial metadata should contain a key value pair.") + return + } + try assertEqual(initialMetadataPair.key, self.initialMetadataName) + try assertEqual(initialMetadataPair.value, Metadata.Value.string(self.initialMetadataValue)) + } + + func checkTrailingMetadata(_ metadata: Metadata) throws { + try assertEqual(metadata.count, 1) + var metadataIterator = metadata.makeIterator() + guard let trailingMetadataPair = metadataIterator.next() else { + try assertFailure("The trailing metadata should contain a key value pair.") + return + } + try assertEqual(trailingMetadataPair.key, self.trailingMetadataName) + try assertEqual(trailingMetadataPair.value, Metadata.Value.binary(self.trailingMetadataValue)) + } + + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + + let unaryRequest = Grpc_Testing_SimpleRequest.with { request in + request.responseSize = 314_159 + request.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 271_828) + } + } + var metadata = Metadata() + metadata.addString(self.initialMetadataValue, forKey: self.initialMetadataName) + metadata.addBinary(self.trailingMetadataValue, forKey: self.trailingMetadataName) + let streamingMetadata = metadata + + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: unaryRequest, metadata: metadata) + ) { response in + // Check the initial metadata. + // Check the initial metadata. + let receivedInitialMetadata = response.metadata + try checkInitialMetadata(receivedInitialMetadata) + + // Check the message. + try assertEqual(response.message.payload.body, Data(count: 314_159)) + + // Check the trailing metadata. + let receivedTrailingMetadata = response.trailingMetadata + try checkTrailingMetadata(receivedInitialMetadata) + } + + let streamingRequest = ClientRequest.Stream(metadata: streamingMetadata) { writer in + let message = Grpc_Testing_StreamingOutputCallRequest.with { + $0.responseParameters = [ + Grpc_Testing_ResponseParameters.with { + $0.size = 314_159 + } + ] + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 271_828) + } + } + try await writer.write(message) + } + + try await testServiceClient.fullDuplexCall(request: streamingRequest) { response in + switch response.accepted { + case .success(let contents): + // Check the initial metadata. + let receivedInitialMetadata = response.metadata + try self.checkInitialMetadata(receivedInitialMetadata) + + var responseParts = contents.bodyParts.makeAsyncIterator() + for _ in 1 ... 2 { + switch try await responseParts.next() { + // Check the message. + case .message(let message): + try assertEqual(message.payload.body, Data(count: 314_159)) + // Check the trailing metadata. + case .trailingMetadata(let receivedTrailingMetadata): + try self.checkTrailingMetadata(receivedTrailingMetadata) + default: + try assertFailure( + "The client should have received only a message and trailing metadata." + ) + } + } + case .failure(_): + try assertFailure("The client should have received a response from the server.") + } + } + } +} + +/// This test verifies unary calls succeed in sending messages, and propagate back status code and +/// message sent along with the messages. +/// +/// Server features: +/// - UnaryCall +/// - FullDuplexCall +/// - Echo Status +/// +/// Procedure: +/// 1. Client calls UnaryCall with: +/// ``` +/// { +/// response_status:{ +/// code: 2 +/// message: "test status message" +/// } +/// } +/// ``` +/// 2. Client calls FullDuplexCall with: +/// ``` +/// { +/// response_status:{ +/// code: 2 +/// message: "test status message" +/// } +/// } +/// ``` +/// 3. and then half-closes +/// +/// Client asserts: +/// - received status code is the same as the sent code for both Procedure steps 1 and 2 +/// - received status message is the same as the sent message for both Procedure steps 1 and 2 +@available(macOS 13.0, *) +struct StatusCodeAndMessage: InteroperabilityTest { + let expectedCode = 2 + let expectedMessage = "test status message" + + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + + let message = Grpc_Testing_SimpleRequest.with { + $0.responseStatus = Grpc_Testing_EchoStatus.with { + $0.code = Int32(self.expectedCode) + $0.message = self.expectedMessage + } + } + + try await testServiceClient.unaryCall(request: ClientRequest.Single(message: message)) { + response in + switch response.accepted { + case .failure(let error): + try assertEqual(error.code.rawValue, self.expectedCode) + try assertEqual(error.message, self.expectedMessage) + case .success(_): + try assertFailure( + "The client should receive an error with the status code and message sent by the client." + ) + } + } + + let request = ClientRequest.Stream { writer in + let message = Grpc_Testing_StreamingOutputCallRequest() + try await writer.write(message) + } + + try await testServiceClient.fullDuplexCall(request: request) { response in + switch response.accepted { + case .failure(let error): + try assertEqual(error.code.rawValue, self.expectedCode) + try assertEqual(error.message, self.expectedMessage) + case .success(_): + try assertFailure( + "The client should receive an error with the status code and message sent by the client." + ) + } + } + } +} + +/// This test verifies Unicode and whitespace is correctly processed in status message. "\t" is +/// horizontal tab. "\r" is carriage return. "\n" is line feed. +/// +/// Server features: +/// - UnaryCall +/// - Echo Status +/// +/// Procedure: +/// 1. Client calls UnaryCall with: +/// ``` +/// { +/// response_status:{ +/// code: 2 +/// message: "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" +/// } +/// } +/// ``` +/// +/// Client asserts: +/// - received status code is the same as the sent code for Procedure step 1 +/// - received status message is the same as the sent message for Procedure step 1, including all +/// whitespace characters +@available(macOS 13.0, *) +struct SpecialStatusMessage: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + + let code = 2 + let responseMessage = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" + let message = Grpc_Testing_SimpleRequest.with { + $0.responseStatus = Grpc_Testing_EchoStatus.with { + $0.code = 2 + $0.message = responseMessage + } + } + try await testServiceClient.unaryCall(request: ClientRequest.Single(message: message)) { + response in + switch response.accepted { + case .success(_): + try assertFailure("The response should be an error with the error code 2.") + case .failure(let error): + try assertEqual(error.code.rawValue, 2) + try assertEqual(error.message, responseMessage) + } + } + } +} + +/// This test verifies that calling an unimplemented RPC method returns the UNIMPLEMENTED status +/// code. +/// +/// Server features: N/A +/// +/// Procedure: +/// 1. Client calls grpc.testing.TestService/UnimplementedCall with an empty request (defined as +/// grpc.testing.Empty): +/// ``` +/// { +/// } +/// ``` +/// +/// Client asserts: +/// - received status code is 12 (UNIMPLEMENTED) +@available(macOS 13.0, *) +struct UnimplementedMethod: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + try await testServiceClient.unimplementedCall( + request: ClientRequest.Single(message: Grpc_Testing_Empty()) + ) { + response in + let result = response.accepted + switch result { + case .success(_): + try assertFailure("The result should be an error.") + case .failure(let error): + try assertEqual(error.code, RPCError.Code.unimplemented) + } + } + } +} + +/// This test verifies calling an unimplemented server returns the UNIMPLEMENTED status code. +/// +/// Server features: N/A +/// +/// Procedure: +/// 1. Client calls grpc.testing.UnimplementedService/UnimplementedCall with an empty request +/// (defined as grpc.testing.Empty): +/// ``` +/// { +/// } +/// ``` +/// +/// Client asserts: +/// - received status code is 12 (UNIMPLEMENTED) +@available(macOS 13.0, *) +struct UnimplementedService: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let unimplementedServiceClient = Grpc_Testing_UnimplementedService.Client(client: client) + try await unimplementedServiceClient.unimplementedCall( + request: ClientRequest.Single(message: Grpc_Testing_Empty()) + ) { + response in + let result = response.accepted + switch result { + case .success(_): + try assertFailure("The result should be an error.") + case .failure(let error): + try assertEqual(error.code, RPCError.Code.unimplemented) + } + } + } +} diff --git a/Sources/InteroperabilityTests/ServerFeature.swift b/Sources/InteroperabilityTests/ServerFeature.swift new file mode 100644 index 000000000..d5636d896 --- /dev/null +++ b/Sources/InteroperabilityTests/ServerFeature.swift @@ -0,0 +1,75 @@ +/* + * Copyright 2024, 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. + */ + +/// Server features which may be required for tests. +/// +/// We use this enum to match up tests we can run on the NIO client against the NIO server at +/// run time. +/// +/// These features are listed in the [gRPC interoperability test description +/// specification](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). +/// +/// Missing features: +/// - compressed response +/// - compressed request +/// - observe `ResponseParameter.interval_us` +/// - echo authenticated username +/// - echo authenticated OAuth scope +/// +/// - Note: This is not a complete set of features, only those used in either the client or server. +public enum ServerFeature { + /// See TestServiceProvider_NIO.emptyCall. + case emptyCall + + /// See TestServiceProvider_NIO.unaryCall. + case unaryCall + + /// See TestServiceProvider_NIO.cacheableUnaryCall. + case cacheableUnaryCall + + /// When the client sets expect_compressed to true, the server expects the client request to be + /// compressed. If it's not, it fails the RPC with INVALID_ARGUMENT. Note that + /// `response_compressed` is present on both SimpleRequest (unary) and StreamingOutputCallRequest + /// (streaming). + case compressedRequest + + /// When the client sets response_compressed to true, the server's response is sent back + /// compressed. Note that response_compressed is present on both SimpleRequest (unary) and + /// StreamingOutputCallRequest (streaming). + case compressedResponse + + /// See TestServiceProvider_NIO.streamingInputCall. + case streamingInputCall + + /// See TestServiceProvider_NIO.streamingOutputCall. + case streamingOutputCall + + /// See TestServiceProvider_NIO.fullDuplexCall. + case fullDuplexCall + + /// When the client sends a `responseStatus` in the request payload, the server closes the stream + /// with the status code and messsage contained within said `responseStatus`. The server will not + /// process any further messages on the stream sent by the client. This can be used by clients to + /// verify correct handling of different status codes and associated status messages end-to-end. + case echoStatus + + /// When the client sends metadata with the key "x-grpc-test-echo-initial" with its request, + /// the server sends back exactly this key and the corresponding value back to the client as + /// part of initial metadata. When the client sends metadata with the key + /// "x-grpc-test-echo-trailing-bin" with its request, the server sends back exactly this key + /// and the corresponding value back to the client as trailing metadata. + case echoMetadata +} From f0141891d32b642a0d86e0e6be6a75a126f7a206 Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Tue, 5 Mar 2024 15:57:04 +0000 Subject: [PATCH 02/11] fixed availability guards --- .../InteroperabilityTestCase.swift | 2 +- .../InteroperabilityTestCases.swift | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift index 0225cb1e0..c885392b1 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift @@ -15,7 +15,7 @@ */ import GRPCCore -@available(macOS 13.0, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol InteroperabilityTest { /// Run a test case using the given connection. /// diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift index be320d35f..2684651da 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -32,7 +32,7 @@ import struct Foundation.Data /// Client asserts: /// - call was successful /// - response is non-null -@available(macOS 13.0, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct EmptyUnary: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -66,7 +66,7 @@ struct EmptyUnary: InteroperabilityTest { /// - response payload body is 314159 bytes in size /// - clients are free to assert that the response payload body contents are zero and comparing /// the entire response message against a golden response -@available(macOS 13.0, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct LargeUnary: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -132,7 +132,7 @@ struct LargeUnary: InteroperabilityTest { /// Client asserts: /// - call was successful /// - response aggregated_payload_size is 74922 -@available(macOS 13.0, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct ClientStreaming: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -183,7 +183,7 @@ struct ClientStreaming: InteroperabilityTest { /// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 /// - clients are free to assert that the response payload body contents are zero and /// comparing the entire response messages against golden responses -@available(macOS 13.0, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct ServerStreaming: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -270,7 +270,7 @@ struct ServerStreaming: InteroperabilityTest { /// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 /// - clients are free to assert that the response payload body contents are zero and /// comparing the entire response messages against golden responses -@available(macOS 13.0, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct PingPong: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -357,7 +357,7 @@ struct PingPong: InteroperabilityTest { /// Client asserts: /// - call was successful /// - exactly zero responses -@available(macOS 13.0, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct EmptyStream: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -417,7 +417,7 @@ struct EmptyStream: InteroperabilityTest { /// received in the initial metadata for calls in Procedure steps 1 and 2. /// - metadata with key "x-grpc-test-echo-trailing-bin" and value 0xababab is received in the /// trailing metadata for calls in Procedure steps 1 and 2. -@available(macOS 13.0, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct CustomMetadata: InteroperabilityTest { let initialMetadataName = "x-grpc-test-echo-initial" let initialMetadataValue = "test_initial_metadata_value" @@ -552,7 +552,7 @@ struct CustomMetadata: InteroperabilityTest { /// Client asserts: /// - received status code is the same as the sent code for both Procedure steps 1 and 2 /// - received status message is the same as the sent message for both Procedure steps 1 and 2 -@available(macOS 13.0, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct StatusCodeAndMessage: InteroperabilityTest { let expectedCode = 2 let expectedMessage = "test status message" @@ -621,7 +621,7 @@ struct StatusCodeAndMessage: InteroperabilityTest { /// - received status code is the same as the sent code for Procedure step 1 /// - received status message is the same as the sent message for Procedure step 1, including all /// whitespace characters -@available(macOS 13.0, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct SpecialStatusMessage: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -662,7 +662,7 @@ struct SpecialStatusMessage: InteroperabilityTest { /// /// Client asserts: /// - received status code is 12 (UNIMPLEMENTED) -@available(macOS 13.0, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct UnimplementedMethod: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -695,7 +695,7 @@ struct UnimplementedMethod: InteroperabilityTest { /// /// Client asserts: /// - received status code is 12 (UNIMPLEMENTED) -@available(macOS 13.0, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct UnimplementedService: InteroperabilityTest { func run(client: GRPCClient) async throws { let unimplementedServiceClient = Grpc_Testing_UnimplementedService.Client(client: client) From becd71f1df15b6e58c8104847007ca733ecfef31 Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Tue, 5 Mar 2024 16:03:54 +0000 Subject: [PATCH 03/11] fixed ServerFeature --- .../InteroperabilityTests/ServerFeature.swift | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/Sources/InteroperabilityTests/ServerFeature.swift b/Sources/InteroperabilityTests/ServerFeature.swift index d5636d896..a9b682acc 100644 --- a/Sources/InteroperabilityTests/ServerFeature.swift +++ b/Sources/InteroperabilityTests/ServerFeature.swift @@ -16,7 +16,7 @@ /// Server features which may be required for tests. /// -/// We use this enum to match up tests we can run on the NIO client against the NIO server at +/// We use this enum to match up tests we can run on the client against the server at /// run time. /// /// These features are listed in the [gRPC interoperability test description @@ -31,37 +31,26 @@ /// /// - Note: This is not a complete set of features, only those used in either the client or server. public enum ServerFeature { - /// See TestServiceProvider_NIO.emptyCall. + /// See TestService.emptyCall. case emptyCall - /// See TestServiceProvider_NIO.unaryCall. + /// See TestService.unaryCall. case unaryCall - /// See TestServiceProvider_NIO.cacheableUnaryCall. + /// See TestService.cacheableUnaryCall. case cacheableUnaryCall - /// When the client sets expect_compressed to true, the server expects the client request to be - /// compressed. If it's not, it fails the RPC with INVALID_ARGUMENT. Note that - /// `response_compressed` is present on both SimpleRequest (unary) and StreamingOutputCallRequest - /// (streaming). - case compressedRequest - - /// When the client sets response_compressed to true, the server's response is sent back - /// compressed. Note that response_compressed is present on both SimpleRequest (unary) and - /// StreamingOutputCallRequest (streaming). - case compressedResponse - - /// See TestServiceProvider_NIO.streamingInputCall. + /// See TestService.streamingInputCall. case streamingInputCall - /// See TestServiceProvider_NIO.streamingOutputCall. + /// See TestService.streamingOutputCall. case streamingOutputCall - /// See TestServiceProvider_NIO.fullDuplexCall. + /// See TestService.fullDuplexCall. case fullDuplexCall /// When the client sends a `responseStatus` in the request payload, the server closes the stream - /// with the status code and messsage contained within said `responseStatus`. The server will not + /// with the status code and message contained within said `responseStatus`. The server will not /// process any further messages on the stream sent by the client. This can be used by clients to /// verify correct handling of different status codes and associated status messages end-to-end. case echoStatus From 468feaa5b5940843900fa018c4a4d659a2b3c44d Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Tue, 5 Mar 2024 16:09:03 +0000 Subject: [PATCH 04/11] fixed some tests --- Sources/InteroperabilityTests/InteroperabilityTestCases.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift index 2684651da..bc8464f04 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -335,7 +335,7 @@ struct PingPong: InteroperabilityTest { case 4: try assertEqual(message.payload.body, Data(count: 58_979)) default: - return + try assertFailure("We should only receive messages with ids between 1 and 4.") } // Add the next id to the continuation. @@ -464,7 +464,6 @@ struct CustomMetadata: InteroperabilityTest { try await testServiceClient.unaryCall( request: ClientRequest.Single(message: unaryRequest, metadata: metadata) ) { response in - // Check the initial metadata. // Check the initial metadata. let receivedInitialMetadata = response.metadata try checkInitialMetadata(receivedInitialMetadata) From 213a53db9ea990ac509556bdca3500a683783819 Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Tue, 5 Mar 2024 16:10:26 +0000 Subject: [PATCH 05/11] fixed header --- Sources/InteroperabilityTests/InteroperabilityTestCases.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift index bc8464f04..895439c13 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -1,6 +1,6 @@ import Dispatch /* - * Copyright 2019, gRPC Authors All rights reserved. + * Copyright 2024, 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. From d9cd6b8712c1cc5ce95c8226d3213d18ca5468d5 Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Tue, 5 Mar 2024 16:28:15 +0000 Subject: [PATCH 06/11] fixed header --- Sources/InteroperabilityTests/InteroperabilityTestCases.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift index 895439c13..254fdec80 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -1,4 +1,3 @@ -import Dispatch /* * Copyright 2024, gRPC Authors All rights reserved. * @@ -14,6 +13,7 @@ import Dispatch * See the License for the specific language governing permissions and * limitations under the License. */ + import GRPCCore import GRPCInProcessTransport From e5d9107a03bd62a3c1d6c751559d0d52bd48f32a Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Wed, 6 Mar 2024 10:25:31 +0000 Subject: [PATCH 07/11] solved async stream issue for older swift versions --- .../InteroperabilityTestCases.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift index 254fdec80..2c4b70427 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -274,9 +274,18 @@ struct ServerStreaming: InteroperabilityTest { struct PingPong: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) + #if swift(>=5.9) let asyncStream = AsyncStream.makeStream(of: Int.self) + let continuation = asyncStream.continuation + let stream = asyncStream.stream + #else + let continuation: AsyncStream.Continuation! + let stream = AsyncStream(Int.self) { streamContinuation in + continuation = streamContinuation + } + #endif let request = ClientRequest.Stream { writer in - for try await id in asyncStream.stream { + for try await id in stream { var message = Grpc_Testing_StreamingOutputCallRequest() switch id { case 1: @@ -321,7 +330,7 @@ struct PingPong: InteroperabilityTest { try await writer.write(message) } } - asyncStream.continuation.yield(1) + continuation.yield(1) try await testServiceClient.fullDuplexCall(request: request) { response in var id = 1 for try await message in response.messages { @@ -340,7 +349,7 @@ struct PingPong: InteroperabilityTest { // Add the next id to the continuation. id += 1 - asyncStream.continuation.yield(id) + continuation.yield(id) } } } @@ -472,8 +481,7 @@ struct CustomMetadata: InteroperabilityTest { try assertEqual(response.message.payload.body, Data(count: 314_159)) // Check the trailing metadata. - let receivedTrailingMetadata = response.trailingMetadata - try checkTrailingMetadata(receivedInitialMetadata) + try checkTrailingMetadata(response.trailingMetadata) } let streamingRequest = ClientRequest.Stream(metadata: streamingMetadata) { writer in @@ -625,7 +633,6 @@ struct SpecialStatusMessage: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) - let code = 2 let responseMessage = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" let message = Grpc_Testing_SimpleRequest.with { $0.responseStatus = Grpc_Testing_EchoStatus.with { From 302d374a6077f400f4a469213359d6484c3481db Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Wed, 6 Mar 2024 10:45:37 +0000 Subject: [PATCH 08/11] added convenience makeStream method for older versions of swift --- .../Internal/AsyncStream+MakeStream.swift | 32 +++++++++++++++++++ .../InteroperabilityTestCases.swift | 18 +++-------- 2 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift diff --git a/Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift b/Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift new file mode 100644 index 000000000..5f1b75dd6 --- /dev/null +++ b/Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift @@ -0,0 +1,32 @@ +/* + * Copyright 2024, 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. + */ + +#if swift(<5.9) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension AsyncStream { + @inlinable + static func makeStream( + of elementType: Element.Type = Element.self, + bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded + ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { + var continuation: AsyncStream.Continuation! + let stream = AsyncStream(Element.self, bufferingPolicy: limit) { + continuation = $0 + } + return (stream, continuation) + } +} +#endif diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift index 2c4b70427..a7ac6cecd 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -14,11 +14,12 @@ * limitations under the License. */ -import GRPCCore import GRPCInProcessTransport import struct Foundation.Data +@testable import GRPCCore + /// This test verifies that implementations support zero-size messages. Ideally, client /// implementations would verify that the request and response were zero bytes serialized, but /// this is generally prohibitive to perform, so is not required. @@ -274,18 +275,9 @@ struct ServerStreaming: InteroperabilityTest { struct PingPong: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) - #if swift(>=5.9) let asyncStream = AsyncStream.makeStream(of: Int.self) - let continuation = asyncStream.continuation - let stream = asyncStream.stream - #else - let continuation: AsyncStream.Continuation! - let stream = AsyncStream(Int.self) { streamContinuation in - continuation = streamContinuation - } - #endif let request = ClientRequest.Stream { writer in - for try await id in stream { + for try await id in asyncStream.stream { var message = Grpc_Testing_StreamingOutputCallRequest() switch id { case 1: @@ -330,7 +322,7 @@ struct PingPong: InteroperabilityTest { try await writer.write(message) } } - continuation.yield(1) + asyncStream.continuation.yield(1) try await testServiceClient.fullDuplexCall(request: request) { response in var id = 1 for try await message in response.messages { @@ -349,7 +341,7 @@ struct PingPong: InteroperabilityTest { // Add the next id to the continuation. id += 1 - continuation.yield(id) + asyncStream.continuation.yield(id) } } } From ae8132b7ea3d319f49ec6fe6e76a5a33e1b3de51 Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Wed, 6 Mar 2024 10:47:07 +0000 Subject: [PATCH 09/11] deleted testable --- Sources/InteroperabilityTests/InteroperabilityTestCases.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift index a7ac6cecd..5b9b73936 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -14,12 +14,11 @@ * limitations under the License. */ +import GRPCCore import GRPCInProcessTransport import struct Foundation.Data -@testable import GRPCCore - /// This test verifies that implementations support zero-size messages. Ideally, client /// implementations would verify that the request and response were zero bytes serialized, but /// this is generally prohibitive to perform, so is not required. From 095c9215fbbbe5ac9fa97f3e132792440faf5e8a Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Wed, 6 Mar 2024 15:17:50 +0000 Subject: [PATCH 10/11] implemented feedback --- Package.swift | 1 - .../AssertionFailure.swift | 9 - .../InteroperabilityTestCase.swift | 28 --- .../InteroperabilityTestCases.swift | 192 +++++++++--------- .../InteroperabilityTests/ServerFeature.swift | 64 ------ 5 files changed, 95 insertions(+), 199 deletions(-) delete mode 100644 Sources/InteroperabilityTests/ServerFeature.swift diff --git a/Package.swift b/Package.swift index 3705ece7b..a4a223855 100644 --- a/Package.swift +++ b/Package.swift @@ -383,7 +383,6 @@ extension Target { name: "InteroperabilityTests", dependencies: [ .grpcCore, - .grpcInProcessTransport, .grpcProtobuf ] ) diff --git a/Sources/InteroperabilityTests/AssertionFailure.swift b/Sources/InteroperabilityTests/AssertionFailure.swift index 9706d9071..48665e30e 100644 --- a/Sources/InteroperabilityTests/AssertionFailure.swift +++ b/Sources/InteroperabilityTests/AssertionFailure.swift @@ -49,12 +49,3 @@ public func assertEqual( line: line ) } - -/// Asserts a failure on a certain conditional branch. -public func assertFailure( - _ message: String = "Failure.", - file: String = #fileID, - line: Int = #line -) throws { - throw AssertionFailure(message: message, file: file, line: line) -} diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift index c885392b1..6a909e6ea 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift @@ -95,32 +95,4 @@ extension InteroperabilityTestCase { return UnimplementedService() } } - - /// The set of server features required to run this test. - public var requiredServerFeatures: Set { - switch self { - case .emptyUnary: - return [.emptyCall] - case .largeUnary: - return [.unaryCall] - case .clientStreaming: - return [.streamingInputCall] - case .serverStreaming: - return [.streamingOutputCall] - case .pingPong: - return [.fullDuplexCall] - case .emptyStream: - return [.fullDuplexCall] - case .customMetadata: - return [.unaryCall, .fullDuplexCall, .echoMetadata] - case .statusCodeAndMessage: - return [.unaryCall, .fullDuplexCall, .echoStatus] - case .specialStatusMessage: - return [.unaryCall, .echoStatus] - case .unimplementedMethod: - return [] - case .unimplementedService: - return [] - } - } } diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift index 5b9b73936..5d97a778a 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -15,7 +15,6 @@ */ import GRPCCore -import GRPCInProcessTransport import struct Foundation.Data @@ -76,8 +75,9 @@ struct LargeUnary: InteroperabilityTest { $0.body = Data(count: 271_828) } } - try await testServiceClient.unaryCall(request: ClientRequest.Single(message: request)) { - response in + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: request) + ) { response in try assertEqual( response.message.payload, Grpc_Testing_Payload.with { @@ -196,8 +196,9 @@ struct ServerStreaming: InteroperabilityTest { } } - try await testServiceClient.streamingOutputCall(request: ClientRequest.Single(message: request)) - { response in + try await testServiceClient.streamingOutputCall( + request: ClientRequest.Single(message: request) + ) { response in var responseParts = response.messages.makeAsyncIterator() // There are 4 response sizes, so if there isn't a message for each one, // it means that the client didn't receive 4 messages back. @@ -205,9 +206,15 @@ struct ServerStreaming: InteroperabilityTest { if let message = try await responseParts.next() { try assertEqual(message.payload.body.count, responseSize) } else { - try assertFailure("There were less than four responses received.") + throw AssertionFailure( + message: "There were less than four responses received.", + file: #fileID, + line: #line + ) } } + // Check that there were not more than 4 responses from the server. + try assertEqual(try await responseParts.next(), nil) } } } @@ -274,54 +281,31 @@ struct ServerStreaming: InteroperabilityTest { struct PingPong: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) - let asyncStream = AsyncStream.makeStream(of: Int.self) + let ids = AsyncStream.makeStream(of: Int.self) let request = ClientRequest.Stream { writer in - for try await id in asyncStream.stream { + for try await id in ids.stream { var message = Grpc_Testing_StreamingOutputCallRequest() + let sizes = [(31_415, 27_182), (9, 8), (2_653, 1_828), (58_979, 45_904)] switch id { - case 1: - message.responseParameters = [ - Grpc_Testing_ResponseParameters.with { - $0.size = 31_415 - } - ] - message.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: 27_182) - } - case 2: - message.responseParameters = [ - Grpc_Testing_ResponseParameters.with { - $0.size = 9 - } - ] - message.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: 8) - } - case 3: - message.responseParameters = [ - Grpc_Testing_ResponseParameters.with { - $0.size = 2_653 - } - ] - message.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: 1_828) - } - case 4: + case 1 ... 4: + let (responseSize, bodySize) = sizes[id - 1] message.responseParameters = [ Grpc_Testing_ResponseParameters.with { - $0.size = 58_979 + $0.size = Int32(responseSize) } ] message.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: 45_904) + $0.body = Data(count: bodySize) } default: + // When the id is higher than 4 it means the client received all the expected responses + // and it doesn't need to send another message. return } try await writer.write(message) } } - asyncStream.continuation.yield(1) + ids.continuation.yield(1) try await testServiceClient.fullDuplexCall(request: request) { response in var id = 1 for try await message in response.messages { @@ -335,12 +319,16 @@ struct PingPong: InteroperabilityTest { case 4: try assertEqual(message.payload.body, Data(count: 58_979)) default: - try assertFailure("We should only receive messages with ids between 1 and 4.") + throw AssertionFailure( + message: "We should only receive messages with ids between 1 and 4.", + file: #fileID, + line: #line + ) } // Add the next id to the continuation. id += 1 - asyncStream.continuation.yield(id) + ids.continuation.yield(id) } } } @@ -361,11 +349,7 @@ struct PingPong: InteroperabilityTest { struct EmptyStream: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) - - let request = ClientRequest.Stream { writer in - let message = Grpc_Testing_StreamingOutputCallRequest() - try await writer.write(message) - } + let request = ClientRequest.Stream { _ in } try await testServiceClient.fullDuplexCall(request: request) { response in var messages = response.messages.makeAsyncIterator() @@ -426,25 +410,13 @@ struct CustomMetadata: InteroperabilityTest { let trailingMetadataValue: [UInt8] = [0xAB, 0xAB, 0xAB] func checkInitialMetadata(_ metadata: Metadata) throws { - try assertEqual(metadata.count, 1) - var metadataIterator = metadata.makeIterator() - guard let initialMetadataPair = metadataIterator.next() else { - try assertFailure("The initial metadata should contain a key value pair.") - return - } - try assertEqual(initialMetadataPair.key, self.initialMetadataName) - try assertEqual(initialMetadataPair.value, Metadata.Value.string(self.initialMetadataValue)) + let values = metadata[self.initialMetadataName] + try assertEqual(Array(values), [.string(self.initialMetadataValue)]) } func checkTrailingMetadata(_ metadata: Metadata) throws { - try assertEqual(metadata.count, 1) - var metadataIterator = metadata.makeIterator() - guard let trailingMetadataPair = metadataIterator.next() else { - try assertFailure("The trailing metadata should contain a key value pair.") - return - } - try assertEqual(trailingMetadataPair.key, self.trailingMetadataName) - try assertEqual(trailingMetadataPair.value, Metadata.Value.binary(self.trailingMetadataValue)) + let values = metadata[self.trailingMetadataName] + try assertEqual(Array(values), [.binary(self.trailingMetadataValue)]) } func run(client: GRPCClient) async throws { @@ -456,10 +428,10 @@ struct CustomMetadata: InteroperabilityTest { $0.body = Data(count: 271_828) } } - var metadata = Metadata() - metadata.addString(self.initialMetadataValue, forKey: self.initialMetadataName) - metadata.addBinary(self.trailingMetadataValue, forKey: self.trailingMetadataName) - let streamingMetadata = metadata + let metadata: Metadata = [ + self.initialMetadataName: .string(self.initialMetadataValue), + self.trailingMetadataName: .binary(self.trailingMetadataValue), + ] try await testServiceClient.unaryCall( request: ClientRequest.Single(message: unaryRequest, metadata: metadata) @@ -475,7 +447,7 @@ struct CustomMetadata: InteroperabilityTest { try checkTrailingMetadata(response.trailingMetadata) } - let streamingRequest = ClientRequest.Stream(metadata: streamingMetadata) { writer in + let streamingRequest = ClientRequest.Stream(metadata: metadata) { writer in let message = Grpc_Testing_StreamingOutputCallRequest.with { $0.responseParameters = [ Grpc_Testing_ResponseParameters.with { @@ -496,23 +468,25 @@ struct CustomMetadata: InteroperabilityTest { let receivedInitialMetadata = response.metadata try self.checkInitialMetadata(receivedInitialMetadata) - var responseParts = contents.bodyParts.makeAsyncIterator() - for _ in 1 ... 2 { - switch try await responseParts.next() { + let parts = try await contents.bodyParts.reduce(into: []) { $0.append($1) } + try assertEqual(parts.count, 2) + + for part in parts { + switch part { // Check the message. case .message(let message): try assertEqual(message.payload.body, Data(count: 314_159)) // Check the trailing metadata. case .trailingMetadata(let receivedTrailingMetadata): try self.checkTrailingMetadata(receivedTrailingMetadata) - default: - try assertFailure( - "The client should have received only a message and trailing metadata." - ) } } case .failure(_): - try assertFailure("The client should have received a response from the server.") + throw AssertionFailure( + message: "The client should have received a response from the server.", + file: #fileID, + line: #line + ) } } } @@ -565,33 +539,46 @@ struct StatusCodeAndMessage: InteroperabilityTest { } } - try await testServiceClient.unaryCall(request: ClientRequest.Single(message: message)) { - response in + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: message) + ) { response in switch response.accepted { case .failure(let error): try assertEqual(error.code.rawValue, self.expectedCode) try assertEqual(error.message, self.expectedMessage) case .success(_): - try assertFailure( - "The client should receive an error with the status code and message sent by the client." + throw AssertionFailure( + message: + "The client should receive an error with the status code and message sent by the client.", + file: #fileID, + line: #line ) } } let request = ClientRequest.Stream { writer in - let message = Grpc_Testing_StreamingOutputCallRequest() + let message = Grpc_Testing_StreamingOutputCallRequest.with { + $0.responseStatus = Grpc_Testing_EchoStatus.with { + $0.code = Int32(self.expectedCode) + $0.message = self.expectedMessage + } + } try await writer.write(message) } try await testServiceClient.fullDuplexCall(request: request) { response in - switch response.accepted { - case .failure(let error): + do { + for try await _ in response.messages { + throw AssertionFailure( + message: + "The client should receive an error with the status code and message sent by the client.", + file: #fileID, + line: #line + ) + } + } catch let error as RPCError { try assertEqual(error.code.rawValue, self.expectedCode) try assertEqual(error.message, self.expectedMessage) - case .success(_): - try assertFailure( - "The client should receive an error with the status code and message sent by the client." - ) } } } @@ -631,11 +618,16 @@ struct SpecialStatusMessage: InteroperabilityTest { $0.message = responseMessage } } - try await testServiceClient.unaryCall(request: ClientRequest.Single(message: message)) { - response in + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: message) + ) { response in switch response.accepted { case .success(_): - try assertFailure("The response should be an error with the error code 2.") + throw AssertionFailure( + message: "The response should be an error with the error code 2.", + file: #fileID, + line: #line + ) case .failure(let error): try assertEqual(error.code.rawValue, 2) try assertEqual(error.message, responseMessage) @@ -665,14 +657,17 @@ struct UnimplementedMethod: InteroperabilityTest { let testServiceClient = Grpc_Testing_TestService.Client(client: client) try await testServiceClient.unimplementedCall( request: ClientRequest.Single(message: Grpc_Testing_Empty()) - ) { - response in + ) { response in let result = response.accepted switch result { case .success(_): - try assertFailure("The result should be an error.") + throw AssertionFailure( + message: "The result should be an error.", + file: #fileID, + line: #line + ) case .failure(let error): - try assertEqual(error.code, RPCError.Code.unimplemented) + try assertEqual(error.code, .unimplemented) } } } @@ -698,14 +693,17 @@ struct UnimplementedService: InteroperabilityTest { let unimplementedServiceClient = Grpc_Testing_UnimplementedService.Client(client: client) try await unimplementedServiceClient.unimplementedCall( request: ClientRequest.Single(message: Grpc_Testing_Empty()) - ) { - response in + ) { response in let result = response.accepted switch result { case .success(_): - try assertFailure("The result should be an error.") + throw AssertionFailure( + message: "The result should be an error.", + file: #fileID, + line: #line + ) case .failure(let error): - try assertEqual(error.code, RPCError.Code.unimplemented) + try assertEqual(error.code, .unimplemented) } } } diff --git a/Sources/InteroperabilityTests/ServerFeature.swift b/Sources/InteroperabilityTests/ServerFeature.swift deleted file mode 100644 index a9b682acc..000000000 --- a/Sources/InteroperabilityTests/ServerFeature.swift +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2024, 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. - */ - -/// Server features which may be required for tests. -/// -/// We use this enum to match up tests we can run on the client against the server at -/// run time. -/// -/// These features are listed in the [gRPC interoperability test description -/// specification](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). -/// -/// Missing features: -/// - compressed response -/// - compressed request -/// - observe `ResponseParameter.interval_us` -/// - echo authenticated username -/// - echo authenticated OAuth scope -/// -/// - Note: This is not a complete set of features, only those used in either the client or server. -public enum ServerFeature { - /// See TestService.emptyCall. - case emptyCall - - /// See TestService.unaryCall. - case unaryCall - - /// See TestService.cacheableUnaryCall. - case cacheableUnaryCall - - /// See TestService.streamingInputCall. - case streamingInputCall - - /// See TestService.streamingOutputCall. - case streamingOutputCall - - /// See TestService.fullDuplexCall. - case fullDuplexCall - - /// When the client sends a `responseStatus` in the request payload, the server closes the stream - /// with the status code and message contained within said `responseStatus`. The server will not - /// process any further messages on the stream sent by the client. This can be used by clients to - /// verify correct handling of different status codes and associated status messages end-to-end. - case echoStatus - - /// When the client sends metadata with the key "x-grpc-test-echo-initial" with its request, - /// the server sends back exactly this key and the corresponding value back to the client as - /// part of initial metadata. When the client sends metadata with the key - /// "x-grpc-test-echo-trailing-bin" with its request, the server sends back exactly this key - /// and the corresponding value back to the client as trailing metadata. - case echoMetadata -} From d849bcd1e4b69dfd3f2243796b5f561d4ad23c6a Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Wed, 6 Mar 2024 17:16:36 +0000 Subject: [PATCH 11/11] added new init for AssertionFailure --- .../AssertionFailure.swift | 6 ++++ .../InteroperabilityTestCases.swift | 35 ++++++------------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/Sources/InteroperabilityTests/AssertionFailure.swift b/Sources/InteroperabilityTests/AssertionFailure.swift index 48665e30e..112a36ee3 100644 --- a/Sources/InteroperabilityTests/AssertionFailure.swift +++ b/Sources/InteroperabilityTests/AssertionFailure.swift @@ -21,6 +21,12 @@ public struct AssertionFailure: Error { public var message: String public var file: String public var line: Int + + public init(message: String, file: String = #fileID, line: Int = #line) { + self.message = message + self.file = file + self.line = line + } } /// Asserts that the value of an expression is `true`. diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift index 5d97a778a..cfebda759 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -207,9 +207,7 @@ struct ServerStreaming: InteroperabilityTest { try assertEqual(message.payload.body.count, responseSize) } else { throw AssertionFailure( - message: "There were less than four responses received.", - file: #fileID, - line: #line + message: "There were less than four responses received." ) } } @@ -282,10 +280,11 @@ struct PingPong: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) let ids = AsyncStream.makeStream(of: Int.self) + let request = ClientRequest.Stream { writer in + let sizes = [(31_415, 27_182), (9, 8), (2_653, 1_828), (58_979, 45_904)] for try await id in ids.stream { var message = Grpc_Testing_StreamingOutputCallRequest() - let sizes = [(31_415, 27_182), (9, 8), (2_653, 1_828), (58_979, 45_904)] switch id { case 1 ... 4: let (responseSize, bodySize) = sizes[id - 1] @@ -320,9 +319,7 @@ struct PingPong: InteroperabilityTest { try assertEqual(message.payload.body, Data(count: 58_979)) default: throw AssertionFailure( - message: "We should only receive messages with ids between 1 and 4.", - file: #fileID, - line: #line + message: "We should only receive messages with ids between 1 and 4." ) } @@ -483,9 +480,7 @@ struct CustomMetadata: InteroperabilityTest { } case .failure(_): throw AssertionFailure( - message: "The client should have received a response from the server.", - file: #fileID, - line: #line + message: "The client should have received a response from the server." ) } } @@ -549,9 +544,7 @@ struct StatusCodeAndMessage: InteroperabilityTest { case .success(_): throw AssertionFailure( message: - "The client should receive an error with the status code and message sent by the client.", - file: #fileID, - line: #line + "The client should receive an error with the status code and message sent by the client." ) } } @@ -571,9 +564,7 @@ struct StatusCodeAndMessage: InteroperabilityTest { for try await _ in response.messages { throw AssertionFailure( message: - "The client should receive an error with the status code and message sent by the client.", - file: #fileID, - line: #line + "The client should receive an error with the status code and message sent by the client." ) } } catch let error as RPCError { @@ -624,9 +615,7 @@ struct SpecialStatusMessage: InteroperabilityTest { switch response.accepted { case .success(_): throw AssertionFailure( - message: "The response should be an error with the error code 2.", - file: #fileID, - line: #line + message: "The response should be an error with the error code 2." ) case .failure(let error): try assertEqual(error.code.rawValue, 2) @@ -662,9 +651,7 @@ struct UnimplementedMethod: InteroperabilityTest { switch result { case .success(_): throw AssertionFailure( - message: "The result should be an error.", - file: #fileID, - line: #line + message: "The result should be an error." ) case .failure(let error): try assertEqual(error.code, .unimplemented) @@ -698,9 +685,7 @@ struct UnimplementedService: InteroperabilityTest { switch result { case .success(_): throw AssertionFailure( - message: "The result should be an error.", - file: #fileID, - line: #line + message: "The result should be an error." ) case .failure(let error): try assertEqual(error.code, .unimplemented)