/src/grpc-swift/Sources/GRPC/FakeChannel.swift
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright 2020, gRPC Authors All rights reserved. |
3 | | * |
4 | | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | | * you may not use this file except in compliance with the License. |
6 | | * You may obtain a copy of the License at |
7 | | * |
8 | | * http://www.apache.org/licenses/LICENSE-2.0 |
9 | | * |
10 | | * Unless required by applicable law or agreed to in writing, software |
11 | | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | | * See the License for the specific language governing permissions and |
14 | | * limitations under the License. |
15 | | */ |
16 | | import Logging |
17 | | import NIOCore |
18 | | import NIOEmbedded |
19 | | import SwiftProtobuf |
20 | | |
21 | | // This type is deprecated, but we need to '@unchecked Sendable' to avoid warnings in our own code. |
22 | | @available(swift, deprecated: 5.6) |
23 | | extension FakeChannel: @unchecked Sendable {} |
24 | | |
25 | | /// A fake channel for use with generated test clients. |
26 | | /// |
27 | | /// The `FakeChannel` provides factories for calls which avoid most of the gRPC stack and don't do |
28 | | /// real networking. Each call relies on either a `FakeUnaryResponse` or a `FakeStreamingResponse` |
29 | | /// to get responses or errors. The fake response of each type should be registered with the channel |
30 | | /// prior to making a call via `makeFakeUnaryResponse` or `makeFakeStreamingResponse` respectively. |
31 | | /// |
32 | | /// Users will typically not be required to interact with the channel directly, instead they should |
33 | | /// do so via a generated test client. |
34 | | @available( |
35 | | swift, |
36 | | deprecated: 5.6, |
37 | | message: |
38 | | "GRPCChannel implementations must be Sendable but this implementation is not. Using a client and server on localhost is the recommended alternative." |
39 | | ) |
40 | | public class FakeChannel: GRPCChannel { |
41 | | /// Fake response streams keyed by their path. |
42 | | private var responseStreams: [String: CircularBuffer<Any>] |
43 | | |
44 | | /// A logger. |
45 | | public let logger: Logger |
46 | | |
47 | | public init( |
48 | | logger: Logger = Logger( |
49 | | label: "io.grpc", |
50 | 0 | factory: { _ in |
51 | 0 | SwiftLogNoOpLogHandler() |
52 | 0 | } |
53 | | ) |
54 | 0 | ) { |
55 | 0 | self.responseStreams = [:] |
56 | 0 | self.logger = logger |
57 | 0 | } |
58 | | |
59 | | /// Make and store a fake unary response for the given path. Users should prefer making a response |
60 | | /// stream for their RPC directly via the appropriate method on their generated test client. |
61 | | public func makeFakeUnaryResponse<Request, Response>( |
62 | | path: String, |
63 | | requestHandler: @escaping (FakeRequestPart<Request>) -> Void |
64 | 0 | ) -> FakeUnaryResponse<Request, Response> { |
65 | 0 | let proxy = FakeUnaryResponse<Request, Response>(requestHandler: requestHandler) |
66 | 0 | self.responseStreams[path, default: []].append(proxy) |
67 | 0 | return proxy |
68 | 0 | } |
69 | | |
70 | | /// Make and store a fake streaming response for the given path. Users should prefer making a |
71 | | /// response stream for their RPC directly via the appropriate method on their generated test |
72 | | /// client. |
73 | | public func makeFakeStreamingResponse<Request, Response>( |
74 | | path: String, |
75 | | requestHandler: @escaping (FakeRequestPart<Request>) -> Void |
76 | 0 | ) -> FakeStreamingResponse<Request, Response> { |
77 | 0 | let proxy = FakeStreamingResponse<Request, Response>(requestHandler: requestHandler) |
78 | 0 | self.responseStreams[path, default: []].append(proxy) |
79 | 0 | return proxy |
80 | 0 | } |
81 | | |
82 | | /// Returns true if there are fake responses enqueued for the given path. |
83 | 0 | public func hasFakeResponseEnqueued(forPath path: String) -> Bool { |
84 | 0 | guard let noStreamsForPath = self.responseStreams[path]?.isEmpty else { |
85 | 0 | return false |
86 | 0 | } |
87 | 0 | return !noStreamsForPath |
88 | 0 | } |
89 | | |
90 | | public func makeCall<Request: Message, Response: Message>( |
91 | | path: String, |
92 | | type: GRPCCallType, |
93 | | callOptions: CallOptions, |
94 | | interceptors: [ClientInterceptor<Request, Response>] |
95 | 0 | ) -> Call<Request, Response> { |
96 | 0 | return self._makeCall( |
97 | 0 | path: path, |
98 | 0 | type: type, |
99 | 0 | callOptions: callOptions, |
100 | 0 | interceptors: interceptors |
101 | 0 | ) |
102 | 0 | } |
103 | | |
104 | | public func makeCall<Request: GRPCPayload, Response: GRPCPayload>( |
105 | | path: String, |
106 | | type: GRPCCallType, |
107 | | callOptions: CallOptions, |
108 | | interceptors: [ClientInterceptor<Request, Response>] |
109 | 0 | ) -> Call<Request, Response> { |
110 | 0 | return self._makeCall( |
111 | 0 | path: path, |
112 | 0 | type: type, |
113 | 0 | callOptions: callOptions, |
114 | 0 | interceptors: interceptors |
115 | 0 | ) |
116 | 0 | } |
117 | | |
118 | | private func _makeCall<Request: Message, Response: Message>( |
119 | | path: String, |
120 | | type: GRPCCallType, |
121 | | callOptions: CallOptions, |
122 | | interceptors: [ClientInterceptor<Request, Response>] |
123 | 0 | ) -> Call<Request, Response> { |
124 | 0 | let stream: _FakeResponseStream<Request, Response>? = self.dequeueResponseStream(forPath: path) |
125 | 0 | let eventLoop = stream?.channel.eventLoop ?? EmbeddedEventLoop() |
126 | 0 | return Call( |
127 | 0 | path: path, |
128 | 0 | type: type, |
129 | 0 | eventLoop: eventLoop, |
130 | 0 | options: callOptions, |
131 | 0 | interceptors: interceptors, |
132 | 0 | transportFactory: .fake(stream) |
133 | 0 | ) |
134 | 0 | } |
135 | | |
136 | | private func _makeCall<Request: GRPCPayload, Response: GRPCPayload>( |
137 | | path: String, |
138 | | type: GRPCCallType, |
139 | | callOptions: CallOptions, |
140 | | interceptors: [ClientInterceptor<Request, Response>] |
141 | 0 | ) -> Call<Request, Response> { |
142 | 0 | let stream: _FakeResponseStream<Request, Response>? = self.dequeueResponseStream(forPath: path) |
143 | 0 | let eventLoop = stream?.channel.eventLoop ?? EmbeddedEventLoop() |
144 | 0 | return Call( |
145 | 0 | path: path, |
146 | 0 | type: type, |
147 | 0 | eventLoop: eventLoop, |
148 | 0 | options: callOptions, |
149 | 0 | interceptors: interceptors, |
150 | 0 | transportFactory: .fake(stream) |
151 | 0 | ) |
152 | 0 | } |
153 | | |
154 | 0 | public func close() -> EventLoopFuture<Void> { |
155 | 0 | // We don't have anything to close. |
156 | 0 | return EmbeddedEventLoop().makeSucceededFuture(()) |
157 | 0 | } |
158 | | } |
159 | | |
160 | | @available(swift, deprecated: 5.6) |
161 | | extension FakeChannel { |
162 | | /// Dequeue a proxy for the given path and casts it to the given type, if one exists. |
163 | | private func dequeueResponseStream<Stream>( |
164 | | forPath path: String, |
165 | | as: Stream.Type = Stream.self |
166 | 0 | ) -> Stream? { |
167 | 0 | guard var streams = self.responseStreams[path], !streams.isEmpty else { |
168 | 0 | return nil |
169 | 0 | } |
170 | 0 |
|
171 | 0 | // This is fine: we know we're non-empty. |
172 | 0 | let first = streams.removeFirst() |
173 | 0 | self.responseStreams.updateValue(streams, forKey: path) |
174 | 0 |
|
175 | 0 | return first as? Stream |
176 | 0 | } |
177 | | |
178 | 0 | private func makeRequestHead(path: String, callOptions: CallOptions) -> _GRPCRequestHead { |
179 | 0 | return _GRPCRequestHead( |
180 | 0 | scheme: "http", |
181 | 0 | path: path, |
182 | 0 | host: "localhost", |
183 | 0 | options: callOptions, |
184 | 0 | requestID: nil |
185 | 0 | ) |
186 | 0 | } |
187 | | } |