docs: add compilable doc-snippets crate

This commit is contained in:
andrussal 2025-12-16 06:55:44 +01:00
parent 4141c98d8d
commit de2e043e2a
59 changed files with 1460 additions and 375 deletions

View File

@ -98,6 +98,37 @@ jobs:
restore-keys: ${{ runner.os }}-target-clippy- restore-keys: ${{ runner.os }}-target-clippy-
- run: cargo +nightly-2025-09-14 clippy --all --all-targets --all-features -- -D warnings - run: cargo +nightly-2025-09-14 clippy --all --all-targets --all-features -- -D warnings
doc_snippets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Load versions
run: |
set -euo pipefail
if [ ! -f versions.env ]; then
echo "versions.env missing; populate VERSION, NOMOS_NODE_REV, NOMOS_BUNDLE_VERSION" >&2
exit 1
fi
set -a
. versions.env
set +a
# $GITHUB_ENV does not accept comments/blank lines; keep only KEY=VALUE exports.
grep -E '^[A-Za-z_][A-Za-z0-9_]*=' versions.env >> "$GITHUB_ENV"
: "${VERSION:?Missing VERSION}"
: "${NOMOS_NODE_REV:?Missing NOMOS_NODE_REV}"
: "${NOMOS_BUNDLE_VERSION:?Missing NOMOS_BUNDLE_VERSION}"
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-09-14
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
- run: cargo +nightly-2025-09-14 check -p doc-snippets
deny: deny:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

13
Cargo.lock generated
View File

@ -1698,6 +1698,19 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "doc-snippets"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"testing-framework-core",
"testing-framework-runner-compose",
"testing-framework-runner-k8s",
"testing-framework-runner-local",
"testing-framework-workflows",
]
[[package]] [[package]]
name = "dtoa" name = "dtoa"
version = "1.0.10" version = "1.0.10"

View File

@ -1,6 +1,7 @@
[workspace] [workspace]
members = [ members = [
"examples", "examples",
"examples/doc-snippets",
"testing-framework/configs", "testing-framework/configs",
"testing-framework/core", "testing-framework/core",
"testing-framework/runners/compose", "testing-framework/runners/compose",

View File

@ -58,23 +58,20 @@ These binaries use the framework API (`ScenarioBuilder`) to construct and execut
Scenarios are defined using a fluent builder pattern: Scenarios are defined using a fluent builder pattern:
```rust ```rust
let mut plan = ScenarioBuilder::topology_with(|t| { use std::time::Duration;
t.network_star() // Topology configuration
.validators(3) use testing_framework_core::scenario::ScenarioBuilder;
.executors(2) use testing_framework_workflows::ScenarioBuilderExt;
})
.wallets(50) // Wallet seeding pub fn scenario_plan() -> testing_framework_core::scenario::Scenario<()> {
.transactions_with(|txs| { ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(2))
txs.rate(5) .wallets(50)
.users(20) .transactions_with(|txs| txs.rate(5).users(20))
}) .da_with(|da| da.channel_rate(1).blob_rate(2))
.da_with(|da| { .expect_consensus_liveness()
da.channel_rate(1) .with_run_duration(Duration::from_secs(90))
.blob_rate(2) .build()
}) }
.expect_consensus_liveness() // Expectations
.with_run_duration(Duration::from_secs(90))
.build();
``` ```
**Key API Points:** **Key API Points:**

View File

@ -20,26 +20,26 @@ recovery. The built-in restart workload lives in
## Usage ## Usage
```rust ```rust
use std::time::Duration; use std::time::Duration;
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::workloads::chaos::RandomRestartWorkload;
let plan = ScenarioBuilder::topology_with(|t| { use testing_framework_core::scenario::ScenarioBuilder;
t.network_star() use testing_framework_workflows::{ScenarioBuilderExt, workloads::chaos::RandomRestartWorkload};
.validators(2)
.executors(1) pub fn random_restart_plan() -> testing_framework_core::scenario::Scenario<
}) testing_framework_core::scenario::NodeControlCapability,
.enable_node_control() > {
.with_workload(RandomRestartWorkload::new( ScenarioBuilder::topology_with(|t| t.network_star().validators(2).executors(1))
Duration::from_secs(45), // min delay .enable_node_control()
Duration::from_secs(75), // max delay .with_workload(RandomRestartWorkload::new(
Duration::from_secs(120), // target cooldown Duration::from_secs(45), // min delay
true, // include validators Duration::from_secs(75), // max delay
true, // include executors Duration::from_secs(120), // target cooldown
)) true, // include validators
.expect_consensus_liveness() true, // include executors
.with_run_duration(Duration::from_secs(150)) ))
.build(); .expect_consensus_liveness()
// deploy with a runner that supports node control and run the scenario .with_run_duration(Duration::from_secs(150))
.build()
}
``` ```
## Expectations to pair ## Expectations to pair

View File

@ -14,10 +14,10 @@ Key ideas:
```rust ```rust
use async_trait::async_trait; use async_trait::async_trait;
use testing_framework_core::scenario::{ use testing_framework_core::{
DynError, Expectation, RunContext, Workload, runtime::context::RunMetrics, scenario::{DynError, Expectation, RunContext, RunMetrics, Workload},
topology::generation::GeneratedTopology,
}; };
use testing_framework_core::topology::generation::GeneratedTopology;
pub struct ReachabilityWorkload { pub struct ReachabilityWorkload {
target_idx: usize, target_idx: usize,
@ -36,16 +36,23 @@ impl Workload for ReachabilityWorkload {
} }
fn expectations(&self) -> Vec<Box<dyn Expectation>> { fn expectations(&self) -> Vec<Box<dyn Expectation>> {
vec![Box::new(ReachabilityExpectation::new(self.target_idx))] vec![Box::new(
crate::custom_workload_example_expectation::ReachabilityExpectation::new(
self.target_idx,
),
)]
} }
fn init( fn init(
&mut self, &mut self,
topology: &GeneratedTopology, topology: &GeneratedTopology,
_metrics: &RunMetrics, _run_metrics: &RunMetrics,
) -> Result<(), DynError> { ) -> Result<(), DynError> {
if topology.validators().get(self.target_idx).is_none() { if topology.validators().get(self.target_idx).is_none() {
return Err("no validator at requested index".into()); return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"no validator at requested index",
)));
} }
Ok(()) Ok(())
} }
@ -55,10 +62,19 @@ impl Workload for ReachabilityWorkload {
.node_clients() .node_clients()
.validator_clients() .validator_clients()
.get(self.target_idx) .get(self.target_idx)
.ok_or("missing target client")?; .ok_or_else(|| {
Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"missing target client",
)) as DynError
})?;
// Lightweight API call to prove reachability. // Lightweight API call to prove reachability.
client.consensus_info().await.map(|_| ()).map_err(|e| e.into()) client
.consensus_info()
.await
.map(|_| ())
.map_err(|e| e.into())
} }
} }
``` ```
@ -94,13 +110,18 @@ impl Expectation for ReachabilityExpectation {
.node_clients() .node_clients()
.validator_clients() .validator_clients()
.get(self.target_idx) .get(self.target_idx)
.ok_or("missing target client")?; .ok_or_else(|| {
Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"missing target client",
)) as DynError
})?;
client client
.consensus_info() .consensus_info()
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|e| format!("target became unreachable during run: {e}").into()) .map_err(|e| e.into())
} }
} }
``` ```

View File

