MethodConfigCodingTests.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  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. maximumAttempts: 3,
  88. initialBackoff: .seconds(1),
  89. maximumBackoff: .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. maximumAttempts: 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. ("100.123s", .milliseconds(100_123)),
  173. ] as [(String, Duration)]
  174. )
  175. func googleProtobufDuration(duration: String, expectedDuration: Duration) throws {
  176. let json = "\"\(duration)\""
  177. let decoded = try self.decodeFromJSONString(json, as: GoogleProtobufDuration.self)
  178. // Conversion is lossy as we go from floating point seconds to integer seconds and
  179. // attoseconds. Allow for millisecond precision.
  180. let divisor: Int64 = 1_000_000_000_000_000
  181. let duration = decoded.duration.components
  182. let expected = expectedDuration.components
  183. #expect(duration.seconds == expected.seconds)
  184. #expect(duration.attoseconds / divisor == expected.attoseconds / divisor)
  185. }
  186. @Test("Invalid GoogleProtobufDuration", arguments: ["1", "1ss", "1S", "1.0S"])
  187. func googleProtobufDuration(invalidDuration: String) throws {
  188. let json = "\"\(invalidDuration)\""
  189. #expect {
  190. try self.decodeFromJSONString(json, as: GoogleProtobufDuration.self)
  191. } throws: { error in
  192. guard let error = error as? RuntimeError else { return false }
  193. return error.code == .invalidArgument
  194. }
  195. }
  196. @Test("GoogleRPCCode from case name", arguments: zip(Self.codeNames, Status.Code.all))
  197. func rpcCode(name: String, expected: Status.Code) throws {
  198. let json = "\"\(name)\""
  199. let decoded = try self.decodeFromJSONString(json, as: GoogleRPCCode.self)
  200. #expect(decoded.code == expected)
  201. }
  202. @Test("GoogleRPCCode from rawValue", arguments: zip(0 ... 16, Status.Code.all))
  203. func rpcCode(rawValue: Int, expected: Status.Code) throws {
  204. let json = "\(rawValue)"
  205. let decoded = try self.decodeFromJSONString(json, as: GoogleRPCCode.self)
  206. #expect(decoded.code == expected)
  207. }
  208. @Test("RetryPolicy")
  209. func retryPolicy() throws {
  210. let decoded = try self.decodeFromFile("method_config.retry_policy", as: RetryPolicy.self)
  211. let expected = RetryPolicy(
  212. maximumAttempts: 3,
  213. initialBackoff: .seconds(1),
  214. maximumBackoff: .seconds(3),
  215. backoffMultiplier: 1.6,
  216. retryableStatusCodes: [.aborted, .unavailable]
  217. )
  218. #expect(decoded == expected)
  219. }
  220. @Test(
  221. "RetryPolicy with invalid values",
  222. arguments: [
  223. "method_config.retry_policy.invalid.backoff_multiplier",
  224. "method_config.retry_policy.invalid.initial_backoff",
  225. "method_config.retry_policy.invalid.max_backoff",
  226. "method_config.retry_policy.invalid.max_attempts",
  227. "method_config.retry_policy.invalid.retryable_status_codes",
  228. ]
  229. )
  230. func invalidRetryPolicy(fileName: String) throws {
  231. #expect(throws: RuntimeError.self) {
  232. try self.decodeFromFile(fileName, as: RetryPolicy.self)
  233. }
  234. }
  235. @Test("HedgingPolicy")
  236. func hedgingPolicy() throws {
  237. let decoded = try self.decodeFromFile("method_config.hedging_policy", as: HedgingPolicy.self)
  238. let expected = HedgingPolicy(
  239. maximumAttempts: 3,
  240. hedgingDelay: .seconds(1),
  241. nonFatalStatusCodes: [.aborted]
  242. )
  243. #expect(decoded == expected)
  244. }
  245. @Test(
  246. "HedgingPolicy with invalid values",
  247. arguments: [
  248. "method_config.hedging_policy.invalid.max_attempts"
  249. ]
  250. )
  251. func invalidHedgingPolicy(fileName: String) throws {
  252. #expect(throws: RuntimeError.self) {
  253. try self.decodeFromFile(fileName, as: HedgingPolicy.self)
  254. }
  255. }
  256. @Test("MethodConfig")
  257. func methodConfig() throws {
  258. let expected = MethodConfig(
  259. names: [
  260. MethodConfig.Name(
  261. service: "echo.Echo",
  262. method: "Get"
  263. )
  264. ],
  265. waitForReady: true,
  266. timeout: .seconds(1),
  267. maxRequestMessageBytes: 1024,
  268. maxResponseMessageBytes: 2048
  269. )
  270. let decoded = try self.decodeFromFile("method_config", as: MethodConfig.self)
  271. #expect(decoded == expected)
  272. }
  273. @Test("MethodConfig with hedging")
  274. func methodConfigWithHedging() throws {
  275. let expected = MethodConfig(
  276. names: [
  277. MethodConfig.Name(
  278. service: "echo.Echo",
  279. method: "Get"
  280. )
  281. ],
  282. waitForReady: true,
  283. timeout: .seconds(1),
  284. maxRequestMessageBytes: 1024,
  285. maxResponseMessageBytes: 2048,
  286. executionPolicy: .hedge(
  287. HedgingPolicy(
  288. maximumAttempts: 3,
  289. hedgingDelay: .seconds(42),
  290. nonFatalStatusCodes: [.aborted, .unimplemented]
  291. )
  292. )
  293. )
  294. let decoded = try self.decodeFromFile("method_config.with_hedging", as: MethodConfig.self)
  295. #expect(decoded == expected)
  296. }
  297. @Test("MethodConfig with retries")
  298. func methodConfigWithRetries() throws {
  299. let expected = MethodConfig(
  300. names: [
  301. MethodConfig.Name(
  302. service: "echo.Echo",
  303. method: "Get"
  304. )
  305. ],
  306. waitForReady: true,
  307. timeout: .seconds(1),
  308. maxRequestMessageBytes: 1024,
  309. maxResponseMessageBytes: 2048,
  310. executionPolicy: .retry(
  311. RetryPolicy(
  312. maximumAttempts: 3,
  313. initialBackoff: .seconds(1),
  314. maximumBackoff: .seconds(3),
  315. backoffMultiplier: 1.6,
  316. retryableStatusCodes: [.aborted, .unimplemented]
  317. )
  318. )
  319. )
  320. let decoded = try self.decodeFromFile("method_config.with_retries", as: MethodConfig.self)
  321. #expect(decoded == expected)
  322. }
  323. }
  324. @Suite("Round-trip tests")
  325. struct RoundTrip {
  326. private func decodeFromFile<Decoded: Decodable>(
  327. _ name: String,
  328. as: Decoded.Type
  329. ) throws -> Decoded {
  330. let input = Bundle.module.url(
  331. forResource: name,
  332. withExtension: "json",
  333. subdirectory: "Inputs"
  334. )
  335. let url = try #require(input)
  336. let data = try Data(contentsOf: url)
  337. let decoder = JSONDecoder()
  338. return try decoder.decode(Decoded.self, from: data)
  339. }
  340. private func decodeFromJSONString<Decoded: Decodable>(
  341. _ json: String,
  342. as: Decoded.Type
  343. ) throws -> Decoded {
  344. let data = Data(json.utf8)
  345. let decoder = JSONDecoder()
  346. return try decoder.decode(Decoded.self, from: data)
  347. }
  348. private func encodeToJSON(_ value: some Encodable) throws -> String {
  349. let encoder = JSONEncoder()
  350. let encoded = try encoder.encode(value)
  351. let json = String(decoding: encoded, as: UTF8.self)
  352. return json
  353. }
  354. private func roundTrip<T: Codable & Equatable>(type: T.Type = T.self, fileName: String) throws {
  355. let decoded = try self.decodeFromFile(fileName, as: T.self)
  356. let encoded = try self.encodeToJSON(decoded)
  357. let decodedAgain = try self.decodeFromJSONString(encoded, as: T.self)
  358. #expect(decoded == decodedAgain)
  359. }
  360. @Test(
  361. "MethodConfig",
  362. arguments: [
  363. "method_config",
  364. "method_config.with_retries",
  365. "method_config.with_hedging",
  366. ]
  367. )
  368. func roundTripCodingAndDecoding(fileName: String) throws {
  369. try self.roundTrip(type: MethodConfig.self, fileName: fileName)
  370. }
  371. }
  372. }