CompressionTests.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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 EchoImplementation
  17. import EchoModel
  18. import GRPC
  19. import NIO
  20. import NIOHPACK
  21. import XCTest
  22. class MessageCompressionTests: GRPCTestCase {
  23. var group: EventLoopGroup!
  24. var server: Server!
  25. var client: ClientConnection!
  26. var defaultTimeout: TimeInterval = 1.0
  27. var echo: Echo_EchoClient!
  28. override func setUp() {
  29. super.setUp()
  30. self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
  31. }
  32. override func tearDown() {
  33. XCTAssertNoThrow(try self.client.close().wait())
  34. XCTAssertNoThrow(try self.server.close().wait())
  35. XCTAssertNoThrow(try self.group.syncShutdownGracefully())
  36. super.tearDown()
  37. }
  38. func setupServer(encoding: ServerMessageEncoding) throws {
  39. self.server = try Server.insecure(group: self.group)
  40. .withServiceProviders([EchoProvider()])
  41. .withMessageCompression(encoding)
  42. .withLogger(self.serverLogger)
  43. .bind(host: "localhost", port: 0)
  44. .wait()
  45. }
  46. func setupClient(encoding: ClientMessageEncoding) {
  47. self.client = ClientConnection.insecure(group: self.group)
  48. .withBackgroundActivityLogger(self.clientLogger)
  49. .connect(host: "localhost", port: self.server.channel.localAddress!.port!)
  50. self.echo = Echo_EchoClient(
  51. channel: self.client,
  52. defaultCallOptions: CallOptions(messageEncoding: encoding, logger: self.clientLogger)
  53. )
  54. }
  55. func testCompressedRequestsUncompressedResponses() throws {
  56. // Enable compression, but don't advertise that it's enabled.
  57. // The spec says that servers should handle compression they support but don't advertise.
  58. try self
  59. .setupServer(encoding: .enabled(.init(enabledAlgorithms: [], decompressionLimit: .ratio(10))))
  60. self.setupClient(encoding: .enabled(.init(
  61. forRequests: .gzip,
  62. acceptableForResponses: [.deflate, .gzip],
  63. decompressionLimit: .ratio(10)
  64. )))
  65. let get = self.echo.get(.with { $0.text = "foo" })
  66. let initialMetadata = self.expectation(description: "received initial metadata")
  67. get.initialMetadata.map {
  68. $0.contains(name: "grpc-encoding")
  69. }.assertEqual(false, fulfill: initialMetadata)
  70. let status = self.expectation(description: "received status")
  71. get.status.map {
  72. $0.code
  73. }.assertEqual(.ok, fulfill: status)
  74. self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout)
  75. }
  76. func testUncompressedRequestsCompressedResponses() throws {
  77. try self.setupServer(encoding: .enabled(.init(decompressionLimit: .ratio(10))))
  78. self.setupClient(encoding: .enabled(.init(
  79. forRequests: .none,
  80. acceptableForResponses: [.deflate, .gzip],
  81. decompressionLimit: .ratio(10)
  82. )))
  83. let get = self.echo.get(.with { $0.text = "foo" })
  84. let initialMetadata = self.expectation(description: "received initial metadata")
  85. get.initialMetadata.map {
  86. $0.first(name: "grpc-encoding")
  87. }.assertEqual("deflate", fulfill: initialMetadata)
  88. let status = self.expectation(description: "received status")
  89. get.status.map {
  90. $0.code
  91. }.assertEqual(.ok, fulfill: status)
  92. self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout)
  93. }
  94. func testServerCanDecompressNonAdvertisedButSupportedCompression() throws {
  95. // Server should be able to decompress a format it supports but does not advertise. In doing
  96. // so it must also return a "grpc-accept-encoding" header which includes the value it did not
  97. // advertise.
  98. try self
  99. .setupServer(encoding: .enabled(.init(
  100. enabledAlgorithms: [.gzip],
  101. decompressionLimit: .ratio(10)
  102. )))
  103. self
  104. .setupClient(encoding: .enabled(.init(
  105. forRequests: .deflate,
  106. acceptableForResponses: [],
  107. decompressionLimit: .ratio(10)
  108. )))
  109. let get = self.echo.get(.with { $0.text = "foo" })
  110. let initialMetadata = self.expectation(description: "received initial metadata")
  111. get.initialMetadata.map {
  112. $0[canonicalForm: "grpc-accept-encoding"]
  113. }.assertEqual(["gzip", "deflate"], fulfill: initialMetadata)
  114. let status = self.expectation(description: "received status")
  115. get.status.map {
  116. $0.code
  117. }.assertEqual(.ok, fulfill: status)
  118. self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout)
  119. }
  120. func testServerCompressesResponseWithDifferentAlgorithmToRequest() throws {
  121. // Server should be able to compress responses with a different method to the client, providing
  122. // the client supports it.
  123. try self
  124. .setupServer(encoding: .enabled(.init(
  125. enabledAlgorithms: [.gzip],
  126. decompressionLimit: .ratio(10)
  127. )))
  128. self.setupClient(encoding: .enabled(.init(
  129. forRequests: .deflate,
  130. acceptableForResponses: [.deflate, .gzip],
  131. decompressionLimit: .ratio(10)
  132. )))
  133. let get = self.echo.get(.with { $0.text = "foo" })
  134. let initialMetadata = self.expectation(description: "received initial metadata")
  135. get.initialMetadata.map {
  136. $0.first(name: "grpc-encoding")
  137. }.assertEqual("gzip", fulfill: initialMetadata)
  138. let status = self.expectation(description: "received status")
  139. get.status.map {
  140. $0.code
  141. }.assertEqual(.ok, fulfill: status)
  142. self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout)
  143. }
  144. func testCompressedRequestWithCompressionNotSupportedOnServer() throws {
  145. try self
  146. .setupServer(encoding: .enabled(.init(
  147. enabledAlgorithms: [.gzip, .deflate],
  148. decompressionLimit: .ratio(10)
  149. )))
  150. // We can't specify a compression we don't support, so we'll specify no compression and then
  151. // send a 'grpc-encoding' with our initial metadata.
  152. self.setupClient(encoding: .enabled(.init(
  153. forRequests: .none,
  154. acceptableForResponses: [.deflate, .gzip],
  155. decompressionLimit: .ratio(10)
  156. )))
  157. let headers: HPACKHeaders = ["grpc-encoding": "you-don't-support-this"]
  158. let get = self.echo.get(
  159. .with { $0.text = "foo" },
  160. callOptions: CallOptions(customMetadata: headers)
  161. )
  162. let response = self.expectation(description: "received response")
  163. get.response.assertError(fulfill: response)
  164. let trailers = self.expectation(description: "received trailing metadata")
  165. get.trailingMetadata.map {
  166. $0[canonicalForm: "grpc-accept-encoding"]
  167. }.assertEqual(["gzip", "deflate"], fulfill: trailers)
  168. let status = self.expectation(description: "received status")
  169. get.status.map {
  170. $0.code
  171. }.assertEqual(.unimplemented, fulfill: status)
  172. self.wait(for: [response, trailers, status], timeout: self.defaultTimeout)
  173. }
  174. func testDecompressionLimitIsRespectedByServerForUnaryCall() throws {
  175. try self.setupServer(encoding: .enabled(.init(decompressionLimit: .absolute(1))))
  176. self
  177. .setupClient(encoding: .enabled(.init(
  178. forRequests: .gzip,
  179. decompressionLimit: .absolute(1024)
  180. )))
  181. let get = self.echo.get(.with { $0.text = "foo" })
  182. let status = self.expectation(description: "received status")
  183. get.status.map {
  184. $0.code
  185. }.assertEqual(.resourceExhausted, fulfill: status)
  186. self.wait(for: [status], timeout: self.defaultTimeout)
  187. }
  188. func testDecompressionLimitIsRespectedByServerForStreamingCall() throws {
  189. try self.setupServer(encoding: .enabled(.init(decompressionLimit: .absolute(1024))))
  190. self
  191. .setupClient(encoding: .enabled(.init(
  192. forRequests: .gzip,
  193. decompressionLimit: .absolute(2048)
  194. )))
  195. let collect = self.echo.collect()
  196. let status = self.expectation(description: "received status")
  197. // Smaller than limit.
  198. collect.sendMessage(.with { $0.text = "foo" }, promise: nil)
  199. // Should be just over the limit.
  200. collect.sendMessage(.with { $0.text = String(repeating: "x", count: 1024) }, promise: nil)
  201. collect.sendEnd(promise: nil)
  202. collect.status.map {
  203. $0.code
  204. }.assertEqual(.resourceExhausted, fulfill: status)
  205. self.wait(for: [status], timeout: self.defaultTimeout)
  206. }
  207. func testDecompressionLimitIsRespectedByClientForUnaryCall() throws {
  208. try self
  209. .setupServer(encoding: .enabled(.init(
  210. enabledAlgorithms: [.gzip],
  211. decompressionLimit: .absolute(1024)
  212. )))
  213. self.setupClient(encoding: .enabled(.responsesOnly(decompressionLimit: .absolute(1))))
  214. let get = self.echo.get(.with { $0.text = "foo" })
  215. let status = self.expectation(description: "received status")
  216. get.status.map {
  217. $0.code
  218. }.assertEqual(.resourceExhausted, fulfill: status)
  219. self.wait(for: [status], timeout: self.defaultTimeout)
  220. }
  221. func testDecompressionLimitIsRespectedByClientForStreamingCall() throws {
  222. try self.setupServer(encoding: .enabled(.init(decompressionLimit: .absolute(2048))))
  223. self
  224. .setupClient(encoding: .enabled(.init(
  225. forRequests: .gzip,
  226. decompressionLimit: .absolute(1024)
  227. )))
  228. var responses: [Echo_EchoResponse] = []
  229. let update = self.echo.update {
  230. responses.append($0)
  231. }
  232. let status = self.expectation(description: "received status")
  233. // Smaller than limit.
  234. update.sendMessage(.with { $0.text = "foo" }, promise: nil)
  235. // Should be just over the limit.
  236. update.sendMessage(.with { $0.text = String(repeating: "x", count: 1024) }, promise: nil)
  237. update.sendEnd(promise: nil)
  238. update.status.map {
  239. $0.code
  240. }.assertEqual(.resourceExhausted, fulfill: status)
  241. self.wait(for: [status], timeout: self.defaultTimeout)
  242. XCTAssertEqual(responses.count, 1)
  243. }
  244. func testIdentityCompressionIsntCompression() throws {
  245. // The client offers "identity" compression, the server doesn't support compression. We should
  246. // tolerate this, as "identity" is no compression at all.
  247. try self
  248. .setupServer(encoding: .disabled)
  249. // We can't specify a compression we don't support, like identity, so we'll specify no compression and then
  250. // send a 'grpc-encoding' with our initial metadata.
  251. self.setupClient(encoding: .disabled)
  252. let headers: HPACKHeaders = ["grpc-encoding": "identity"]
  253. let get = self.echo.get(
  254. .with { $0.text = "foo" },
  255. callOptions: CallOptions(customMetadata: headers)
  256. )
  257. let initialMetadata = self.expectation(description: "received initial metadata")
  258. get.initialMetadata.map {
  259. $0.contains(name: "grpc-encoding")
  260. }.assertEqual(false, fulfill: initialMetadata)
  261. let status = self.expectation(description: "received status")
  262. get.status.map {
  263. $0.code
  264. }.assertEqual(.ok, fulfill: status)
  265. self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout)
  266. }
  267. func testCompressedRequestWithDisabledServerCompressionAndUnknownCompressionAlgorithm() throws {
  268. try self.setupServer(encoding: .disabled)
  269. // We can't specify a compression we don't support, so we'll specify no compression and then
  270. // send a 'grpc-encoding' with our initial metadata.
  271. self.setupClient(encoding: .enabled(.init(
  272. forRequests: .none,
  273. acceptableForResponses: [.deflate, .gzip],
  274. decompressionLimit: .ratio(10)
  275. )))
  276. let headers: HPACKHeaders = ["grpc-encoding": "you-don't-support-this"]
  277. let get = self.echo.get(
  278. .with { $0.text = "foo" },
  279. callOptions: CallOptions(customMetadata: headers)
  280. )
  281. let response = self.expectation(description: "received response")
  282. get.response.assertError(fulfill: response)
  283. let trailers = self.expectation(description: "received trailing metadata")
  284. get.trailingMetadata.map {
  285. $0.contains(name: "grpc-accept-encoding")
  286. }.assertEqual(false, fulfill: trailers)
  287. let status = self.expectation(description: "received status")
  288. get.status.map {
  289. $0.code
  290. }.assertEqual(.unimplemented, fulfill: status)
  291. self.wait(for: [response, trailers, status], timeout: self.defaultTimeout)
  292. }
  293. }