mirror of
https://github.com/logos-blockchain/logos-blockchain-testing.git
synced 2026-01-10 09:13:13 +00:00
docs: add compilable doc-snippets crate
This commit is contained in:
parent
4141c98d8d
commit
de2e043e2a
31
.github/workflows/lint.yml
vendored
31
.github/workflows/lint.yml
vendored
@ -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
13
Cargo.lock
generated
@ -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"
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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:**
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@ -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(())
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@ -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(())
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@ -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(())
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
23
examples/doc-snippets/Cargo.toml
Normal file
23
examples/doc-snippets/Cargo.toml
Normal 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
|
||||||
@ -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()
|
||||||
|
}
|
||||||
21
examples/doc-snippets/src/chaos_workloads_random_restart.rs
Normal file
21
examples/doc-snippets/src/chaos_workloads_random_restart.rs
Normal 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()
|
||||||
|
}
|
||||||
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
6
examples/doc-snippets/src/dsl_cheat_sheet_build.rs
Normal file
6
examples/doc-snippets/src/dsl_cheat_sheet_build.rs
Normal 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
|
||||||
|
}
|
||||||
@ -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(())
|
||||||
|
}
|
||||||
14
examples/doc-snippets/src/dsl_cheat_sheet_deployers.rs
Normal file
14
examples/doc-snippets/src/dsl_cheat_sheet_deployers.rs
Normal 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();
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
7
examples/doc-snippets/src/dsl_cheat_sheet_imports.rs
Normal file
7
examples/doc-snippets/src/dsl_cheat_sheet_imports.rs
Normal 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};
|
||||||
10
examples/doc-snippets/src/dsl_cheat_sheet_run_duration.rs
Normal file
10
examples/doc-snippets/src/dsl_cheat_sheet_run_duration.rs
Normal 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()
|
||||||
|
}
|
||||||
9
examples/doc-snippets/src/dsl_cheat_sheet_topology.rs
Normal file
9
examples/doc-snippets/src/dsl_cheat_sheet_topology.rs
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
8
examples/doc-snippets/src/dsl_cheat_sheet_wallets.rs
Normal file
8
examples/doc-snippets/src/dsl_cheat_sheet_wallets.rs
Normal 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()
|
||||||
|
}
|
||||||
17
examples/doc-snippets/src/dsl_cheat_sheet_workload_chaos.rs
Normal file
17
examples/doc-snippets/src/dsl_cheat_sheet_workload_chaos.rs
Normal 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()
|
||||||
|
}
|
||||||
13
examples/doc-snippets/src/dsl_cheat_sheet_workload_da.rs
Normal file
13
examples/doc-snippets/src/dsl_cheat_sheet_workload_da.rs
Normal 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()
|
||||||
|
}
|
||||||
@ -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(())
|
||||||
|
}
|
||||||
@ -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(())
|
||||||
|
}
|
||||||
@ -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(())
|
||||||
|
}
|
||||||
@ -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(())
|
||||||
|
}
|
||||||
29
examples/doc-snippets/src/examples_chaos_resilience.rs
Normal file
29
examples/doc-snippets/src/examples_chaos_resilience.rs
Normal 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(())
|
||||||
|
}
|
||||||
22
examples/doc-snippets/src/examples_da_and_transactions.rs
Normal file
22
examples/doc-snippets/src/examples_da_and_transactions.rs
Normal 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(())
|
||||||
|
}
|
||||||
19
examples/doc-snippets/src/examples_simple_consensus.rs
Normal file
19
examples/doc-snippets/src/examples_simple_consensus.rs
Normal 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(())
|
||||||
|
}
|
||||||
21
examples/doc-snippets/src/examples_transaction_workload.rs
Normal file
21
examples/doc-snippets/src/examples_transaction_workload.rs
Normal 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(())
|
||||||
|
}
|
||||||
@ -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!()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
46
examples/doc-snippets/src/lib.rs
Normal file
46
examples/doc-snippets/src/lib.rs
Normal 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;
|
||||||
19
examples/doc-snippets/src/node_control_accessing_control.rs
Normal file
19
examples/doc-snippets/src/node_control_accessing_control.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
8
examples/doc-snippets/src/node_control_trait.rs
Normal file
8
examples/doc-snippets/src/node_control_trait.rs
Normal 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>;
|
||||||
|
}
|
||||||
16
examples/doc-snippets/src/quickstart_adjust_topology.rs
Normal file
16
examples/doc-snippets/src/quickstart_adjust_topology.rs
Normal 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(())
|
||||||
|
}
|
||||||
31
examples/doc-snippets/src/quickstart_core_api_pattern.rs
Normal file
31
examples/doc-snippets/src/quickstart_core_api_pattern.rs
Normal 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(())
|
||||||
|
}
|
||||||
9
examples/doc-snippets/src/quickstart_step_1_topology.rs
Normal file
9
examples/doc-snippets/src/quickstart_step_1_topology.rs
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
6
examples/doc-snippets/src/quickstart_step_2_wallets.rs
Normal file
6
examples/doc-snippets/src/quickstart_step_2_wallets.rs
Normal 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
|
||||||
|
}
|
||||||
16
examples/doc-snippets/src/quickstart_step_3_workloads.rs
Normal file
16
examples/doc-snippets/src/quickstart_step_3_workloads.rs
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -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.
|
||||||
|
}
|
||||||
@ -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))
|
||||||
|
}
|
||||||
@ -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(())
|
||||||
|
}
|
||||||
@ -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(())
|
||||||
|
}
|
||||||
@ -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(); }
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user