@ -5,123 +5,199 @@ Quick reference for the scenario builder DSL. All methods are chainable.
## Imports ## Imports
```rust ```rust
use std::time::Duration;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder}; use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer;
use testing_framework_runner_compose::ComposeDeployer; use testing_framework_runner_compose::ComposeDeployer;
use testing_framework_runner_k8s::K8sDeployer; use testing_framework_runner_k8s::K8sDeployer;
use testing_framework_workflows::{ScenarioBuilderExt, ChaosBuilderExt}; use testing_framework_runner_local::LocalDeployer;
use std::time::Duration; use testing_framework_workflows::{ChaosBuilderExt, ScenarioBuilderExt};
``` ```
## Topology ## Topology
```rust ```rust
ScenarioBuilder::topology_with(|t| { use testing_framework_core::scenario::{Builder, ScenarioBuilder};
t.network_star() // Star topology (all connect to seed node)
.validators(3) // Number of validator nodes pub fn topology() -> Builder<()> {
.executors(2) // Number of executor nodes ScenarioBuilder::topology_with(|t| {
}) // Finish topology configuration t.network_star() // Star topology (all connect to seed node)
.validators(3) // Number of validator nodes
.executors(2) // Number of executor nodes
})
}
``` ```
## Wallets ## Wallets
```rust ```rust
.wallets(50) // Seed 50 funded wallet accounts use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn wallets_plan() -> testing_framework_core::scenario::Scenario<()> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(0))
.wallets(50) // Seed 50 funded wallet accounts
.build()
}
``` ```
## Transaction Workload ## Transaction Workload
```rust ```rust
.transactions_with(|txs| { use testing_framework_core::scenario::ScenarioBuilder;
txs.rate(5) // 5 transactions per block use testing_framework_workflows::ScenarioBuilderExt;
.users(20) // Use 20 of the seeded wallets
}) // Finish transaction workload config pub fn transactions_plan() -> testing_framework_core::scenario::Scenario<()> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(0))
.wallets(50)
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
.users(20) // Use 20 of the seeded wallets
}) // Finish transaction workload config
.build()
}
``` ```
## DA Workload ## DA Workload
```rust ```rust
.da_with(|da| { use testing_framework_core::scenario::ScenarioBuilder;
da.channel_rate(1) // number of DA channels to run use testing_framework_workflows::ScenarioBuilderExt;
.blob_rate(2) // target 2 blobs per block (headroom applied)
.headroom_percent(20)// optional headroom when sizing channels pub fn da_plan() -> testing_framework_core::scenario::Scenario<()> {
}) // Finish DA workload config ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(1))
.wallets(50)
.da_with(|da| {
da.channel_rate(1) // number of DA channels to run
.blob_rate(2) // target 2 blobs per block (headroom applied)
.headroom_percent(20) // optional headroom when sizing channels
}) // Finish DA workload config
.build()
}
``` ```
## Chaos Workload (Requires `enable_node_control()`) ## Chaos Workload (Requires `enable_node_control()`)
```rust ```rust
.enable_node_control() // Enable node control capability use std::time::Duration;
.chaos_with(|c| {
c.restart() // Random restart chaos use testing_framework_core::scenario::{NodeControlCapability, ScenarioBuilder};
.min_delay(Duration::from_secs(30)) // Min time between restarts use testing_framework_workflows::{ChaosBuilderExt, ScenarioBuilderExt};
.max_delay(Duration::from_secs(60)) // Max time between restarts
.target_cooldown(Duration::from_secs(45)) // Cooldown after restart pub fn chaos_plan() -> testing_framework_core::scenario::Scenario<NodeControlCapability> {
.apply() // Required for chaos configuration ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(2))
}) .enable_node_control() // Enable node control capability
.chaos_with(|c| {
c.restart() // Random restart chaos
.min_delay(Duration::from_secs(30)) // Min time between restarts
.max_delay(Duration::from_secs(60)) // Max time between restarts
.target_cooldown(Duration::from_secs(45)) // Cooldown after restart
.apply() // Required for chaos configuration
})
.build()
}
``` ```
## Expectations ## Expectations
```rust ```rust
.expect_consensus_liveness() // Assert blocks are produced continuously use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn expectations_plan() -> testing_framework_core::scenario::Scenario<()> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(0))
.expect_consensus_liveness() // Assert blocks are produced continuously
.build()
}
``` ```
## Run Duration ## Run Duration
```rust ```rust
.with_run_duration(Duration::from_secs(120)) // Run for 120 seconds use std::time::Duration;
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn run_duration_plan() -> testing_framework_core::scenario::Scenario<()> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(0))
.with_run_duration(Duration::from_secs(120)) // Run for 120 seconds
.build()
}
``` ```
## Build ## Build
```rust ```rust
.build() // Construct the final Scenario use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn build_plan() -> testing_framework_core::scenario::Scenario<()> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(0)).build() // Construct the final Scenario
}
``` ```
## Deployers ## Deployers
```rust ```rust
// Local processes use testing_framework_runner_compose::ComposeDeployer;
let deployer = LocalDeployer::default(); use testing_framework_runner_k8s::K8sDeployer;
use testing_framework_runner_local::LocalDeployer;
// Docker Compose pub fn deployers() {
let deployer = ComposeDeployer::default(); // Local processes
let _deployer = LocalDeployer::default();
// Kubernetes // Docker Compose
let deployer = K8sDeployer::default(); let _deployer = ComposeDeployer::default();
// Kubernetes
let _deployer = K8sDeployer::default();
}
``` ```
## Execution ## Execution
```rust ```rust
let runner = deployer.deploy(&plan).await?; use anyhow::Result;
let _handle = runner.run(&mut plan).await?; use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer;
use testing_framework_workflows::ScenarioBuilderExt;
pub async fn execution() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(0))
.expect_consensus_liveness()
.build();
let deployer = LocalDeployer::default();
let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?;
Ok(())
}
``` ```
## Complete Example ## Complete Example
```rust ```rust
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder}; use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer; use testing_framework_runner_local::LocalDeployer;
use testing_framework_workflows::ScenarioBuilderExt; use testing_framework_workflows::ScenarioBuilderExt;
use std::time::Duration;
async fn run_test() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { pub async fn run_test() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| { let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(2))
t.network_star()
.validators(3)
.executors(2)
})
.wallets(50) .wallets(50)
.transactions_with(|txs| { .transactions_with(|txs| {
txs.rate(5) // 5 transactions per block txs.rate(5) // 5 transactions per block
.users(20) .users(20)
}) })
.da_with(|da| { .da_with(|da| {
da.channel_rate(1) // number of DA channels da.channel_rate(1) // number of DA channels
.blob_rate(2) // target 2 blobs per block .blob_rate(2) // target 2 blobs per block
.headroom_percent(20) // optional channel headroom .headroom_percent(20) // optional channel headroom
}) })
.expect_consensus_liveness() .expect_consensus_liveness()
.with_run_duration(Duration::from_secs(90)) .with_run_duration(Duration::from_secs(90))
@ -130,7 +206,7 @@ async fn run_test() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let deployer = LocalDeployer::default(); let deployer = LocalDeployer::default();
let runner = deployer.deploy(&plan).await?; let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?; let _handle = runner.run(&mut plan).await?;
Ok(()) Ok(())
} }
``` ```

View File

