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

tests(app): 💎 polish mock consensus test infrastructure #4185

Merged
merged 8 commits into from
Apr 11, 2024
19 changes: 15 additions & 4 deletions crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
mod common;

use {
self::common::BuilderExt,
anyhow::anyhow,
cnidarium::TempStorage,
penumbra_app::{genesis::AppState, server::consensus::Consensus},
penumbra_keys::test_keys,
penumbra_mock_client::MockClient,
penumbra_mock_consensus::TestNode,
Expand All @@ -13,16 +13,27 @@ use {
memo::MemoPlaintext, plan::MemoPlan, TransactionParameters, TransactionPlan,
},
rand_core::OsRng,
tap::Tap,
tap::{Tap, TapFallible},
tracing::info,
};

mod common;

#[tokio::test]
async fn app_can_spend_notes_and_detect_outputs() -> anyhow::Result<()> {
// Install a test logger, acquire some temporary storage, and start the test node.
let guard = common::set_tracing_subscriber();
let storage = TempStorage::new().await?;
let mut test_node = common::start_test_node(&storage).await?;
let mut test_node = {
let app_state = AppState::default();
let consensus = Consensus::new(storage.as_ref().clone());
TestNode::builder()
.single_validator()
.with_penumbra_auto_app_state(app_state)?
.init_chain(consensus)
.await
.tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))?
};

// Sync the mock client, using the test wallet's spend key, to the latest snapshot.
let mut client = MockClient::new(test_keys::SPEND_KEY.clone())
Expand Down
139 changes: 16 additions & 123 deletions crates/core/app/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,134 +1,27 @@
//! Shared integration testing facilities.

// NB: Allow dead code, and unused imports. these are shared and consumed by files in `tests/`.
#![allow(dead_code, unused_imports)]

pub use self::test_node_builder_ext::BuilderExt;

use {
async_trait::async_trait,
cnidarium::TempStorage,
penumbra_app::{
app::App,
genesis::AppState,
server::consensus::{Consensus, ConsensusService},
},
penumbra_mock_consensus::TestNode,
std::ops::Deref,
// NB: these reëxports are shared and consumed by files in `tests/`.
#[allow(unused_imports)]
pub use self::{
temp_storage_ext::TempStorageExt, test_node_builder_ext::BuilderExt,
test_node_ext::TestNodeExt, tracing_subscriber::set_tracing_subscriber,
};

/// Penumbra-specific extensions to the mock consensus builder.
///
/// See [`BuilderExt`].
mod test_node_builder_ext;

// Installs a tracing subscriber to log events until the returned guard is dropped.
pub fn set_tracing_subscriber() -> tracing::subscriber::DefaultGuard {
use tracing_subscriber::filter::EnvFilter;

let filter = "info,penumbra_app=trace,penumbra_mock_consensus=trace";
let filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new(filter))
.expect("should have a valid filter directive")
// Without explicitly disabling the `r1cs` target, the ZK proof implementations
// will spend an enormous amount of CPU and memory building useless tracing output.
.add_directive(
"r1cs=off"
.parse()
.expect("rics=off is a valid filter directive"),
);

let subscriber = tracing_subscriber::fmt()
.with_env_filter(filter)
.pretty()
.with_test_writer()
.finish();

tracing::subscriber::set_default(subscriber)
}

/// A [`TestNode`] coupled with Penumbra's [`Consensus`] service.
pub type PenumbraTestNode = TestNode<ConsensusService>;

/// Returns a new [`PenumbraTestNode`] backed by the given temporary storage.
pub async fn start_test_node(storage: &TempStorage) -> anyhow::Result<PenumbraTestNode> {
use tap::TapFallible;
let app_state = AppState::default();
let consensus = Consensus::new(storage.as_ref().clone());
TestNode::builder()
.single_validator()
.with_penumbra_auto_app_state(app_state)?
.init_chain(consensus)
.await
.tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))
}

#[async_trait]
pub trait TempStorageExt: Sized {
async fn apply_genesis(self, genesis: AppState) -> anyhow::Result<Self>;
async fn apply_default_genesis(self) -> anyhow::Result<Self>;
}
/// Extensions to [`TempStorage`][cnidarium::TempStorage].
mod temp_storage_ext;

#[async_trait]
impl TempStorageExt for TempStorage {
async fn apply_genesis(self, genesis: AppState) -> anyhow::Result<Self> {
// Check that we haven't already applied a genesis state:
if self.latest_version() != u64::MAX {
anyhow::bail!("database already initialized");
}

// Apply the genesis state to the storage
let mut app = App::new(self.latest_snapshot()).await?;
app.init_chain(&genesis).await;
app.commit(self.deref().clone()).await;

Ok(self)
}

async fn apply_default_genesis(self) -> anyhow::Result<Self> {
self.apply_genesis(Default::default()).await
}
}

#[async_trait]
pub trait TestNodeExt: Sized {
async fn fast_forward_to_next_epoch(
&mut self,
storage: &TempStorage,
) -> anyhow::Result<penumbra_sct::epoch::Epoch>;
}

