DetailedErrorTests.swift 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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 GRPCCore
  17. import GRPCInProcessTransport
  18. import GRPCProtobuf
  19. import SwiftProtobuf
  20. import Testing
  21. struct DetailedErrorTests {
  22. @Test(
  23. "Google RPC Status is transferred over the wire",
  24. arguments: [
  25. ([], []),
  26. (["ErrorInfo"], [.errorInfo(.testValue)]),
  27. (["RetryInfo"], [.retryInfo(.testValue)]),
  28. (["DebugInfo"], [.debugInfo(.testValue)]),
  29. (["QuotaFailure"], [.quotaFailure(.testValue)]),
  30. (["PreconditionFailure"], [.preconditionFailure(.testValue)]),
  31. (["BadRequest"], [.badRequest(.testValue)]),
  32. (["RequestInfo"], [.requestInfo(.testValue)]),
  33. (["ResourceInfo"], [.resourceInfo(.testValue)]),
  34. (["Help"], [.help(.testValue)]),
  35. (["LocalizedMessage"], [.localizedMessage(.testValue)]),
  36. (["DebugInfo", "RetryInfo"], [.debugInfo(.testValue), .retryInfo(.testValue)]),
  37. (["Help", "PreconditionFailure"], [.help(.testValue), .preconditionFailure(.testValue)]),
  38. (["Help", "Help", "Help"], [.help(.testValue), .help(.testValue), .help(.testValue)]),
  39. ] as [([String], [ErrorDetails])]
  40. )
  41. func rpcStatus(details: [String], expected: [ErrorDetails]) async throws {
  42. let inProcess = InProcessTransport()
  43. try await withGRPCServer(transport: inProcess.server, services: [ErrorThrowingService()]) { _ in
  44. try await withGRPCClient(transport: inProcess.client) { client in
  45. let errorClient = ErrorService.Client(wrapping: client)
  46. let subkinds = details.joined(separator: ",")
  47. let kind = "status/\(subkinds)"
  48. await #expect {
  49. try await errorClient.throwError(.with { $0.kind = kind })
  50. } throws: { error in
  51. guard let rpcError = error as? RPCError else { return false }
  52. guard let status = try? rpcError.unpackGoogleRPCStatus() else { return false }
  53. // Code/message should be the same.
  54. #expect(status.code == rpcError.code)
  55. #expect(status.message == rpcError.message)
  56. // Set by the service.
  57. #expect(status.code == .unknown)
  58. #expect(status.message == subkinds)
  59. #expect(status.details == expected)
  60. return true
  61. }
  62. }
  63. }
  64. }
  65. @Test(
  66. arguments: [
  67. (.errorInfo(.testValue), #"ErrorInfo(reason: "r", domain: "d", metadata: ["k": "v"])"#),
  68. (.retryInfo(.testValue), #"RetryInfo(delay: 1.0 seconds)"#),
  69. (.debugInfo(.testValue), #"DebugInfo(stack: ["foo.foo()", "foo.bar()"], detail: "detail")"#),
  70. (
  71. .quotaFailure(.testValue),
  72. #"QuotaFailure(violations: [Violation(subject: "s", violationDescription: "d")])"#
  73. ),
  74. (
  75. .preconditionFailure(.testValue),
  76. #"PreconditionFailure(violations: [Violation(subject: "s", type: "t", violationDescription: "d")])"#
  77. ),
  78. (
  79. .badRequest(.testValue),
  80. #"BadRequest(violations: [FieldViolation(field: "f", violationDescription: "d")])"#
  81. ),
  82. (.requestInfo(.testValue), #"RequestInfo(requestID: "id", servingData: "d")"#),
  83. (
  84. .resourceInfo(.testValue),
  85. #"ResourceInfo(name: "n", owner: "", type: "t", errorDescription: "d")"#
  86. ),
  87. (.help(.testValue), #"Help(links: [Link(url: "url", linkDescription: "d")])"#),
  88. (.localizedMessage(.testValue), #"LocalizedMessage(locale: "l", message: "m")"#),
  89. ] as [(ErrorDetails, String)]
  90. )
  91. func errorInfoDescription(_ details: ErrorDetails, expected: String) {
  92. #expect(String(describing: details) == expected)
  93. }
  94. @Test("Round-trip encoding of GoogleRPCStatus")
  95. func googleRPCStatusRoundTripCoding() throws {
  96. let detail = ErrorDetails.BadRequest(violations: [.init(field: "foo", description: "bar")])
  97. let status = GoogleRPCStatus(code: .dataLoss, message: "Uh oh", details: [.badRequest(detail)])
  98. let serialized: [UInt8] = try status.serializedBytes()
  99. let deserialized = try GoogleRPCStatus(serializedBytes: serialized)
  100. #expect(deserialized.code == status.code)
  101. #expect(deserialized.message == status.message)
  102. #expect(deserialized.details.count == status.details.count)
  103. #expect(deserialized.details.first?.badRequest == detail)
  104. }
  105. }
  106. private struct ErrorThrowingService: ErrorService.SimpleServiceProtocol {
  107. func throwError(
  108. request: ThrowInput,
  109. context: ServerContext
  110. ) async throws -> Google_Protobuf_Empty {
  111. if request.kind.starts(with: "status/") {
  112. try self.throwStatusError(kind: String(request.kind.dropFirst("status/".count)))
  113. } else {
  114. throw RPCError(code: .invalidArgument, message: "'\(request.kind)' is invalid.")
  115. }
  116. }
  117. private func throwStatusError(kind: String) throws(GoogleRPCStatus) -> Never {
  118. var details: [ErrorDetails] = []
  119. for subkind in kind.split(separator: ",") {
  120. if let detail = self.errorDetails(kind: String(subkind)) {
  121. details.append(detail)
  122. } else {
  123. throw GoogleRPCStatus(
  124. code: .invalidArgument,
  125. message: "Unknown error subkind",
  126. details: [
  127. .badRequest(
  128. violations: [
  129. ErrorDetails.BadRequest.FieldViolation(
  130. field: "kind",
  131. description: "'\(kind)' is invalid"
  132. )
  133. ]
  134. )
  135. ]
  136. )
  137. }
  138. }
  139. throw GoogleRPCStatus(code: .unknown, message: kind, details: details)
  140. }
  141. private func errorDetails(kind: String) -> ErrorDetails? {
  142. let details: ErrorDetails?
  143. switch kind {
  144. case "ErrorInfo":
  145. details = .errorInfo(.testValue)
  146. case "RetryInfo":
  147. details = .retryInfo(.testValue)
  148. case "DebugInfo":
  149. details = .debugInfo(.testValue)
  150. case "QuotaFailure":
  151. details = .quotaFailure(.testValue)
  152. case "PreconditionFailure":
  153. details = .preconditionFailure(.testValue)
  154. case "BadRequest":
  155. details = .badRequest(.testValue)
  156. case "RequestInfo":
  157. details = .requestInfo(.testValue)
  158. case "ResourceInfo":
  159. details = .resourceInfo(.testValue)
  160. case "Help":
  161. details = .help(.testValue)
  162. case "LocalizedMessage":
  163. details = .localizedMessage(.testValue)
  164. default:
  165. details = nil
  166. }
  167. return details
  168. }
  169. }
  170. extension ErrorDetails.ErrorInfo {
  171. fileprivate static let testValue = Self(reason: "r", domain: "d", metadata: ["k": "v"])
  172. }
  173. extension ErrorDetails.RetryInfo {
  174. fileprivate static let testValue = Self(delay: .seconds(1))
  175. }
  176. extension ErrorDetails.DebugInfo {
  177. fileprivate static let testValue = Self(
  178. stack: ["foo.foo()", "foo.bar()"],
  179. detail: "detail"
  180. )
  181. }
  182. extension ErrorDetails.QuotaFailure {
  183. fileprivate static let testValue = Self(
  184. violations: [
  185. Violation(subject: "s", description: "d")
  186. ]
  187. )
  188. }
  189. extension ErrorDetails.PreconditionFailure {
  190. fileprivate static let testValue = Self(
  191. violations: [
  192. Violation(type: "t", subject: "s", description: "d")
  193. ]
  194. )
  195. }
  196. extension ErrorDetails.BadRequest {
  197. fileprivate static let testValue = Self(
  198. violations: [
  199. FieldViolation(field: "f", description: "d")
  200. ]
  201. )
  202. }
  203. extension ErrorDetails.RequestInfo {
  204. fileprivate static let testValue = Self(requestID: "id", servingData: "d")
  205. }
  206. extension ErrorDetails.ResourceInfo {
  207. fileprivate static let testValue = Self(type: "t", name: "n", errorDescription: "d")
  208. }
  209. extension ErrorDetails.Help {
  210. fileprivate static let testValue = Self(
  211. links: [
  212. Link(url: "url", description: "d")
  213. ]
  214. )
  215. }
  216. extension ErrorDetails.LocalizedMessage {
  217. fileprivate static let testValue = Self(locale: "l", message: "m")
  218. }