@ -15,34 +15,30 @@ Realistic advanced scenarios demonstrating framework capabilities for production
Test consensus under progressively increasing transaction load: Test consensus under progressively increasing transaction load:
```rust ```rust
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder}; use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_compose::ComposeDeployer; use testing_framework_runner_compose::ComposeDeployer;
use testing_framework_workflows::ScenarioBuilderExt; use testing_framework_workflows::ScenarioBuilderExt;
use std::time::Duration;
async fn load_progression_test() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { pub async fn load_progression_test() -> Result<()> {
for rate in [5, 10, 20, 30] { for rate in [5, 10, 20, 30] {
println!("Testing with rate: {}", rate); println!("Testing with rate: {}", rate);
let mut plan = ScenarioBuilder::topology_with(|t| { let mut plan =
t.network_star() ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(2))
.validators(3) .wallets(50)
.executors(2) .transactions_with(|txs| txs.rate(rate).users(20))
}) .expect_consensus_liveness()
.wallets(50) .with_run_duration(Duration::from_secs(60))
.transactions_with(|txs| { .build();
txs.rate(rate)
.users(20)
})
.expect_consensus_liveness()
.with_run_duration(Duration::from_secs(60))
.build();
let deployer = ComposeDeployer::default(); let deployer = ComposeDeployer::default();
let runner = deployer.deploy(&plan).await?; let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?; let _handle = runner.run(&mut plan).await?;
} }
Ok(()) Ok(())
} }
``` ```
@ -54,26 +50,18 @@ async fn load_progression_test() -> Result<(), Box<dyn std::error::Error + Send
Run high transaction and DA load for extended duration: Run high transaction and DA load for extended duration:
```rust ```rust
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder}; use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_compose::ComposeDeployer; use testing_framework_runner_compose::ComposeDeployer;
use testing_framework_workflows::ScenarioBuilderExt; use testing_framework_workflows::ScenarioBuilderExt;
use std::time::Duration;
async fn sustained_load_test() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { pub async fn sustained_load_test() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| { let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(4).executors(2))
t.network_star()
.validators(4)
.executors(2)
})
.wallets(100) .wallets(100)
.transactions_with(|txs| { .transactions_with(|txs| txs.rate(15).users(50))
txs.rate(15) .da_with(|da| da.channel_rate(2).blob_rate(3))
.users(50)
})
.da_with(|da| {
da.channel_rate(2)
.blob_rate(3)
})
.expect_consensus_liveness() .expect_consensus_liveness()
.with_run_duration(Duration::from_secs(300)) .with_run_duration(Duration::from_secs(300))
.build(); .build();
@ -81,7 +69,7 @@ async fn sustained_load_test() -> Result<(), Box<dyn std::error::Error + Send +
let deployer = ComposeDeployer::default(); let deployer = ComposeDeployer::default();
let runner = deployer.deploy(&plan).await?; let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?; let _handle = runner.run(&mut plan).await?;
Ok(()) Ok(())
} }
``` ```
@ -93,23 +81,18 @@ async fn sustained_load_test() -> Result<(), Box<dyn std::error::Error + Send +
Frequent node restarts with active traffic: Frequent node restarts with active traffic:
```rust ```rust
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_compose::ComposeDeployer;
use testing_framework_workflows::{ScenarioBuilderExt, ChaosBuilderExt};
use std::time::Duration; use std::time::Duration;
async fn aggressive_chaos_test() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { use anyhow::Result;
let mut plan = ScenarioBuilder::topology_with(|t| { use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
t.network_star() use testing_framework_runner_compose::ComposeDeployer;
.validators(4) use testing_framework_workflows::{ChaosBuilderExt, ScenarioBuilderExt};
.executors(2)
}) pub async fn aggressive_chaos_test() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(4).executors(2))
.enable_node_control() .enable_node_control()
.wallets(50) .wallets(50)
.transactions_with(|txs| { .transactions_with(|txs| txs.rate(10).users(20))
txs.rate(10)
.users(20)
})
.chaos_with(|c| { .chaos_with(|c| {
c.restart() c.restart()
.min_delay(Duration::from_secs(10)) .min_delay(Duration::from_secs(10))
@ -124,7 +107,7 @@ async fn aggressive_chaos_test() -> Result<(), Box<dyn std::error::Error + Send
let deployer = ComposeDeployer::default(); let deployer = ComposeDeployer::default();
let runner = deployer.deploy(&plan).await?; let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?; let _handle = runner.run(&mut plan).await?;
Ok(()) Ok(())
} }
``` ```

View File

@ -21,17 +21,15 @@ and expectations.
Minimal test that validates basic block production: Minimal test that validates basic block production:
```rust ```rust
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder}; use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer; use testing_framework_runner_local::LocalDeployer;
use testing_framework_workflows::ScenarioBuilderExt; use testing_framework_workflows::ScenarioBuilderExt;
use std::time::Duration;
async fn simple_consensus() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { pub async fn simple_consensus() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| { let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(0))
t.network_star()
.validators(3)
.executors(0)
})
.expect_consensus_liveness() .expect_consensus_liveness()
.with_run_duration(Duration::from_secs(30)) .with_run_duration(Duration::from_secs(30))
.build(); .build();
@ -39,7 +37,7 @@ async fn simple_consensus() -> Result<(), Box<dyn std::error::Error + Send + Syn
let deployer = LocalDeployer::default(); let deployer = LocalDeployer::default();
let runner = deployer.deploy(&plan).await?; let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?; let _handle = runner.run(&mut plan).await?;
Ok(()) Ok(())
} }
``` ```
@ -51,22 +49,17 @@ async fn simple_consensus() -> Result<(), Box<dyn std::error::Error + Send + Syn
Test consensus under transaction load: Test consensus under transaction load:
```rust ```rust
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder}; use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer; use testing_framework_runner_local::LocalDeployer;
use testing_framework_workflows::ScenarioBuilderExt; use testing_framework_workflows::ScenarioBuilderExt;
use std::time::Duration;
async fn transaction_workload() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { pub async fn transaction_workload() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| { let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(2).executors(0))
t.network_star()
.validators(2)
.executors(0)
})
.wallets(20) .wallets(20)
.transactions_with(|txs| { .transactions_with(|txs| txs.rate(5).users(10))
txs.rate(5)
.users(10)
})
.expect_consensus_liveness() .expect_consensus_liveness()
.with_run_duration(Duration::from_secs(60)) .with_run_duration(Duration::from_secs(60))
.build(); .build();
@ -74,7 +67,7 @@ async fn transaction_workload() -> Result<(), Box<dyn std::error::Error + Send +
let deployer = LocalDeployer::default(); let deployer = LocalDeployer::default();
let runner = deployer.deploy(&plan).await?; let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?; let _handle = runner.run(&mut plan).await?;
Ok(()) Ok(())
} }
``` ```
@ -86,26 +79,18 @@ async fn transaction_workload() -> Result<(), Box<dyn std::error::Error + Send +
Combined test stressing both transaction and DA layers: Combined test stressing both transaction and DA layers:
```rust ```rust
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder}; use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer; use testing_framework_runner_local::LocalDeployer;
use testing_framework_workflows::ScenarioBuilderExt; use testing_framework_workflows::ScenarioBuilderExt;
use std::time::Duration;
async fn da_and_transactions() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { pub async fn da_and_transactions() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| { let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(2))
t.network_star()
.validators(3)
.executors(2)
})
.wallets(30) .wallets(30)
.transactions_with(|txs| { .transactions_with(|txs| txs.rate(5).users(15))
txs.rate(5) .da_with(|da| da.channel_rate(2).blob_rate(2))
.users(15)
})
.da_with(|da| {
da.channel_rate(2)
.blob_rate(2)
})
.expect_consensus_liveness() .expect_consensus_liveness()
.with_run_duration(Duration::from_secs(90)) .with_run_duration(Duration::from_secs(90))
.build(); .build();
@ -113,7 +98,7 @@ async fn da_and_transactions() -> Result<(), Box<dyn std::error::Error + Send +
let deployer = LocalDeployer::default(); let deployer = LocalDeployer::default();
let runner = deployer.deploy(&plan).await?; let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?; let _handle = runner.run(&mut plan).await?;
Ok(()) Ok(())
} }
``` ```
@ -125,23 +110,18 @@ async fn da_and_transactions() -> Result<(), Box<dyn std::error::Error + Send +
Test system resilience under node restarts: Test system resilience under node restarts:
```rust ```rust
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_compose::ComposeDeployer;
use testing_framework_workflows::{ScenarioBuilderExt, ChaosBuilderExt};
use std::time::Duration; use std::time::Duration;
async fn chaos_resilience() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { use anyhow::Result;
let mut plan = ScenarioBuilder::topology_with(|t| { use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
t.network_star() use testing_framework_runner_compose::ComposeDeployer;
.validators(4) use testing_framework_workflows::{ChaosBuilderExt, ScenarioBuilderExt};
.executors(2)
}) pub async fn chaos_resilience() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(4).executors(2))
.enable_node_control() .enable_node_control()
.wallets(20) .wallets(20)
.transactions_with(|txs| { .transactions_with(|txs| txs.rate(3).users(10))
txs.rate(3)
.users(10)
})
.chaos_with(|c| { .chaos_with(|c| {
c.restart() c.restart()
.min_delay(Duration::from_secs(20)) .min_delay(Duration::from_secs(20))
@ -156,7 +136,7 @@ async fn chaos_resilience() -> Result<(), Box<dyn std::error::Error + Send + Syn
let deployer = ComposeDeployer::default(); let deployer = ComposeDeployer::default();
let runner = deployer.deploy(&plan).await?; let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?; let _handle = runner.run(&mut plan).await?;
Ok(()) Ok(())
} }
``` ```

View File

@ -30,92 +30,142 @@ High-level roles of the crates that make up the framework:
### Adding a New Workload ### Adding a New Workload
1. **Define the workload** in `testing-framework/workflows/src/workloads/your_workload.rs`: 1. **Define the workload** in `testing-framework/workflows/src/workloads/your_workload.rs`:
```rust ```rust
use async_trait::async_trait; use async_trait::async_trait;
use testing_framework_core::scenario::{Workload, RunContext, DynError}; use testing_framework_core::scenario::{DynError, RunContext, Workload};
pub struct YourWorkload { pub struct YourWorkload;
// config fields
} #[async_trait]
impl Workload for YourWorkload {
#[async_trait] fn name(&self) -> &'static str {
impl Workload for YourWorkload { "your_workload"
fn name(&self) -> &'static str { "your_workload" } }
async fn start(&self, ctx: &RunContext) -> Result<(), DynError> {
// implementation async fn start(&self, _ctx: &RunContext) -> Result<(), DynError> {
Ok(()) // implementation
} Ok(())
} }
``` }
```
2. **Add builder extension** in `testing-framework/workflows/src/builder/mod.rs`: 2. **Add builder extension** in `testing-framework/workflows/src/builder/mod.rs`:
```rust ```rust
pub trait ScenarioBuilderExt { pub struct YourWorkloadBuilder;
fn your_workload(self) -> YourWorkloadBuilder;
} impl YourWorkloadBuilder {
``` pub fn some_config(self) -> Self {
self
}
}
pub trait ScenarioBuilderExt: Sized {
fn your_workload(self) -> YourWorkloadBuilder;
}
```
3. **Use in examples** in `examples/src/bin/your_scenario.rs`: 3. **Use in examples** in `examples/src/bin/your_scenario.rs`:
```rust ```rust
let mut plan = ScenarioBuilder::topology_with(|t| { use testing_framework_core::scenario::ScenarioBuilder;
t.network_star()
.validators(3) pub struct YourWorkloadBuilder;
.executors(0)
}) impl YourWorkloadBuilder {
.your_workload_with(|w| { // Your new DSL method with closure pub fn some_config(self) -> Self {
w.some_config() self
}) }
.build(); }
```
pub trait YourWorkloadDslExt: Sized {
fn your_workload_with<F>(self, configurator: F) -> Self
where
F: FnOnce(YourWorkloadBuilder) -> YourWorkloadBuilder;
}
impl<Caps> YourWorkloadDslExt for testing_framework_core::scenario::Builder<Caps> {
fn your_workload_with<F>(self, configurator: F) -> Self
where
F: FnOnce(YourWorkloadBuilder) -> YourWorkloadBuilder,
{
let _ = configurator(YourWorkloadBuilder);
self
}
}
pub fn use_in_examples() {
let _plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(0))
.your_workload_with(|w| w.some_config())
.build();
}
```
### Adding a New Expectation ### Adding a New Expectation
1. **Define the expectation** in `testing-framework/workflows/src/expectations/your_expectation.rs`: 1. **Define the expectation** in `testing-framework/workflows/src/expectations/your_expectation.rs`:
```rust ```rust
use async_trait::async_trait; use async_trait::async_trait;
use testing_framework_core::scenario::{Expectation, RunContext, DynError}; use testing_framework_core::scenario::{DynError, Expectation, RunContext};
pub struct YourExpectation { pub struct YourExpectation;
// config fields
} #[async_trait]
impl Expectation for YourExpectation {
#[async_trait] fn name(&self) -> &'static str {
impl Expectation for YourExpectation { "your_expectation"
fn name(&self) -> &str { "your_expectation" } }
async fn evaluate(&mut self, ctx: &RunContext) -> Result<(), DynError> {
// implementation async fn evaluate(&mut self, _ctx: &RunContext) -> Result<(), DynError> {
Ok(()) // implementation
} Ok(())
} }
``` }
```
2. **Add builder extension** in `testing-framework/workflows/src/builder/mod.rs`: 2. **Add builder extension** in `testing-framework/workflows/src/builder/mod.rs`:
```rust ```rust
pub trait ScenarioBuilderExt { use testing_framework_core::scenario::ScenarioBuilder;
fn expect_your_condition(self) -> Self;
} pub trait YourExpectationDslExt: Sized {
``` fn expect_your_condition(self) -> Self;
}
impl<Caps> YourExpectationDslExt for testing_framework_core::scenario::Builder<Caps> {
fn expect_your_condition(self) -> Self {
self
}
}
pub fn use_in_examples() {
let _plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(0))
.expect_your_condition()
.build();
}
```
### Adding a New Deployer ### Adding a New Deployer
1. **Implement `Deployer` trait** in `testing-framework/runners/your_runner/src/deployer.rs`: 1. **Implement `Deployer` trait** in `testing-framework/runners/your_runner/src/deployer.rs`:
```rust ```rust
use async_trait::async_trait; use async_trait::async_trait;
use testing_framework_core::scenario::{Deployer, Runner, Scenario}; use testing_framework_core::scenario::{Deployer, Runner, Scenario};
pub struct YourDeployer; #[derive(Debug)]
pub struct YourError;
#[async_trait]
impl Deployer for YourDeployer { pub struct YourDeployer;
type Error = YourError;
#[async_trait]
async fn deploy(&self, scenario: &Scenario) -> Result<Runner, Self::Error> { impl Deployer for YourDeployer {
// Provision infrastructure type Error = YourError;
// Wait for readiness
// Return Runner async fn deploy(&self, _scenario: &Scenario<()>) -> Result<Runner, Self::Error> {
} // Provision infrastructure
} // Wait for readiness
``` // Return Runner
todo!()
}
}
```
2. **Provide cleanup** and handle node control if supported. 2. **Provide cleanup** and handle node control if supported.

View File

@ -39,7 +39,9 @@ struct RestartWorkload;
#[async_trait] #[async_trait]
impl Workload for RestartWorkload { impl Workload for RestartWorkload {
fn name(&self) -> &str { "restart_workload" } fn name(&self) -> &str {
"restart_workload"
}
async fn start(&self, ctx: &RunContext) -> Result<(), DynError> { async fn start(&self, ctx: &RunContext) -> Result<(), DynError> {
if let Some(control) = ctx.node_control() { if let Some(control) = ctx.node_control() {
@ -59,6 +61,10 @@ scenario builder and deploy with a runner that supports it.
The `NodeControlHandle` trait currently provides: The `NodeControlHandle` trait currently provides:
```rust ```rust
use async_trait::async_trait;
use testing_framework_core::scenario::DynError;
#[async_trait]
pub trait NodeControlHandle: Send + Sync { pub trait NodeControlHandle: Send + Sync {
async fn restart_validator(&self, index: usize) -> Result<(), DynError>; async fn restart_validator(&self, index: usize) -> Result<(), DynError>;
async fn restart_executor(&self, index: usize) -> Result<(), DynError>; async fn restart_executor(&self, index: usize) -> Result<(), DynError>;

View File

@ -38,35 +38,37 @@ POL_PROOF_DEV_MODE=true cargo run -p runner-examples --bin local_runner
**Core API Pattern** (simplified example): **Core API Pattern** (simplified example):
```rust ```rust
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder}; use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer; use testing_framework_runner_local::LocalDeployer;
use testing_framework_workflows::ScenarioBuilderExt; use testing_framework_workflows::ScenarioBuilderExt;
use std::time::Duration;
// Define the scenario (1 validator + 1 executor, tx + DA workload) pub async fn run_local_demo() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| { // Define the scenario (1 validator + 1 executor, tx + DA workload)
t.network_star() let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(1))
.validators(1) .wallets(1_000)
.executors(1) .transactions_with(|txs| {
}) txs.rate(5) // 5 transactions per block
.wallets(1_000) .users(500) // use 500 of the seeded wallets
.transactions_with(|txs| { })
txs.rate(5) // 5 transactions per block .da_with(|da| {
.users(500) // use 500 of the seeded wallets da.channel_rate(1) // 1 channel
}) .blob_rate(1) // target 1 blob per block
.da_with(|da| { .headroom_percent(20) // default headroom when sizing channels
da.channel_rate(1) // 1 channel })
.blob_rate(1) // target 1 blob per block .expect_consensus_liveness()
.headroom_percent(20) // default headroom when sizing channels .with_run_duration(Duration::from_secs(60))
}) .build();
.expect_consensus_liveness()
.with_run_duration(Duration::from_secs(60))
.build();
// Deploy and run // Deploy and run
let deployer = LocalDeployer::default(); let deployer = LocalDeployer::default();
let runner = deployer.deploy(&plan).await?; let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?; let _handle = runner.run(&mut plan).await?;
Ok(())
}
``` ```
**Note:** The examples are binaries with `#[tokio::main]`, not test functions. If you want to write integration tests, wrap this pattern in `#[tokio::test]` functions in your own test suite. **Note:** The examples are binaries with `#[tokio::main]`, not test functions. If you want to write integration tests, wrap this pattern in `#[tokio::test]` functions in your own test suite.
@ -87,11 +89,15 @@ Let's unpack the code:
### 1. Topology Configuration ### 1. Topology Configuration
```rust ```rust
ScenarioBuilder::topology_with(|t| { use testing_framework_core::scenario::ScenarioBuilder;
t.network_star() // Star topology: all nodes connect to seed
.validators(1) // 1 validator node pub fn step_1_topology() -> testing_framework_core::scenario::Builder<()> {
.executors(1) // 1 executor node (validator + DA dispersal) ScenarioBuilder::topology_with(|t| {
t.network_star() // Star topology: all nodes connect to seed
.validators(1) // 1 validator node
.executors(1) // 1 executor node (validator + DA dispersal)
}) })
}
``` ```
This defines **what** your test network looks like. This defines **what** your test network looks like.
@ -99,7 +105,12 @@ This defines **what** your test network looks like.
### 2. Wallet Seeding ### 2. Wallet Seeding
```rust ```rust
.wallets(1_000) // Seed 1,000 funded wallet accounts use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn step_2_wallets() -> testing_framework_core::scenario::Builder<()> {
ScenarioBuilder::with_node_counts(1, 1).wallets(1_000) // Seed 1,000 funded wallet accounts
}
``` ```
Provides funded accounts for transaction submission. Provides funded accounts for transaction submission.
@ -107,15 +118,22 @@ Provides funded accounts for transaction submission.
### 3. Workloads ### 3. Workloads
```rust ```rust
.transactions_with(|txs| { use testing_framework_core::scenario::ScenarioBuilder;
txs.rate(5) // 5 transactions per block use testing_framework_workflows::ScenarioBuilderExt;
.users(500) // Use 500 of the 1,000 wallets
}) pub fn step_3_workloads() -> testing_framework_core::scenario::Builder<()> {
.da_with(|da| { ScenarioBuilder::with_node_counts(1, 1)
da.channel_rate(1) // 1 DA channel (more spawned with headroom) .wallets(1_000)
.blob_rate(1) // target 1 blob per block .transactions_with(|txs| {
.headroom_percent(20)// default headroom when sizing channels txs.rate(5) // 5 transactions per block
}) .users(500) // Use 500 of the 1,000 wallets
})
.da_with(|da| {
da.channel_rate(1) // 1 DA channel (more spawned with headroom)
.blob_rate(1) // target 1 blob per block
.headroom_percent(20) // default headroom when sizing channels
})
}
``` ```
Generates both transaction and DA traffic to stress both subsystems. Generates both transaction and DA traffic to stress both subsystems.
@ -123,7 +141,12 @@ Generates both transaction and DA traffic to stress both subsystems.
### 4. Expectation ### 4. Expectation
```rust ```rust
.expect_consensus_liveness() use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn step_4_expectation() -> testing_framework_core::scenario::Builder<()> {
ScenarioBuilder::with_node_counts(1, 1).expect_consensus_liveness() // This says what success means: blocks must be produced continuously.
}
``` ```
This says **what success means**: blocks must be produced continuously. This says **what success means**: blocks must be produced continuously.
@ -131,7 +154,13 @@ This says **what success means**: blocks must be produced continuously.
### 5. Run Duration ### 5. Run Duration
```rust ```rust
.with_run_duration(Duration::from_secs(60)) use std::time::Duration;
use testing_framework_core::scenario::ScenarioBuilder;
pub fn step_5_run_duration() -> testing_framework_core::scenario::Builder<()> {
ScenarioBuilder::with_node_counts(1, 1).with_run_duration(Duration::from_secs(60))
}
``` ```
Run for 60 seconds (~27 blocks with default 2s slots, 0.9 coefficient). Framework ensures this is at least 2× the consensus slot duration. Run for 60 seconds (~27 blocks with default 2s slots, 0.9 coefficient). Framework ensures this is at least 2× the consensus slot duration.
@ -139,9 +168,19 @@ Run for 60 seconds (~27 blocks with default 2s slots, 0.9 coefficient). Framewor
### 6. Deploy and Execute ### 6. Deploy and Execute
```rust ```rust
let deployer = LocalDeployer::default(); // Use local process deployer use anyhow::Result;
let runner = deployer.deploy(&plan).await?; // Provision infrastructure use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
let _handle = runner.run(&mut plan).await?; // Execute workloads & expectations use testing_framework_runner_local::LocalDeployer;
pub async fn step_6_deploy_and_execute() -> Result<()> {
let mut plan = ScenarioBuilder::with_node_counts(1, 1).build();
let deployer = LocalDeployer::default(); // Use local process deployer
let runner = deployer.deploy(&plan).await?; // Provision infrastructure
let _handle = runner.run(&mut plan).await?; // Execute workloads & expectations
Ok(())
}
``` ```
**Deployer** provisions the infrastructure. **Runner** orchestrates execution. **Deployer** provisions the infrastructure. **Runner** orchestrates execution.
@ -207,13 +246,20 @@ cargo run -p runner-examples --bin compose_runner
**In code:** Just swap the deployer: **In code:** Just swap the deployer:
```rust ```rust
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_compose::ComposeDeployer; use testing_framework_runner_compose::ComposeDeployer;
// ... same scenario definition ... pub async fn run_with_compose_deployer() -> Result<()> {
// ... same scenario definition ...
let mut plan = ScenarioBuilder::with_node_counts(1, 1).build();
let deployer = ComposeDeployer::default(); // Use Docker Compose let deployer = ComposeDeployer::default(); // Use Docker Compose
let runner = deployer.deploy(&plan).await?; let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?; let _handle = runner.run(&mut plan).await?;
Ok(())
}
``` ```
## Next Steps ## Next Steps

