use exonum::{
helpers::Height,
runtime::{versioning::Version, InstanceStatus, SUPERVISOR_INSTANCE_ID},
};
use exonum_derive::*;
use exonum_explorer_service::api::BlocksRange;
use exonum_rust_runtime::{
api::ServiceApiBuilder, spec::Spec, DefaultInstance, Service, ServiceFactory,
};
use exonum_supervisor::api::DispatcherInfo;
use lazy_static::lazy_static;
use reqwest::RequestBuilder;
use serde::de::DeserializeOwned;
use tempfile::TempDir;
use tokio::time::delay_for;
use std::{
net::{Ipv4Addr, SocketAddr, TcpListener},
time::Duration,
};
use exonum_cli::NodeBuilder;
const PORTS: usize = 1;
lazy_static! {
static ref PUBLIC_ADDRS: Vec<SocketAddr> = unused_addresses(8_000, PORTS);
static ref PRIVATE_ADDRS: Vec<SocketAddr> = unused_addresses(9_000, PORTS);
}
fn unused_addresses(start: u16, count: usize) -> Vec<SocketAddr> {
let listeners: Vec<_> = (start..)
.filter_map(|port| TcpListener::bind((Ipv4Addr::LOCALHOST, port)).ok())
.take(count)
.collect();
listeners
.into_iter()
.map(|listener| listener.local_addr().unwrap())
.collect()
}
#[derive(Debug, Clone, Copy, ServiceDispatcher, ServiceFactory)]
#[service_factory(artifact_name = "simple-service", artifact_version = "0.1.0")]
struct SimpleService;
impl Service for SimpleService {
fn wire_api(&self, builder: &mut ServiceApiBuilder) {
builder
.public_scope()
.endpoint("answer", |_state, _query: ()| async { Ok(42) });
}
}
impl DefaultInstance for SimpleService {
const INSTANCE_ID: u32 = 100;
const INSTANCE_NAME: &'static str = "simple";
}
async fn send_request<T>(request: RequestBuilder) -> anyhow::Result<T>
where
T: DeserializeOwned,
{
request
.send()
.await?
.error_for_status()?
.json()
.await
.map_err(From::from)
}
#[tokio::test]
async fn node_basic_workflow() -> anyhow::Result<()> {
let public_addr = PUBLIC_ADDRS[0];
let public_api_root = format!("http://{}/api", public_addr);
let public_addr = public_addr.to_string();
let private_addr = PRIVATE_ADDRS[0].to_string();
let dir = TempDir::new()?;
let dir_path = dir.path().as_os_str();
let args = vec![
"run-dev".as_ref(),
"--blockchain-path".as_ref(),
dir_path,
"--public-api-address".as_ref(),
public_addr.as_ref(),
"--private-api-address".as_ref(),
private_addr.as_ref(),
];
let node = NodeBuilder::with_args(args)
.with(
Spec::new(SimpleService)
.with_default_instance()
.with_instance(200, "other", ()),
)
.execute_command()?
.unwrap();
let shutdown_handle = node.shutdown_handle();
let node_task = tokio::spawn(node.run());
delay_for(Duration::from_secs(2)).await;
let client = reqwest::Client::new();
let url = format!("{}/services/supervisor/services", public_api_root);
let info: DispatcherInfo = send_request(client.get(&url)).await?;
let simple_service_artifact = SimpleService.artifact_id();
assert!(info.artifacts.contains(&simple_service_artifact));
let has_simple_service = info.services.iter().any(|instance_state| {
let spec = &instance_state.spec;
spec.id == SimpleService::INSTANCE_ID
&& spec.name == SimpleService::INSTANCE_NAME
&& spec.artifact.name == "simple-service"
&& spec.artifact.version == Version::new(0, 1, 0)
&& instance_state.status == Some(InstanceStatus::Active)
});
assert!(has_simple_service);
let has_supervisor = info.services.iter().any(|instance_state| {
let spec = &instance_state.spec;
spec.id == SUPERVISOR_INSTANCE_ID && spec.name == "supervisor"
});
assert!(has_supervisor);
let url = format!(
"{}/explorer/v1/blocks?count=1&add_precommits=true",
public_api_root
);
loop {
let BlocksRange { blocks, .. } = send_request(client.get(&url)).await?;
assert_eq!(blocks.len(), 1);
if blocks[0].block.height > Height(0) {
assert_eq!(blocks[0].precommits.as_ref().unwrap().len(), 1);
break;
}
delay_for(Duration::from_millis(200)).await;
}
let url = format!("{}/services/simple/answer", public_api_root);
let answer: u64 = send_request(client.get(&url)).await?;
assert_eq!(answer, 42);
let url = format!("{}/services/other/answer", public_api_root);
let answer: u64 = send_request(client.get(&url)).await?;
assert_eq!(answer, 42);
shutdown_handle.shutdown().await?;
node_task.await??;
Ok(())
}