#[async_trait]
impl<C> TestNodeExt for TestNode<C>
where
C: tower::Service<
tendermint::v0_37::abci::ConsensusRequest,
Response = tendermint::v0_37::abci::ConsensusResponse,
Error = tower::BoxError,
> + Send
+ Clone
+ 'static,
C::Future: Send + 'static,
C::Error: Sized,
{
async fn fast_forward_to_next_epoch(
&mut self,
storage: &TempStorage,
) -> Result<penumbra_sct::epoch::Epoch, anyhow::Error> {
use {penumbra_sct::component::clock::EpochRead, tap::Tap};

let get_epoch = || async { storage.latest_snapshot().get_current_epoch().await };
let start = get_epoch()
.await?
.tap(|start| tracing::info!(?start, "fast forwarding to next epoch"));
/// Penumbra-specific extensions to the mock consensus test node.
///
/// See [`TestNodeExt`].
mod test_node_ext;

loop {
self.block().execute().await?;
let current = get_epoch().await?;
if current != start {
tracing::debug!(end = ?current, ?start, "reached next epoch");
return Ok(current);
}
}
}
}
/// A pretty [`tracing`] subscriber for use in test cases.
///
/// NB: this subscriber makes use of a test writer, that is compatible with `cargo test`'s output
/// capturing.
mod tracing_subscriber;
33 changes: 33 additions & 0 deletions crates/core/app/tests/common/temp_storage_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use {
async_trait::async_trait,
cnidarium::TempStorage,
penumbra_app::{app::App, genesis::AppState},
std::ops::Deref,
};

#[async_trait]
pub trait TempStorageExt: Sized {
async fn apply_genesis(self, genesis: AppState) -> anyhow::Result<Self>;
async fn apply_default_genesis(self) -> anyhow::Result<Self>;
}

#[async_trait]
impl TempStorageExt for TempStorage {
async fn apply_genesis(self, genesis: AppState) -> anyhow::Result<Self> {
// Check that we haven't already applied a genesis state:
if self.latest_version() != u64::MAX {
anyhow::bail!("database already initialized");
}

// Apply the genesis state to the storage
let mut app = App::new(self.latest_snapshot()).await?;
app.init_chain(&genesis).await;
app.commit(self.deref().clone()).await;

Ok(self)
}

async fn apply_default_genesis(self) -> anyhow::Result<Self> {
self.apply_genesis(Default::default()).await
}
}
11 changes: 5 additions & 6 deletions crates/core/app/tests/common/test_node_builder_ext.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
use {
decaf377_rdsa::VerificationKey,
penumbra_app::genesis::AppState,
penumbra_keys::keys::{SpendKey, SpendKeyBytes},
penumbra_mock_consensus::builder::Builder,
penumbra_proto::{
core::keys::v1::{GovernanceKey, IdentityKey},
penumbra::core::component::stake::v1::Validator as PenumbraValidator,
},
penumbra_shielded_pool::genesis::Allocation,
penumbra_stake::DelegationToken,
rand::Rng,
rand_core::OsRng,
tracing::trace,
};

Expand Down Expand Up @@ -57,12 +62,6 @@ impl BuilderExt for Builder {
fn generate_penumbra_validator(
consensus_key: &ed25519_consensus::VerificationKey,
) -> (PenumbraValidator, Allocation) {
use decaf377_rdsa::VerificationKey;
use penumbra_keys::keys::{SpendKey, SpendKeyBytes};
use penumbra_stake::DelegationToken;
use rand::Rng;
use rand_core::OsRng;

let seed = SpendKeyBytes(OsRng.gen());
let spend_key = SpendKey::from(seed.clone());
let validator_id_sk = spend_key.spend_auth_key();
Expand Down
45 changes: 45 additions & 0 deletions crates/core/app/tests/common/test_node_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use {
async_trait::async_trait, cnidarium::TempStorage, penumbra_mock_consensus::TestNode,
penumbra_sct::component::clock::EpochRead as _, tap::Tap,
};

#[async_trait]
pub trait TestNodeExt: Sized {
async fn fast_forward_to_next_epoch(
&mut self,
storage: &TempStorage,
) -> anyhow::Result<penumbra_sct::epoch::Epoch>;
}

#[async_trait]
impl<C> TestNodeExt for TestNode<C>
where
C: tower::Service<
tendermint::v0_37::abci::ConsensusRequest,
Response = tendermint::v0_37::abci::ConsensusResponse,
Error = tower::BoxError,
> + Send
+ Clone
+ 'static,
C::Future: Send + 'static,
C::Error: Sized,
{
async fn fast_forward_to_next_epoch(
&mut self,
storage: &TempStorage,
) -> Result<penumbra_sct::epoch::Epoch, anyhow::Error> {
let get_epoch = || async { storage.latest_snapshot().get_current_epoch().await };
let start = get_epoch()
.await?
.tap(|start| tracing::info!(?start, "fast forwarding to next epoch"));

loop {
self.block().execute().await?;
let current = get_epoch().await?;
if current != start {
tracing::debug!(end = ?current, ?start, "reached next epoch");
return Ok(current);
}
}
}
}
29 changes: 29 additions & 0 deletions crates/core/app/tests/common/tracing_subscriber.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use {
tracing::subscriber::{set_default, DefaultGuard},
tracing_subscriber::{filter::EnvFilter, fmt},
};

/// Installs a tracing subscriber to log events until the returned guard is dropped.
// NB: this is marked as "dead code" but it is used by integration tests.
#[allow(dead_code)]
pub fn set_tracing_subscriber() -> DefaultGuard {
let filter = "info,penumbra_app=trace,penumbra_mock_consensus=trace";
let filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new(filter))
.expect("should have a valid filter directive")
// Without explicitly disabling the `r1cs` target, the ZK proof implementations
// will spend an enormous amount of CPU and memory building useless tracing output.
.add_directive(
"r1cs=off"
.parse()
.expect("rics=off is a valid filter directive"),
);

let subscriber = fmt()
.with_env_filter(filter)
.pretty()
.with_test_writer()
.finish();

set_default(subscriber)
}
Loading
Loading