View File

@ -9,21 +9,22 @@ interpret results correctly.
Describe **what** you want to test, not **how** to orchestrate it: Describe **what** you want to test, not **how** to orchestrate it:
```rust ```rust
// Good: declarative use testing_framework_core::scenario::ScenarioBuilder;
ScenarioBuilder::topology_with(|t| { use testing_framework_workflows::ScenarioBuilderExt;
t.network_star()
.validators(2)
.executors(1)
})
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
})
.expect_consensus_liveness()
.build();
// Bad: imperative (framework doesn't work this way) pub fn declarative_over_imperative() {
// spawn_validator(); spawn_executor(); // Good: declarative
// loop { submit_tx(); check_block(); } let _plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(2).executors(1))
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
})
.expect_consensus_liveness()
.build();
// Bad: imperative (framework doesn't work this way)
// spawn_validator(); spawn_executor();
// loop { submit_tx(); check_block(); }
}
``` ```
**Why it matters:** The framework handles deployment, readiness, and cleanup. **Why it matters:** The framework handles deployment, readiness, and cleanup.
@ -39,22 +40,25 @@ Reason in **blocks** and **consensus intervals**, not wall-clock seconds.
- Expected rate: ~27 blocks per minute - Expected rate: ~27 blocks per minute
```rust ```rust
// Good: protocol-oriented thinking use std::time::Duration;
let plan = ScenarioBuilder::topology_with(|t| {
t.network_star()
.validators(2)
.executors(1)
})
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
})
.with_run_duration(Duration::from_secs(60)) // Let framework calculate expected blocks
.expect_consensus_liveness() // "Did we produce the expected blocks?"
.build();
// Bad: wall-clock assumptions use testing_framework_core::scenario::ScenarioBuilder;
// "I expect exactly 30 blocks in 60 seconds" use testing_framework_workflows::ScenarioBuilderExt;
// This breaks on slow CI where slot timing might drift
pub fn protocol_time_not_wall_time() {
// Good: protocol-oriented thinking
let _plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(2).executors(1))
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
})
.with_run_duration(Duration::from_secs(60)) // Let framework calculate expected blocks
.expect_consensus_liveness() // "Did we produce the expected blocks?"
.build();
// Bad: wall-clock assumptions
// "I expect exactly 30 blocks in 60 seconds"
// This breaks on slow CI where slot timing might drift
}
``` ```
**Why it matters:** Slot timing is fixed (2s by default, NTP-synchronized), so the **Why it matters:** Slot timing is fixed (2s by default, NTP-synchronized), so the
@ -73,37 +77,37 @@ not "blocks produced in exact wall-clock seconds".
**Chaos is opt-in:** **Chaos is opt-in:**
```rust ```rust
// Separate: functional test (deterministic) use std::time::Duration;
let plan = ScenarioBuilder::topology_with(|t| {
t.network_star()
.validators(2)
.executors(1)
})
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
})
.expect_consensus_liveness()
.build();
// Separate: chaos test (introduces randomness) use testing_framework_core::scenario::ScenarioBuilder;
let chaos_plan = ScenarioBuilder::topology_with(|t| { use testing_framework_workflows::{ChaosBuilderExt, ScenarioBuilderExt};
t.network_star()
.validators(3) pub fn determinism_first() {
.executors(2) // Separate: functional test (deterministic)
}) let _plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(2).executors(1))
.enable_node_control() .transactions_with(|txs| {
.chaos_with(|c| { txs.rate(5) // 5 transactions per block
c.restart() })
.min_delay(Duration::from_secs(30)) .expect_consensus_liveness()
.max_delay(Duration::from_secs(60)) .build();
.target_cooldown(Duration::from_secs(45))
.apply() // Separate: chaos test (introduces randomness)
}) let _chaos_plan =
.transactions_with(|txs| { ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(2))
txs.rate(5) // 5 transactions per block .enable_node_control()
}) .chaos_with(|c| {
.expect_consensus_liveness() c.restart()
.build(); .min_delay(Duration::from_secs(30))
.max_delay(Duration::from_secs(60))
.target_cooldown(Duration::from_secs(45))
.apply()
})
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
})
.expect_consensus_liveness()
.build();
}
``` ```
**Why it matters:** Mixing determinism with chaos creates noisy, hard-to-debug **Why it matters:** Mixing determinism with chaos creates noisy, hard-to-debug
@ -132,11 +136,25 @@ perspective.
Always run long enough for **meaningful block production**: Always run long enough for **meaningful block production**:
```rust ```rust
// Bad: too short use std::time::Duration;
.with_run_duration(Duration::from_secs(5)) // ~2 blocks (with default 2s slots, 0.9 coeff)
// Good: enough blocks for assertions use testing_framework_core::scenario::ScenarioBuilder;
.with_run_duration(Duration::from_secs(60)) // ~27 blocks (with default 2s slots, 0.9 coeff) use testing_framework_workflows::ScenarioBuilderExt;
pub fn minimum_run_windows() {
// Bad: too short (~2 blocks with default 2s slots, 0.9 coeff)
let _too_short = ScenarioBuilder::with_node_counts(1, 0)
.with_run_duration(Duration::from_secs(5))
.expect_consensus_liveness()
.build();
// Good: enough blocks for assertions (~27 blocks with default 2s slots, 0.9
// coeff)
let _good = ScenarioBuilder::with_node_counts(1, 0)
.with_run_duration(Duration::from_secs(60))
.expect_consensus_liveness()
.build();
}
``` ```
**Note:** Block counts assume default consensus parameters: **Note:** Block counts assume default consensus parameters:

