| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- /*
- * Copyright 2024, gRPC Authors All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- import GRPCCore
- import GRPCInProcessTransport
- import GRPCProtobuf
- import SwiftProtobuf
- import Testing
- struct DetailedErrorTests {
- @Test(
- "Google RPC Status is transferred over the wire",
- arguments: [
- ([], []),
- (["ErrorInfo"], [.errorInfo(.testValue)]),
- (["RetryInfo"], [.retryInfo(.testValue)]),
- (["DebugInfo"], [.debugInfo(.testValue)]),
- (["QuotaFailure"], [.quotaFailure(.testValue)]),
- (["PreconditionFailure"], [.preconditionFailure(.testValue)]),
- (["BadRequest"], [.badRequest(.testValue)]),
- (["RequestInfo"], [.requestInfo(.testValue)]),
- (["ResourceInfo"], [.resourceInfo(.testValue)]),
- (["Help"], [.help(.testValue)]),
- (["LocalizedMessage"], [.localizedMessage(.testValue)]),
- (["DebugInfo", "RetryInfo"], [.debugInfo(.testValue), .retryInfo(.testValue)]),
- (["Help", "PreconditionFailure"], [.help(.testValue), .preconditionFailure(.testValue)]),
- (["Help", "Help", "Help"], [.help(.testValue), .help(.testValue), .help(.testValue)]),
- ] as [([String], [ErrorDetails])]
- )
- func rpcStatus(details: [String], expected: [ErrorDetails]) async throws {
- let inProcess = InProcessTransport()
- try await withGRPCServer(transport: inProcess.server, services: [ErrorThrowingService()]) { _ in
- try await withGRPCClient(transport: inProcess.client) { client in
- let errorClient = ErrorService.Client(wrapping: client)
- let subkinds = details.joined(separator: ",")
- let kind = "status/\(subkinds)"
- await #expect {
- try await errorClient.throwError(.with { $0.kind = kind })
- } throws: { error in
- guard let rpcError = error as? RPCError else { return false }
- guard let status = try? rpcError.unpackGoogleRPCStatus() else { return false }
- // Code/message should be the same.
- #expect(status.code == rpcError.code)
- #expect(status.message == rpcError.message)
- // Set by the service.
- #expect(status.code == .unknown)
- #expect(status.message == subkinds)
- #expect(status.details == expected)
- return true
- }
- }
- }
- }
- @Test(
- arguments: [
- (.errorInfo(.testValue), #"ErrorInfo(reason: "r", domain: "d", metadata: ["k": "v"])"#),
- (.retryInfo(.testValue), #"RetryInfo(delay: 1.0 seconds)"#),
- (.debugInfo(.testValue), #"DebugInfo(stack: ["foo.foo()", "foo.bar()"], detail: "detail")"#),
- (
- .quotaFailure(.testValue),
- #"QuotaFailure(violations: [Violation(subject: "s", violationDescription: "d")])"#
- ),
- (
- .preconditionFailure(.testValue),
- #"PreconditionFailure(violations: [Violation(subject: "s", type: "t", violationDescription: "d")])"#
- ),
- (
- .badRequest(.testValue),
- #"BadRequest(violations: [FieldViolation(field: "f", violationDescription: "d")])"#
- ),
- (.requestInfo(.testValue), #"RequestInfo(requestID: "id", servingData: "d")"#),
- (
- .resourceInfo(.testValue),
- #"ResourceInfo(name: "n", owner: "", type: "t", errorDescription: "d")"#
- ),
- (.help(.testValue), #"Help(links: [Link(url: "url", linkDescription: "d")])"#),
- (.localizedMessage(.testValue), #"LocalizedMessage(locale: "l", message: "m")"#),
- ] as [(ErrorDetails, String)]
- )
- func errorInfoDescription(_ details: ErrorDetails, expected: String) {
- #expect(String(describing: details) == expected)
- }
- }
- private struct ErrorThrowingService: ErrorService.SimpleServiceProtocol {
- func throwError(
- request: ThrowInput,
- context: ServerContext
- ) async throws -> Google_Protobuf_Empty {
- if request.kind.starts(with: "status/") {
- try self.throwStatusError(kind: String(request.kind.dropFirst("status/".count)))
- } else {
- throw RPCError(code: .invalidArgument, message: "'\(request.kind)' is invalid.")
- }
- }
- private func throwStatusError(kind: String) throws(GoogleRPCStatus) -> Never {
- var details: [ErrorDetails] = []
- for subkind in kind.split(separator: ",") {
- if let detail = self.errorDetails(kind: String(subkind)) {
- details.append(detail)
- } else {
- throw GoogleRPCStatus(
- code: .invalidArgument,
- message: "Unknown error subkind",
- details: [
- .badRequest(
- violations: [
- ErrorDetails.BadRequest.FieldViolation(
- field: "kind",
- description: "'\(kind)' is invalid"
- )
- ]
- )
- ]
- )
- }
- }
- throw GoogleRPCStatus(code: .unknown, message: kind, details: details)
- }
- private func errorDetails(kind: String) -> ErrorDetails? {
- let details: ErrorDetails?
- switch kind {
- case "ErrorInfo":
- details = .errorInfo(.testValue)
- case "RetryInfo":
- details = .retryInfo(.testValue)
- case "DebugInfo":
- details = .debugInfo(.testValue)
- case "QuotaFailure":
- details = .quotaFailure(.testValue)
- case "PreconditionFailure":
- details = .preconditionFailure(.testValue)
- case "BadRequest":
- details = .badRequest(.testValue)
- case "RequestInfo":
- details = .requestInfo(.testValue)
- case "ResourceInfo":
- details = .resourceInfo(.testValue)
- case "Help":
- details = .help(.testValue)
- case "LocalizedMessage":
- details = .localizedMessage(.testValue)
- default:
- details = nil
- }
- return details
- }
- }
- extension ErrorDetails.ErrorInfo {
- fileprivate static let testValue = Self(reason: "r", domain: "d", metadata: ["k": "v"])
- }
- extension ErrorDetails.RetryInfo {
- fileprivate static let testValue = Self(delay: .seconds(1))
- }
- extension ErrorDetails.DebugInfo {
- fileprivate static let testValue = Self(
- stack: ["foo.foo()", "foo.bar()"],
- detail: "detail"
- )
- }
- extension ErrorDetails.QuotaFailure {
- fileprivate static let testValue = Self(
- violations: [
- Violation(subject: "s", description: "d")
- ]
- )
- }
- extension ErrorDetails.PreconditionFailure {
- fileprivate static let testValue = Self(
- violations: [
- Violation(type: "t", subject: "s", description: "d")
- ]
- )
- }
- extension ErrorDetails.BadRequest {
- fileprivate static let testValue = Self(
- violations: [
- FieldViolation(field: "f", description: "d")
- ]
- )
- }
- extension ErrorDetails.RequestInfo {
- fileprivate static let testValue = Self(requestID: "id", servingData: "d")
- }
- extension ErrorDetails.ResourceInfo {
- fileprivate static let testValue = Self(type: "t", name: "n", errorDescription: "d")
- }
- extension ErrorDetails.Help {
- fileprivate static let testValue = Self(
- links: [
- Link(url: "url", description: "d")
- ]
- )
- }
- extension ErrorDetails.LocalizedMessage {
- fileprivate static let testValue = Self(locale: "l", message: "m")
- }
|