Skip to content

Commit

Permalink
Update conformance runner & tests to v1.0.1 (#247)
Browse files Browse the repository at this point in the history
Updates the Connect conformance runner and test suite to
[v1.0.1](https://github.com/connectrpc/conformance/releases/tag/v1.0.1)
and fixes a few failures from new tests that were added. It also opts
out of a few edge cases which are not yet fully specced out / required
by the Connect protocol but are now enforced by the conformance runner
for the sake of consistency. We may implement logic to conform to these
in the future.
  • Loading branch information
rebello95 authored Apr 8, 2024
1 parent 6b70174 commit 3f6e556
Show file tree
Hide file tree
Showing 27 changed files with 1,274 additions and 462 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ extension ConnectInterceptor: UnaryInterceptor {
var headers = request.headers
headers[HeaderConstants.connectProtocolVersion] = [Self.protocolVersion]
headers[HeaderConstants.acceptEncoding] = self.config.acceptCompressionPoolNames()
if let timeout = self.config.timeout {
headers[HeaderConstants.connectTimeoutMs] = ["\(Int(timeout * 1_000))"]
}

let requestBody = request.message ?? Data()
let finalRequestBody: Data
Expand Down Expand Up @@ -117,6 +120,9 @@ extension ConnectInterceptor: StreamInterceptor {
.requestCompression.map { [$0.pool.name()] }
headers[HeaderConstants.connectStreamingAcceptEncoding] = self.config
.acceptCompressionPoolNames()
if let timeout = self.config.timeout {
headers[HeaderConstants.connectTimeoutMs] = ["\(Int(timeout * 1_000))"]
}
proceed(.success(HTTPRequest(
url: request.url,
headers: headers,
Expand Down
3 changes: 3 additions & 0 deletions Libraries/Connect/PackageInternal/Headers+GRPC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ extension Headers {
.acceptCompressionPoolNames()
headers[HeaderConstants.grpcContentEncoding] = config.requestCompression
.map { [$0.pool.name()] }
if let timeout = config.timeout {
headers[HeaderConstants.grpcTimeout] = ["\(Int(timeout * 1_000))m"]
}
if grpcWeb {
headers[HeaderConstants.contentType] = [
"application/grpc-web+\(config.codec.name())",
Expand Down
22 changes: 4 additions & 18 deletions Libraries/Connect/Public/Interfaces/Code.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,35 +78,21 @@ public enum Code: Int, CaseIterable, Equatable, Sendable {
case 200:
return .ok
case 400:
return .invalidArgument
return .internalError
case 401:
return .unauthenticated
case 403:
return .permissionDenied
case 404:
return .unimplemented
case 408:
return .deadlineExceeded
case 409:
return .aborted
case 412:
return .failedPrecondition
case 413:
return .resourceExhausted
case 415:
return .internalError
case 429:
return .unavailable
case 431:
return .resourceExhausted
case 502, 503, 504:
case 429, 502, 503, 504:
return .unavailable
default:
return .unknown
}
}

public static func fromName(_ name: String) -> Self {
return Self.allCases.first { $0.name == name } ?? .unknown
public static func fromName(_ name: String) -> Self? {
return Self.allCases.first { $0.name == name }
}
}
57 changes: 38 additions & 19 deletions Libraries/Connect/Public/Interfaces/ConnectError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public struct ConnectError: Swift.Error, Sendable {
/// List of typed errors that were provided by the server. See `unpackedDetails()`.
public let details: [Detail]
/// Additional key-values that were provided by the server.
public private(set) var metadata: Headers
public let metadata: Headers

/// Unpacks values from `self.details` and returns all matching errors.
///
Expand Down Expand Up @@ -90,22 +90,10 @@ public struct ConnectError: Swift.Error, Sendable {
}
}

extension ConnectError: Swift.Decodable {
private enum CodingKeys: String, CodingKey {
case code = "code"
case message = "message"
case details = "details"
}

extension ConnectError: Decodable {
public init(from decoder: Swift.Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.init(
code: Code.fromName(try container.decode(String.self, forKey: .code)),
message: try container.decodeIfPresent(String.self, forKey: .message),
exception: nil,
details: try container.decodeIfPresent([Detail].self, forKey: .details) ?? [],
metadata: [:]
)
let decodedError = try DecodableConnectError(from: decoder)
self.init(decodedError: decodedError, defaultCode: .unknown, metadata: [:])
}
}

Expand All @@ -131,9 +119,7 @@ extension ConnectError {
}

do {
var connectError = try Foundation.JSONDecoder().decode(ConnectError.self, from: source)
connectError.metadata = metadata
return connectError
return try ConnectError.decode(from: source, defaultCode: code, metadata: metadata)
} catch let error {
return .init(
code: code, message: String(data: source, encoding: .utf8),
Expand All @@ -150,6 +136,39 @@ extension ConnectError {
}
}

// MARK: - Internal

/// Allows for decoding all fields as optionals using `Swift.Decodable`.
private struct DecodableConnectError: Decodable {
let code: String?
let message: String?
let details: [ConnectError.Detail]?

private enum CodingKeys: String, CodingKey {
case code = "code"
case message = "message"
case details = "details"
}
}

private extension ConnectError {
init(decodedError: DecodableConnectError, defaultCode: Code, metadata: Headers) {
self.init(
code: decodedError.code.flatMap(Code.fromName) ?? defaultCode,
message: decodedError.message,
exception: nil,
details: decodedError.details ?? [],
metadata: metadata
)
}

static func decode(from data: Data, defaultCode: Code, metadata: Headers) throws -> Self {
let decodedError = try Foundation.JSONDecoder()
.decode(DecodableConnectError.self, from: data)
return .init(decodedError: decodedError, defaultCode: defaultCode, metadata: metadata)
}
}

extension String {
func addingBase64PaddingIfNeeded() -> Self {
// Base64-encoded strings should be a length that is a multiple of four. If the
Expand Down
2 changes: 2 additions & 0 deletions Libraries/Connect/Public/Interfaces/HeaderConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public enum HeaderConstants {
public static let contentType = "content-type"

public static let connectProtocolVersion = "connect-protocol-version"
public static let connectTimeoutMs = "connect-timeout-ms"

public static let connectStreamingAcceptEncoding = "connect-accept-encoding"
public static let connectStreamingContentEncoding = "connect-content-encoding"
Expand All @@ -29,5 +30,6 @@ public enum HeaderConstants {
public static let grpcMessage = "grpc-message"
public static let grpcStatus = "grpc-status"
public static let grpcStatusDetails = "grpc-status-details-bin"
public static let grpcTimeout = "grpc-timeout"
public static let grpcTE = "te"
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public struct ProtocolClientConfig: Sendable {
public let responseCompressionPools: [CompressionPool]
/// List of interceptor factories that should be used to produce interceptor chains.
public let interceptors: [InterceptorFactory]
/// Timeout that should be given to the server for requests to complete.
public let timeout: TimeInterval?

/// Configuration used to specify if/how requests should be compressed.
public struct RequestCompression: Sendable {
Expand Down Expand Up @@ -79,6 +81,7 @@ public struct ProtocolClientConfig: Sendable {
networkProtocol: NetworkProtocol = .connect,
codec: Codec = JSONCodec(),
unaryGET: UnaryGET = .disabled,
timeout: TimeInterval? = nil,
requestCompression: RequestCompression? = nil,
responseCompressionPools: [CompressionPool] = [GzipCompressionPool()],
interceptors: [InterceptorFactory] = []
Expand All @@ -87,6 +90,7 @@ public struct ProtocolClientConfig: Sendable {
self.host = host
self.networkProtocol = networkProtocol
self.codec = codec
self.timeout = timeout
self.requestCompression = requestCompression
self.responseCompressionPools = responseCompressionPools

Expand Down
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ MAKEFLAGS += --no-builtin-rules
MAKEFLAGS += --no-print-directory
BIN := .tmp/bin
LICENSE_HEADER_YEAR_RANGE := 2022-2023
CONFORMANCE_PROTO_REF := 8ab24b156f5d3f8e7824b85732fa9765ab084879
CONFORMANCE_RUNNER_TAG := v1.0.0-rc2
CONFORMANCE_PROTO_REF := 33aff469e6358823e925f2ef3f915b9b809e8124
CONFORMANCE_RUNNER_TAG := v1.0.1
EXAMPLES_PROTO_REF := e74547031f662f81a62f5e95ebaa9f7037e0c41b
LICENSE_HEADER_VERSION := v1.12.0
LICENSE_IGNORE := -e Package.swift \
Expand Down Expand Up @@ -78,8 +78,8 @@ $(BIN)/license-headers: Makefile
testconformance: ## Run all conformance tests
swift build -c release --product ConnectConformanceClient
mv ./.build/release/ConnectConformanceClient $(BIN)
PATH="$(abspath $(BIN)):$(PATH)" connectconformance -v --conf ./Tests/ConformanceClient/InvocationConfigs/urlsession.yaml --known-failing ./Tests/ConformanceClient/InvocationConfigs/opt-outs.txt --mode client $(BIN)/ConnectConformanceClient httpclient=urlsession
PATH="$(abspath $(BIN)):$(PATH)" connectconformance -v --conf ./Tests/ConformanceClient/InvocationConfigs/nio.yaml --known-failing ./Tests/ConformanceClient/InvocationConfigs/opt-outs.txt --mode client $(BIN)/ConnectConformanceClient httpclient=nio
PATH="$(abspath $(BIN)):$(PATH)" connectconformance --trace --conf ./Tests/ConformanceClient/InvocationConfigs/urlsession.yaml --known-failing @./Tests/ConformanceClient/InvocationConfigs/urlsession-opt-outs.txt --mode client $(BIN)/ConnectConformanceClient httpclient=urlsession
PATH="$(abspath $(BIN)):$(PATH)" connectconformance --trace --conf ./Tests/ConformanceClient/InvocationConfigs/nio.yaml --known-failing @./Tests/ConformanceClient/InvocationConfigs/nio-opt-outs.txt --mode client $(BIN)/ConnectConformanceClient httpclient=nio

.PHONY: testunit
testunit: ## Run all unit tests
Expand Down
Loading

0 comments on commit 3f6e556

Please sign in to comment.