View File

@ -0,0 +1,23 @@
[package]
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
name = "doc-snippets"
publish = false
readme.workspace = true
repository.workspace = true
version.workspace = true
[dependencies]
anyhow = "1"
async-trait = { workspace = true }
testing-framework-core = { workspace = true }
testing-framework-runner-compose = { workspace = true }
testing-framework-runner-k8s = { workspace = true }
testing-framework-runner-local = { workspace = true }
testing-framework-workflows = { workspace = true }
[lints]
workspace = true

View File

@ -0,0 +1,14 @@
use std::time::Duration;
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn scenario_plan() -> testing_framework_core::scenario::Scenario<()> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(2))
.wallets(50)
.transactions_with(|txs| txs.rate(5).users(20))
.da_with(|da| da.channel_rate(1).blob_rate(2))
.expect_consensus_liveness()
.with_run_duration(Duration::from_secs(90))
.build()
}

View File

@ -0,0 +1,21 @@
use std::time::Duration;
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::{ScenarioBuilderExt, workloads::chaos::RandomRestartWorkload};
pub fn random_restart_plan() -> testing_framework_core::scenario::Scenario<
testing_framework_core::scenario::NodeControlCapability,
> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(2).executors(1))
.enable_node_control()
.with_workload(RandomRestartWorkload::new(
Duration::from_secs(45), // min delay
Duration::from_secs(75), // max delay
Duration::from_secs(120), // target cooldown
true, // include validators
true, // include executors
))
.expect_consensus_liveness()
.with_run_duration(Duration::from_secs(150))
.build()
}

