Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ABCI integration test for minimal ABCI crate #797

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cargo/config
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
build-all = "build --workspace --all-targets --"
build-wasm-tendermint = "build -p tendermint --manifest-path tendermint/Cargo.toml --target wasm32-unknown-unknown --release --no-default-features --"
build-wasm-light-client = "build -p tendermint-light-client --manifest-path light-client/Cargo.toml --target wasm32-unknown-unknown --release --no-default-features --"
build-abci = "build --manifest-path abci/Cargo.toml --bin kvstore-rs --features binary,kvstore-app"
test-all-features = "test --all-features --no-fail-fast"
19 changes: 19 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Cargo
# will have compiled files and executables
/target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# These are log files emitted by model-based tests
**/*.log

# RPC probe results
/rpc-probe/probe-results/

# Proptest regressions dumps
**/*.proptest-regressions
1 change: 1 addition & 0 deletions tools/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]

members = [
"abci-test",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this now, in a follow-up PR, I'd like to replace the abci-test crate with the same kvstore-test that we use for testing Tendermint RPC compatibility. It'll take some more extensive testing, however, and this test does the job for now.

"kvstore-test",
"proto-compiler",
"rpc-probe"
Expand Down
20 changes: 20 additions & 0 deletions tools/abci-test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "abci-test"
version = "0.18.0"
authors = ["Thane Thomson <thane@informal.systems>"]
edition = "2018"
description = """
abci-test provides some end-to-end integration testing between
tendermint-abci and a full Tendermint node.
"""

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
futures = "0.3"
log = "0.4"
simple_logger = "1.11"
structopt = "0.3"
tendermint = { version = "0.18.0", path = "../../tendermint" }
tendermint-rpc = { version = "0.18.0", path = "../../rpc", features = [ "websocket-client" ] }
tokio = { version = "1", features = ["full"] }
52 changes: 52 additions & 0 deletions tools/abci-test/Makefile.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[env]
CONTAINER_NAME = "abci-test"
DOCKER_IMAGE = "informaldev/abci-harness:0.34.0"
HOST_RPC_PORT = 26657
CARGO_MAKE_WAIT_MILLISECONDS = 3500

# abci-test infrastructure:
# cargo make build-linux-abci - build the ABCI app using Docker (helpful on a Mac)
# cargo make - run the test harness and all tests. Expects a Linux ABCI app already built.
# cargo make docker-up-debug - troubleshoot the infra setup (useful to see docker error messages or the kvstore log).

[tasks.default]
clear = true
dependencies = [ "docker-up", "wait", "test", "docker-down" ]

[tasks.build-linux-abci]
command = "docker"
args = [ "run", "--rm", "--volume", "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/..:/usr/src/myapp", "--workdir", "/usr/src/myapp", "rust:latest", "cargo", "build-abci" ]

[tasks.docker-down]
dependencies = [ "docker-stop", "docker-rm" ]

[tasks.docker-up]
command = "docker"
args = ["run", "--name", "${CONTAINER_NAME}", "--rm", "--publish", "26657:${HOST_RPC_PORT}", "--volume", "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/../target/debug:/abci", "--detach", "${DOCKER_IMAGE}", "--verbose" ]
dependencies = ["docker-up-stop-old", "docker-up-rm-old"]

[tasks.docker-up-debug]
command = "docker"
args = ["run", "--name", "${CONTAINER_NAME}", "--rm", "--publish", "26657:${HOST_RPC_PORT}", "--volume", "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/../target/debug:/abci", "${DOCKER_IMAGE}", "--verbose" ]
dependencies = ["docker-up-stop-old", "docker-up-rm-old"]

[tasks.test]
args = ["run", "--all-features"]

[tasks.docker-stop]
command = "docker"
args = ["stop", "${CONTAINER_NAME}"]
ignore_errors = true
private = true

[tasks.docker-rm]
command = "docker"
args = ["rm", "--force", "${CONTAINER_NAME}"]
ignore_errors = true
private = true

[tasks.docker-up-stop-old]
alias = "docker-stop"

[tasks.docker-up-rm-old]
alias = "docker-rm"
179 changes: 179 additions & 0 deletions tools/abci-test/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
//! ABCI key/value store integration test application.

use futures::StreamExt;
use log::{debug, error, info, LevelFilter};
use simple_logger::SimpleLogger;
use structopt::StructOpt;
use tendermint::abci::Transaction;
use tendermint::net::Address;
use tendermint_rpc::event::EventData;
use tendermint_rpc::query::EventType;
use tendermint_rpc::{Client, SubscriptionClient, WebSocketClient};
use tokio::time::Duration;

#[derive(Debug, StructOpt)]
/// A harness for testing tendermint-abci through a full Tendermint node
/// running our in-memory key/value store application (kvstore-rs).
struct Opt {
/// Tendermint RPC host address.
#[structopt(short, long, default_value = "127.0.0.1")]
host: String,

/// Tendermint RPC port.
#[structopt(short, long, default_value = "26657")]
port: u16,

#[structopt(short, long)]
verbose: bool,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let opt: Opt = Opt::from_args();
SimpleLogger::new()
.with_level(if opt.verbose {
LevelFilter::Debug
} else {
LevelFilter::Info
})
.init()
.unwrap();

info!("Connecting to Tendermint node at {}:{}", opt.host, opt.port);
let (mut client, driver) = WebSocketClient::new(Address::Tcp {
peer_id: None,
host: opt.host,
port: opt.port,
})
.await?;
let driver_handle = tokio::spawn(async move { driver.run().await });
let result = run_tests(&mut client).await;
client.close()?;
driver_handle.await??;

match result {
Ok(_) => {
info!("Success!");
Ok(())
}
Err(e) => {
error!("Test failed: {:?}", e);
Err(e)
}
}
}

async fn run_tests(client: &mut WebSocketClient) -> Result<(), Box<dyn std::error::Error>> {
info!("Checking ABCI application version");
let abci_info = client.abci_info().await?;
debug!("Received: {:?}", abci_info);
if abci_info.data != "kvstore-rs" {
fail("abci_info", "data", "kvstore-rs", abci_info.data)?;
}

info!("Subscribing to transaction events");
let mut tx_subs = client.subscribe(EventType::Tx.into()).await?;

info!("Submitting a transaction");
let raw_tx_key = "test-key".as_bytes().to_vec();
let raw_tx_value = "test-value".as_bytes().to_vec();
let mut raw_tx = raw_tx_key.clone();
raw_tx.push('=' as u8);
raw_tx.extend(raw_tx_value.clone());

let _ = client
.broadcast_tx_async(Transaction::from(raw_tx.clone()))
.await?;

info!("Checking for transaction events");
let tx = tokio::time::timeout(Duration::from_secs(3), tx_subs.next())
.await?
.ok_or_else(|| {
fail(
"transaction subscription",
"transaction",
"returned",
"nothing",
)
.unwrap_err()
})??;
debug!("Got event: {:?}", tx);
match tx.data {
EventData::Tx { tx_result } => {
if tx_result.tx != raw_tx {
fail("transaction subscription", "tx", raw_tx, tx_result.tx)?;
}
}
_ => fail(
"transaction subscription",
"event data",
"of type Tx",
tx.data,
)?,
}
// Terminate our transaction subscription
drop(tx_subs);

info!("Waiting for at least one block to pass to ensure transaction has been committed");
let mut new_block_subs = client.subscribe(EventType::NewBlock.into()).await?;
let _ = new_block_subs.next().await.ok_or_else(|| {
fail("new block subscription", "event", "returned", "nothing").unwrap_err()
})??;
drop(new_block_subs);

info!(
"Querying for the value associated with key {}",
String::from_utf8(raw_tx_key.clone()).unwrap()
);
let res = client
.abci_query(None, raw_tx_key.clone(), None, false)
.await?;
if res.key != raw_tx_key {
fail("abci_query", "key", raw_tx_key, res.key)?;
}
if res.value != raw_tx_value {
fail("abci_query", "value", raw_tx_value, res.value)?;
}

Ok(())
}

fn fail<S1, S2, S3, S4>(
ctx: S1,
what: S2,
expected: S3,
actual: S4,
) -> Result<(), Box<dyn std::error::Error>>
where
S1: ToString,
S2: ToString,
S3: std::fmt::Debug,
S4: std::fmt::Debug,
{
Err(Box::new(AssertionError {
ctx: ctx.to_string(),
what: what.to_string(),
expected: format!("{:?}", expected),
actual: format!("{:?}", actual),
}))
}

#[derive(Debug)]
struct AssertionError {
ctx: String,
what: String,
expected: String,
actual: String,
}

impl std::fmt::Display for AssertionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"for {}, expected {} to be {}, but got {}",
self.ctx, self.what, self.expected, self.actual
)
}
}

