MethodConfigCodingTests.swift 13 KB

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