From de2e043e2a6f989eb46cd55e57f05bdd726548e5 Mon Sep 17 00:00:00 2001 From: andrussal Date: Tue, 16 Dec 2025 06:55:44 +0100 Subject: [PATCH] docs: add compilable doc-snippets crate --- .github/workflows/lint.yml | 31 +++ Cargo.lock | 13 ++ Cargo.toml | 1 + book/src/architecture-overview.md | 31 ++- book/src/chaos.md | 38 ++-- book/src/custom-workload-example.md | 41 +++- book/src/dsl-cheat-sheet.md | 174 +++++++++++----- book/src/examples-advanced.md | 77 +++---- book/src/examples.md | 80 +++----- book/src/internal-crate-reference.md | 194 +++++++++++------- book/src/node-control.md | 8 +- book/src/quickstart.md | 140 ++++++++----- book/src/testing-philosophy.md | 144 +++++++------ examples/doc-snippets/Cargo.toml | 23 +++ .../src/architecture_overview_builder_api.rs | 14 ++ .../src/chaos_workloads_random_restart.rs | 21 ++ .../custom_workload_example_expectation.rs | 38 ++++ .../src/custom_workload_example_workload.rs | 64 ++++++ .../doc-snippets/src/dsl_cheat_sheet_build.rs | 6 + .../dsl_cheat_sheet_build_complete_example.rs | 29 +++ .../src/dsl_cheat_sheet_deployers.rs | 14 ++ .../src/dsl_cheat_sheet_expectations.rs | 8 + .../src/dsl_cheat_sheet_imports.rs | 7 + .../src/dsl_cheat_sheet_run_duration.rs | 10 + .../src/dsl_cheat_sheet_topology.rs | 9 + .../dsl_cheat_sheet_transactions_workload.rs | 12 ++ .../src/dsl_cheat_sheet_wallets.rs | 8 + .../src/dsl_cheat_sheet_workload_chaos.rs | 17 ++ .../src/dsl_cheat_sheet_workload_da.rs | 13 ++ .../src/dsl_cheat_sheet_workload_execution.rs | 16 ++ ...examples_advanced_aggressive_chaos_test.rs | 29 +++ ...examples_advanced_load_progression_test.rs | 26 +++ .../examples_advanced_sustained_load_test.rs | 22 ++ .../src/examples_chaos_resilience.rs | 29 +++ .../src/examples_da_and_transactions.rs | 22 ++ .../src/examples_simple_consensus.rs | 19 ++ .../src/examples_transaction_workload.rs | 21 ++ .../internal_crate_reference_add_deployer.rs | 19 ++ ...e_reference_add_expectation_builder_ext.rs | 17 ++ ...l_crate_reference_add_expectation_trait.rs | 16 ++ ...rate_reference_add_workload_builder_ext.rs | 11 + ...rnal_crate_reference_add_workload_trait.rs | 16 ++ ..._reference_add_workload_use_in_examples.rs | 31 +++ examples/doc-snippets/src/lib.rs | 46 +++++ .../src/node_control_accessing_control.rs | 19 ++ .../doc-snippets/src/node_control_trait.rs | 8 + .../src/quickstart_adjust_topology.rs | 16 ++ .../src/quickstart_core_api_pattern.rs | 31 +++ .../src/quickstart_step_1_topology.rs | 9 + .../src/quickstart_step_2_wallets.rs | 6 + .../src/quickstart_step_3_workloads.rs | 16 ++ .../src/quickstart_step_4_expectation.rs | 6 + .../src/quickstart_step_5_run_duration.rs | 7 + .../quickstart_step_6_deploy_and_execute.rs | 13 ++ .../src/quickstart_swap_deployer_compose.rs | 14 ++ ..._philosophy_declarative_over_imperative.rs | 16 ++ .../testing_philosophy_determinism_first.rs | 31 +++ .../testing_philosophy_minimum_run_windows.rs | 19 ++ ..._philosophy_protocol_time_not_wall_time.rs | 19 ++ 59 files changed, 1460 insertions(+), 375 deletions(-) create mode 100644 examples/doc-snippets/Cargo.toml create mode 100644 examples/doc-snippets/src/architecture_overview_builder_api.rs create mode 100644 examples/doc-snippets/src/chaos_workloads_random_restart.rs create mode 100644 examples/doc-snippets/src/custom_workload_example_expectation.rs create mode 100644 examples/doc-snippets/src/custom_workload_example_workload.rs create mode 100644 examples/doc-snippets/src/dsl_cheat_sheet_build.rs create mode 100644 examples/doc-snippets/src/dsl_cheat_sheet_build_complete_example.rs create mode 100644 examples/doc-snippets/src/dsl_cheat_sheet_deployers.rs create mode 100644 examples/doc-snippets/src/dsl_cheat_sheet_expectations.rs create mode 100644 examples/doc-snippets/src/dsl_cheat_sheet_imports.rs create mode 100644 examples/doc-snippets/src/dsl_cheat_sheet_run_duration.rs create mode 100644 examples/doc-snippets/src/dsl_cheat_sheet_topology.rs create mode 100644 examples/doc-snippets/src/dsl_cheat_sheet_transactions_workload.rs create mode 100644 examples/doc-snippets/src/dsl_cheat_sheet_wallets.rs create mode 100644 examples/doc-snippets/src/dsl_cheat_sheet_workload_chaos.rs create mode 100644 examples/doc-snippets/src/dsl_cheat_sheet_workload_da.rs create mode 100644 examples/doc-snippets/src/dsl_cheat_sheet_workload_execution.rs create mode 100644 examples/doc-snippets/src/examples_advanced_aggressive_chaos_test.rs create mode 100644 examples/doc-snippets/src/examples_advanced_load_progression_test.rs create mode 100644 examples/doc-snippets/src/examples_advanced_sustained_load_test.rs create mode 100644 examples/doc-snippets/src/examples_chaos_resilience.rs create mode 100644 examples/doc-snippets/src/examples_da_and_transactions.rs create mode 100644 examples/doc-snippets/src/examples_simple_consensus.rs create mode 100644 examples/doc-snippets/src/examples_transaction_workload.rs create mode 100644 examples/doc-snippets/src/internal_crate_reference_add_deployer.rs create mode 100644 examples/doc-snippets/src/internal_crate_reference_add_expectation_builder_ext.rs create mode 100644 examples/doc-snippets/src/internal_crate_reference_add_expectation_trait.rs create mode 100644 examples/doc-snippets/src/internal_crate_reference_add_workload_builder_ext.rs create mode 100644 examples/doc-snippets/src/internal_crate_reference_add_workload_trait.rs create mode 100644 examples/doc-snippets/src/internal_crate_reference_add_workload_use_in_examples.rs create mode 100644 examples/doc-snippets/src/lib.rs create mode 100644 examples/doc-snippets/src/node_control_accessing_control.rs create mode 100644 examples/doc-snippets/src/node_control_trait.rs create mode 100644 examples/doc-snippets/src/quickstart_adjust_topology.rs create mode 100644 examples/doc-snippets/src/quickstart_core_api_pattern.rs create mode 100644 examples/doc-snippets/src/quickstart_step_1_topology.rs create mode 100644 examples/doc-snippets/src/quickstart_step_2_wallets.rs create mode 100644 examples/doc-snippets/src/quickstart_step_3_workloads.rs create mode 100644 examples/doc-snippets/src/quickstart_step_4_expectation.rs create mode 100644 examples/doc-snippets/src/quickstart_step_5_run_duration.rs create mode 100644 examples/doc-snippets/src/quickstart_step_6_deploy_and_execute.rs create mode 100644 examples/doc-snippets/src/quickstart_swap_deployer_compose.rs create mode 100644 examples/doc-snippets/src/testing_philosophy_declarative_over_imperative.rs create mode 100644 examples/doc-snippets/src/testing_philosophy_determinism_first.rs create mode 100644 examples/doc-snippets/src/testing_philosophy_minimum_run_windows.rs create mode 100644 examples/doc-snippets/src/testing_philosophy_protocol_time_not_wall_time.rs diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 96de3c5..2914dab 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -98,6 +98,37 @@ jobs: restore-keys: ${{ runner.os }}-target-clippy- - 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: runs-on: ubuntu-latest steps: diff --git a/Cargo.lock b/Cargo.lock index 7539748..97c6194 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1698,6 +1698,19 @@ dependencies = [ "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]] name = "dtoa" version = "1.0.10" diff --git a/Cargo.toml b/Cargo.toml index a120a18..d7a3054 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "examples", + "examples/doc-snippets", "testing-framework/configs", "testing-framework/core", "testing-framework/runners/compose", diff --git a/book/src/architecture-overview.md b/book/src/architecture-overview.md index 963855d..0c5ce8d 100644 --- a/book/src/architecture-overview.md +++ b/book/src/architecture-overview.md @@ -58,23 +58,20 @@ These binaries use the framework API (`ScenarioBuilder`) to construct and execut Scenarios are defined using a fluent builder pattern: ```rust -let mut plan = ScenarioBuilder::topology_with(|t| { - t.network_star() // Topology configuration - .validators(3) - .executors(2) - }) - .wallets(50) // Wallet seeding - .transactions_with(|txs| { - txs.rate(5) - .users(20) - }) - .da_with(|da| { - da.channel_rate(1) - .blob_rate(2) - }) - .expect_consensus_liveness() // Expectations - .with_run_duration(Duration::from_secs(90)) - .build(); +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() +} ``` **Key API Points:** diff --git a/book/src/chaos.md b/book/src/chaos.md index 50c3c5f..6a9bf90 100644 --- a/book/src/chaos.md +++ b/book/src/chaos.md @@ -20,26 +20,26 @@ recovery. The built-in restart workload lives in ## Usage ```rust use std::time::Duration; -use testing_framework_core::scenario::ScenarioBuilder; -use testing_framework_workflows::workloads::chaos::RandomRestartWorkload; -let plan = 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(); -// deploy with a runner that supports node control and run the scenario +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() +} ``` ## Expectations to pair diff --git a/book/src/custom-workload-example.md b/book/src/custom-workload-example.md index 72a29c7..6d02993 100644 --- a/book/src/custom-workload-example.md +++ b/book/src/custom-workload-example.md @@ -14,10 +14,10 @@ Key ideas: ```rust use async_trait::async_trait; -use testing_framework_core::scenario::{ - DynError, Expectation, RunContext, Workload, runtime::context::RunMetrics, +use testing_framework_core::{ + scenario::{DynError, Expectation, RunContext, RunMetrics, Workload}, + topology::generation::GeneratedTopology, }; -use testing_framework_core::topology::generation::GeneratedTopology; pub struct ReachabilityWorkload { target_idx: usize, @@ -36,16 +36,23 @@ impl Workload for ReachabilityWorkload { } fn expectations(&self) -> Vec> { - vec![Box::new(ReachabilityExpectation::new(self.target_idx))] + vec![Box::new( + crate::custom_workload_example_expectation::ReachabilityExpectation::new( + self.target_idx, + ), + )] } fn init( &mut self, topology: &GeneratedTopology, - _metrics: &RunMetrics, + _run_metrics: &RunMetrics, ) -> Result<(), DynError> { 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(()) } @@ -55,10 +62,19 @@ impl Workload for ReachabilityWorkload { .node_clients() .validator_clients() .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. - 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() .validator_clients() .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 .consensus_info() .await .map(|_| ()) - .map_err(|e| format!("target became unreachable during run: {e}").into()) + .map_err(|e| e.into()) } } ``` diff --git a/book/src/dsl-cheat-sheet.md b/book/src/dsl-cheat-sheet.md index 32776e3..509142e 100644 --- a/book/src/dsl-cheat-sheet.md +++ b/book/src/dsl-cheat-sheet.md @@ -5,123 +5,199 @@ Quick reference for the scenario builder DSL. All methods are chainable. ## Imports ```rust +use std::time::Duration; + use testing_framework_core::scenario::{Deployer, ScenarioBuilder}; -use testing_framework_runner_local::LocalDeployer; use testing_framework_runner_compose::ComposeDeployer; use testing_framework_runner_k8s::K8sDeployer; -use testing_framework_workflows::{ScenarioBuilderExt, ChaosBuilderExt}; -use std::time::Duration; +use testing_framework_runner_local::LocalDeployer; +use testing_framework_workflows::{ChaosBuilderExt, ScenarioBuilderExt}; ``` ## Topology ```rust -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 - }) // Finish topology configuration +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 + }) +} ``` ## Wallets ```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 ```rust -.transactions_with(|txs| { - txs.rate(5) // 5 transactions per block - .users(20) // Use 20 of the seeded wallets -}) // Finish transaction workload config +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() +} ``` ## DA Workload ```rust -.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 +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() +} ``` ## Chaos Workload (Requires `enable_node_control()`) ```rust -.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 -}) +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 { + 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 ```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 ```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 ```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 ```rust -// Local processes -let deployer = LocalDeployer::default(); +use testing_framework_runner_compose::ComposeDeployer; +use testing_framework_runner_k8s::K8sDeployer; +use testing_framework_runner_local::LocalDeployer; -// Docker Compose -let deployer = ComposeDeployer::default(); +pub fn deployers() { + // Local processes + let _deployer = LocalDeployer::default(); -// Kubernetes -let deployer = K8sDeployer::default(); + // Docker Compose + let _deployer = ComposeDeployer::default(); + + // Kubernetes + let _deployer = K8sDeployer::default(); +} ``` ## Execution ```rust -let runner = deployer.deploy(&plan).await?; -let _handle = runner.run(&mut plan).await?; +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(()) +} ``` ## Complete Example ```rust +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; -use std::time::Duration; -async fn run_test() -> Result<(), Box> { - let mut plan = ScenarioBuilder::topology_with(|t| { - t.network_star() - .validators(3) - .executors(2) - }) +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 + 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 + 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)) @@ -130,7 +206,7 @@ async fn run_test() -> Result<(), Box> { let deployer = LocalDeployer::default(); let runner = deployer.deploy(&plan).await?; let _handle = runner.run(&mut plan).await?; - + Ok(()) } ``` diff --git a/book/src/examples-advanced.md b/book/src/examples-advanced.md index b593ce4..4d63526 100644 --- a/book/src/examples-advanced.md +++ b/book/src/examples-advanced.md @@ -15,34 +15,30 @@ Realistic advanced scenarios demonstrating framework capabilities for production Test consensus under progressively increasing transaction load: ```rust +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; -use std::time::Duration; -async fn load_progression_test() -> Result<(), Box> { +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 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(()) } ``` @@ -54,26 +50,18 @@ async fn load_progression_test() -> Result<(), Box Result<(), Box> { - let mut plan = ScenarioBuilder::topology_with(|t| { - t.network_star() - .validators(4) - .executors(2) - }) +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) - }) + .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(); @@ -81,7 +69,7 @@ async fn sustained_load_test() -> Result<(), Box Result<(), Box Result<(), Box> { - let mut plan = ScenarioBuilder::topology_with(|t| { - t.network_star() - .validators(4) - .executors(2) - }) +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) - }) + .transactions_with(|txs| txs.rate(10).users(20)) .chaos_with(|c| { c.restart() .min_delay(Duration::from_secs(10)) @@ -124,7 +107,7 @@ async fn aggressive_chaos_test() -> Result<(), Box Result<(), Box> { - let mut plan = ScenarioBuilder::topology_with(|t| { - t.network_star() - .validators(3) - .executors(0) - }) +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(); @@ -39,7 +37,7 @@ async fn simple_consensus() -> Result<(), Box Result<(), Box Result<(), Box> { - let mut plan = ScenarioBuilder::topology_with(|t| { - t.network_star() - .validators(2) - .executors(0) - }) +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) - }) + .transactions_with(|txs| txs.rate(5).users(10)) .expect_consensus_liveness() .with_run_duration(Duration::from_secs(60)) .build(); @@ -74,7 +67,7 @@ async fn transaction_workload() -> Result<(), Box Result<(), Box Result<(), Box> { - let mut plan = ScenarioBuilder::topology_with(|t| { - t.network_star() - .validators(3) - .executors(2) - }) +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) - }) + .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(); @@ -113,7 +98,7 @@ async fn da_and_transactions() -> Result<(), Box Result<(), Box Result<(), Box> { - let mut plan = ScenarioBuilder::topology_with(|t| { - t.network_star() - .validators(4) - .executors(2) - }) +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) - }) + .transactions_with(|txs| txs.rate(3).users(10)) .chaos_with(|c| { c.restart() .min_delay(Duration::from_secs(20)) @@ -156,7 +136,7 @@ async fn chaos_resilience() -> Result<(), Box &'static str { "your_workload" } - async fn start(&self, ctx: &RunContext) -> Result<(), DynError> { - // implementation - Ok(()) - } - } - ``` +```rust +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(()) + } +} +``` 2. **Add builder extension** in `testing-framework/workflows/src/builder/mod.rs`: - ```rust - pub trait ScenarioBuilderExt { - fn your_workload(self) -> YourWorkloadBuilder; - } - ``` +```rust +pub struct 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`: - ```rust - let mut plan = ScenarioBuilder::topology_with(|t| { - t.network_star() - .validators(3) - .executors(0) - }) - .your_workload_with(|w| { // Your new DSL method with closure - w.some_config() - }) - .build(); - ``` +```rust +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(self, configurator: F) -> Self + where + F: FnOnce(YourWorkloadBuilder) -> YourWorkloadBuilder; +} + +impl YourWorkloadDslExt for testing_framework_core::scenario::Builder { + fn your_workload_with(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 1. **Define the expectation** in `testing-framework/workflows/src/expectations/your_expectation.rs`: - ```rust - use async_trait::async_trait; - use testing_framework_core::scenario::{Expectation, RunContext, DynError}; - - pub struct YourExpectation { - // config fields - } - - #[async_trait] - impl Expectation for YourExpectation { - fn name(&self) -> &str { "your_expectation" } - async fn evaluate(&mut self, ctx: &RunContext) -> Result<(), DynError> { - // implementation - Ok(()) - } - } - ``` +```rust +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(()) + } +} +``` 2. **Add builder extension** in `testing-framework/workflows/src/builder/mod.rs`: - ```rust - pub trait ScenarioBuilderExt { - fn expect_your_condition(self) -> Self; - } - ``` +```rust +use testing_framework_core::scenario::ScenarioBuilder; + +pub trait YourExpectationDslExt: Sized { + fn expect_your_condition(self) -> Self; +} + +impl YourExpectationDslExt for testing_framework_core::scenario::Builder { + 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 1. **Implement `Deployer` trait** in `testing-framework/runners/your_runner/src/deployer.rs`: - ```rust - use async_trait::async_trait; - use testing_framework_core::scenario::{Deployer, Runner, Scenario}; - - pub struct YourDeployer; - - #[async_trait] - impl Deployer for YourDeployer { - type Error = YourError; - - async fn deploy(&self, scenario: &Scenario) -> Result { - // Provision infrastructure - // Wait for readiness - // Return Runner - } - } - ``` +```rust +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 { + // Provision infrastructure + // Wait for readiness + // Return Runner + todo!() + } +} +``` 2. **Provide cleanup** and handle node control if supported. diff --git a/book/src/node-control.md b/book/src/node-control.md index 25aca79..95adbe5 100644 --- a/book/src/node-control.md +++ b/book/src/node-control.md @@ -39,7 +39,9 @@ struct RestartWorkload; #[async_trait] impl Workload for RestartWorkload { - fn name(&self) -> &str { "restart_workload" } + fn name(&self) -> &str { + "restart_workload" + } async fn start(&self, ctx: &RunContext) -> Result<(), DynError> { 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: ```rust +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>; diff --git a/book/src/quickstart.md b/book/src/quickstart.md index 82be830..b7fcd60 100644 --- a/book/src/quickstart.md +++ b/book/src/quickstart.md @@ -38,35 +38,37 @@ POL_PROOF_DEV_MODE=true cargo run -p runner-examples --bin local_runner **Core API Pattern** (simplified example): ```rust +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; -use std::time::Duration; -// 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(); +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?; + // Deploy and run + let deployer = LocalDeployer::default(); + let runner = deployer.deploy(&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. @@ -87,11 +89,15 @@ Let's unpack the code: ### 1. Topology Configuration ```rust -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) +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) }) +} ``` This defines **what** your test network looks like. @@ -99,7 +105,12 @@ This defines **what** your test network looks like. ### 2. Wallet Seeding ```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. @@ -107,15 +118,22 @@ Provides funded accounts for transaction submission. ### 3. Workloads ```rust -.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 -}) +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 + }) +} ``` 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 ```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. @@ -131,7 +154,13 @@ This says **what success means**: blocks must be produced continuously. ### 5. Run Duration ```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. @@ -139,9 +168,19 @@ Run for 60 seconds (~27 blocks with default 2s slots, 0.9 coefficient). Framewor ### 6. Deploy and Execute ```rust -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 +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(()) +} ``` **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: ```rust +use anyhow::Result; +use testing_framework_core::scenario::{Deployer, ScenarioBuilder}; 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 runner = deployer.deploy(&plan).await?; -let _handle = runner.run(&mut plan).await?; + let deployer = ComposeDeployer::default(); // Use Docker Compose + let runner = deployer.deploy(&plan).await?; + let _handle = runner.run(&mut plan).await?; + + Ok(()) +} ``` ## Next Steps diff --git a/book/src/testing-philosophy.md b/book/src/testing-philosophy.md index 56cf3b1..e68741a 100644 --- a/book/src/testing-philosophy.md +++ b/book/src/testing-philosophy.md @@ -9,21 +9,22 @@ interpret results correctly. Describe **what** you want to test, not **how** to orchestrate it: ```rust -// Good: declarative -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(); +use testing_framework_core::scenario::ScenarioBuilder; +use testing_framework_workflows::ScenarioBuilderExt; -// Bad: imperative (framework doesn't work this way) -// spawn_validator(); spawn_executor(); -// loop { submit_tx(); check_block(); } +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(); } +} ``` **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 ```rust -// 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(); +use std::time::Duration; -// Bad: wall-clock assumptions -// "I expect exactly 30 blocks in 60 seconds" -// This breaks on slow CI where slot timing might drift +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 +} ``` **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:** ```rust -// 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(); +use std::time::Duration; -// 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(); +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(); +} ``` **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**: ```rust -// Bad: too short -.with_run_duration(Duration::from_secs(5)) // ~2 blocks (with default 2s slots, 0.9 coeff) +use std::time::Duration; -// Good: enough blocks for assertions -.with_run_duration(Duration::from_secs(60)) // ~27 blocks (with default 2s slots, 0.9 coeff) +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(); +} ``` **Note:** Block counts assume default consensus parameters: diff --git a/examples/doc-snippets/Cargo.toml b/examples/doc-snippets/Cargo.toml new file mode 100644 index 0000000..147f5c9 --- /dev/null +++ b/examples/doc-snippets/Cargo.toml @@ -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 diff --git a/examples/doc-snippets/src/architecture_overview_builder_api.rs b/examples/doc-snippets/src/architecture_overview_builder_api.rs new file mode 100644 index 0000000..de50079 --- /dev/null +++ b/examples/doc-snippets/src/architecture_overview_builder_api.rs @@ -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() +} diff --git a/examples/doc-snippets/src/chaos_workloads_random_restart.rs b/examples/doc-snippets/src/chaos_workloads_random_restart.rs new file mode 100644 index 0000000..e7bde64 --- /dev/null +++ b/examples/doc-snippets/src/chaos_workloads_random_restart.rs @@ -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() +} diff --git a/examples/doc-snippets/src/custom_workload_example_expectation.rs b/examples/doc-snippets/src/custom_workload_example_expectation.rs new file mode 100644 index 0000000..243027e --- /dev/null +++ b/examples/doc-snippets/src/custom_workload_example_expectation.rs @@ -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()) + } +} diff --git a/examples/doc-snippets/src/custom_workload_example_workload.rs b/examples/doc-snippets/src/custom_workload_example_workload.rs new file mode 100644 index 0000000..cd17837 --- /dev/null +++ b/examples/doc-snippets/src/custom_workload_example_workload.rs @@ -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> { + 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()) + } +} diff --git a/examples/doc-snippets/src/dsl_cheat_sheet_build.rs b/examples/doc-snippets/src/dsl_cheat_sheet_build.rs new file mode 100644 index 0000000..cdbbe0d --- /dev/null +++ b/examples/doc-snippets/src/dsl_cheat_sheet_build.rs @@ -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 +} diff --git a/examples/doc-snippets/src/dsl_cheat_sheet_build_complete_example.rs b/examples/doc-snippets/src/dsl_cheat_sheet_build_complete_example.rs new file mode 100644 index 0000000..8ae96f7 --- /dev/null +++ b/examples/doc-snippets/src/dsl_cheat_sheet_build_complete_example.rs @@ -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(()) +} diff --git a/examples/doc-snippets/src/dsl_cheat_sheet_deployers.rs b/examples/doc-snippets/src/dsl_cheat_sheet_deployers.rs new file mode 100644 index 0000000..827c83f --- /dev/null +++ b/examples/doc-snippets/src/dsl_cheat_sheet_deployers.rs @@ -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(); +} diff --git a/examples/doc-snippets/src/dsl_cheat_sheet_expectations.rs b/examples/doc-snippets/src/dsl_cheat_sheet_expectations.rs new file mode 100644 index 0000000..c6ee4b7 --- /dev/null +++ b/examples/doc-snippets/src/dsl_cheat_sheet_expectations.rs @@ -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() +} diff --git a/examples/doc-snippets/src/dsl_cheat_sheet_imports.rs b/examples/doc-snippets/src/dsl_cheat_sheet_imports.rs new file mode 100644 index 0000000..9108f22 --- /dev/null +++ b/examples/doc-snippets/src/dsl_cheat_sheet_imports.rs @@ -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}; diff --git a/examples/doc-snippets/src/dsl_cheat_sheet_run_duration.rs b/examples/doc-snippets/src/dsl_cheat_sheet_run_duration.rs new file mode 100644 index 0000000..bdbc042 --- /dev/null +++ b/examples/doc-snippets/src/dsl_cheat_sheet_run_duration.rs @@ -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() +} diff --git a/examples/doc-snippets/src/dsl_cheat_sheet_topology.rs b/examples/doc-snippets/src/dsl_cheat_sheet_topology.rs new file mode 100644 index 0000000..9ed2208 --- /dev/null +++ b/examples/doc-snippets/src/dsl_cheat_sheet_topology.rs @@ -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 + }) +} diff --git a/examples/doc-snippets/src/dsl_cheat_sheet_transactions_workload.rs b/examples/doc-snippets/src/dsl_cheat_sheet_transactions_workload.rs new file mode 100644 index 0000000..91acfa0 --- /dev/null +++ b/examples/doc-snippets/src/dsl_cheat_sheet_transactions_workload.rs @@ -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() +} diff --git a/examples/doc-snippets/src/dsl_cheat_sheet_wallets.rs b/examples/doc-snippets/src/dsl_cheat_sheet_wallets.rs new file mode 100644 index 0000000..994e172 --- /dev/null +++ b/examples/doc-snippets/src/dsl_cheat_sheet_wallets.rs @@ -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() +} diff --git a/examples/doc-snippets/src/dsl_cheat_sheet_workload_chaos.rs b/examples/doc-snippets/src/dsl_cheat_sheet_workload_chaos.rs new file mode 100644 index 0000000..95ba6fe --- /dev/null +++ b/examples/doc-snippets/src/dsl_cheat_sheet_workload_chaos.rs @@ -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 { + 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() +} diff --git a/examples/doc-snippets/src/dsl_cheat_sheet_workload_da.rs b/examples/doc-snippets/src/dsl_cheat_sheet_workload_da.rs new file mode 100644 index 0000000..603c89d --- /dev/null +++ b/examples/doc-snippets/src/dsl_cheat_sheet_workload_da.rs @@ -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() +} diff --git a/examples/doc-snippets/src/dsl_cheat_sheet_workload_execution.rs b/examples/doc-snippets/src/dsl_cheat_sheet_workload_execution.rs new file mode 100644 index 0000000..8975f9c --- /dev/null +++ b/examples/doc-snippets/src/dsl_cheat_sheet_workload_execution.rs @@ -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(()) +} diff --git a/examples/doc-snippets/src/examples_advanced_aggressive_chaos_test.rs b/examples/doc-snippets/src/examples_advanced_aggressive_chaos_test.rs new file mode 100644 index 0000000..4315859 --- /dev/null +++ b/examples/doc-snippets/src/examples_advanced_aggressive_chaos_test.rs @@ -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(()) +} diff --git a/examples/doc-snippets/src/examples_advanced_load_progression_test.rs b/examples/doc-snippets/src/examples_advanced_load_progression_test.rs new file mode 100644 index 0000000..f5771ac --- /dev/null +++ b/examples/doc-snippets/src/examples_advanced_load_progression_test.rs @@ -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(()) +} diff --git a/examples/doc-snippets/src/examples_advanced_sustained_load_test.rs b/examples/doc-snippets/src/examples_advanced_sustained_load_test.rs new file mode 100644 index 0000000..fbc919d --- /dev/null +++ b/examples/doc-snippets/src/examples_advanced_sustained_load_test.rs @@ -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(()) +} diff --git a/examples/doc-snippets/src/examples_chaos_resilience.rs b/examples/doc-snippets/src/examples_chaos_resilience.rs new file mode 100644 index 0000000..d892cfe --- /dev/null +++ b/examples/doc-snippets/src/examples_chaos_resilience.rs @@ -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(()) +} diff --git a/examples/doc-snippets/src/examples_da_and_transactions.rs b/examples/doc-snippets/src/examples_da_and_transactions.rs new file mode 100644 index 0000000..44a8191 --- /dev/null +++ b/examples/doc-snippets/src/examples_da_and_transactions.rs @@ -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(()) +} diff --git a/examples/doc-snippets/src/examples_simple_consensus.rs b/examples/doc-snippets/src/examples_simple_consensus.rs new file mode 100644 index 0000000..a5c0f49 --- /dev/null +++ b/examples/doc-snippets/src/examples_simple_consensus.rs @@ -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(()) +} diff --git a/examples/doc-snippets/src/examples_transaction_workload.rs b/examples/doc-snippets/src/examples_transaction_workload.rs new file mode 100644 index 0000000..fe105d9 --- /dev/null +++ b/examples/doc-snippets/src/examples_transaction_workload.rs @@ -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(()) +} diff --git a/examples/doc-snippets/src/internal_crate_reference_add_deployer.rs b/examples/doc-snippets/src/internal_crate_reference_add_deployer.rs new file mode 100644 index 0000000..48f784e --- /dev/null +++ b/examples/doc-snippets/src/internal_crate_reference_add_deployer.rs @@ -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 { + // Provision infrastructure + // Wait for readiness + // Return Runner + todo!() + } +} diff --git a/examples/doc-snippets/src/internal_crate_reference_add_expectation_builder_ext.rs b/examples/doc-snippets/src/internal_crate_reference_add_expectation_builder_ext.rs new file mode 100644 index 0000000..be19df7 --- /dev/null +++ b/examples/doc-snippets/src/internal_crate_reference_add_expectation_builder_ext.rs @@ -0,0 +1,17 @@ +use testing_framework_core::scenario::ScenarioBuilder; + +pub trait YourExpectationDslExt: Sized { + fn expect_your_condition(self) -> Self; +} + +impl YourExpectationDslExt for testing_framework_core::scenario::Builder { + 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(); +} diff --git a/examples/doc-snippets/src/internal_crate_reference_add_expectation_trait.rs b/examples/doc-snippets/src/internal_crate_reference_add_expectation_trait.rs new file mode 100644 index 0000000..6fa1ac7 --- /dev/null +++ b/examples/doc-snippets/src/internal_crate_reference_add_expectation_trait.rs @@ -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(()) + } +} diff --git a/examples/doc-snippets/src/internal_crate_reference_add_workload_builder_ext.rs b/examples/doc-snippets/src/internal_crate_reference_add_workload_builder_ext.rs new file mode 100644 index 0000000..39e20f5 --- /dev/null +++ b/examples/doc-snippets/src/internal_crate_reference_add_workload_builder_ext.rs @@ -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; +} diff --git a/examples/doc-snippets/src/internal_crate_reference_add_workload_trait.rs b/examples/doc-snippets/src/internal_crate_reference_add_workload_trait.rs new file mode 100644 index 0000000..03daab8 --- /dev/null +++ b/examples/doc-snippets/src/internal_crate_reference_add_workload_trait.rs @@ -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(()) + } +} diff --git a/examples/doc-snippets/src/internal_crate_reference_add_workload_use_in_examples.rs b/examples/doc-snippets/src/internal_crate_reference_add_workload_use_in_examples.rs new file mode 100644 index 0000000..0e1e794 --- /dev/null +++ b/examples/doc-snippets/src/internal_crate_reference_add_workload_use_in_examples.rs @@ -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(self, configurator: F) -> Self + where + F: FnOnce(YourWorkloadBuilder) -> YourWorkloadBuilder; +} + +impl YourWorkloadDslExt for testing_framework_core::scenario::Builder { + fn your_workload_with(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(); +} diff --git a/examples/doc-snippets/src/lib.rs b/examples/doc-snippets/src/lib.rs new file mode 100644 index 0000000..0b7b592 --- /dev/null +++ b/examples/doc-snippets/src/lib.rs @@ -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; diff --git a/examples/doc-snippets/src/node_control_accessing_control.rs b/examples/doc-snippets/src/node_control_accessing_control.rs new file mode 100644 index 0000000..08f3af8 --- /dev/null +++ b/examples/doc-snippets/src/node_control_accessing_control.rs @@ -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(()) + } +} diff --git a/examples/doc-snippets/src/node_control_trait.rs b/examples/doc-snippets/src/node_control_trait.rs new file mode 100644 index 0000000..1a438f9 --- /dev/null +++ b/examples/doc-snippets/src/node_control_trait.rs @@ -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>; +} diff --git a/examples/doc-snippets/src/quickstart_adjust_topology.rs b/examples/doc-snippets/src/quickstart_adjust_topology.rs new file mode 100644 index 0000000..b3ac7bf --- /dev/null +++ b/examples/doc-snippets/src/quickstart_adjust_topology.rs @@ -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(()) +} diff --git a/examples/doc-snippets/src/quickstart_core_api_pattern.rs b/examples/doc-snippets/src/quickstart_core_api_pattern.rs new file mode 100644 index 0000000..54135b9 --- /dev/null +++ b/examples/doc-snippets/src/quickstart_core_api_pattern.rs @@ -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(()) +} diff --git a/examples/doc-snippets/src/quickstart_step_1_topology.rs b/examples/doc-snippets/src/quickstart_step_1_topology.rs new file mode 100644 index 0000000..ce2c7cb --- /dev/null +++ b/examples/doc-snippets/src/quickstart_step_1_topology.rs @@ -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) + }) +} diff --git a/examples/doc-snippets/src/quickstart_step_2_wallets.rs b/examples/doc-snippets/src/quickstart_step_2_wallets.rs new file mode 100644 index 0000000..19aad8c --- /dev/null +++ b/examples/doc-snippets/src/quickstart_step_2_wallets.rs @@ -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 +} diff --git a/examples/doc-snippets/src/quickstart_step_3_workloads.rs b/examples/doc-snippets/src/quickstart_step_3_workloads.rs new file mode 100644 index 0000000..51f2af0 --- /dev/null +++ b/examples/doc-snippets/src/quickstart_step_3_workloads.rs @@ -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 + }) +} diff --git a/examples/doc-snippets/src/quickstart_step_4_expectation.rs b/examples/doc-snippets/src/quickstart_step_4_expectation.rs new file mode 100644 index 0000000..9a24312 --- /dev/null +++ b/examples/doc-snippets/src/quickstart_step_4_expectation.rs @@ -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. +} diff --git a/examples/doc-snippets/src/quickstart_step_5_run_duration.rs b/examples/doc-snippets/src/quickstart_step_5_run_duration.rs new file mode 100644 index 0000000..4043973 --- /dev/null +++ b/examples/doc-snippets/src/quickstart_step_5_run_duration.rs @@ -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)) +} diff --git a/examples/doc-snippets/src/quickstart_step_6_deploy_and_execute.rs b/examples/doc-snippets/src/quickstart_step_6_deploy_and_execute.rs new file mode 100644 index 0000000..1679793 --- /dev/null +++ b/examples/doc-snippets/src/quickstart_step_6_deploy_and_execute.rs @@ -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(()) +} diff --git a/examples/doc-snippets/src/quickstart_swap_deployer_compose.rs b/examples/doc-snippets/src/quickstart_swap_deployer_compose.rs new file mode 100644 index 0000000..3195a51 --- /dev/null +++ b/examples/doc-snippets/src/quickstart_swap_deployer_compose.rs @@ -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(()) +} diff --git a/examples/doc-snippets/src/testing_philosophy_declarative_over_imperative.rs b/examples/doc-snippets/src/testing_philosophy_declarative_over_imperative.rs new file mode 100644 index 0000000..49796b4 --- /dev/null +++ b/examples/doc-snippets/src/testing_philosophy_declarative_over_imperative.rs @@ -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(); } +} diff --git a/examples/doc-snippets/src/testing_philosophy_determinism_first.rs b/examples/doc-snippets/src/testing_philosophy_determinism_first.rs new file mode 100644 index 0000000..cd779d2 --- /dev/null +++ b/examples/doc-snippets/src/testing_philosophy_determinism_first.rs @@ -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(); +} diff --git a/examples/doc-snippets/src/testing_philosophy_minimum_run_windows.rs b/examples/doc-snippets/src/testing_philosophy_minimum_run_windows.rs new file mode 100644 index 0000000..6ea2724 --- /dev/null +++ b/examples/doc-snippets/src/testing_philosophy_minimum_run_windows.rs @@ -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(); +} diff --git a/examples/doc-snippets/src/testing_philosophy_protocol_time_not_wall_time.rs b/examples/doc-snippets/src/testing_philosophy_protocol_time_not_wall_time.rs new file mode 100644 index 0000000..7da836a --- /dev/null +++ b/examples/doc-snippets/src/testing_philosophy_protocol_time_not_wall_time.rs @@ -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 +}