View File

@ -0,0 +1,38 @@
use async_trait::async_trait;
use testing_framework_core::scenario::{DynError, Expectation, RunContext};
pub struct ReachabilityExpectation {
target_idx: usize,
}
impl ReachabilityExpectation {
pub fn new(target_idx: usize) -> Self {
Self { target_idx }
}
}
#[async_trait]
impl Expectation for ReachabilityExpectation {
fn name(&self) -> &str {
"target_reachable"
}
async fn evaluate(&mut self, ctx: &RunContext) -> Result<(), DynError> {
let client = ctx
.node_clients()
.validator_clients()
.get(self.target_idx)
.ok_or_else(|| {
Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"missing target client",
)) as DynError
})?;
client
.consensus_info()
.await
.map(|_| ())
.map_err(|e| e.into())
}
}

View File

@ -0,0 +1,64 @@
use async_trait::async_trait;
use testing_framework_core::{
scenario::{DynError, Expectation, RunContext, RunMetrics, Workload},
topology::generation::GeneratedTopology,
};
pub struct ReachabilityWorkload {
target_idx: usize,
}
impl ReachabilityWorkload {
pub fn new(target_idx: usize) -> Self {
Self { target_idx }
}
}
#[async_trait]
impl Workload for ReachabilityWorkload {
fn name(&self) -> &str {
"reachability_workload"
}
fn expectations(&self) -> Vec<Box<dyn Expectation>> {
vec![Box::new(
crate::custom_workload_example_expectation::ReachabilityExpectation::new(
self.target_idx,
),
)]
}
fn init(
&mut self,
topology: &GeneratedTopology,
_run_metrics: &RunMetrics,
) -> Result<(), DynError> {
if topology.validators().get(self.target_idx).is_none() {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"no validator at requested index",
)));
}
Ok(())
}
async fn start(&self, ctx: &RunContext) -> Result<(), DynError> {
let client = ctx
.node_clients()
.validator_clients()
.get(self.target_idx)
.ok_or_else(|| {
Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"missing target client",
)) as DynError
})?;
// Lightweight API call to prove reachability.
client
.consensus_info()
.await
.map(|_| ())
.map_err(|e| e.into())
}
}

View File

@ -0,0 +1,6 @@
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn build_plan() -> testing_framework_core::scenario::Scenario<()> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(0)).build() // Construct the final Scenario
}

View File

@ -0,0 +1,29 @@
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer;
use testing_framework_workflows::ScenarioBuilderExt;
pub async fn run_test() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(2))
.wallets(50)
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
.users(20)
})
.da_with(|da| {
da.channel_rate(1) // number of DA channels
.blob_rate(2) // target 2 blobs per block
.headroom_percent(20) // optional channel headroom
})
.expect_consensus_liveness()
.with_run_duration(Duration::from_secs(90))
.build();
let deployer = LocalDeployer::default();
let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?;
Ok(())
}

View File

@ -0,0 +1,14 @@
use testing_framework_runner_compose::ComposeDeployer;
use testing_framework_runner_k8s::K8sDeployer;
use testing_framework_runner_local::LocalDeployer;
pub fn deployers() {
// Local processes
let _deployer = LocalDeployer::default();
// Docker Compose
let _deployer = ComposeDeployer::default();
// Kubernetes
let _deployer = K8sDeployer::default();
}

View File

@ -0,0 +1,8 @@
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn expectations_plan() -> testing_framework_core::scenario::Scenario<()> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(0))
.expect_consensus_liveness() // Assert blocks are produced continuously
.build()
}

View File

@ -0,0 +1,7 @@
use std::time::Duration;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_compose::ComposeDeployer;
use testing_framework_runner_k8s::K8sDeployer;
use testing_framework_runner_local::LocalDeployer;
use testing_framework_workflows::{ChaosBuilderExt, ScenarioBuilderExt};

View File

@ -0,0 +1,10 @@
use std::time::Duration;
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn run_duration_plan() -> testing_framework_core::scenario::Scenario<()> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(0))
.with_run_duration(Duration::from_secs(120)) // Run for 120 seconds
.build()
}

View File

@ -0,0 +1,9 @@
use testing_framework_core::scenario::{Builder, ScenarioBuilder};
pub fn topology() -> Builder<()> {
ScenarioBuilder::topology_with(|t| {
t.network_star() // Star topology (all connect to seed node)
.validators(3) // Number of validator nodes
.executors(2) // Number of executor nodes
})
}

View File

@ -0,0 +1,12 @@
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn transactions_plan() -> testing_framework_core::scenario::Scenario<()> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(0))
.wallets(50)
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
.users(20) // Use 20 of the seeded wallets
}) // Finish transaction workload config
.build()
}

View File

@ -0,0 +1,8 @@
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn wallets_plan() -> testing_framework_core::scenario::Scenario<()> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(0))
.wallets(50) // Seed 50 funded wallet accounts
.build()
}

View File