impl std::error::Error for AssertionError {}
20 changes: 20 additions & 0 deletions tools/docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,23 @@ Both wallets have `uatom`, `stake` and `n0token` added.
Both wallets have an initial signed transaction created for easier population of the network before testing. These transactions
will send uatom tokens from c0 -> c1 and vice versa. They are both signed as `sequence 0` in the wallet, so they can only
be executed as the first transaction of the corresponding wallet.

# abci-harness
This image is used during CI testing in the abci-rs crate.
It tests compatibility with the Tendermint Go implementation.
It derives from the Tendermint Docker image above, but it expects a volume attached at `/abci` that contains the ABCI
application to be tested. The name of the ABCI application is `kvstore-rs` by default. This can be changed by setting the
`ABCI_APP` environment variable.

The image will fire up a Tendermint node (auto-creating the configuration) and then execute the ABCI application
from the attached volume. It logs the Tendermint node log into kvstore-rs.tendermint and the ABCI application log into
kvstore-rs.log on the attached volume.

This image has both the `muslc` and `glibc` libraries installed for easy testing of dynamically linked binaries.

Example:
```bash
docker run -it --rm -v $PWD/target/debug:/abci -p 26657:26657 informaldev/abci-harness:0.34.0
```

The image version reflects the Tendermint Go binary version.
31 changes: 31 additions & 0 deletions tools/docker/abci-harness-0.34.0/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
FROM alpine:3.13.1
LABEL maintainer="hello@informal.systems"

