CompressionTests.swift 12 KB

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