@ -0,0 +1,17 @@
use std::time::Duration;
use testing_framework_core::scenario::{NodeControlCapability, ScenarioBuilder};
use testing_framework_workflows::{ChaosBuilderExt, ScenarioBuilderExt};
pub fn chaos_plan() -> testing_framework_core::scenario::Scenario<NodeControlCapability> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(2))
.enable_node_control() // Enable node control capability
.chaos_with(|c| {
c.restart() // Random restart chaos
.min_delay(Duration::from_secs(30)) // Min time between restarts
.max_delay(Duration::from_secs(60)) // Max time between restarts
.target_cooldown(Duration::from_secs(45)) // Cooldown after restart
.apply() // Required for chaos configuration
})
.build()
}

View File

@ -0,0 +1,13 @@
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn da_plan() -> testing_framework_core::scenario::Scenario<()> {
ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(1))
.wallets(50)
.da_with(|da| {
da.channel_rate(1) // number of DA channels to run
.blob_rate(2) // target 2 blobs per block (headroom applied)
.headroom_percent(20) // optional headroom when sizing channels
}) // Finish DA workload config
.build()
}

View File

@ -0,0 +1,16 @@
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer;
use testing_framework_workflows::ScenarioBuilderExt;
pub async fn execution() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(0))
.expect_consensus_liveness()
.build();
let deployer = LocalDeployer::default();
let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?;
Ok(())
}

View File

@ -0,0 +1,29 @@
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_compose::ComposeDeployer;
use testing_framework_workflows::{ChaosBuilderExt, ScenarioBuilderExt};
pub async fn aggressive_chaos_test() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(4).executors(2))
.enable_node_control()
.wallets(50)
.transactions_with(|txs| txs.rate(10).users(20))
.chaos_with(|c| {
c.restart()
.min_delay(Duration::from_secs(10))
.max_delay(Duration::from_secs(20))
.target_cooldown(Duration::from_secs(15))
.apply()
})
.expect_consensus_liveness()
.with_run_duration(Duration::from_secs(180))
.build();
let deployer = ComposeDeployer::default();
let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?;
Ok(())
}

View File

@ -0,0 +1,26 @@
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_compose::ComposeDeployer;
use testing_framework_workflows::ScenarioBuilderExt;
pub async fn load_progression_test() -> Result<()> {
for rate in [5, 10, 20, 30] {
println!("Testing with rate: {}", rate);
let mut plan =
ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(2))
.wallets(50)
.transactions_with(|txs| txs.rate(rate).users(20))
.expect_consensus_liveness()
.with_run_duration(Duration::from_secs(60))
.build();
let deployer = ComposeDeployer::default();
let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?;
}
Ok(())
}

View File

@ -0,0 +1,22 @@
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_compose::ComposeDeployer;
use testing_framework_workflows::ScenarioBuilderExt;
pub async fn sustained_load_test() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(4).executors(2))
.wallets(100)
.transactions_with(|txs| txs.rate(15).users(50))
.da_with(|da| da.channel_rate(2).blob_rate(3))
.expect_consensus_liveness()
.with_run_duration(Duration::from_secs(300))
.build();
let deployer = ComposeDeployer::default();
let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?;
Ok(())
}

View File

@ -0,0 +1,29 @@
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_compose::ComposeDeployer;
use testing_framework_workflows::{ChaosBuilderExt, ScenarioBuilderExt};
pub async fn chaos_resilience() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(4).executors(2))
.enable_node_control()
.wallets(20)
.transactions_with(|txs| txs.rate(3).users(10))
.chaos_with(|c| {
c.restart()
.min_delay(Duration::from_secs(20))
.max_delay(Duration::from_secs(40))
.target_cooldown(Duration::from_secs(30))
.apply()
})
.expect_consensus_liveness()
.with_run_duration(Duration::from_secs(120))
.build();
let deployer = ComposeDeployer::default();
let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?;
Ok(())
}

View File

@ -0,0 +1,22 @@
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer;
use testing_framework_workflows::ScenarioBuilderExt;
pub async fn da_and_transactions() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(2))
.wallets(30)
.transactions_with(|txs| txs.rate(5).users(15))
.da_with(|da| da.channel_rate(2).blob_rate(2))
.expect_consensus_liveness()
.with_run_duration(Duration::from_secs(90))
.build();
let deployer = LocalDeployer::default();
let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?;
Ok(())
}

View File

@ -0,0 +1,19 @@
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer;
use testing_framework_workflows::ScenarioBuilderExt;
pub async fn simple_consensus() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(0))
.expect_consensus_liveness()
.with_run_duration(Duration::from_secs(30))
.build();
let deployer = LocalDeployer::default();
let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?;
Ok(())
}

View File

@ -0,0 +1,21 @@
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer;
use testing_framework_workflows::ScenarioBuilderExt;
pub async fn transaction_workload() -> Result<()> {
let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(2).executors(0))
.wallets(20)
.transactions_with(|txs| txs.rate(5).users(10))
.expect_consensus_liveness()
.with_run_duration(Duration::from_secs(60))
.build();
let deployer = LocalDeployer::default();
let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?;
Ok(())
}

View File

@ -0,0 +1,19 @@
use async_trait::async_trait;
use testing_framework_core::scenario::{Deployer, Runner, Scenario};
#[derive(Debug)]
pub struct YourError;
pub struct YourDeployer;
#[async_trait]
impl Deployer for YourDeployer {
type Error = YourError;
async fn deploy(&self, _scenario: &Scenario<()>) -> Result<Runner, Self::Error> {
// Provision infrastructure
// Wait for readiness
// Return Runner
todo!()
}
}

View File

@ -0,0 +1,17 @@
use testing_framework_core::scenario::ScenarioBuilder;
pub trait YourExpectationDslExt: Sized {
fn expect_your_condition(self) -> Self;
}
impl<Caps> YourExpectationDslExt for testing_framework_core::scenario::Builder<Caps> {
fn expect_your_condition(self) -> Self {
self
}
}
pub fn use_in_examples() {
let _plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(0))
.expect_your_condition()
.build();
}

View File

@ -0,0 +1,16 @@
use async_trait::async_trait;
use testing_framework_core::scenario::{DynError, Expectation, RunContext};
pub struct YourExpectation;
#[async_trait]
impl Expectation for YourExpectation {
fn name(&self) -> &'static str {
"your_expectation"
}
async fn evaluate(&mut self, _ctx: &RunContext) -> Result<(), DynError> {
// implementation
Ok(())
}
}

View File

@ -0,0 +1,11 @@
pub struct YourWorkloadBuilder;
impl YourWorkloadBuilder {
pub fn some_config(self) -> Self {
self
}
}
pub trait ScenarioBuilderExt: Sized {
fn your_workload(self) -> YourWorkloadBuilder;
}

View File

@ -0,0 +1,16 @@
use async_trait::async_trait;
use testing_framework_core::scenario::{DynError, RunContext, Workload};
pub struct YourWorkload;
#[async_trait]
impl Workload for YourWorkload {
fn name(&self) -> &'static str {
"your_workload"
}
async fn start(&self, _ctx: &RunContext) -> Result<(), DynError> {
// implementation
Ok(())
}
}

View File

@ -0,0 +1,31 @@
use testing_framework_core::scenario::ScenarioBuilder;
pub struct YourWorkloadBuilder;
impl YourWorkloadBuilder {
pub fn some_config(self) -> Self {
self
}
}
pub trait YourWorkloadDslExt: Sized {
fn your_workload_with<F>(self, configurator: F) -> Self
where
F: FnOnce(YourWorkloadBuilder) -> YourWorkloadBuilder;
}
impl<Caps> YourWorkloadDslExt for testing_framework_core::scenario::Builder<Caps> {
fn your_workload_with<F>(self, configurator: F) -> Self
where
F: FnOnce(YourWorkloadBuilder) -> YourWorkloadBuilder,
{
let _ = configurator(YourWorkloadBuilder);
self
}
}
pub fn use_in_examples() {
let _plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(0))
.your_workload_with(|w| w.some_config())
.build();
}

View File

