diff --git a/Package.swift b/Package.swift index 3f7b9a8b7..16aea5d15 100644 --- a/Package.swift +++ b/Package.swift @@ -119,6 +119,8 @@ extension Target.Dependency { name: "SwiftProtobufPluginLibrary", package: "swift-protobuf" ) + + static let grpcCore: Self = .target(name: "GRPCCore") } // MARK: - Targets @@ -146,6 +148,12 @@ extension Target { path: "Sources/GRPC" ) + static let grpcCore: Target = .target( + name: "GRPCCore", + dependencies: [], + path: "Sources/GRPCCore" + ) + static let cgrpcZlib: Target = .target( name: cgrpcZlibTargetName, path: "Sources/CGRPCZlib", @@ -200,6 +208,13 @@ extension Target { ] ) + static let grpcCoreTests: Target = .testTarget( + name: "GRPCCoreTests", + dependencies: [ + .grpcCore, + ] + ) + static let interopTestModels: Target = .target( name: "GRPCInteroperabilityTestModels", dependencies: [ @@ -476,6 +491,12 @@ let package = Package( .routeGuideClient, .routeGuideServer, .packetCapture, + + // v2 + .grpcCore, + + // v2 tests + .grpcCoreTests, ] ) diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift new file mode 100644 index 000000000..5a72fd161 --- /dev/null +++ b/Sources/GRPCCore/Metadata.swift @@ -0,0 +1,18 @@ +/* + * Copyright 2023, 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. + */ + +// FIXME: placeholder. +public typealias Metadata = [String: String] diff --git a/Sources/GRPCCore/RPCError.swift b/Sources/GRPCCore/RPCError.swift new file mode 100644 index 000000000..46c27a4ce --- /dev/null +++ b/Sources/GRPCCore/RPCError.swift @@ -0,0 +1,244 @@ +/* + * Copyright 2023, 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. + */ + +/// An error representing the outcome of an RPC. +/// +/// See also ``Status``. +public struct RPCError: @unchecked Sendable, Hashable, Error { + // @unchecked because it relies on heap allocated storage and 'isKnownUniquelyReferenced' + + private var storage: Storage + private mutating func ensureStorageIsUnique() { + if !isKnownUniquelyReferenced(&self.storage) { + self.storage = self.storage.copy() + } + } + + /// A code representing the high-level domain of the error. + public var code: Code { + get { self.storage.code } + set { + self.ensureStorageIsUnique() + self.storage.code = newValue + } + } + + /// A message providing additional context about the error. + public var message: String { + get { self.storage.message } + set { + self.ensureStorageIsUnique() + self.storage.message = newValue + } + } + + /// Metadata associated with the error. + /// + /// Any metadata included in the error thrown from a service will be sent back to the client and + /// conversely any ``RPCError`` received by the client may include metadata sent by a service. + /// + /// Note that clients and servers may synthesise errors which may not include metadata. + public var metadata: Metadata { + get { self.storage.metadata } + set { + self.ensureStorageIsUnique() + self.storage.metadata = newValue + } + } + + /// Create a new RPC error. + /// + /// - Parameters: + /// - code: The status code. + /// - message: A message providing additional context about the code. + /// - metadata: Any metadata to attach to the error. + public init(code: Code, message: String, metadata: Metadata = [:]) { + self.storage = Storage(code: code, message: message, metadata: metadata) + } + + /// Create a new RPC error from the provided ``Status``. + /// + /// Returns `nil` if the provided ``Status`` has code ``Status/Code-swift.struct/ok``. + /// + /// - Parameter status: The status to convert. + public init?(status: Status) { + guard let code = Code(statusCode: status.code) else { return nil } + self.init(code: code, message: status.message, metadata: [:]) + } +} + +extension RPCError: CustomStringConvertible { + public var description: String { + "\(self.code): \"\(self.message)\"" + } +} + +extension RPCError { + private final class Storage: Hashable { + var code: RPCError.Code + var message: String + var metadata: Metadata + + init(code: RPCError.Code, message: String, metadata: Metadata) { + self.code = code + self.message = message + self.metadata = metadata + } + + func copy() -> Self { + Self(code: self.code, message: self.message, metadata: self.metadata) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.code) + hasher.combine(self.message) + hasher.combine(self.metadata) + } + + static func == (lhs: RPCError.Storage, rhs: RPCError.Storage) -> Bool { + return lhs.code == rhs.code && lhs.message == rhs.message && lhs.metadata == rhs.metadata + } + } +} + +extension RPCError { + public struct Code: Hashable, Sendable, CustomStringConvertible { + /// The numeric value of the error code. + public var rawValue: Int { Int(self.wrapped.rawValue) } + + private var wrapped: Status.Code.Wrapped + private init(_ wrapped: Status.Code.Wrapped) { + self.wrapped = wrapped + } + + internal init?(statusCode: Status.Code) { + if statusCode == .ok { + return nil + } else { + self.wrapped = statusCode.wrapped + } + } + + public var description: String { + String(describing: self.wrapped) + } + } +} + +extension RPCError.Code { + /// The operation was cancelled (typically by the caller). + public static let cancelled = Self(.cancelled) + + /// Unknown error. An example of where this error may be returned is if a + /// Status value received from another address space belongs to an error-space + /// that is not known in this address space. Also errors raised by APIs that + /// do not return enough error information may be converted to this error. + public static let unknown = Self(.unknown) + + /// Client specified an invalid argument. Note that this differs from + /// ``failedPrecondition``. ``invalidArgument`` indicates arguments that are + /// problematic regardless of the state of the system (e.g., a malformed file + /// name). + public static let invalidArgument = Self(.invalidArgument) + + /// Deadline expired before operation could complete. For operations that + /// change the state of the system, this error may be returned even if the + /// operation has completed successfully. For example, a successful response + /// from a server could have been delayed long enough for the deadline to + /// expire. + public static let deadlineExceeded = Self(.deadlineExceeded) + + /// Some requested entity (e.g., file or directory) was not found. + public static let notFound = Self(.notFound) + + /// Some entity that we attempted to create (e.g., file or directory) already + /// exists. + public static let alreadyExists = Self(.alreadyExists) + + /// The caller does not have permission to execute the specified operation. + /// ``permissionDenied`` must not be used for rejections caused by exhausting + /// some resource (use ``resourceExhausted`` instead for those errors). + /// ``permissionDenied`` must not be used if the caller can not be identified + /// (use ``unauthenticated`` instead for those errors). + public static let permissionDenied = Self(.permissionDenied) + + /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the + /// entire file system is out of space. + public static let resourceExhausted = Self(.resourceExhausted) + + /// Operation was rejected because the system is not in a state required for + /// the operation's execution. For example, directory to be deleted may be + /// non-empty, an rmdir operation is applied to a non-directory, etc. + /// + /// A litmus test that may help a service implementor in deciding + /// between ``failedPrecondition``, ``aborted``, and ``unavailable``: + /// - Use ``unavailable`` if the client can retry just the failing call. + /// - Use ``aborted`` if the client should retry at a higher-level + /// (e.g., restarting a read-modify-write sequence). + /// - Use ``failedPrecondition`` if the client should not retry until + /// the system state has been explicitly fixed. E.g., if an "rmdir" + /// fails because the directory is non-empty, ``failedPrecondition`` + /// should be returned since the client should not retry unless + /// they have first fixed up the directory by deleting files from it. + /// - Use ``failedPrecondition`` if the client performs conditional + /// REST Get/Update/Delete on a resource and the resource on the + /// server does not match the condition. E.g., conflicting + /// read-modify-write on the same resource. + public static let failedPrecondition = Self(.failedPrecondition) + + /// The operation was aborted, typically due to a concurrency issue like + /// sequencer check failures, transaction aborts, etc. + /// + /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``, + /// and ``unavailable``. + public static let aborted = Self(.aborted) + + /// Operation was attempted past the valid range. E.g., seeking or reading + /// past end of file. + /// + /// Unlike ``invalidArgument``, this error indicates a problem that may be fixed + /// if the system state changes. For example, a 32-bit file system will + /// generate ``invalidArgument`` if asked to read at an offset that is not in the + /// range [0,2^32-1], but it will generate ``outOfRange`` if asked to read from + /// an offset past the current file size. + /// + /// There is a fair bit of overlap between ``failedPrecondition`` and + /// ``outOfRange``. We recommend using ``outOfRange`` (the more specific error) + /// when it applies so that callers who are iterating through a space can + /// easily look for an ``outOfRange`` error to detect when they are done. + public static let outOfRange = Self(.outOfRange) + + /// Operation is not implemented or not supported/enabled in this service. + public static let unimplemented = Self(.unimplemented) + + /// Internal errors. Means some invariants expected by underlying System has + /// been broken. If you see one of these errors, Something is very broken. + public static let internalError = Self(.internalError) + + /// The service is currently unavailable. This is a most likely a transient + /// condition and may be corrected by retrying with a backoff. + /// + /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``, + /// and ``unavailable``. + public static let unavailable = Self(.unavailable) + + /// Unrecoverable data loss or corruption. + public static let dataLoss = Self(.dataLoss) + + /// The request does not have valid authentication credentials for the + /// operation. + public static let unauthenticated = Self(.unauthenticated) +} diff --git a/Sources/GRPCCore/Status.swift b/Sources/GRPCCore/Status.swift new file mode 100644 index 000000000..69e30bdb2 --- /dev/null +++ b/Sources/GRPCCore/Status.swift @@ -0,0 +1,264 @@ +/* + * Copyright 2023, 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. + */ + +/// A status object represents the outcome of an RPC. +/// +/// Each ``Status`` is composed of a ``Status/code-swift.property`` and ``Status/message``. Each +/// service implementation chooses the code and message returned to the client for each RPC +/// it implements. However, client and server implementations may also generate status objects +/// on their own if an error happens. +/// +/// ``Status`` represents the raw outcome of an RPC whether it was successful or not; ``RPCError`` +/// is similar to ``Status`` but only represents error cases, in other words represents all status +/// codes apart from ``Code-swift.struct/ok``. +public struct Status: @unchecked Sendable, Hashable { + // @unchecked because it relies on heap allocated storage and 'isKnownUniquelyReferenced' + + private var storage: Storage + private mutating func ensureStorageIsUnique() { + if !isKnownUniquelyReferenced(&self.storage) { + self.storage = self.storage.copy() + } + } + + /// A code representing the high-level domain of the status. + public var code: Code { + get { self.storage.code } + set { + self.ensureStorageIsUnique() + self.storage.code = newValue + } + } + + /// A message providing additional context about the status. + public var message: String { + get { self.storage.message } + set { + self.ensureStorageIsUnique() + self.storage.message = newValue + } + } + + /// Create a new status. + /// + /// - Parameters: + /// - code: The status code. + /// - message: A message providing additional context about the code. + public init(code: Code, message: String) { + if code == .ok, message.isEmpty { + // Avoid a heap allocation for the common case. + self.storage = Storage.okWithNoMessage + } else { + self.storage = Storage(code: code, message: message) + } + } +} + +extension Status: CustomStringConvertible { + public var description: String { + "\(self.code): \"\(self.message)\"" + } +} + +extension Status { + private final class Storage: Hashable { + static let okWithNoMessage = Storage(code: .ok, message: "") + + var code: Status.Code + var message: String + + init(code: Status.Code, message: String) { + self.code = code + self.message = message + } + + func copy() -> Self { + Self(code: self.code, message: self.message) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.code) + hasher.combine(self.message) + } + + static func == (lhs: Status.Storage, rhs: Status.Storage) -> Bool { + return lhs.code == rhs.code && lhs.message == rhs.message + } + } +} + +extension Status { + /// Status codes for gRPC operations. + /// + /// The outcome of every RPC is indicated by a status code. + public struct Code: Hashable, CustomStringConvertible, Sendable { + // Source: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md + enum Wrapped: UInt8, Hashable, Sendable { + case ok = 0 + case cancelled = 1 + case unknown = 2 + case invalidArgument = 3 + case deadlineExceeded = 4 + case notFound = 5 + case alreadyExists = 6 + case permissionDenied = 7 + case resourceExhausted = 8 + case failedPrecondition = 9 + case aborted = 10 + case outOfRange = 11 + case unimplemented = 12 + case internalError = 13 + case unavailable = 14 + case dataLoss = 15 + case unauthenticated = 16 + } + + /// The underlying value. + let wrapped: Wrapped + + /// The numeric value of the error code. + public var rawValue: Int { Int(self.wrapped.rawValue) } + + /// Creates a status codes from its raw value. + /// + /// - Parameters: + /// - rawValue: The numeric value to create the code from. + /// Returns `nil` if the `rawValue` isn't a valid error code. + public init?(rawValue: Int) { + if let value = UInt8(exactly: rawValue), let wrapped = Wrapped(rawValue: value) { + self.wrapped = wrapped + } else { + return nil + } + } + + private init(_ wrapped: Wrapped) { + self.wrapped = wrapped + } + + public var description: String { + String(describing: self.wrapped) + } + } +} + +extension Status.Code { + /// The operation completed successfully. + public static let ok = Self(.ok) + + /// The operation was cancelled (typically by the caller). + public static let cancelled = Self(.cancelled) + + /// Unknown error. An example of where this error may be returned is if a + /// Status value received from another address space belongs to an error-space + /// that is not known in this address space. Also errors raised by APIs that + /// do not return enough error information may be converted to this error. + public static let unknown = Self(.unknown) + + /// Client specified an invalid argument. Note that this differs from + /// ``failedPrecondition``. ``invalidArgument`` indicates arguments that are + /// problematic regardless of the state of the system (e.g., a malformed file + /// name). + public static let invalidArgument = Self(.invalidArgument) + + /// Deadline expired before operation could complete. For operations that + /// change the state of the system, this error may be returned even if the + /// operation has completed successfully. For example, a successful response + /// from a server could have been delayed long enough for the deadline to + /// expire. + public static let deadlineExceeded = Self(.deadlineExceeded) + + /// Some requested entity (e.g., file or directory) was not found. + public static let notFound = Self(.notFound) + + /// Some entity that we attempted to create (e.g., file or directory) already + /// exists. + public static let alreadyExists = Self(.alreadyExists) + + /// The caller does not have permission to execute the specified operation. + /// ``permissionDenied`` must not be used for rejections caused by exhausting + /// some resource (use ``resourceExhausted`` instead for those errors). + /// ``permissionDenied`` must not be used if the caller can not be identified + /// (use ``unauthenticated`` instead for those errors). + public static let permissionDenied = Self(.permissionDenied) + + /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the + /// entire file system is out of space. + public static let resourceExhausted = Self(.resourceExhausted) + + /// Operation was rejected because the system is not in a state required for + /// the operation's execution. For example, directory to be deleted may be + /// non-empty, an rmdir operation is applied to a non-directory, etc. + /// + /// A litmus test that may help a service implementor in deciding + /// between ``failedPrecondition``, ``aborted``, and ``unavailable``: + /// - Use ``unavailable`` if the client can retry just the failing call. + /// - Use ``aborted`` if the client should retry at a higher-level + /// (e.g., restarting a read-modify-write sequence). + /// - Use ``failedPrecondition`` if the client should not retry until + /// the system state has been explicitly fixed. E.g., if an "rmdir" + /// fails because the directory is non-empty, ``failedPrecondition`` + /// should be returned since the client should not retry unless + /// they have first fixed up the directory by deleting files from it. + /// - Use ``failedPrecondition`` if the client performs conditional + /// REST Get/Update/Delete on a resource and the resource on the + /// server does not match the condition. E.g., conflicting + /// read-modify-write on the same resource. + public static let failedPrecondition = Self(.failedPrecondition) + + /// The operation was aborted, typically due to a concurrency issue like + /// sequencer check failures, transaction aborts, etc. + /// + /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``, + /// and ``unavailable``. + public static let aborted = Self(.aborted) + + /// Operation was attempted past the valid range. E.g., seeking or reading + /// past end of file. + /// + /// Unlike ``invalidArgument``, this error indicates a problem that may be fixed + /// if the system state changes. For example, a 32-bit file system will + /// generate ``invalidArgument`` if asked to read at an offset that is not in the + /// range [0,2^32-1], but it will generate ``outOfRange`` if asked to read from + /// an offset past the current file size. + /// + /// There is a fair bit of overlap between ``failedPrecondition`` and + /// ``outOfRange``. We recommend using ``outOfRange`` (the more specific error) + /// when it applies so that callers who are iterating through a space can + /// easily look for an ``outOfRange`` error to detect when they are done. + public static let outOfRange = Self(.outOfRange) + + /// Operation is not implemented or not supported/enabled in this service. + public static let unimplemented = Self(.unimplemented) + + /// Internal errors. Means some invariants expected by underlying System has + /// been broken. If you see one of these errors, Something is very broken. + public static let internalError = Self(.internalError) + + /// The service is currently unavailable. This is a most likely a transient + /// condition and may be corrected by retrying with a backoff. + /// + /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``, + /// and ``unavailable``. + public static let unavailable = Self(.unavailable) + + /// Unrecoverable data loss or corruption. + public static let dataLoss = Self(.dataLoss) + + /// The request does not have valid authentication credentials for the + /// operation. + public static let unauthenticated = Self(.unauthenticated) +} diff --git a/Tests/GRPCCoreTests/RPCErrorTests.swift b/Tests/GRPCCoreTests/RPCErrorTests.swift new file mode 100644 index 000000000..877af2aee --- /dev/null +++ b/Tests/GRPCCoreTests/RPCErrorTests.swift @@ -0,0 +1,101 @@ +/* + * Copyright 2023, 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 XCTest + +final class RPCErrorTests: XCTestCase { + private static let statusCodeRawValue: [(RPCError.Code, Int)] = [ + (.cancelled, 1), + (.unknown, 2), + (.invalidArgument, 3), + (.deadlineExceeded, 4), + (.notFound, 5), + (.alreadyExists, 6), + (.permissionDenied, 7), + (.resourceExhausted, 8), + (.failedPrecondition, 9), + (.aborted, 10), + (.outOfRange, 11), + (.unimplemented, 12), + (.internalError, 13), + (.unavailable, 14), + (.dataLoss, 15), + (.unauthenticated, 16), + ] + + func testCustomStringConvertible() { + XCTAssertDescription(RPCError(code: .dataLoss, message: ""), #"dataLoss: """#) + XCTAssertDescription(RPCError(code: .unknown, message: "message"), #"unknown: "message""#) + XCTAssertDescription(RPCError(code: .aborted, message: "message"), #"aborted: "message""#) + } + + func testErrorFromStatus() throws { + var status = Status(code: .ok, message: "") + // ok isn't an error + XCTAssertNil(RPCError(status: status)) + + status.code = .invalidArgument + var error = try XCTUnwrap(RPCError(status: status)) + XCTAssertEqual(error.code, .invalidArgument) + XCTAssertEqual(error.message, "") + XCTAssertEqual(error.metadata, [:]) + + status.code = .cancelled + status.message = "an error message" + error = try XCTUnwrap(RPCError(status: status)) + XCTAssertEqual(error.code, .cancelled) + XCTAssertEqual(error.message, "an error message") + XCTAssertEqual(error.metadata, [:]) + } + + func testEquatableConformance() { + XCTAssertEqual( + RPCError(code: .cancelled, message: ""), + RPCError(code: .cancelled, message: "") + ) + + XCTAssertEqual( + RPCError(code: .cancelled, message: "message"), + RPCError(code: .cancelled, message: "message") + ) + + XCTAssertEqual( + RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]), + RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]) + ) + + XCTAssertNotEqual( + RPCError(code: .cancelled, message: ""), + RPCError(code: .cancelled, message: "message") + ) + + XCTAssertNotEqual( + RPCError(code: .cancelled, message: "message"), + RPCError(code: .unknown, message: "message") + ) + + XCTAssertNotEqual( + RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]), + RPCError(code: .cancelled, message: "message", metadata: ["foo": "baz"]) + ) + } + + func testStatusCodeRawValues() { + for (code, expected) in Self.statusCodeRawValue { + XCTAssertEqual(code.rawValue, expected, "\(code) had unexpected raw value") + } + } +} diff --git a/Tests/GRPCCoreTests/StatusTests.swift b/Tests/GRPCCoreTests/StatusTests.swift new file mode 100644 index 000000000..29c48b287 --- /dev/null +++ b/Tests/GRPCCoreTests/StatusTests.swift @@ -0,0 +1,98 @@ +/* + * Copyright 2023, 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 XCTest + +final class StatusTests: XCTestCase { + private static let statusCodeRawValue: [(Status.Code, Int)] = [ + (.ok, 0), + (.cancelled, 1), + (.unknown, 2), + (.invalidArgument, 3), + (.deadlineExceeded, 4), + (.notFound, 5), + (.alreadyExists, 6), + (.permissionDenied, 7), + (.resourceExhausted, 8), + (.failedPrecondition, 9), + (.aborted, 10), + (.outOfRange, 11), + (.unimplemented, 12), + (.internalError, 13), + (.unavailable, 14), + (.dataLoss, 15), + (.unauthenticated, 16), + ] + + func testCustomStringConvertible() { + XCTAssertDescription(Status(code: .ok, message: ""), #"ok: """#) + XCTAssertDescription(Status(code: .dataLoss, message: "message"), #"dataLoss: "message""#) + XCTAssertDescription(Status(code: .unknown, message: "message"), #"unknown: "message""#) + XCTAssertDescription(Status(code: .aborted, message: "message"), #"aborted: "message""#) + } + + func testStatusCodeRawValues() { + for (code, expected) in Self.statusCodeRawValue { + XCTAssertEqual(code.rawValue, expected, "\(code) had unexpected raw value") + } + } + + func testStatusCodeFromValidRawValue() { + for (expected, rawValue) in Self.statusCodeRawValue { + XCTAssertEqual( + Status.Code(rawValue: rawValue), + expected, + "\(rawValue) didn't convert to expected code \(expected)" + ) + } + } + + func testStatusCodeFromInvalidRawValue() { + // Internally represented as a `UInt8`; try all other values. + for rawValue in UInt8(17) ... UInt8.max { + XCTAssertNil(Status.Code(rawValue: Int(rawValue))) + } + + // API accepts `Int` so try invalid `Int` values too. + XCTAssertNil(Status.Code(rawValue: -1)) + XCTAssertNil(Status.Code(rawValue: 1000)) + XCTAssertNil(Status.Code(rawValue: .max)) + } + + func testEquatableConformance() { + XCTAssertEqual(Status(code: .ok, message: ""), Status(code: .ok, message: "")) + XCTAssertEqual(Status(code: .ok, message: "message"), Status(code: .ok, message: "message")) + + XCTAssertNotEqual( + Status(code: .ok, message: ""), + Status(code: .ok, message: "message") + ) + + XCTAssertNotEqual( + Status(code: .ok, message: "message"), + Status(code: .internalError, message: "message") + ) + + XCTAssertNotEqual( + Status(code: .ok, message: "message"), + Status(code: .ok, message: "different message") + ) + } + + func testFitsInExistentialContainer() { + XCTAssertLessThanOrEqual(MemoryLayout.size, 24) + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift new file mode 100644 index 000000000..275396d4f --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -0,0 +1,25 @@ +/* + * Copyright 2023, 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 XCTest + +func XCTAssertDescription( + _ subject: some CustomStringConvertible, + _ expected: String, + file: StaticString = #filePath, + line: UInt = #line +) { + XCTAssertEqual(String(describing: subject), expected, file: file, line: line) +}