MethodConfigCodingTests.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. /*
  2. * Copyright 2024, 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 Foundation
  17. import SwiftProtobuf
  18. import Testing
  19. @testable import GRPCCore
  20. @Suite("MethodConfig coding tests")
  21. struct MethodConfigCodingTests {
  22. @Suite("Encoding")
  23. struct Encoding {
  24. private func encodeToJSON(_ value: some Encodable) throws -> String {
  25. let encoder = JSONEncoder()
  26. encoder.outputFormatting = .sortedKeys
  27. let encoded = try encoder.encode(value)
  28. let json = String(decoding: encoded, as: UTF8.self)
  29. return json
  30. }
  31. @Test(
  32. "Name",
  33. arguments: [
  34. (
  35. MethodConfig.Name(service: "foo.bar", method: "baz"),
  36. #"{"method":"baz","service":"foo.bar"}"#
  37. ),
  38. (MethodConfig.Name(service: "foo.bar", method: ""), #"{"method":"","service":"foo.bar"}"#),
  39. (MethodConfig.Name(service: "", method: ""), #"{"method":"","service":""}"#),
  40. ] as [(MethodConfig.Name, String)]
  41. )
  42. @available(gRPCSwift 2.0, *)
  43. func methodConfigName(name: MethodConfig.Name, expected: String) throws {
  44. let json = try self.encodeToJSON(name)
  45. #expect(json == expected)
  46. }
  47. @Test(
  48. "GoogleProtobufDuration",
  49. arguments: [
  50. (.seconds(1), #""1.0s""#),
  51. (.zero, #""0.0s""#),
  52. (.milliseconds(100_123), #""100.123s""#),
  53. ] as [(Duration, String)]
  54. )
  55. @available(gRPCSwift 2.0, *)
  56. func protobufDuration(duration: Duration, expected: String) throws {
  57. let json = try self.encodeToJSON(GoogleProtobufDuration(duration: duration))
  58. #expect(json == expected)
  59. }
  60. @Test(
  61. "GoogleRPCCode",
  62. arguments: [
  63. (.ok, #""OK""#),
  64. (.cancelled, #""CANCELLED""#),
  65. (.unknown, #""UNKNOWN""#),
  66. (.invalidArgument, #""INVALID_ARGUMENT""#),
  67. (.deadlineExceeded, #""DEADLINE_EXCEEDED""#),
  68. (.notFound, #""NOT_FOUND""#),
  69. (.alreadyExists, #""ALREADY_EXISTS""#),
  70. (.permissionDenied, #""PERMISSION_DENIED""#),
  71. (.resourceExhausted, #""RESOURCE_EXHAUSTED""#),
  72. (.failedPrecondition, #""FAILED_PRECONDITION""#),
  73. (.aborted, #""ABORTED""#),
  74. (.outOfRange, #""OUT_OF_RANGE""#),
  75. (.unimplemented, #""UNIMPLEMENTED""#),
  76. (.internalError, #""INTERNAL""#),
  77. (.unavailable, #""UNAVAILABLE""#),
  78. (.dataLoss, #""DATA_LOSS""#),
  79. (.unauthenticated, #""UNAUTHENTICATED""#),
  80. ] as [(Status.Code, String)]
  81. )
  82. @available(gRPCSwift 2.0, *)
  83. func rpcCode(code: Status.Code, expected: String) throws {
  84. let json = try self.encodeToJSON(GoogleRPCCode(code: code))
  85. #expect(json == expected)
  86. }
  87. @Test("RetryPolicy")
  88. @available(gRPCSwift 2.0, *)
  89. func retryPolicy() throws {
  90. let policy = RetryPolicy(
  91. maxAttempts: 3,
  92. initialBackoff: .seconds(1),
  93. maxBackoff: .seconds(3),
  94. backoffMultiplier: 1.6,
  95. retryableStatusCodes: [.aborted]
  96. )
  97. let json = try self.encodeToJSON(policy)
  98. let expected =
  99. #"{"backoffMultiplier":1.6,"initialBackoff":"1.0s","maxAttempts":3,"maxBackoff":"3.0s","retryableStatusCodes":["ABORTED"]}"#
  100. #expect(json == expected)
  101. }
  102. @Test("HedgingPolicy")
  103. @available(gRPCSwift 2.0, *)
  104. func hedgingPolicy() throws {
  105. let policy = HedgingPolicy(
  106. maxAttempts: 3,
  107. hedgingDelay: .seconds(1),
  108. nonFatalStatusCodes: [.aborted]
  109. )
  110. let json = try self.encodeToJSON(policy)
  111. let expected = #"{"hedgingDelay":"1.0s","maxAttempts":3,"nonFatalStatusCodes":["ABORTED"]}"#
  112. #expect(json == expected)
  113. }
  114. }
  115. @Suite("Decoding")
  116. struct Decoding {
  117. private func decodeFromFile<Decoded: Decodable>(
  118. _ name: String,
  119. as: Decoded.Type
  120. ) throws -> Decoded {
  121. let input = Bundle.module.url(
  122. forResource: name,
  123. withExtension: "json",
  124. subdirectory: "Inputs"
  125. )
  126. let url = try #require(input)
  127. let data = try Data(contentsOf: url)
  128. let decoder = JSONDecoder()
  129. return try decoder.decode(Decoded.self, from: data)
  130. }
  131. private func decodeFromJSONString<Decoded: Decodable>(
  132. _ json: String,
  133. as: Decoded.Type
  134. ) throws -> Decoded {
  135. let data = Data(json.utf8)
  136. let decoder = JSONDecoder()
  137. return try decoder.decode(Decoded.self, from: data)
  138. }
  139. private static let codeNames: [String] = [
  140. "OK",
  141. "CANCELLED",
  142. "UNKNOWN",
  143. "INVALID_ARGUMENT",
  144. "DEADLINE_EXCEEDED",
  145. "NOT_FOUND",
  146. "ALREADY_EXISTS",
  147. "PERMISSION_DENIED",
  148. "RESOURCE_EXHAUSTED",
  149. "FAILED_PRECONDITION",
  150. "ABORTED",
  151. "OUT_OF_RANGE",
  152. "UNIMPLEMENTED",
  153. "INTERNAL",
  154. "UNAVAILABLE",
  155. "DATA_LOSS",
  156. "UNAUTHENTICATED",
  157. ]
  158. @Test(
  159. "Name",
  160. arguments: [
  161. ("method_config.name.full", MethodConfig.Name(service: "foo.bar", method: "baz")),
  162. ("method_config.name.service_only", MethodConfig.Name(service: "foo.bar", method: "")),
  163. ("method_config.name.empty", MethodConfig.Name(service: "", method: "")),
  164. ] as [(String, MethodConfig.Name)]
  165. )
  166. @available(gRPCSwift 2.0, *)
  167. func name(_ fileName: String, expected: MethodConfig.Name) throws {
  168. let decoded = try self.decodeFromFile(fileName, as: MethodConfig.Name.self)
  169. #expect(decoded == expected)
  170. }
  171. @Test(
  172. "GoogleProtobufDuration",
  173. arguments: [
  174. ("1.0s", .seconds(1)),
  175. ("1s", .seconds(1)),
  176. ("1.000000s", .seconds(1)),
  177. ("0s", .zero),
  178. ("0.1s", .milliseconds(100)),
  179. ("100.123s", .milliseconds(100_123)),
  180. ] as [(String, Duration)]
  181. )
  182. @available(gRPCSwift 2.0, *)
  183. func googleProtobufDuration(duration: String, expectedDuration: Duration) throws {
  184. let json = "\"\(duration)\""
  185. let decoded = try self.decodeFromJSONString(json, as: GoogleProtobufDuration.self)
  186. // Conversion is lossy as we go from floating point seconds to integer seconds and
  187. // attoseconds. Allow for millisecond precision.
  188. let divisor: Int64 = 1_000_000_000_000_000
  189. let duration = decoded.duration.components
  190. let expected = expectedDuration.components
  191. #expect(duration.seconds == expected.seconds)
  192. #expect(duration.attoseconds / divisor == expected.attoseconds / divisor)
  193. }
  194. @Test("Invalid GoogleProtobufDuration", arguments: ["1", "1ss", "1S", "1.0S"])
  195. @available(gRPCSwift 2.0, *)
  196. func googleProtobufDuration(invalidDuration: String) throws {
  197. let json = "\"\(invalidDuration)\""
  198. #expect {
  199. try self.decodeFromJSONString(json, as: GoogleProtobufDuration.self)
  200. } throws: { error in
  201. guard let error = error as? RuntimeError else { return false }
  202. return error.code == .invalidArgument
  203. }
  204. }
  205. @Test("GoogleRPCCode from case name", arguments: zip(Self.codeNames, Status.Code.all))
  206. @available(gRPCSwift 2.0, *)
  207. func rpcCode(name: String, expected: Status.Code) throws {
  208. let json = "\"\(name)\""
  209. let decoded = try self.decodeFromJSONString(json, as: GoogleRPCCode.self)
  210. #expect(decoded.code == expected)
  211. }
  212. @Test("GoogleRPCCode from rawValue", arguments: zip(0 ... 16, Status.Code.all))
  213. @available(gRPCSwift 2.0, *)
  214. func rpcCode(rawValue: Int, expected: Status.Code) throws {
  215. let json = "\(rawValue)"
  216. let decoded = try self.decodeFromJSONString(json, as: GoogleRPCCode.self)
  217. #expect(decoded.code == expected)
  218. }
  219. @Test("RetryPolicy")
  220. @available(gRPCSwift 2.0, *)
  221. func retryPolicy() throws {
  222. let decoded = try self.decodeFromFile("method_config.retry_policy", as: RetryPolicy.self)
  223. let expected = RetryPolicy(
  224. maxAttempts: 3,
  225. initialBackoff: .seconds(1),
  226. maxBackoff: .seconds(3),
  227. backoffMultiplier: 1.6,
  228. retryableStatusCodes: [.aborted, .unavailable]
  229. )
  230. #expect(decoded == expected)
  231. }
  232. @Test(
  233. "RetryPolicy with invalid values",
  234. arguments: [
  235. "method_config.retry_policy.invalid.backoff_multiplier",
  236. "method_config.retry_policy.invalid.initial_backoff",
  237. "method_config.retry_policy.invalid.max_backoff",
  238. "method_config.retry_policy.invalid.max_attempts",
  239. "method_config.retry_policy.invalid.retryable_status_codes",
  240. ]
  241. )
  242. @available(gRPCSwift 2.0, *)
  243. func invalidRetryPolicy(fileName: String) throws {
  244. #expect(throws: RuntimeError.self) {
  245. try self.decodeFromFile(fileName, as: RetryPolicy.self)
  246. }
  247. }
  248. @Test("HedgingPolicy")
  249. @available(gRPCSwift 2.0, *)
  250. func hedgingPolicy() throws {
  251. let decoded = try self.decodeFromFile("method_config.hedging_policy", as: HedgingPolicy.self)
  252. let expected = HedgingPolicy(
  253. maxAttempts: 3,
  254. hedgingDelay: .seconds(1),
  255. nonFatalStatusCodes: [.aborted]
  256. )
  257. #expect(decoded == expected)
  258. }
  259. @Test(
  260. "HedgingPolicy with invalid values",
  261. arguments: [
  262. "method_config.hedging_policy.invalid.max_attempts"
  263. ]
  264. )
  265. @available(gRPCSwift 2.0, *)
  266. func invalidHedgingPolicy(fileName: String) throws {
  267. #expect(throws: RuntimeError.self) {
  268. try self.decodeFromFile(fileName, as: HedgingPolicy.self)
  269. }
  270. }
  271. @Test("MethodConfig")
  272. @available(gRPCSwift 2.0, *)
  273. func methodConfig() throws {
  274. let expected = MethodConfig(
  275. names: [
  276. MethodConfig.Name(
  277. service: "echo.Echo",
  278. method: "Get"
  279. )
  280. ],
  281. waitForReady: true,
  282. timeout: .seconds(1),
  283. maxRequestMessageBytes: 1024,
  284. maxResponseMessageBytes: 2048
  285. )
  286. let decoded = try self.decodeFromFile("method_config", as: MethodConfig.self)
  287. #expect(decoded == expected)
  288. }
  289. @Test("MethodConfig with hedging")
  290. @available(gRPCSwift 2.0, *)
  291. func methodConfigWithHedging() throws {
  292. let expected = MethodConfig(
  293. names: [
  294. MethodConfig.Name(
  295. service: "echo.Echo",
  296. method: "Get"
  297. )
  298. ],
  299. waitForReady: true,
  300. timeout: .seconds(1),
  301. maxRequestMessageBytes: 1024,
  302. maxResponseMessageBytes: 2048,
  303. executionPolicy: .hedge(
  304. HedgingPolicy(
  305. maxAttempts: 3,
  306. hedgingDelay: .seconds(42),
  307. nonFatalStatusCodes: [.aborted, .unimplemented]
  308. )
  309. )
  310. )
  311. let decoded = try self.decodeFromFile("method_config.with_hedging", as: MethodConfig.self)
  312. #expect(decoded == expected)
  313. }
  314. @Test("MethodConfig with retries")
  315. @available(gRPCSwift 2.0, *)
  316. func methodConfigWithRetries() throws {
  317. let expected = MethodConfig(
  318. names: [
  319. MethodConfig.Name(
  320. service: "echo.Echo",
  321. method: "Get"
  322. )
  323. ],
  324. waitForReady: true,
  325. timeout: .seconds(1),
  326. maxRequestMessageBytes: 1024,
  327. maxResponseMessageBytes: 2048,
  328. executionPolicy: .retry(
  329. RetryPolicy(
  330. maxAttempts: 3,
  331. initialBackoff: .seconds(1),
  332. maxBackoff: .seconds(3),
  333. backoffMultiplier: 1.6,
  334. retryableStatusCodes: [.aborted, .unimplemented]
  335. )
  336. )
  337. )
  338. let decoded = try self.decodeFromFile("method_config.with_retries", as: MethodConfig.self)
  339. #expect(decoded == expected)
  340. }
  341. }
  342. @Suite("Round-trip tests")
  343. struct RoundTrip {
  344. private func decodeFromFile<Decoded: Decodable>(
  345. _ name: String,
  346. as: Decoded.Type
  347. ) throws -> Decoded {
  348. let input = Bundle.module.url(
  349. forResource: name,
  350. withExtension: "json",
  351. subdirectory: "Inputs"
  352. )
  353. let url = try #require(input)
  354. let data = try Data(contentsOf: url)
  355. let decoder = JSONDecoder()
  356. return try decoder.decode(Decoded.self, from: data)
  357. }
  358. private func decodeFromJSONString<Decoded: Decodable>(
  359. _ json: String,
  360. as: Decoded.Type
  361. ) throws -> Decoded {
  362. let data = Data(json.utf8)
  363. let decoder = JSONDecoder()
  364. return try decoder.decode(Decoded.self, from: data)
  365. }
  366. private func encodeToJSON(_ value: some Encodable) throws -> String {
  367. let encoder = JSONEncoder()
  368. let encoded = try encoder.encode(value)
  369. let json = String(decoding: encoded, as: UTF8.self)
  370. return json
  371. }
  372. private func roundTrip<T: Codable & Equatable>(type: T.Type = T.self, fileName: String) throws {
  373. let decoded = try self.decodeFromFile(fileName, as: T.self)
  374. let encoded = try self.encodeToJSON(decoded)
  375. let decodedAgain = try self.decodeFromJSONString(encoded, as: T.self)
  376. #expect(decoded == decodedAgain)
  377. }
  378. @Test(
  379. "MethodConfig",
  380. arguments: [
  381. "method_config",
  382. "method_config.with_retries",
  383. "method_config.with_hedging",
  384. ]
  385. )
  386. @available(gRPCSwift 2.0, *)
  387. func roundTripCodingAndDecoding(fileName: String) throws {
  388. try self.roundTrip(type: MethodConfig.self, fileName: fileName)
  389. }
  390. }
  391. }