feat(rpc): Implement `getblockchaininfo` RPC method (#3891)
* Implement `getblockchaininfo` RPC method * add a test for `get_blockchain_info` * fix tohex/fromhex * move comment * Update lightwalletd acceptance test for getblockchaininfo RPC (#3914) * change(rpc): Return getblockchaininfo network upgrades in height order (#3915) * Update lightwalletd acceptance test for getblockchaininfo RPC * Update some doc comments for network upgrades * List network upgrades in order in the getblockchaininfo RPC Also: - Use a constant for the "missing consensus branch ID" RPC value - Simplify fetching consensus branch IDs - Make RPC type derives consistent - Update RPC type documentation * Make RPC type derives consistent * Fix a confusing test comment * get hashand height at the same time * fix estimated_height * fix lint * add extra check Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> * fix typo Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> * split test Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> * fix(rpc): ignore an expected error in the RPC acceptance tests (#3961) * Add ignored regexes to test command failure regex methods * Ignore empty chain error in getblockchaininfo We expect this error when zebrad starts up with an empty state. Co-authored-by: teor <teor@riseup.net> Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
parent
ed5e85f8ae
commit
f687ab947f
|
@ -1942,6 +1942,7 @@ checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg 1.1.0",
|
"autocfg 1.1.0",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3870,6 +3871,7 @@ version = "1.0.79"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
"itoa 1.0.1",
|
"itoa 1.0.1",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -5746,9 +5748,11 @@ dependencies = [
|
||||||
name = "zebra-rpc"
|
name = "zebra-rpc"
|
||||||
version = "1.0.0-beta.0"
|
version = "1.0.0-beta.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"hex",
|
"hex",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
"indexmap",
|
||||||
"jsonrpc-core",
|
"jsonrpc-core",
|
||||||
"jsonrpc-derive",
|
"jsonrpc-derive",
|
||||||
"jsonrpc-http-server",
|
"jsonrpc-http-server",
|
||||||
|
|
|
@ -25,6 +25,9 @@ pub trait ChainTip {
|
||||||
/// Return the block hash of the best chain tip.
|
/// Return the block hash of the best chain tip.
|
||||||
fn best_tip_hash(&self) -> Option<block::Hash>;
|
fn best_tip_hash(&self) -> Option<block::Hash>;
|
||||||
|
|
||||||
|
/// Return the height and the hash of the best chain tip.
|
||||||
|
fn best_tip_height_and_hash(&self) -> Option<(block::Height, block::Hash)>;
|
||||||
|
|
||||||
/// Return the block time of the best chain tip.
|
/// Return the block time of the best chain tip.
|
||||||
fn best_tip_block_time(&self) -> Option<DateTime<Utc>>;
|
fn best_tip_block_time(&self) -> Option<DateTime<Utc>>;
|
||||||
|
|
||||||
|
@ -70,6 +73,10 @@ impl ChainTip for NoChainTip {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn best_tip_height_and_hash(&self) -> Option<(block::Height, block::Hash)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn best_tip_block_time(&self) -> Option<DateTime<Utc>> {
|
fn best_tip_block_time(&self) -> Option<DateTime<Utc>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,9 @@ pub struct MockChainTipSender {
|
||||||
/// A sender that sets the `best_tip_height` of a [`MockChainTip`].
|
/// A sender that sets the `best_tip_height` of a [`MockChainTip`].
|
||||||
best_tip_height: watch::Sender<Option<block::Height>>,
|
best_tip_height: watch::Sender<Option<block::Height>>,
|
||||||
|
|
||||||
|
/// A sender that sets the `best_tip_hash` of a [`MockChainTip`].
|
||||||
|
best_tip_hash: watch::Sender<Option<block::Hash>>,
|
||||||
|
|
||||||
/// A sender that sets the `best_tip_block_time` of a [`MockChainTip`].
|
/// A sender that sets the `best_tip_block_time` of a [`MockChainTip`].
|
||||||
best_tip_block_time: watch::Sender<Option<DateTime<Utc>>>,
|
best_tip_block_time: watch::Sender<Option<DateTime<Utc>>>,
|
||||||
}
|
}
|
||||||
|
@ -22,6 +25,9 @@ pub struct MockChainTip {
|
||||||
/// A mocked `best_tip_height` value set by the [`MockChainTipSender`].
|
/// A mocked `best_tip_height` value set by the [`MockChainTipSender`].
|
||||||
best_tip_height: watch::Receiver<Option<block::Height>>,
|
best_tip_height: watch::Receiver<Option<block::Height>>,
|
||||||
|
|
||||||
|
/// A mocked `best_tip_hash` value set by the [`MockChainTipSender`].
|
||||||
|
best_tip_hash: watch::Receiver<Option<block::Hash>>,
|
||||||
|
|
||||||
/// A mocked `best_tip_height` value set by the [`MockChainTipSender`].
|
/// A mocked `best_tip_height` value set by the [`MockChainTipSender`].
|
||||||
best_tip_block_time: watch::Receiver<Option<DateTime<Utc>>>,
|
best_tip_block_time: watch::Receiver<Option<DateTime<Utc>>>,
|
||||||
}
|
}
|
||||||
|
@ -35,15 +41,18 @@ impl MockChainTip {
|
||||||
/// Initially, the best tip height is [`None`].
|
/// Initially, the best tip height is [`None`].
|
||||||
pub fn new() -> (Self, MockChainTipSender) {
|
pub fn new() -> (Self, MockChainTipSender) {
|
||||||
let (height_sender, height_receiver) = watch::channel(None);
|
let (height_sender, height_receiver) = watch::channel(None);
|
||||||
|
let (hash_sender, hash_receiver) = watch::channel(None);
|
||||||
let (time_sender, time_receiver) = watch::channel(None);
|
let (time_sender, time_receiver) = watch::channel(None);
|
||||||
|
|
||||||
let mock_chain_tip = MockChainTip {
|
let mock_chain_tip = MockChainTip {
|
||||||
best_tip_height: height_receiver,
|
best_tip_height: height_receiver,
|
||||||
|
best_tip_hash: hash_receiver,
|
||||||
best_tip_block_time: time_receiver,
|
best_tip_block_time: time_receiver,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mock_chain_tip_sender = MockChainTipSender {
|
let mock_chain_tip_sender = MockChainTipSender {
|
||||||
best_tip_height: height_sender,
|
best_tip_height: height_sender,
|
||||||
|
best_tip_hash: hash_sender,
|
||||||
best_tip_block_time: time_sender,
|
best_tip_block_time: time_sender,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,7 +66,14 @@ impl ChainTip for MockChainTip {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn best_tip_hash(&self) -> Option<block::Hash> {
|
fn best_tip_hash(&self) -> Option<block::Hash> {
|
||||||
unreachable!("Method not used in tests");
|
*self.best_tip_hash.borrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn best_tip_height_and_hash(&self) -> Option<(block::Height, block::Hash)> {
|
||||||
|
let height = (*self.best_tip_height.borrow())?;
|
||||||
|
let hash = (*self.best_tip_hash.borrow())?;
|
||||||
|
|
||||||
|
Some((height, hash))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn best_tip_block_time(&self) -> Option<DateTime<Utc>> {
|
fn best_tip_block_time(&self) -> Option<DateTime<Utc>> {
|
||||||
|
@ -84,6 +100,13 @@ impl MockChainTipSender {
|
||||||
.expect("attempt to send a best tip height to a dropped `MockChainTip`");
|
.expect("attempt to send a best tip height to a dropped `MockChainTip`");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a new best tip hash to the [`MockChainTip`].
|
||||||
|
pub fn send_best_tip_hash(&self, hash: impl Into<Option<block::Hash>>) {
|
||||||
|
self.best_tip_hash
|
||||||
|
.send(hash.into())
|
||||||
|
.expect("attempt to send a best tip hash to a dropped `MockChainTip`");
|
||||||
|
}
|
||||||
|
|
||||||
/// Send a new best tip block time to the [`MockChainTip`].
|
/// Send a new best tip block time to the [`MockChainTip`].
|
||||||
pub fn send_best_tip_block_time(&self, block_time: impl Into<Option<DateTime<Utc>>>) {
|
pub fn send_best_tip_block_time(&self, block_time: impl Into<Option<DateTime<Utc>>>) {
|
||||||
self.best_tip_block_time
|
self.best_tip_block_time
|
||||||
|
|
|
@ -101,6 +101,15 @@ impl Network {
|
||||||
(canopy_activation + ZIP_212_GRACE_PERIOD_DURATION)
|
(canopy_activation + ZIP_212_GRACE_PERIOD_DURATION)
|
||||||
.expect("ZIP-212 grace period ends at a valid block height")
|
.expect("ZIP-212 grace period ends at a valid block height")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the network name as defined in
|
||||||
|
/// [BIP70](https://github.com/bitcoin/bips/blob/master/bip-0070.mediawiki#paymentdetailspaymentrequest)
|
||||||
|
pub fn bip70_network_name(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Network::Mainnet => "main".to_string(),
|
||||||
|
Network::Testnet => "test".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Network {
|
impl Default for Network {
|
||||||
|
|
|
@ -6,9 +6,11 @@ use crate::block;
|
||||||
use crate::parameters::{Network, Network::*};
|
use crate::parameters::{Network, Network::*};
|
||||||
|
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use std::fmt;
|
||||||
use std::ops::Bound::*;
|
use std::ops::Bound::*;
|
||||||
|
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
|
use hex::{FromHex, ToHex};
|
||||||
|
|
||||||
#[cfg(any(test, feature = "proptest-impl"))]
|
#[cfg(any(test, feature = "proptest-impl"))]
|
||||||
use proptest_derive::Arbitrary;
|
use proptest_derive::Arbitrary;
|
||||||
|
@ -118,15 +120,60 @@ const FAKE_TESTNET_ACTIVATION_HEIGHTS: &[(block::Height, NetworkUpgrade)] = &[
|
||||||
|
|
||||||
/// The Consensus Branch Id, used to bind transactions and blocks to a
|
/// The Consensus Branch Id, used to bind transactions and blocks to a
|
||||||
/// particular network upgrade.
|
/// particular network upgrade.
|
||||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct ConsensusBranchId(u32);
|
pub struct ConsensusBranchId(u32);
|
||||||
|
|
||||||
|
impl ConsensusBranchId {
|
||||||
|
/// Return the hash bytes in big-endian byte-order suitable for printing out byte by byte.
|
||||||
|
///
|
||||||
|
/// Zebra displays consensus branch IDs in big-endian byte-order,
|
||||||
|
/// following the convention set by zcashd.
|
||||||
|
fn bytes_in_display_order(&self) -> [u8; 4] {
|
||||||
|
self.0.to_be_bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<ConsensusBranchId> for u32 {
|
impl From<ConsensusBranchId> for u32 {
|
||||||
fn from(branch: ConsensusBranchId) -> u32 {
|
fn from(branch: ConsensusBranchId) -> u32 {
|
||||||
branch.0
|
branch.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToHex for &ConsensusBranchId {
|
||||||
|
fn encode_hex<T: FromIterator<char>>(&self) -> T {
|
||||||
|
self.bytes_in_display_order().encode_hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
|
||||||
|
self.bytes_in_display_order().encode_hex_upper()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToHex for ConsensusBranchId {
|
||||||
|
fn encode_hex<T: FromIterator<char>>(&self) -> T {
|
||||||
|
self.bytes_in_display_order().encode_hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
|
||||||
|
self.bytes_in_display_order().encode_hex_upper()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromHex for ConsensusBranchId {
|
||||||
|
type Error = <[u8; 4] as FromHex>::Error;
|
||||||
|
|
||||||
|
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
|
||||||
|
let branch = <[u8; 4]>::from_hex(hex)?;
|
||||||
|
Ok(ConsensusBranchId(u32::from_be_bytes(branch)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ConsensusBranchId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.write_str(&self.encode_hex::<String>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Network Upgrade Consensus Branch Ids.
|
/// Network Upgrade Consensus Branch Ids.
|
||||||
///
|
///
|
||||||
/// Branch ids are the same for mainnet and testnet. If there is a testnet
|
/// Branch ids are the same for mainnet and testnet. If there is a testnet
|
||||||
|
@ -175,8 +222,8 @@ const TESTNET_MINIMUM_DIFFICULTY_START_HEIGHT: block::Height = block::Height(299
|
||||||
pub const TESTNET_MAX_TIME_START_HEIGHT: block::Height = block::Height(653_606);
|
pub const TESTNET_MAX_TIME_START_HEIGHT: block::Height = block::Height(653_606);
|
||||||
|
|
||||||
impl NetworkUpgrade {
|
impl NetworkUpgrade {
|
||||||
/// Returns a BTreeMap of activation heights and network upgrades for
|
/// Returns a map between activation heights and network upgrades for `network`,
|
||||||
/// `network`.
|
/// in ascending height order.
|
||||||
///
|
///
|
||||||
/// If the activation height of a future upgrade is not known, that
|
/// If the activation height of a future upgrade is not known, that
|
||||||
/// network upgrade does not appear in the list.
|
/// network upgrade does not appear in the list.
|
||||||
|
@ -186,7 +233,7 @@ impl NetworkUpgrade {
|
||||||
/// When the environment variable TEST_FAKE_ACTIVATION_HEIGHTS is set
|
/// When the environment variable TEST_FAKE_ACTIVATION_HEIGHTS is set
|
||||||
/// and it's a test build, this returns a list of fake activation heights
|
/// and it's a test build, this returns a list of fake activation heights
|
||||||
/// used by some tests.
|
/// used by some tests.
|
||||||
pub(crate) fn activation_list(network: Network) -> BTreeMap<block::Height, NetworkUpgrade> {
|
pub fn activation_list(network: Network) -> BTreeMap<block::Height, NetworkUpgrade> {
|
||||||
let (mainnet_heights, testnet_heights) = {
|
let (mainnet_heights, testnet_heights) = {
|
||||||
#[cfg(not(feature = "zebra-test"))]
|
#[cfg(not(feature = "zebra-test"))]
|
||||||
{
|
{
|
||||||
|
@ -263,7 +310,7 @@ impl NetworkUpgrade {
|
||||||
NetworkUpgrade::activation_list(network).contains_key(&height)
|
NetworkUpgrade::activation_list(network).contains_key(&height)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a BTreeMap of NetworkUpgrades and their ConsensusBranchIds.
|
/// Returns an unordered mapping between NetworkUpgrades and their ConsensusBranchIds.
|
||||||
///
|
///
|
||||||
/// Branch ids are the same for mainnet and testnet.
|
/// Branch ids are the same for mainnet and testnet.
|
||||||
///
|
///
|
||||||
|
@ -410,6 +457,16 @@ impl NetworkUpgrade {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConsensusBranchId {
|
impl ConsensusBranchId {
|
||||||
|
/// The value used by `zcashd` RPCs for missing consensus branch IDs.
|
||||||
|
///
|
||||||
|
/// # Consensus
|
||||||
|
///
|
||||||
|
/// This value must only be used in RPCs.
|
||||||
|
///
|
||||||
|
/// The consensus rules handle missing branch IDs by rejecting blocks and transactions,
|
||||||
|
/// so this substitute value must not be used in consensus-critical code.
|
||||||
|
pub const RPC_MISSING_ID: ConsensusBranchId = ConsensusBranchId(0);
|
||||||
|
|
||||||
/// Returns the current consensus branch id for `network` and `height`.
|
/// Returns the current consensus branch id for `network` and `height`.
|
||||||
///
|
///
|
||||||
/// Returns None if the network has no branch id at this height.
|
/// Returns None if the network has no branch id at this height.
|
||||||
|
|
|
@ -233,3 +233,21 @@ fn branch_id_consistent(network: Network) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: split this file in unit.rs and prop.rs
|
||||||
|
use hex::{FromHex, ToHex};
|
||||||
|
use proptest::prelude::*;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test]
|
||||||
|
fn branch_id_hex_roundtrip(nu in any::<NetworkUpgrade>()) {
|
||||||
|
zebra_test::init();
|
||||||
|
|
||||||
|
if let Some(branch) = nu.branch_id() {
|
||||||
|
let hex_branch: String = branch.encode_hex();
|
||||||
|
let new_branch = ConsensusBranchId::from_hex(hex_branch.clone()).expect("hex branch_id should parse");
|
||||||
|
prop_assert_eq!(branch, new_branch);
|
||||||
|
prop_assert_eq!(hex_branch, new_branch.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ zebra-network = { path = "../zebra-network" }
|
||||||
zebra-node-services = { path = "../zebra-node-services" }
|
zebra-node-services = { path = "../zebra-node-services" }
|
||||||
zebra-state = { path = "../zebra-state" }
|
zebra-state = { path = "../zebra-state" }
|
||||||
|
|
||||||
|
chrono = "0.4.19"
|
||||||
futures = "0.3.21"
|
futures = "0.3.21"
|
||||||
|
|
||||||
# lightwalletd sends JSON-RPC requests over HTTP 1.1
|
# lightwalletd sends JSON-RPC requests over HTTP 1.1
|
||||||
|
@ -21,6 +22,9 @@ hyper = { version = "0.14.17", features = ["http1", "server"] }
|
||||||
jsonrpc-core = "18.0.0"
|
jsonrpc-core = "18.0.0"
|
||||||
jsonrpc-derive = "18.0.0"
|
jsonrpc-derive = "18.0.0"
|
||||||
jsonrpc-http-server = "18.0.0"
|
jsonrpc-http-server = "18.0.0"
|
||||||
|
# zebra-rpc needs the preserve_order feature in serde_json, which is a dependency of jsonrpc-core
|
||||||
|
serde_json = { version = "1.0.79", features = ["preserve_order"] }
|
||||||
|
indexmap = { version = "1.8.0", features = ["serde"] }
|
||||||
|
|
||||||
tokio = { version = "1.17.0", features = ["time", "rt-multi-thread", "macros", "tracing"] }
|
tokio = { version = "1.17.0", features = ["time", "rt-multi-thread", "macros", "tracing"] }
|
||||||
tower = "0.4.12"
|
tower = "0.4.12"
|
||||||
|
|
|
@ -4,20 +4,22 @@
|
||||||
//! as used by `lightwalletd.`
|
//! as used by `lightwalletd.`
|
||||||
//!
|
//!
|
||||||
//! Some parts of the `zcashd` RPC documentation are outdated.
|
//! Some parts of the `zcashd` RPC documentation are outdated.
|
||||||
//! So this implementation follows the `lightwalletd` client implementation.
|
//! So this implementation follows the `zcashd` server and `lightwalletd` client implementations.
|
||||||
|
|
||||||
use std::{collections::HashSet, io, sync::Arc};
|
use std::{collections::HashSet, io, sync::Arc};
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
use futures::{FutureExt, TryFutureExt};
|
use futures::{FutureExt, TryFutureExt};
|
||||||
use hex::{FromHex, ToHex};
|
use hex::{FromHex, ToHex};
|
||||||
|
use indexmap::IndexMap;
|
||||||
use jsonrpc_core::{self, BoxFuture, Error, ErrorCode, Result};
|
use jsonrpc_core::{self, BoxFuture, Error, ErrorCode, Result};
|
||||||
use jsonrpc_derive::rpc;
|
use jsonrpc_derive::rpc;
|
||||||
use tower::{buffer::Buffer, Service, ServiceExt};
|
use tower::{buffer::Buffer, Service, ServiceExt};
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block::{self, SerializedBlock},
|
block::{self, Height, SerializedBlock},
|
||||||
chain_tip::ChainTip,
|
chain_tip::ChainTip,
|
||||||
parameters::Network,
|
parameters::{ConsensusBranchId, Network, NetworkUpgrade},
|
||||||
serialization::{SerializationError, ZcashDeserialize},
|
serialization::{SerializationError, ZcashDeserialize},
|
||||||
transaction::{self, SerializedTransaction, Transaction},
|
transaction::{self, SerializedTransaction, Transaction},
|
||||||
};
|
};
|
||||||
|
@ -49,9 +51,10 @@ pub trait Rpc {
|
||||||
///
|
///
|
||||||
/// zcashd reference: [`getblockchaininfo`](https://zcash.github.io/rpc/getblockchaininfo.html)
|
/// zcashd reference: [`getblockchaininfo`](https://zcash.github.io/rpc/getblockchaininfo.html)
|
||||||
///
|
///
|
||||||
/// TODO in the context of https://github.com/ZcashFoundation/zebra/issues/3143:
|
/// # Notes
|
||||||
/// - list the arguments and fields that lightwalletd uses
|
///
|
||||||
/// - note any other lightwalletd changes
|
/// Some fields from the zcashd reference are missing from Zebra's [`GetBlockChainInfo`]. It only contains the fields
|
||||||
|
/// [required for lightwalletd support.](https://github.com/zcash/lightwalletd/blob/v0.4.9/common/common.go#L72-L89)
|
||||||
#[rpc(name = "getblockchaininfo")]
|
#[rpc(name = "getblockchaininfo")]
|
||||||
fn get_blockchain_info(&self) -> Result<GetBlockChainInfo>;
|
fn get_blockchain_info(&self) -> Result<GetBlockChainInfo>;
|
||||||
|
|
||||||
|
@ -216,11 +219,96 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_blockchain_info(&self) -> Result<GetBlockChainInfo> {
|
fn get_blockchain_info(&self) -> Result<GetBlockChainInfo> {
|
||||||
// TODO: dummy output data, fix in the context of #3143
|
let network = self.network;
|
||||||
// use self.latest_chain_tip.estimate_network_chain_tip_height()
|
|
||||||
// to estimate the current block height on the network
|
// `chain` field
|
||||||
|
let chain = self.network.bip70_network_name();
|
||||||
|
|
||||||
|
// `blocks` and `best_block_hash` fields
|
||||||
|
let (tip_height, tip_hash) = self
|
||||||
|
.latest_chain_tip
|
||||||
|
.best_tip_height_and_hash()
|
||||||
|
.ok_or_else(|| Error {
|
||||||
|
code: ErrorCode::ServerError(0),
|
||||||
|
message: "No Chain tip available yet".to_string(),
|
||||||
|
data: None,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// `estimated_height` field
|
||||||
|
let current_block_time =
|
||||||
|
self.latest_chain_tip
|
||||||
|
.best_tip_block_time()
|
||||||
|
.ok_or_else(|| Error {
|
||||||
|
code: ErrorCode::ServerError(0),
|
||||||
|
message: "No Chain tip available yet".to_string(),
|
||||||
|
data: None,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let zebra_estimated_height = self
|
||||||
|
.latest_chain_tip
|
||||||
|
.estimate_network_chain_tip_height(network, Utc::now())
|
||||||
|
.ok_or_else(|| Error {
|
||||||
|
code: ErrorCode::ServerError(0),
|
||||||
|
message: "No Chain tip available yet".to_string(),
|
||||||
|
data: None,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let estimated_height =
|
||||||
|
if current_block_time > Utc::now() || zebra_estimated_height < tip_height {
|
||||||
|
tip_height
|
||||||
|
} else {
|
||||||
|
zebra_estimated_height
|
||||||
|
};
|
||||||
|
|
||||||
|
// `upgrades` object
|
||||||
|
//
|
||||||
|
// Get the network upgrades in height order, like `zcashd`.
|
||||||
|
let mut upgrades = IndexMap::new();
|
||||||
|
for (activation_height, network_upgrade) in NetworkUpgrade::activation_list(network) {
|
||||||
|
// Zebra defines network upgrades based on incompatible consensus rule changes,
|
||||||
|
// but zcashd defines them based on ZIPs.
|
||||||
|
//
|
||||||
|
// All the network upgrades with a consensus branch ID are the same in Zebra and zcashd.
|
||||||
|
if let Some(branch_id) = network_upgrade.branch_id() {
|
||||||
|
// zcashd's RPC seems to ignore Disabled network upgrades, so Zebra does too.
|
||||||
|
let status = if tip_height >= activation_height {
|
||||||
|
NetworkUpgradeStatus::Active
|
||||||
|
} else {
|
||||||
|
NetworkUpgradeStatus::Pending
|
||||||
|
};
|
||||||
|
|
||||||
|
let upgrade = NetworkUpgradeInfo {
|
||||||
|
name: network_upgrade,
|
||||||
|
activation_height,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
upgrades.insert(ConsensusBranchIdHex(branch_id), upgrade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `consensus` object
|
||||||
|
let next_block_height =
|
||||||
|
(tip_height + 1).expect("valid chain tips are a lot less than Height::MAX");
|
||||||
|
let consensus = TipConsensusBranch {
|
||||||
|
chain_tip: ConsensusBranchIdHex(
|
||||||
|
NetworkUpgrade::current(network, tip_height)
|
||||||
|
.branch_id()
|
||||||
|
.unwrap_or(ConsensusBranchId::RPC_MISSING_ID),
|
||||||
|
),
|
||||||
|
next_block: ConsensusBranchIdHex(
|
||||||
|
NetworkUpgrade::current(network, next_block_height)
|
||||||
|
.branch_id()
|
||||||
|
.unwrap_or(ConsensusBranchId::RPC_MISSING_ID),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
let response = GetBlockChainInfo {
|
let response = GetBlockChainInfo {
|
||||||
chain: "TODO: main".to_string(),
|
chain,
|
||||||
|
blocks: tip_height.0,
|
||||||
|
best_block_hash: GetBestBlockHash(tip_hash),
|
||||||
|
estimated_height: estimated_height.0,
|
||||||
|
upgrades,
|
||||||
|
consensus,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
|
@ -432,44 +520,85 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
|
||||||
/// Response to a `getinfo` RPC request.
|
/// Response to a `getinfo` RPC request.
|
||||||
///
|
///
|
||||||
/// See the notes for the [`Rpc::get_info` method].
|
/// See the notes for the [`Rpc::get_info` method].
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct GetInfo {
|
pub struct GetInfo {
|
||||||
build: String,
|
build: String,
|
||||||
subversion: String,
|
subversion: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
|
||||||
/// Response to a `getblockchaininfo` RPC request.
|
/// Response to a `getblockchaininfo` RPC request.
|
||||||
///
|
///
|
||||||
/// See the notes for the [`Rpc::get_blockchain_info` method].
|
/// See the notes for the [`Rpc::get_blockchain_info` method].
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct GetBlockChainInfo {
|
pub struct GetBlockChainInfo {
|
||||||
chain: String,
|
chain: String,
|
||||||
// TODO: add other fields used by lightwalletd (#3143)
|
blocks: u32,
|
||||||
|
#[serde(rename = "bestblockhash")]
|
||||||
|
best_block_hash: GetBestBlockHash,
|
||||||
|
#[serde(rename = "estimatedheight")]
|
||||||
|
estimated_height: u32,
|
||||||
|
upgrades: IndexMap<ConsensusBranchIdHex, NetworkUpgradeInfo>,
|
||||||
|
consensus: TipConsensusBranch,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A hex-encoded [`ConsensusBranchId`] string.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct ConsensusBranchIdHex(#[serde(with = "hex")] ConsensusBranchId);
|
||||||
|
|
||||||
|
/// Information about [`NetworkUpgrade`] activation.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct NetworkUpgradeInfo {
|
||||||
|
name: NetworkUpgrade,
|
||||||
|
#[serde(rename = "activationheight")]
|
||||||
|
activation_height: Height,
|
||||||
|
status: NetworkUpgradeStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The activation status of a [`NetworkUpgrade`].
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
enum NetworkUpgradeStatus {
|
||||||
|
#[serde(rename = "active")]
|
||||||
|
Active,
|
||||||
|
#[serde(rename = "disabled")]
|
||||||
|
Disabled,
|
||||||
|
#[serde(rename = "pending")]
|
||||||
|
Pending,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [`ConsensusBranchId`]s for the tip and the next block.
|
||||||
|
///
|
||||||
|
/// These branch IDs are different when the next block is a network upgrade activation block.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct TipConsensusBranch {
|
||||||
|
#[serde(rename = "chaintip")]
|
||||||
|
chain_tip: ConsensusBranchIdHex,
|
||||||
|
#[serde(rename = "nextblock")]
|
||||||
|
next_block: ConsensusBranchIdHex,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
||||||
/// Response to a `sendrawtransaction` RPC request.
|
/// Response to a `sendrawtransaction` RPC request.
|
||||||
///
|
///
|
||||||
/// Contains the hex-encoded hash of the sent transaction.
|
/// Contains the hex-encoded hash of the sent transaction.
|
||||||
///
|
///
|
||||||
/// See the notes for the [`Rpc::send_raw_transaction` method].
|
/// See the notes for the [`Rpc::send_raw_transaction` method].
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct SentTransactionHash(#[serde(with = "hex")] transaction::Hash);
|
pub struct SentTransactionHash(#[serde(with = "hex")] transaction::Hash);
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
/// Response to a `getblock` RPC request.
|
/// Response to a `getblock` RPC request.
|
||||||
///
|
///
|
||||||
/// See the notes for the [`Rpc::get_block` method].
|
/// See the notes for the [`Rpc::get_block` method].
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||||
pub struct GetBlock(#[serde(with = "hex")] SerializedBlock);
|
pub struct GetBlock(#[serde(with = "hex")] SerializedBlock);
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, serde::Serialize)]
|
|
||||||
/// Response to a `getbestblockhash` RPC request.
|
/// Response to a `getbestblockhash` RPC request.
|
||||||
///
|
///
|
||||||
/// Contains the hex-encoded hash of the tip block.
|
/// Contains the hex-encoded hash of the tip block.
|
||||||
///
|
///
|
||||||
/// Also see the notes for the [`Rpc::get_best_block_hash` method].
|
/// Also see the notes for the [`Rpc::get_best_block_hash` method].
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct GetBestBlockHash(#[serde(with = "hex")] block::Hash);
|
pub struct GetBestBlockHash(#[serde(with = "hex")] block::Hash);
|
||||||
|
|
||||||
/// Response to a `getrawtransaction` RPC request.
|
/// Response to a `getrawtransaction` RPC request.
|
||||||
|
|
|
@ -9,8 +9,12 @@ use thiserror::Error;
|
||||||
use tower::buffer::Buffer;
|
use tower::buffer::Buffer;
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
chain_tip::NoChainTip,
|
block::{Block, Height},
|
||||||
parameters::Network::*,
|
chain_tip::{mock::MockChainTip, NoChainTip},
|
||||||
|
parameters::{
|
||||||
|
Network::{self, *},
|
||||||
|
NetworkUpgrade,
|
||||||
|
},
|
||||||
serialization::{ZcashDeserialize, ZcashSerialize},
|
serialization::{ZcashDeserialize, ZcashSerialize},
|
||||||
transaction::{self, Transaction, UnminedTx, UnminedTxId},
|
transaction::{self, Transaction, UnminedTx, UnminedTxId},
|
||||||
};
|
};
|
||||||
|
@ -19,7 +23,7 @@ use zebra_state::BoxError;
|
||||||
|
|
||||||
use zebra_test::mock_service::MockService;
|
use zebra_test::mock_service::MockService;
|
||||||
|
|
||||||
use super::super::{Rpc, RpcImpl, SentTransactionHash};
|
use super::super::{NetworkUpgradeStatus, Rpc, RpcImpl, SentTransactionHash};
|
||||||
|
|
||||||
proptest! {
|
proptest! {
|
||||||
/// Test that when sending a raw transaction, it is received by the mempool service.
|
/// Test that when sending a raw transaction, it is received by the mempool service.
|
||||||
|
@ -416,6 +420,94 @@ proptest! {
|
||||||
),
|
),
|
||||||
"Result is not an invalid parameters error: {result:?}"
|
"Result is not an invalid parameters error: {result:?}"
|
||||||
);
|
);
|
||||||
|
Ok::<_, TestCaseError>(())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test the `get_blockchain_info` response when Zebra's state is empty.
|
||||||
|
#[test]
|
||||||
|
fn get_blockchain_info_response_without_a_chain_tip(network in any::<Network>()) {
|
||||||
|
let runtime = zebra_test::init_async();
|
||||||
|
let _guard = runtime.enter();
|
||||||
|
let mut mempool = MockService::build().for_prop_tests();
|
||||||
|
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
|
||||||
|
// look for an error with a `NoChainTip`
|
||||||
|
let rpc = RpcImpl::new(
|
||||||
|
"RPC test",
|
||||||
|
Buffer::new(mempool.clone(), 1),
|
||||||
|
Buffer::new(state.clone(), 1),
|
||||||
|
NoChainTip,
|
||||||
|
network,
|
||||||
|
);
|
||||||
|
let response = rpc.get_blockchain_info();
|
||||||
|
prop_assert_eq!(&response.err().unwrap().message, "No Chain tip available yet");
|
||||||
|
runtime.block_on(async move {
|
||||||
|
mempool.expect_no_requests().await?;
|
||||||
|
state.expect_no_requests().await?;
|
||||||
|
Ok::<_, TestCaseError>(())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test the `get_blockchain_info` response using an arbitrary block as the `ChainTip`.
|
||||||
|
#[test]
|
||||||
|
fn get_blockchain_info_response_with_an_arbitrary_chain_tip(
|
||||||
|
network in any::<Network>(),
|
||||||
|
block in any::<Block>(),
|
||||||
|
) {
|
||||||
|
let runtime = zebra_test::init_async();
|
||||||
|
let _guard = runtime.enter();
|
||||||
|
let mut mempool = MockService::build().for_prop_tests();
|
||||||
|
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
|
||||||
|
|
||||||
|
// get block data
|
||||||
|
let block_height = block.coinbase_height().unwrap();
|
||||||
|
let block_hash = block.hash();
|
||||||
|
let block_time = block.header.time;
|
||||||
|
|
||||||
|
// create a mocked `ChainTip`
|
||||||
|
let (chain_tip, mock_chain_tip_sender) = MockChainTip::new();
|
||||||
|
mock_chain_tip_sender.send_best_tip_height(block_height);
|
||||||
|
mock_chain_tip_sender.send_best_tip_hash(block_hash);
|
||||||
|
mock_chain_tip_sender.send_best_tip_block_time(block_time);
|
||||||
|
|
||||||
|
// Start RPC with the mocked `ChainTip`
|
||||||
|
let rpc = RpcImpl::new(
|
||||||
|
"RPC test",
|
||||||
|
Buffer::new(mempool.clone(), 1),
|
||||||
|
Buffer::new(state.clone(), 1),
|
||||||
|
chain_tip,
|
||||||
|
network,
|
||||||
|
);
|
||||||
|
let response = rpc.get_blockchain_info();
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
match response {
|
||||||
|
Ok(info) => {
|
||||||
|
prop_assert_eq!(info.chain, network.bip70_network_name());
|
||||||
|
prop_assert_eq!(info.blocks, block_height.0);
|
||||||
|
prop_assert_eq!(info.best_block_hash.0, block_hash);
|
||||||
|
prop_assert!(info.estimated_height < Height::MAX.0);
|
||||||
|
|
||||||
|
prop_assert_eq!(info.consensus.chain_tip.0, NetworkUpgrade::current(network, block_height).branch_id().unwrap());
|
||||||
|
prop_assert_eq!(info.consensus.next_block.0, NetworkUpgrade::current(network, (block_height + 1).unwrap()).branch_id().unwrap());
|
||||||
|
|
||||||
|
for u in info.upgrades {
|
||||||
|
let mut status = NetworkUpgradeStatus::Active;
|
||||||
|
if block_height < u.1.activation_height {
|
||||||
|
status = NetworkUpgradeStatus::Pending;
|
||||||
|
}
|
||||||
|
prop_assert_eq!(u.1.status, status);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
unreachable!("Test should never error with the data we are feeding it")
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// check no requests were made during this test
|
||||||
|
runtime.block_on(async move {
|
||||||
|
mempool.expect_no_requests().await?;
|
||||||
|
state.expect_no_requests().await?;
|
||||||
|
|
||||||
Ok::<_, TestCaseError>(())
|
Ok::<_, TestCaseError>(())
|
||||||
})?;
|
})?;
|
||||||
|
|
|
@ -333,6 +333,11 @@ impl ChainTip for LatestChainTip {
|
||||||
self.with_chain_tip_block(|block| block.hash)
|
self.with_chain_tip_block(|block| block.hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
fn best_tip_height_and_hash(&self) -> Option<(block::Height, block::Hash)> {
|
||||||
|
self.with_chain_tip_block(|block| (block.height, block.hash))
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
fn best_tip_block_time(&self) -> Option<DateTime<Utc>> {
|
fn best_tip_block_time(&self) -> Option<DateTime<Utc>> {
|
||||||
self.with_chain_tip_block(|block| block.time)
|
self.with_chain_tip_block(|block| block.time)
|
||||||
|
|
|
@ -104,6 +104,7 @@ impl CommandExt for Command {
|
||||||
stdout: None,
|
stdout: None,
|
||||||
stderr: None,
|
stderr: None,
|
||||||
failure_regexes: RegexSet::empty(),
|
failure_regexes: RegexSet::empty(),
|
||||||
|
ignore_regexes: RegexSet::empty(),
|
||||||
deadline: None,
|
deadline: None,
|
||||||
bypass_test_capture: false,
|
bypass_test_capture: false,
|
||||||
})
|
})
|
||||||
|
@ -204,6 +205,15 @@ pub struct TestChild<T> {
|
||||||
/// If any line matches any failure regex, the test fails.
|
/// If any line matches any failure regex, the test fails.
|
||||||
failure_regexes: RegexSet,
|
failure_regexes: RegexSet,
|
||||||
|
|
||||||
|
/// Command outputs which are ignored when checking for test failure.
|
||||||
|
/// These regexes override `failure_regexes`.
|
||||||
|
///
|
||||||
|
/// This list of regexes is matches against `stdout` or `stderr`,
|
||||||
|
/// in every method that reads command output.
|
||||||
|
///
|
||||||
|
/// If a line matches any ignore regex, the failure regex check is skipped for that line.
|
||||||
|
ignore_regexes: RegexSet,
|
||||||
|
|
||||||
/// The deadline for this command to finish.
|
/// The deadline for this command to finish.
|
||||||
///
|
///
|
||||||
/// Only checked when the command outputs each new line (#1140).
|
/// Only checked when the command outputs each new line (#1140).
|
||||||
|
@ -215,23 +225,51 @@ pub struct TestChild<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks command output log `line` from `cmd` against a `failure_regexes` regex set,
|
/// Checks command output log `line` from `cmd` against a `failure_regexes` regex set,
|
||||||
/// and panics if any regex matches the log line.
|
/// and panics if any regex matches. The line is skipped if it matches `ignore_regexes`.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// - if any stdout or stderr lines match any failure regex
|
/// - if any stdout or stderr lines match any failure regex, but do not match any ignore regex
|
||||||
pub fn check_failure_regexes(
|
pub fn check_failure_regexes(
|
||||||
line: &std::io::Result<String>,
|
line: &std::io::Result<String>,
|
||||||
failure_regexes: &RegexSet,
|
failure_regexes: &RegexSet,
|
||||||
|
ignore_regexes: &RegexSet,
|
||||||
cmd: &str,
|
cmd: &str,
|
||||||
|
bypass_test_capture: bool,
|
||||||
) {
|
) {
|
||||||
if let Ok(line) = line {
|
if let Ok(line) = line {
|
||||||
|
let ignore_matches = ignore_regexes.matches(line);
|
||||||
|
let ignore_matches: Vec<&str> = ignore_matches
|
||||||
|
.iter()
|
||||||
|
.map(|index| ignore_regexes.patterns()[index].as_str())
|
||||||
|
.collect();
|
||||||
|
|
||||||
let failure_matches = failure_regexes.matches(line);
|
let failure_matches = failure_regexes.matches(line);
|
||||||
let failure_matches: Vec<&str> = failure_matches
|
let failure_matches: Vec<&str> = failure_matches
|
||||||
.iter()
|
.iter()
|
||||||
.map(|index| failure_regexes.patterns()[index].as_str())
|
.map(|index| failure_regexes.patterns()[index].as_str())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
if !ignore_matches.is_empty() {
|
||||||
|
let ignore_matches = ignore_matches.join(",");
|
||||||
|
|
||||||
|
let ignore_msg = if failure_matches.is_empty() {
|
||||||
|
format!(
|
||||||
|
"Log matched ignore regexes: {:?}, but no failure regexes",
|
||||||
|
ignore_matches,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let failure_matches = failure_matches.join(",");
|
||||||
|
format!(
|
||||||
|
"Ignoring failure regexes: {:?}, because log matched ignore regexes: {:?}",
|
||||||
|
failure_matches, ignore_matches,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
write_to_test_logs(ignore_msg, bypass_test_capture);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
failure_matches.is_empty(),
|
failure_matches.is_empty(),
|
||||||
"test command:\n\
|
"test command:\n\
|
||||||
|
@ -247,64 +285,124 @@ pub fn check_failure_regexes(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write `line` to stdout, so it can be seen in the test logs.
|
||||||
|
///
|
||||||
|
/// Set `bypass_test_capture` to `true` or
|
||||||
|
/// use `cargo test -- --nocapture` to see this output.
|
||||||
|
///
|
||||||
|
/// May cause weird reordering for stdout / stderr.
|
||||||
|
/// Uses stdout even if the original lines were from stderr.
|
||||||
|
#[allow(clippy::print_stdout)]
|
||||||
|
fn write_to_test_logs<S>(line: S, bypass_test_capture: bool)
|
||||||
|
where
|
||||||
|
S: AsRef<str>,
|
||||||
|
{
|
||||||
|
let line = line.as_ref();
|
||||||
|
|
||||||
|
if bypass_test_capture {
|
||||||
|
// Send lines directly to the terminal (or process stdout file redirect).
|
||||||
|
#[allow(clippy::explicit_write)]
|
||||||
|
writeln!(std::io::stdout(), "{}", line).unwrap();
|
||||||
|
} else {
|
||||||
|
// If the test fails, the test runner captures and displays this output.
|
||||||
|
// To show this output unconditionally, use `cargo test -- --nocapture`.
|
||||||
|
println!("{}", line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some OSes require a flush to send all output to the terminal.
|
||||||
|
let _ = std::io::stdout().lock().flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`CollectRegexSet`] iterator that never matches anything.
|
||||||
|
///
|
||||||
|
/// Used to work around type inference issues in [`TestChild::with_failure_regex_iter`].
|
||||||
|
pub const NO_MATCHES_REGEX_ITER: &[&str] = &[];
|
||||||
|
|
||||||
impl<T> TestChild<T> {
|
impl<T> TestChild<T> {
|
||||||
/// Sets up command output so it is checked against a failure regex set.
|
/// Sets up command output so each line is checked against a failure regex set,
|
||||||
|
/// unless it matches any of the ignore regexes.
|
||||||
|
///
|
||||||
/// The failure regexes are ignored by `wait_with_output`.
|
/// The failure regexes are ignored by `wait_with_output`.
|
||||||
///
|
///
|
||||||
/// [`TestChild::with_failure_regexes`] wrapper for strings, [`Regex`]es,
|
/// To never match any log lines, use `RegexSet::empty()`.
|
||||||
/// and [`RegexSet`]s.
|
///
|
||||||
|
/// This method is a [`TestChild::with_failure_regexes`] wrapper for
|
||||||
|
/// strings, [`Regex`]es, and [`RegexSet`]s.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// - adds a panic to any method that reads output,
|
/// - adds a panic to any method that reads output,
|
||||||
/// if any stdout or stderr lines match any failure regex
|
/// if any stdout or stderr lines match any failure regex
|
||||||
pub fn with_failure_regex_set<R>(self, failure_regexes: R) -> Self
|
pub fn with_failure_regex_set<F, X>(self, failure_regexes: F, ignore_regexes: X) -> Self
|
||||||
where
|
where
|
||||||
R: ToRegexSet,
|
F: ToRegexSet,
|
||||||
|
X: ToRegexSet,
|
||||||
{
|
{
|
||||||
let failure_regexes = failure_regexes
|
let failure_regexes = failure_regexes
|
||||||
.to_regex_set()
|
.to_regex_set()
|
||||||
.expect("regexes must be valid");
|
.expect("failure regexes must be valid");
|
||||||
|
|
||||||
self.with_failure_regexes(failure_regexes)
|
let ignore_regexes = ignore_regexes
|
||||||
|
.to_regex_set()
|
||||||
|
.expect("ignore regexes must be valid");
|
||||||
|
|
||||||
|
self.with_failure_regexes(failure_regexes, ignore_regexes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets up command output so it is checked against a failure regex set.
|
/// Sets up command output so each line is checked against a failure regex set,
|
||||||
|
/// unless it matches any of the ignore regexes.
|
||||||
|
///
|
||||||
/// The failure regexes are ignored by `wait_with_output`.
|
/// The failure regexes are ignored by `wait_with_output`.
|
||||||
///
|
///
|
||||||
/// [`TestChild::with_failure_regexes`] wrapper for regular expression iterators.
|
/// To never match any log lines, use [`NO_MATCHES_REGEX_ITER`].
|
||||||
|
///
|
||||||
|
/// This method is a [`TestChild::with_failure_regexes`] wrapper for
|
||||||
|
/// regular expression iterators.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// - adds a panic to any method that reads output,
|
/// - adds a panic to any method that reads output,
|
||||||
/// if any stdout or stderr lines match any failure regex
|
/// if any stdout or stderr lines match any failure regex
|
||||||
pub fn with_failure_regex_iter<I>(self, failure_regexes: I) -> Self
|
pub fn with_failure_regex_iter<F, X>(self, failure_regexes: F, ignore_regexes: X) -> Self
|
||||||
where
|
where
|
||||||
I: CollectRegexSet,
|
F: CollectRegexSet,
|
||||||
|
X: CollectRegexSet,
|
||||||
{
|
{
|
||||||
let failure_regexes = failure_regexes
|
let failure_regexes = failure_regexes
|
||||||
.collect_regex_set()
|
.collect_regex_set()
|
||||||
.expect("regexes must be valid");
|
.expect("failure regexes must be valid");
|
||||||
|
|
||||||
self.with_failure_regexes(failure_regexes)
|
let ignore_regexes = ignore_regexes
|
||||||
|
.collect_regex_set()
|
||||||
|
.expect("ignore regexes must be valid");
|
||||||
|
|
||||||
|
self.with_failure_regexes(failure_regexes, ignore_regexes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets up command output so it is checked against a failure regex set.
|
/// Sets up command output so each line is checked against a failure regex set,
|
||||||
|
/// unless it matches any of the ignore regexes.
|
||||||
|
///
|
||||||
/// The failure regexes are ignored by `wait_with_output`.
|
/// The failure regexes are ignored by `wait_with_output`.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// - adds a panic to any method that reads output,
|
/// - adds a panic to any method that reads output,
|
||||||
/// if any stdout or stderr lines match any failure regex
|
/// if any stdout or stderr lines match any failure regex
|
||||||
pub fn with_failure_regexes(mut self, failure_regexes: RegexSet) -> Self {
|
pub fn with_failure_regexes(
|
||||||
|
mut self,
|
||||||
|
failure_regexes: RegexSet,
|
||||||
|
ignore_regexes: impl Into<Option<RegexSet>>,
|
||||||
|
) -> Self {
|
||||||
self.failure_regexes = failure_regexes;
|
self.failure_regexes = failure_regexes;
|
||||||
|
self.ignore_regexes = ignore_regexes.into().unwrap_or_else(RegexSet::empty);
|
||||||
|
|
||||||
self.apply_failure_regexes_to_outputs();
|
self.apply_failure_regexes_to_outputs();
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Applies the failure regex set to command output.
|
/// Applies the failure and ignore regex sets to command output.
|
||||||
|
///
|
||||||
/// The failure regexes are ignored by `wait_with_output`.
|
/// The failure regexes are ignored by `wait_with_output`.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
|
@ -329,7 +427,8 @@ impl<T> TestChild<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maps a reader into a string line iterator.
|
/// Maps a reader into a string line iterator,
|
||||||
|
/// and applies the failure and ignore regex sets to it.
|
||||||
fn map_into_string_lines<R>(
|
fn map_into_string_lines<R>(
|
||||||
&self,
|
&self,
|
||||||
reader: R,
|
reader: R,
|
||||||
|
@ -338,11 +437,20 @@ impl<T> TestChild<T> {
|
||||||
R: Read + Debug + 'static,
|
R: Read + Debug + 'static,
|
||||||
{
|
{
|
||||||
let failure_regexes = self.failure_regexes.clone();
|
let failure_regexes = self.failure_regexes.clone();
|
||||||
|
let ignore_regexes = self.ignore_regexes.clone();
|
||||||
let cmd = self.cmd.clone();
|
let cmd = self.cmd.clone();
|
||||||
|
let bypass_test_capture = self.bypass_test_capture;
|
||||||
|
|
||||||
let reader = BufReader::new(reader);
|
let reader = BufReader::new(reader);
|
||||||
let lines = BufRead::lines(reader)
|
let lines = BufRead::lines(reader).inspect(move |line| {
|
||||||
.inspect(move |line| check_failure_regexes(line, &failure_regexes, &cmd));
|
check_failure_regexes(
|
||||||
|
line,
|
||||||
|
&failure_regexes,
|
||||||
|
&ignore_regexes,
|
||||||
|
&cmd,
|
||||||
|
bypass_test_capture,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
Box::new(lines) as _
|
Box::new(lines) as _
|
||||||
}
|
}
|
||||||
|
@ -385,6 +493,7 @@ impl<T> TestChild<T> {
|
||||||
while self.wait_for_stdout_line(None) {}
|
while self.wait_for_stdout_line(None) {}
|
||||||
|
|
||||||
if wrote_lines {
|
if wrote_lines {
|
||||||
|
// Write an empty line, to make output more readable
|
||||||
self.write_to_test_logs("");
|
self.write_to_test_logs("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -683,20 +792,7 @@ impl<T> TestChild<T> {
|
||||||
where
|
where
|
||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
{
|
{
|
||||||
let line = line.as_ref();
|
write_to_test_logs(line, self.bypass_test_capture);
|
||||||
|
|
||||||
if self.bypass_test_capture {
|
|
||||||
// Send lines directly to the terminal (or process stdout file redirect).
|
|
||||||
#[allow(clippy::explicit_write)]
|
|
||||||
writeln!(std::io::stdout(), "{}", line).unwrap();
|
|
||||||
} else {
|
|
||||||
// If the test fails, the test runner captures and displays this output.
|
|
||||||
// To show this output unconditionally, use `cargo test -- --nocapture`.
|
|
||||||
println!("{}", line);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some OSes require a flush to send all output to the terminal.
|
|
||||||
let _ = std::io::stdout().lock().flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kill `child`, wait for its output, and use that output as the context for
|
/// Kill `child`, wait for its output, and use that output as the context for
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
use std::{process::Command, time::Duration};
|
use std::{process::Command, time::Duration};
|
||||||
|
|
||||||
use color_eyre::eyre::{eyre, Result};
|
use color_eyre::eyre::{eyre, Result};
|
||||||
|
use regex::RegexSet;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
use zebra_test::{command::TestDirExt, prelude::Stdio};
|
use zebra_test::{
|
||||||
|
command::{TestDirExt, NO_MATCHES_REGEX_ITER},
|
||||||
|
prelude::Stdio,
|
||||||
|
};
|
||||||
|
|
||||||
/// Returns true if `cmd` with `args` runs successfully.
|
/// Returns true if `cmd` with `args` runs successfully.
|
||||||
///
|
///
|
||||||
|
@ -200,7 +204,7 @@ fn failure_regex_matches_stdout_failure_message() {
|
||||||
.spawn_child_with_command(TEST_CMD, &["failure_message"])
|
.spawn_child_with_command(TEST_CMD, &["failure_message"])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_timeout(Duration::from_secs(2))
|
.with_timeout(Duration::from_secs(2))
|
||||||
.with_failure_regex_set("fail");
|
.with_failure_regex_set("fail", RegexSet::empty());
|
||||||
|
|
||||||
// Any method that reads output should work here.
|
// Any method that reads output should work here.
|
||||||
// We use a non-matching regex, to trigger the failure panic.
|
// We use a non-matching regex, to trigger the failure panic.
|
||||||
|
@ -236,7 +240,7 @@ fn failure_regex_matches_stderr_failure_message() {
|
||||||
.spawn_child_with_command(TEST_CMD, &["-c", "read -t 1 -p failure_message"])
|
.spawn_child_with_command(TEST_CMD, &["-c", "read -t 1 -p failure_message"])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_timeout(Duration::from_secs(5))
|
.with_timeout(Duration::from_secs(5))
|
||||||
.with_failure_regex_set("fail");
|
.with_failure_regex_set("fail", RegexSet::empty());
|
||||||
|
|
||||||
// Any method that reads output should work here.
|
// Any method that reads output should work here.
|
||||||
// We use a non-matching regex, to trigger the failure panic.
|
// We use a non-matching regex, to trigger the failure panic.
|
||||||
|
@ -266,7 +270,7 @@ fn failure_regex_matches_stdout_failure_message_drop() {
|
||||||
.spawn_child_with_command(TEST_CMD, &["failure_message"])
|
.spawn_child_with_command(TEST_CMD, &["failure_message"])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_timeout(Duration::from_secs(5))
|
.with_timeout(Duration::from_secs(5))
|
||||||
.with_failure_regex_set("fail");
|
.with_failure_regex_set("fail", RegexSet::empty());
|
||||||
|
|
||||||
// Give the child process enough time to print its output.
|
// Give the child process enough time to print its output.
|
||||||
std::thread::sleep(Duration::from_secs(1));
|
std::thread::sleep(Duration::from_secs(1));
|
||||||
|
@ -295,7 +299,7 @@ fn failure_regex_matches_stdout_failure_message_kill() {
|
||||||
.spawn_child_with_command(TEST_CMD, &["failure_message"])
|
.spawn_child_with_command(TEST_CMD, &["failure_message"])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_timeout(Duration::from_secs(5))
|
.with_timeout(Duration::from_secs(5))
|
||||||
.with_failure_regex_set("fail");
|
.with_failure_regex_set("fail", RegexSet::empty());
|
||||||
|
|
||||||
// Give the child process enough time to print its output.
|
// Give the child process enough time to print its output.
|
||||||
std::thread::sleep(Duration::from_secs(1));
|
std::thread::sleep(Duration::from_secs(1));
|
||||||
|
@ -326,7 +330,7 @@ fn failure_regex_matches_stdout_failure_message_kill_on_error() {
|
||||||
.spawn_child_with_command(TEST_CMD, &["failure_message"])
|
.spawn_child_with_command(TEST_CMD, &["failure_message"])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_timeout(Duration::from_secs(5))
|
.with_timeout(Duration::from_secs(5))
|
||||||
.with_failure_regex_set("fail");
|
.with_failure_regex_set("fail", RegexSet::empty());
|
||||||
|
|
||||||
// Give the child process enough time to print its output.
|
// Give the child process enough time to print its output.
|
||||||
std::thread::sleep(Duration::from_secs(1));
|
std::thread::sleep(Duration::from_secs(1));
|
||||||
|
@ -358,7 +362,7 @@ fn failure_regex_matches_stdout_failure_message_no_kill_on_error() {
|
||||||
.spawn_child_with_command(TEST_CMD, &["failure_message"])
|
.spawn_child_with_command(TEST_CMD, &["failure_message"])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_timeout(Duration::from_secs(5))
|
.with_timeout(Duration::from_secs(5))
|
||||||
.with_failure_regex_set("fail");
|
.with_failure_regex_set("fail", RegexSet::empty());
|
||||||
|
|
||||||
// Give the child process enough time to print its output.
|
// Give the child process enough time to print its output.
|
||||||
std::thread::sleep(Duration::from_secs(1));
|
std::thread::sleep(Duration::from_secs(1));
|
||||||
|
@ -397,7 +401,7 @@ fn failure_regex_timeout_continuous_output() {
|
||||||
.spawn_child_with_command(TEST_CMD, &["-v", "/dev/zero"])
|
.spawn_child_with_command(TEST_CMD, &["-v", "/dev/zero"])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_timeout(Duration::from_secs(2))
|
.with_timeout(Duration::from_secs(2))
|
||||||
.with_failure_regex_set("0");
|
.with_failure_regex_set("0", RegexSet::empty());
|
||||||
|
|
||||||
// We need to use expect_stdout_line_matches, because wait_with_output ignores timeouts.
|
// We need to use expect_stdout_line_matches, because wait_with_output ignores timeouts.
|
||||||
// We use a non-matching regex, to trigger the timeout and the failure panic.
|
// We use a non-matching regex, to trigger the timeout and the failure panic.
|
||||||
|
@ -429,7 +433,7 @@ fn failure_regex_matches_stdout_failure_message_wait_for_output() {
|
||||||
.spawn_child_with_command(TEST_CMD, &["failure_message"])
|
.spawn_child_with_command(TEST_CMD, &["failure_message"])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_timeout(Duration::from_secs(5))
|
.with_timeout(Duration::from_secs(5))
|
||||||
.with_failure_regex_set("fail");
|
.with_failure_regex_set("fail", RegexSet::empty());
|
||||||
|
|
||||||
// Give the child process enough time to print its output.
|
// Give the child process enough time to print its output.
|
||||||
std::thread::sleep(Duration::from_secs(1));
|
std::thread::sleep(Duration::from_secs(1));
|
||||||
|
@ -438,3 +442,80 @@ fn failure_regex_matches_stdout_failure_message_wait_for_output() {
|
||||||
// or the output should be read on drop.
|
// or the output should be read on drop.
|
||||||
child.wait_with_output().unwrap_err();
|
child.wait_with_output().unwrap_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Make sure failure regex iters detect when a child process prints a failure message to stdout,
|
||||||
|
/// and panic with a test failure message.
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "Logged a failure message")]
|
||||||
|
fn failure_regex_iter_matches_stdout_failure_message() {
|
||||||
|
zebra_test::init();
|
||||||
|
|
||||||
|
const TEST_CMD: &str = "echo";
|
||||||
|
// Skip the test if the test system does not have the command
|
||||||
|
if !is_command_available(TEST_CMD, &[]) {
|
||||||
|
panic!(
|
||||||
|
"skipping test: command not available\n\
|
||||||
|
fake panic message: Logged a failure message"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut child = tempdir()
|
||||||
|
.unwrap()
|
||||||
|
.spawn_child_with_command(TEST_CMD, &["failure_message"])
|
||||||
|
.unwrap()
|
||||||
|
.with_timeout(Duration::from_secs(2))
|
||||||
|
.with_failure_regex_iter(
|
||||||
|
["fail"].iter().cloned(),
|
||||||
|
NO_MATCHES_REGEX_ITER.iter().cloned(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Any method that reads output should work here.
|
||||||
|
// We use a non-matching regex, to trigger the failure panic.
|
||||||
|
child
|
||||||
|
.expect_stdout_line_matches("this regex should not match")
|
||||||
|
.unwrap_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make sure ignore regexes override failure regexes.
|
||||||
|
#[test]
|
||||||
|
fn ignore_regex_ignores_stdout_failure_message() {
|
||||||
|
zebra_test::init();
|
||||||
|
|
||||||
|
const TEST_CMD: &str = "echo";
|
||||||
|
// Skip the test if the test system does not have the command
|
||||||
|
if !is_command_available(TEST_CMD, &[]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut child = tempdir()
|
||||||
|
.unwrap()
|
||||||
|
.spawn_child_with_command(TEST_CMD, &["failure_message ignore_message"])
|
||||||
|
.unwrap()
|
||||||
|
.with_timeout(Duration::from_secs(2))
|
||||||
|
.with_failure_regex_set("fail", "ignore");
|
||||||
|
|
||||||
|
// Any method that reads output should work here.
|
||||||
|
child.expect_stdout_line_matches("ignore_message").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make sure ignore regex iters override failure regex iters.
|
||||||
|
#[test]
|
||||||
|
fn ignore_regex_iter_ignores_stdout_failure_message() {
|
||||||
|
zebra_test::init();
|
||||||
|
|
||||||
|
const TEST_CMD: &str = "echo";
|
||||||
|
// Skip the test if the test system does not have the command
|
||||||
|
if !is_command_available(TEST_CMD, &[]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut child = tempdir()
|
||||||
|
.unwrap()
|
||||||
|
.spawn_child_with_command(TEST_CMD, &["failure_message ignore_message"])
|
||||||
|
.unwrap()
|
||||||
|
.with_timeout(Duration::from_secs(2))
|
||||||
|
.with_failure_regex_iter(["fail"].iter().cloned(), ["ignore"].iter().cloned());
|
||||||
|
|
||||||
|
// Any method that reads output should work here.
|
||||||
|
child.expect_stdout_line_matches("ignore_message").unwrap();
|
||||||
|
}
|
||||||
|
|
|
@ -61,7 +61,8 @@ abscissa_core = { version = "0.5", features = ["testing"] }
|
||||||
once_cell = "1.10.0"
|
once_cell = "1.10.0"
|
||||||
regex = "1.5.5"
|
regex = "1.5.5"
|
||||||
semver = "1.0.6"
|
semver = "1.0.6"
|
||||||
serde_json = "1.0"
|
# zebra-rpc needs the preserve_order feature, it also makes test results more stable
|
||||||
|
serde_json = { version = "1.0.79", features = ["preserve_order"] }
|
||||||
tempfile = "3.3.0"
|
tempfile = "3.3.0"
|
||||||
tokio = { version = "1.17.0", features = ["full", "test-util"] }
|
tokio = { version = "1.17.0", features = ["full", "test-util"] }
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,11 @@ use zebra_chain::{
|
||||||
use zebra_network::constants::PORT_IN_USE_ERROR;
|
use zebra_network::constants::PORT_IN_USE_ERROR;
|
||||||
use zebra_state::constants::LOCK_FILE_ERROR;
|
use zebra_state::constants::LOCK_FILE_ERROR;
|
||||||
|
|
||||||
use zebra_test::{command::ContextFrom, net::random_known_port, prelude::*};
|
use zebra_test::{
|
||||||
|
command::{ContextFrom, NO_MATCHES_REGEX_ITER},
|
||||||
|
net::random_known_port,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
|
@ -989,6 +993,9 @@ async fn rpc_endpoint() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Failure log messages for any process, from the OS or shell.
|
/// Failure log messages for any process, from the OS or shell.
|
||||||
|
///
|
||||||
|
/// These messages show that the child process has failed.
|
||||||
|
/// So when we see them in the logs, we make the test fail.
|
||||||
const PROCESS_FAILURE_MESSAGES: &[&str] = &[
|
const PROCESS_FAILURE_MESSAGES: &[&str] = &[
|
||||||
// Linux
|
// Linux
|
||||||
"Aborted",
|
"Aborted",
|
||||||
|
@ -998,6 +1005,9 @@ const PROCESS_FAILURE_MESSAGES: &[&str] = &[
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Failure log messages from Zebra.
|
/// Failure log messages from Zebra.
|
||||||
|
///
|
||||||
|
/// These `zebrad` messages show that the `lightwalletd` integration test has failed.
|
||||||
|
/// So when we see them in the logs, we make the test fail.
|
||||||
const ZEBRA_FAILURE_MESSAGES: &[&str] = &[
|
const ZEBRA_FAILURE_MESSAGES: &[&str] = &[
|
||||||
// Rust-specific panics
|
// Rust-specific panics
|
||||||
"The application panicked",
|
"The application panicked",
|
||||||
|
@ -1020,6 +1030,9 @@ const ZEBRA_FAILURE_MESSAGES: &[&str] = &[
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Failure log messages from lightwalletd.
|
/// Failure log messages from lightwalletd.
|
||||||
|
///
|
||||||
|
/// These `lightwalletd` messages show that the `lightwalletd` integration test has failed.
|
||||||
|
/// So when we see them in the logs, we make the test fail.
|
||||||
const LIGHTWALLETD_FAILURE_MESSAGES: &[&str] = &[
|
const LIGHTWALLETD_FAILURE_MESSAGES: &[&str] = &[
|
||||||
// Go-specific panics
|
// Go-specific panics
|
||||||
"panic:",
|
"panic:",
|
||||||
|
@ -1039,7 +1052,7 @@ const LIGHTWALLETD_FAILURE_MESSAGES: &[&str] = &[
|
||||||
// Go json package error messages:
|
// Go json package error messages:
|
||||||
"json: cannot unmarshal",
|
"json: cannot unmarshal",
|
||||||
"into Go value of type",
|
"into Go value of type",
|
||||||
// lightwalletd RPC error messages from:
|
// lightwalletd custom RPC error messages from:
|
||||||
// https://github.com/adityapk00/lightwalletd/blob/master/common/common.go
|
// https://github.com/adityapk00/lightwalletd/blob/master/common/common.go
|
||||||
"block requested is newer than latest block",
|
"block requested is newer than latest block",
|
||||||
"Cache add failed",
|
"Cache add failed",
|
||||||
|
@ -1077,6 +1090,21 @@ const LIGHTWALLETD_FAILURE_MESSAGES: &[&str] = &[
|
||||||
// get_address_utxos
|
// get_address_utxos
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Ignored failure logs for lightwalletd.
|
||||||
|
/// These regexes override the [`LIGHTWALLETD_FAILURE_MESSAGES`].
|
||||||
|
///
|
||||||
|
/// These `lightwalletd` messages look like failure messages, but they are actually ok.
|
||||||
|
/// So when we see them in the logs, we make the test continue.
|
||||||
|
const LIGHTWALLETD_IGNORE_MESSAGES: &[&str] = &[
|
||||||
|
// Exceptions to lightwalletd custom RPC error messages:
|
||||||
|
//
|
||||||
|
// This log matches the "error with" RPC error message,
|
||||||
|
// but we expect Zebra to start with an empty state.
|
||||||
|
//
|
||||||
|
// TODO: this exception should not be used for the cached state tests (#3511)
|
||||||
|
r#"No Chain tip available yet","level":"warning","msg":"error with getblockchaininfo rpc, retrying"#,
|
||||||
|
];
|
||||||
|
|
||||||
/// Launch `zebrad` with an RPC port, and make sure `lightwalletd` works with Zebra.
|
/// Launch `zebrad` with an RPC port, and make sure `lightwalletd` works with Zebra.
|
||||||
///
|
///
|
||||||
/// This test only runs when the `ZEBRA_TEST_LIGHTWALLETD` env var is set.
|
/// This test only runs when the `ZEBRA_TEST_LIGHTWALLETD` env var is set.
|
||||||
|
@ -1108,6 +1136,7 @@ fn lightwalletd_integration() -> Result<()> {
|
||||||
.iter()
|
.iter()
|
||||||
.chain(PROCESS_FAILURE_MESSAGES)
|
.chain(PROCESS_FAILURE_MESSAGES)
|
||||||
.cloned(),
|
.cloned(),
|
||||||
|
NO_MATCHES_REGEX_ITER.iter().cloned(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait until `zebrad` has opened the RPC endpoint
|
// Wait until `zebrad` has opened the RPC endpoint
|
||||||
|
@ -1132,6 +1161,8 @@ fn lightwalletd_integration() -> Result<()> {
|
||||||
.iter()
|
.iter()
|
||||||
.chain(PROCESS_FAILURE_MESSAGES)
|
.chain(PROCESS_FAILURE_MESSAGES)
|
||||||
.cloned(),
|
.cloned(),
|
||||||
|
// TODO: some exceptions do not apply to the cached state tests (#3511)
|
||||||
|
LIGHTWALLETD_IGNORE_MESSAGES.iter().cloned(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait until `lightwalletd` has launched
|
// Wait until `lightwalletd` has launched
|
||||||
|
@ -1142,28 +1173,42 @@ fn lightwalletd_integration() -> Result<()> {
|
||||||
|
|
||||||
// getblockchaininfo
|
// getblockchaininfo
|
||||||
//
|
//
|
||||||
// TODO: add correct sapling height, chain, branchID (PR #3891)
|
// TODO: update branchID when we're using cached state (#3511)
|
||||||
// add "Waiting for zcashd height to reach Sapling activation height"
|
// add "Waiting for zcashd height to reach Sapling activation height"
|
||||||
let result = lightwalletd.expect_stdout_line_matches("Got sapling height");
|
let result = lightwalletd.expect_stdout_line_matches(
|
||||||
|
"Got sapling height 419200 block height [0-9]+ chain main branchID 00000000",
|
||||||
|
);
|
||||||
let (_, zebrad) = zebrad.kill_on_error(result)?;
|
let (_, zebrad) = zebrad.kill_on_error(result)?;
|
||||||
|
|
||||||
let result = lightwalletd.expect_stdout_line_matches("Found 0 blocks in cache");
|
let result = lightwalletd.expect_stdout_line_matches("Found 0 blocks in cache");
|
||||||
let (_, zebrad) = zebrad.kill_on_error(result)?;
|
let (_, zebrad) = zebrad.kill_on_error(result)?;
|
||||||
|
|
||||||
// getblock with block 1 in Zebra's state
|
// getblock with the first Sapling block in Zebra's state
|
||||||
//
|
//
|
||||||
// zcash/lightwalletd calls getbestblockhash here, but
|
// zcash/lightwalletd calls getbestblockhash here, but
|
||||||
// adityapk00/lightwalletd calls getblock
|
// adityapk00/lightwalletd calls getblock
|
||||||
//
|
//
|
||||||
// Until block 1 has been downloaded, lightwalletd will log Zebra's RPC error:
|
// The log also depends on what is in Zebra's state:
|
||||||
|
//
|
||||||
|
// # Empty Zebra State
|
||||||
|
//
|
||||||
|
// lightwalletd tries to download the Sapling activation block, but it's not in the state.
|
||||||
|
//
|
||||||
|
// Until the Sapling activation block has been downloaded, lightwalletd will log Zebra's RPC error:
|
||||||
// "error requesting block: 0: Block not found"
|
// "error requesting block: 0: Block not found"
|
||||||
// But we can't check for that, because Zebra might download genesis before lightwalletd asks.
|
|
||||||
// We also get a similar log when lightwalletd reaches the end of Zebra's cache.
|
// We also get a similar log when lightwalletd reaches the end of Zebra's cache.
|
||||||
//
|
//
|
||||||
// After the first getblock call, lightwalletd will log:
|
// # Cached Zebra State
|
||||||
|
//
|
||||||
|
// After the first successful getblock call, lightwalletd will log:
|
||||||
// "Block hash changed, clearing mempool clients"
|
// "Block hash changed, clearing mempool clients"
|
||||||
// But we can't check for that, because it can come before or after the Ingestor log.
|
// But we can't check for that, because it can come before or after the Ingestor log.
|
||||||
let result = lightwalletd.expect_stdout_line_matches("Ingestor adding block to cache");
|
//
|
||||||
|
// TODO: expect Ingestor log when we're using cached state (#3511)
|
||||||
|
// "Ingestor adding block to cache"
|
||||||
|
let result = lightwalletd.expect_stdout_line_matches(
|
||||||
|
r#"error requesting block: 0: Block not found","height":419200"#,
|
||||||
|
);
|
||||||
let (_, zebrad) = zebrad.kill_on_error(result)?;
|
let (_, zebrad) = zebrad.kill_on_error(result)?;
|
||||||
|
|
||||||
// (next RPC)
|
// (next RPC)
|
||||||
|
|
Loading…
Reference in New Issue