Coverage Report

Created: 2025-06-24 06:59

/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
}