ENV TMHOME=/tendermint
#GLIBC for Alpine from: https://github.com/sgerrand/alpine-pkg-glibc
RUN wget https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub \
-O /etc/apk/keys/sgerrand.rsa.pub && \
wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.32-r0/glibc-2.32-r0.apk \
https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.32-r0/glibc-bin-2.32-r0.apk \
https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.32-r0/glibc-i18n-2.32-r0.apk && \
apk add --no-cache glibc-2.32-r0.apk glibc-bin-2.32-r0.apk glibc-i18n-2.32-r0.apk && \
rm glibc-2.32-r0.apk glibc-bin-2.32-r0.apk glibc-i18n-2.32-r0.apk && \
/usr/glibc-compat/bin/localedef -i en_US -f UTF-8 en_US.UTF-8 && \
apk --no-cache add jq bash file && \
wget https://github.com/freshautomations/sconfig/releases/download/v0.1.0/sconfig_linux_amd64 \
-O /usr/bin/sconfig && \
chmod 755 /usr/bin/sconfig && \
addgroup tendermint && \
adduser -S -G tendermint tendermint -h "$TMHOME"
USER tendermint
WORKDIR $TMHOME

EXPOSE 26656 26657 26658 26660
STOPSIGNAL SIGTERM

ARG TENDERMINT=tendermint
COPY $TENDERMINT /usr/bin/tendermint

COPY entrypoint /usr/bin/entrypoint
ENTRYPOINT ["/usr/bin/entrypoint"]
VOLUME [ "$TMHOME", "/abci" ]
40 changes: 40 additions & 0 deletions tools/docker/abci-harness-0.34.0/entrypoint
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env sh
set -euo pipefail

ABCI_PATH="/abci/${ABCI_APP:-kvstore-rs}"

if [ ! -x "${ABCI_PATH}" ]; then
echo "Could not find executable ABCI app at ${ABCI_PATH} ."
echo "Add a volume with the file and use the ABCI_APP environment variable to point to a different file."
exit 1
else
FILE_TYPE="$(file -b "${ABCI_PATH}")"
if [ -n "${FILE_TYPE##ELF 64-bit*}" ]; then
echo "File is not an ELF 64-bit binary (${FILE_TYPE})."
echo "Build the ABCI application for Linux using Docker:"
echo "docker run -it --rm --user \"\$(id -u)\":\"\$(id -g)\" -v \"\$PWD\":/usr/src/myapp -w /usr/src/myapp rust:latest cargo build-abci"
exit 1
fi
fi

if [ ! -d "${TMHOME}/config" ]; then

echo "Running tendermint init to create configuration."
/usr/bin/tendermint init

sconfig -s ${TMHOME}/config/config.toml \
moniker=${MONIKER:-dockernode} \
consensus.timeout_commit=500ms \
rpc.laddr=tcp://0.0.0.0:26657 \
p2p.addr_book_strict=false \
instrumentation.prometheus=true

sconfig -s ${TMHOME}/config/genesis.json \
chain_id=${CHAIN_ID:-dockerchain} \
consensus_params.block.time_iota_ms=500

fi

exec /usr/bin/tendermint node 2>&1 > "${ABCI_PATH}.tendermint" &

exec "${ABCI_PATH}" "$@" 2>&1 | tee "${ABCI_PATH}.log"
1 change: 1 addition & 0 deletions tools/docker/tendermint-0.34.0/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tendermint