/* * Copyright 2019, 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 Foundation import GRPC import NIO import NIOSSL import GRPCInteroperabilityTests import Commander enum InteroperabilityTestError: LocalizedError { case testNotFound(String) case testFailed(Error) var errorDescription: String? { switch self { case .testNotFound(let name): return "No test named '\(name)' was found" case .testFailed(let error): return "Test failed with error: \(error)" } } } /// Runs the test instance using the given connection. /// /// Success or failure is indicated by the lack or presence of thrown errors, respectively. /// /// - Parameters: /// - instance: `InteroperabilityTest` instance to run. /// - name: the name of the test, use for logging only. /// - connection: client connection to use for running the test. /// - Throws: `InteroperabilityTestError` if the test fails. func runTest(_ instance: InteroperabilityTest, name: String, connection: ClientConnection) throws { do { print("Running '\(name)' ... ", terminator: "") try instance.run(using: connection) print("PASSED") } catch { print("FAILED") throw InteroperabilityTestError.testFailed(error) } } /// Creates a new `InteroperabilityTest` instance with the given name, or throws an /// `InteroperabilityTestError` if no test matches the given name. Implemented test names can be /// found by running the `list_tests` target. func makeRunnableTest(name: String) throws -> InteroperabilityTest { guard let testCase = InteroperabilityTestCase(rawValue: name) else { throw InteroperabilityTestError.testNotFound(name) } return testCase.makeTest() } /// Runs the given block and exits with code 1 if the block throws an error. /// /// The "Commander" CLI elides thrown errors in favour of its own. This function is intended purely /// to work around this limitation by printing any errors before exiting. func exitOnThrow(block: () throws -> T) -> T { do { return try block() } catch { print(error) exit(1) } } // MARK: - Command line options and "main". let serverHostOption = Option( "server_host", default: "localhost", description: "The server host to connect to.") let serverPortOption = Option( "server_port", default: 8080, description: "The server port to connect to.") let testCaseOption = Option( "test_case", default: InteroperabilityTestCase.emptyUnary.name, description: "The name of the test case to execute.") /// The spec requires a string (as opposed to having a flag) to indicate whether TLS is enabled or /// disabled. let useTLSOption = Option( "use_tls", default: "false", description: "Whether to use an encrypted or plaintext connection (true|false).") { value in let lowercased = value.lowercased() switch lowercased { case "true", "false": return lowercased default: throw ArgumentError.invalidType(value: value, type: "boolean", argument: "use_tls") } } let portOption = Option( "port", default: 8080, description: "The port to listen on.") let group = Group { group in group.command( "run_test", serverHostOption, serverPortOption, useTLSOption, testCaseOption, description: "Run a single test. See 'list_tests' for available test names." ) { host, port, useTLS, testCaseName in let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { try? eventLoopGroup.syncShutdownGracefully() } exitOnThrow { let instance = try makeRunnableTest(name: testCaseName) let connection = try makeInteroperabilityTestClientConnection( host: host, port: port, eventLoopGroup: eventLoopGroup, useTLS: useTLS == "true") try runTest(instance, name: testCaseName, connection: connection) } } group.command( "start_server", portOption, useTLSOption, description: "Starts the test server." ) { port, useTls in let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { try? eventLoopGroup.syncShutdownGracefully() } let server = exitOnThrow { return try makeInteroperabilityTestServer( host: "localhost", port: port, eventLoopGroup: eventLoopGroup, useTLS: useTls == "true") } server.map { $0.channel.localAddress?.port }.whenSuccess { print("Server started on port \($0!)") } // We never call close; run until we get killed. try server.flatMap { $0.onClose }.wait() } group.command( "list_tests", description: "List available test case names." ) { InteroperabilityTestCase.allCases.forEach { print($0.name) } } } group.run()