@ -0,0 +1,46 @@
#![allow(dead_code, unused_imports, unused_variables)]
mod architecture_overview_builder_api;
mod chaos_workloads_random_restart;
mod custom_workload_example_expectation;
mod custom_workload_example_workload;
mod dsl_cheat_sheet_build;
mod dsl_cheat_sheet_build_complete_example;
mod dsl_cheat_sheet_deployers;
mod dsl_cheat_sheet_expectations;
mod dsl_cheat_sheet_imports;
mod dsl_cheat_sheet_run_duration;
mod dsl_cheat_sheet_topology;
mod dsl_cheat_sheet_transactions_workload;
mod dsl_cheat_sheet_wallets;
mod dsl_cheat_sheet_workload_chaos;
mod dsl_cheat_sheet_workload_da;
mod dsl_cheat_sheet_workload_execution;
mod examples_advanced_aggressive_chaos_test;
mod examples_advanced_load_progression_test;
mod examples_advanced_sustained_load_test;
mod examples_chaos_resilience;
mod examples_da_and_transactions;
mod examples_simple_consensus;
mod examples_transaction_workload;
mod internal_crate_reference_add_deployer;
mod internal_crate_reference_add_expectation_builder_ext;
mod internal_crate_reference_add_expectation_trait;
mod internal_crate_reference_add_workload_builder_ext;
mod internal_crate_reference_add_workload_trait;
mod internal_crate_reference_add_workload_use_in_examples;
mod node_control_accessing_control;
mod node_control_trait;
mod quickstart_adjust_topology;
mod quickstart_core_api_pattern;
mod quickstart_step_1_topology;
mod quickstart_step_2_wallets;
mod quickstart_step_3_workloads;
mod quickstart_step_4_expectation;
mod quickstart_step_5_run_duration;
mod quickstart_step_6_deploy_and_execute;
mod quickstart_swap_deployer_compose;
mod testing_philosophy_declarative_over_imperative;
mod testing_philosophy_determinism_first;
mod testing_philosophy_minimum_run_windows;
mod testing_philosophy_protocol_time_not_wall_time;

View File

@ -0,0 +1,19 @@
use async_trait::async_trait;
use testing_framework_core::scenario::{DynError, RunContext, Workload};
struct RestartWorkload;
#[async_trait]
impl Workload for RestartWorkload {
fn name(&self) -> &str {
"restart_workload"
}
async fn start(&self, ctx: &RunContext) -> Result<(), DynError> {
if let Some(control) = ctx.node_control() {
// Restart the first validator (index 0) if supported.
control.restart_validator(0).await?;
}
Ok(())
}
}

View File

@ -0,0 +1,8 @@
use async_trait::async_trait;
use testing_framework_core::scenario::DynError;
#[async_trait]
pub trait NodeControlHandle: Send + Sync {
async fn restart_validator(&self, index: usize) -> Result<(), DynError>;
async fn restart_executor(&self, index: usize) -> Result<(), DynError>;
}

View File

@ -0,0 +1,16 @@
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer;
pub async fn run_with_env_overrides() -> Result<()> {
// Uses NOMOS_DEMO_* env vars (or legacy *_DEMO_* vars)
let mut plan = ScenarioBuilder::with_node_counts(3, 2)
.with_run_duration(std::time::Duration::from_secs(120))
.build();
let deployer = LocalDeployer::default();
let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?;
Ok(())
}

View File

@ -0,0 +1,31 @@
use std::time::Duration;
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer;
use testing_framework_workflows::ScenarioBuilderExt;
pub async fn run_local_demo() -> Result<()> {
// Define the scenario (1 validator + 1 executor, tx + DA workload)
let mut plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(1).executors(1))
.wallets(1_000)
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
.users(500) // use 500 of the seeded wallets
})
.da_with(|da| {
da.channel_rate(1) // 1 channel
.blob_rate(1) // target 1 blob per block
.headroom_percent(20) // default headroom when sizing channels
})
.expect_consensus_liveness()
.with_run_duration(Duration::from_secs(60))
.build();
// Deploy and run
let deployer = LocalDeployer::default();
let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?;
Ok(())
}

View File

@ -0,0 +1,9 @@
use testing_framework_core::scenario::ScenarioBuilder;
pub fn step_1_topology() -> testing_framework_core::scenario::Builder<()> {
ScenarioBuilder::topology_with(|t| {
t.network_star() // Star topology: all nodes connect to seed
.validators(1) // 1 validator node
.executors(1) // 1 executor node (validator + DA dispersal)
})
}

View File

@ -0,0 +1,6 @@
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn step_2_wallets() -> testing_framework_core::scenario::Builder<()> {
ScenarioBuilder::with_node_counts(1, 1).wallets(1_000) // Seed 1,000 funded wallet accounts
}

View File

@ -0,0 +1,16 @@
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn step_3_workloads() -> testing_framework_core::scenario::Builder<()> {
ScenarioBuilder::with_node_counts(1, 1)
.wallets(1_000)
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
.users(500) // Use 500 of the 1,000 wallets
})
.da_with(|da| {
da.channel_rate(1) // 1 DA channel (more spawned with headroom)
.blob_rate(1) // target 1 blob per block
.headroom_percent(20) // default headroom when sizing channels
})
}

View File

@ -0,0 +1,6 @@
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn step_4_expectation() -> testing_framework_core::scenario::Builder<()> {
ScenarioBuilder::with_node_counts(1, 1).expect_consensus_liveness() // This says what success means: blocks must be produced continuously.
}

View File

@ -0,0 +1,7 @@
use std::time::Duration;
use testing_framework_core::scenario::ScenarioBuilder;
pub fn step_5_run_duration() -> testing_framework_core::scenario::Builder<()> {
ScenarioBuilder::with_node_counts(1, 1).with_run_duration(Duration::from_secs(60))
}

View File

@ -0,0 +1,13 @@
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_local::LocalDeployer;
pub async fn step_6_deploy_and_execute() -> Result<()> {
let mut plan = ScenarioBuilder::with_node_counts(1, 1).build();
let deployer = LocalDeployer::default(); // Use local process deployer
let runner = deployer.deploy(&plan).await?; // Provision infrastructure
let _handle = runner.run(&mut plan).await?; // Execute workloads & expectations
Ok(())
}

View File

@ -0,0 +1,14 @@
use anyhow::Result;
use testing_framework_core::scenario::{Deployer, ScenarioBuilder};
use testing_framework_runner_compose::ComposeDeployer;
pub async fn run_with_compose_deployer() -> Result<()> {
// ... same scenario definition ...
let mut plan = ScenarioBuilder::with_node_counts(1, 1).build();
let deployer = ComposeDeployer::default(); // Use Docker Compose
let runner = deployer.deploy(&plan).await?;
let _handle = runner.run(&mut plan).await?;
Ok(())
}

View File

@ -0,0 +1,16 @@
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn declarative_over_imperative() {
// Good: declarative
let _plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(2).executors(1))
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
})
.expect_consensus_liveness()
.build();
// Bad: imperative (framework doesn't work this way)
// spawn_validator(); spawn_executor();
// loop { submit_tx(); check_block(); }
}

View File

@ -0,0 +1,31 @@
use std::time::Duration;
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::{ChaosBuilderExt, ScenarioBuilderExt};
pub fn determinism_first() {
// Separate: functional test (deterministic)
let _plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(2).executors(1))
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
})
.expect_consensus_liveness()
.build();
// Separate: chaos test (introduces randomness)
let _chaos_plan =
ScenarioBuilder::topology_with(|t| t.network_star().validators(3).executors(2))
.enable_node_control()
.chaos_with(|c| {
c.restart()
.min_delay(Duration::from_secs(30))
.max_delay(Duration::from_secs(60))
.target_cooldown(Duration::from_secs(45))
.apply()
})
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
})
.expect_consensus_liveness()
.build();
}

View File

@ -0,0 +1,19 @@
use std::time::Duration;
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn minimum_run_windows() {
// Bad: too short (~2 blocks with default 2s slots, 0.9 coeff)
let _too_short = ScenarioBuilder::with_node_counts(1, 0)
.with_run_duration(Duration::from_secs(5))
.expect_consensus_liveness()
.build();
// Good: enough blocks for assertions (~27 blocks with default 2s slots, 0.9
// coeff)
let _good = ScenarioBuilder::with_node_counts(1, 0)
.with_run_duration(Duration::from_secs(60))
.expect_consensus_liveness()
.build();
}

View File

@ -0,0 +1,19 @@
use std::time::Duration;
use testing_framework_core::scenario::ScenarioBuilder;
use testing_framework_workflows::ScenarioBuilderExt;
pub fn protocol_time_not_wall_time() {
// Good: protocol-oriented thinking
let _plan = ScenarioBuilder::topology_with(|t| t.network_star().validators(2).executors(1))
.transactions_with(|txs| {
txs.rate(5) // 5 transactions per block
})
.with_run_duration(Duration::from_secs(60)) // Let framework calculate expected blocks
.expect_consensus_liveness() // "Did we produce the expected blocks?"
.build();
// Bad: wall-clock assumptions
// "I expect exactly 30 blocks in 60 seconds"
// This breaks on slow CI where slot timing might drift
}