From e9acd00badeb766c97c279503b92c50109c1c0a3 Mon Sep 17 00:00:00 2001 From: Michael Bisgaard Olesen <mo@concordium.com> Date: Fri, 26 Jan 2024 13:44:45 +0100 Subject: [PATCH] Replace example tool and gRPC client "tests" with CLI (#14) The CLI is able to invoke all currently defined methods of `ConcordiumNodeClient`. The unit tests (which didn't actually perform any assertions) have been replaced with a script `test.sh` inside the CLI folder which executes all the same calls as these tests. This removes the need for an accessible node when running unit tests, meaning that we can start running `swift test` as part of the CI workflow. --- .github/workflows/build+test.yml | 4 +- .gitignore | 6 +- .../GrpcNodeClientTests.swift | 77 --------- examples/GetCryptographicParameters/README.md | 25 --- .../GetCryptographicParameters/main.swift | 26 --- .../Package.resolved | 48 ++++-- .../Package.swift | 8 +- examples/GrpcCli/README.md | 29 ++++ .../GrpcCli/Sources/GrpcCli/GrpcCli.swift | 153 ++++++++++++++++++ examples/GrpcCli/test.sh | 36 +++++ 10 files changed, 262 insertions(+), 150 deletions(-) delete mode 100644 Tests/ConcordiumSwiftSdkTests/GrpcNodeClientTests.swift delete mode 100644 examples/GetCryptographicParameters/README.md delete mode 100644 examples/GetCryptographicParameters/Sources/GetCryptographicParameters/main.swift rename examples/{GetCryptographicParameters => GrpcCli}/Package.resolved (74%) rename examples/{GetCryptographicParameters => GrpcCli}/Package.swift (56%) create mode 100644 examples/GrpcCli/README.md create mode 100644 examples/GrpcCli/Sources/GrpcCli/GrpcCli.swift create mode 100755 examples/GrpcCli/test.sh diff --git a/.github/workflows/build+test.yml b/.github/workflows/build+test.yml index 89c0e47..12f9dc3 100644 --- a/.github/workflows/build+test.yml +++ b/.github/workflows/build+test.yml @@ -34,5 +34,5 @@ jobs: run: swift package plugin --allow-writing-to-package-directory swiftformat --lint - name: Build project run: swift build - #- name: Run tests - # run: swift test + - name: Run tests + run: swift test diff --git a/.gitignore b/.gitignore index 61871ee..a855473 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .DS_Store -.build -/Packages +/.build/ +/Packages/ +/examples/*/.build/ +/examples/*/Packages/ xcuserdata/ DerivedData/ .swiftpm/configuration/registries.json diff --git a/Tests/ConcordiumSwiftSdkTests/GrpcNodeClientTests.swift b/Tests/ConcordiumSwiftSdkTests/GrpcNodeClientTests.swift deleted file mode 100644 index 0590662..0000000 --- a/Tests/ConcordiumSwiftSdkTests/GrpcNodeClientTests.swift +++ /dev/null @@ -1,77 +0,0 @@ -import Base58Check -@testable import ConcordiumSwiftSdk -import GRPC -import NIOPosix -import XCTest - -/// Temporary test for exercising the gRPC client. Note that there are no assertions on the result; it's only printed for manual inspection. -/// -/// To run one or more tests, adjust the channel target static field to point to a running node. -/// Alternatively, run the following command in a terminal to make the OS automatically -/// forward requests for localhost:20000 to [IP]:[port]: -/// -/// socat TCP-LISTEN:20000,fork TCP:[IP]:[port] -final class GrpcNodeClientTests: XCTestCase { - let channelTarget = ConnectionTarget.host("localhost", port: 20000) - let someBlockHash = "a21c1c18b70c64680a4eceea655ab68d164e8f1c82b8b8566388391d8da81e41" - let someAccountAddress = "35CJPZohio6Ztii2zy1AYzJKvuxbGG44wrBn7hLHiYLoF2nxnh" - - func testClientGetCryptographicParameters() async throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - let channel = try GRPCChannelPool.with( - target: channelTarget, - transportSecurity: .plaintext, - eventLoopGroup: group - ) - defer { - try! channel.close().wait() - } - let client = ConcordiumNodeGrpcClient(channel: channel) - let hash = try BlockHash(fromHexString: someBlockHash) - let res = try await client.getCryptographicParameters(at: .hash(hash)) - print(res) - } - - func testClientGetNextAccountSequenceNumber() async throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - let channel = try GRPCChannelPool.with( - target: channelTarget, - transportSecurity: .plaintext, - eventLoopGroup: group - ) - defer { - try! channel.close().wait() - } - let client = ConcordiumNodeGrpcClient(channel: channel) - let addr = try AccountAddress(base58Check: someAccountAddress) - let res = try await client.getNextAccountSequenceNumber(of: addr) - print(res) - } - - func testClientGetAccountInfo() async throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - let channel = try GRPCChannelPool.with( - target: channelTarget, - transportSecurity: .plaintext, - eventLoopGroup: group - ) - defer { - try! channel.close().wait() - } - let client = ConcordiumNodeGrpcClient(channel: channel) - let addr = try AccountAddress(base58Check: someAccountAddress) - let hash = try BlockHash(fromHexString: someBlockHash) - let account = AccountIdentifier.address(addr) - let res = try await client.getAccountInfo(of: account, at: .hash(hash)) - print(res) - } -} diff --git a/examples/GetCryptographicParameters/README.md b/examples/GetCryptographicParameters/README.md deleted file mode 100644 index fd932f5..0000000 --- a/examples/GetCryptographicParameters/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Example: getCryptographicParameters - -Small example program for illustrating how to use the SDK from a consuming application. -The SDK is added with the version defined as the `main` branch. -Note that this is not a fixed reference and the cache may retain old revisions of that branch. -Real applications should always use a specific version. - -The program just creates a `Client` and calls the method `getCryptographicParameters` on it (of last finalized block). -No command line arguments are accepted. -The program assumes to find a running node on localhost port 20000. -The tool `socat` may be used to redirect the requests to a node running on IP `<ip>`, port `<port>`: - -```shell -socat TCP-LISTEN:20000,fork TCP:<ip>:<port> -``` - -The result is printed as a simple struct dump. - -## Usage - -Build and run the program: - -```shell -swift run -``` diff --git a/examples/GetCryptographicParameters/Sources/GetCryptographicParameters/main.swift b/examples/GetCryptographicParameters/Sources/GetCryptographicParameters/main.swift deleted file mode 100644 index 332a63e..0000000 --- a/examples/GetCryptographicParameters/Sources/GetCryptographicParameters/main.swift +++ /dev/null @@ -1,26 +0,0 @@ -import ConcordiumSwiftSDK -import GRPC -import NIOPosix -import XCTest - -// Run the following command in a terminal to redirect to a node running on a different host/port: -// socat TCP-LISTEN:20000,fork TCP:<ip>:<port> -let channelTarget = ConnectionTarget.host("localhost", port: 20000) - -let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) -defer { - try! group.syncShutdownGracefully() -} - -let channel = try GRPCChannelPool.with( - target: channelTarget, - transportSecurity: .plaintext, - eventLoopGroup: group -) -defer { - try! channel.close().wait() -} - -let client = Client(channel: channel) -let res = try await client.getCryptographicParameters(at: .lastFinal) -print(res) diff --git a/examples/GetCryptographicParameters/Package.resolved b/examples/GrpcCli/Package.resolved similarity index 74% rename from examples/GetCryptographicParameters/Package.resolved rename to examples/GrpcCli/Package.resolved index 3c7f888..26d5e83 100644 --- a/examples/GetCryptographicParameters/Package.resolved +++ b/examples/GrpcCli/Package.resolved @@ -33,7 +33,7 @@ "location" : "https://github.com/Concordium/concordium-swift-sdk.git", "state" : { "branch" : "main", - "revision" : "89c89678e905fa5bca721a83213201d5b04b3b92" + "revision" : "0a5af878e0b3bf7d7e8c28715788ad34bb38961b" } }, { @@ -45,6 +45,15 @@ "version" : "1.21.0" } }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version" : "1.3.0" + } + }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -68,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-types", "state" : { - "revision" : "1827dc94bdab2eb5f2fc804e9b0cb43574282566", - "version" : "1.0.2" + "revision" : "12358d55a3824bd5fed310b999ea8cf83a9a1a65", + "version" : "1.0.3" } }, { @@ -77,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", - "version" : "1.5.3" + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" } }, { @@ -86,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "702cd7c56d5d44eeba73fdf83918339b26dc855c", - "version" : "2.62.0" + "revision" : "635b2589494c97e48c62514bc8b37ced762e0a62", + "version" : "2.63.0" } }, { @@ -95,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "798c962495593a23fdea0c0c63fd55571d8dff51", - "version" : "1.20.0" + "revision" : "363da63c1966405764f380c627409b2f9d9e710b", + "version" : "1.21.0" } }, { @@ -104,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { - "revision" : "3bd9004b9d685ed6b629760fc84903e48efec806", - "version" : "1.29.0" + "revision" : "0904bf0feb5122b7e5c3f15db7df0eabe623dd87", + "version" : "1.30.0" } }, { @@ -113,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9", - "version" : "2.25.0" + "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5", + "version" : "2.26.0" } }, { @@ -122,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-transport-services.git", "state" : { - "revision" : "ebf8b9c365a6ce043bf6e6326a04b15589bd285e", - "version" : "1.20.0" + "revision" : "6cbe0ed2b394f21ab0d46b9f0c50c6be964968ce", + "version" : "1.20.1" } }, { @@ -135,6 +144,15 @@ "version" : "1.25.2" } }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", + "version" : "1.2.1" + } + }, { "identity" : "swiftformat", "kind" : "remoteSourceControl", diff --git a/examples/GetCryptographicParameters/Package.swift b/examples/GrpcCli/Package.swift similarity index 56% rename from examples/GetCryptographicParameters/Package.swift rename to examples/GrpcCli/Package.swift index 51a9cff..e0cf224 100644 --- a/examples/GetCryptographicParameters/Package.swift +++ b/examples/GrpcCli/Package.swift @@ -3,18 +3,20 @@ import PackageDescription let package = Package( - name: "GetCryptographicParameters", + name: "GrpcCli", platforms: [ .macOS(.v10_15), ], dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"), .package(url: "https://github.com/Concordium/concordium-swift-sdk.git", branch: "main"), ], targets: [ .executableTarget( - name: "GetCryptographicParameters", + name: "GrpcCli", dependencies: [ - .product(name: "ConcordiumSwiftSDK", package: "concordium-swift-sdk"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "ConcordiumSwiftSdk", package: "concordium-swift-sdk"), ] ), ] diff --git a/examples/GrpcCli/README.md b/examples/GrpcCli/README.md new file mode 100644 index 0000000..a03c6a0 --- /dev/null +++ b/examples/GrpcCli/README.md @@ -0,0 +1,29 @@ +# GrpcCli + +A small tool for demonstrating how to integrate the SDK as well as exercising the gRPC client +which is otherwise hard to cover with unit tests. + +The CLI is organized into subcommands such as + +```shell +GrpcCli cryptographic-parameters +``` + +for retrieving the cryptographic parameters of the chain and + +```shell +GrpcCli account <account-address> info --block-hash=<block-hash> +``` + +for retrieving information about the account `<account-address>` as of block `<block-hash>`. + +By default, the tool attempts to query the gRPC interface of a node running on `localhost:20000`. +Use the options `--host` and `--ip` (or a relay tool such as `socat`) to point it somewhere else. + +Use `GrpcCli --help` to explore the full command interface. + +The script [`./test.sh`](./test.sh) invokes all available commands. +This will reveal if any of the commands exit with failure. +Note, however, that it only "checks" if the commands exit successfully; +it does not make assertions about the outputs. +Use the environment variables `HOST` and `IP` to specify the gRPC endpoint to use in the script. diff --git a/examples/GrpcCli/Sources/GrpcCli/GrpcCli.swift b/examples/GrpcCli/Sources/GrpcCli/GrpcCli.swift new file mode 100644 index 0000000..b97de3d --- /dev/null +++ b/examples/GrpcCli/Sources/GrpcCli/GrpcCli.swift @@ -0,0 +1,153 @@ +import ArgumentParser +import ConcordiumSwiftSdk +import GRPC +import NIOPosix + +struct GrpcOptions: ParsableArguments { + @Option(help: "IP or DNS name of the Node.") + var host = "localhost" + + @Option(help: "Port of the Node.") + var port = 20000 + + var target: ConnectionTarget { + ConnectionTarget.host(host, port: port) + } +} + +struct BlockOption: ParsableArguments { + @Option(help: "Hash of the block to query against. Defaults to last finalized block.") + var blockHash: String? + + var block: BlockIdentifier { + get throws { + if let blockHash { + return try .hash(BlockHash(fromHexString: blockHash)) + } + return .lastFinal + } + } +} + +struct AccountOption: ParsableArguments { + @Argument(help: "Address of the account to inspect.") + var accountAddress: String + + var address: AccountAddress { + get throws { + try AccountAddress(base58Check: accountAddress) + } + } + + var identifier: AccountIdentifier { + get throws { + try .address(address) + } + } +} + +@main +struct GrpcCli: AsyncParsableCommand { + @OptionGroup + var options: GrpcOptions + + static var configuration = CommandConfiguration( + abstract: "A CLI for demonstrating and testing use of the gRPC client of the SDK.", + version: "1.0.0", + subcommands: [CryptographicParameters.self, Account.self] + ) + + struct CryptographicParameters: AsyncParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Display the cryptographic parameters of the chain." + ) + + @OptionGroup + var root: GrpcCli + + @OptionGroup + var block: BlockOption + + func run() async throws { + let res = try await withClient(target: root.options.target) { + try await $0.getCryptographicParameters( + at: block.block + ) + } + print(res) + } + } + + struct Account: AsyncParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Subcommands related to a particular account.", + subcommands: [NextSequenceNumber.self, Info.self] + ) + + @OptionGroup + var account: AccountOption + + struct NextSequenceNumber: AsyncParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Display the next sequence number of the provided account." + ) + + @OptionGroup + var root: GrpcCli + + @OptionGroup + var account: Account + + func run() async throws { + let res = try await withClient(target: root.options.target) { + try await $0.getNextAccountSequenceNumber( + of: account.account.address + ) + } + print(res) + } + } + + struct Info: AsyncParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Display info of the provided account." + ) + + @OptionGroup + var root: GrpcCli + + @OptionGroup + var block: BlockOption + + @OptionGroup + var account: Account + + func run() async throws { + let res = try await withClient(target: root.options.target) { + try await $0.getAccountInfo( + of: account.account.identifier, + at: block.block + ) + } + print(res) + } + } + } +} + +func withClient<T>(target: ConnectionTarget, _ cmd: (ConcordiumNodeClient) async throws -> T) async throws -> T { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + try! group.syncShutdownGracefully() + } + let channel = try GRPCChannelPool.with( + target: target, + transportSecurity: .plaintext, + eventLoopGroup: group + ) + defer { + try! channel.close().wait() + } + let client = ConcordiumNodeGrpcClient(channel: channel) + return try await cmd(client) +} diff --git a/examples/GrpcCli/test.sh b/examples/GrpcCli/test.sh new file mode 100755 index 0000000..c7ff16c --- /dev/null +++ b/examples/GrpcCli/test.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env sh + +# Script that exercises all the commands of the CLI. +# If the build or the execution of any of the test/sample commands fail, +# then the script will exit immediately with a non-zero status code. +# However, even if the script completed successfully, +# the output still needs to be inspected manually to assert correctness +# as no such checks are performed by the script itself. + +set -eux + +# Location of the gRPC interface of a Concordium Node running on mainnet. +# +# Override via environment variables or use the following command to +# forward traffic for the default target (localhost:20000) to <HOST>:<PORT>: +# +# socat TCP-LISTEN:20000,fork TCP:[IP]:[port] + +host="${HOST-localhost}" +port="${PORT-20000}" + +# Test data (picked randomly from mainnet). +some_block_hash="a21c1c18b70c64680a4eceea655ab68d164e8f1c82b8b8566388391d8da81e41" +some_account_address="35CJPZohio6Ztii2zy1AYzJKvuxbGG44wrBn7hLHiYLoF2nxnh" + +# Build CLI. +swift build +dir_path="$(swift build --show-bin-path)" +cli_path="${dir_path}/GrpcCli" + +# Execute "tests". +"${cli_path}" --host="${host}" --port="${port}" cryptographic-parameters +"${cli_path}" --host="${host}" --port="${port}" cryptographic-parameters --block-hash="${some_block_hash}" +"${cli_path}" --host="${host}" --port="${port}" account "${some_account_address}" next-sequence-number +"${cli_path}" --host="${host}" --port="${port}" account "${some_account_address}" info +"${cli_path}" --host="${host}" --port="${port}" account "${some_account_address}" info --block-hash="${some_block_hash}"