From 68a074950e8cec71546c6b2b41672f57844285a1 Mon Sep 17 00:00:00 2001 From: tolulopedd26 Date: Mon, 1 Jun 2026 10:58:41 +0100 Subject: [PATCH 1/2] feat(harness): wire delay/error-injection in serve, add uptime_secs to /health, interval rotation Closes #222 Closes #223 Closes #224 Closes #225 Co-Authored-By: tolulopedd26 --- packages/devkit/src/cli/export.rs | 13 ++--- packages/devkit/src/cli/mod.rs | 6 +- packages/devkit/src/cli/replay.rs | 59 ++++++++------------ packages/devkit/src/harness/horizon_mock.rs | 36 +++++++++++- packages/devkit/src/harness/scenarios/mod.rs | 37 ++++++++++++ packages/devkit/src/test_helpers/mod.rs | 27 ++++----- 6 files changed, 117 insertions(+), 61 deletions(-) diff --git a/packages/devkit/src/cli/export.rs b/packages/devkit/src/cli/export.rs index 0eedccc..4202a2c 100644 --- a/packages/devkit/src/cli/export.rs +++ b/packages/devkit/src/cli/export.rs @@ -1,9 +1,7 @@ -use crate::simulation::fee_model::FeePoint; +use crate::simulation::fee_model::FeePoint; use std::fmt::Write as FmtWrite; use std::path::{Path, PathBuf}; -use crate::simulation::fee_model::FeePoint; - /// Arguments for the `export` subcommand. pub struct ExportArgs { /// Source SQLite database path. @@ -56,7 +54,6 @@ impl Window { pub struct Export; impl Export { - /// Serialize fee points to CSV. /// Filter points by window relative to the latest timestamp. pub fn filter_window<'a>(points: &'a [FeePoint], window: Window) -> &'a [FeePoint] { match window.cutoff_seconds() { @@ -116,6 +113,10 @@ mod tests { ] } + fn sample() -> Vec { + vec![FeePoint { timestamp: 1000, fee: 100, ledger: 1, is_spike: false }] + } + #[test] fn window_1h_filters() { let p = pts(); @@ -128,8 +129,6 @@ mod tests { fn window_all_keeps_all() { let p = pts(); assert_eq!(Export::filter_window(&p, Window::All).len(), 2); - fn sample() -> Vec { - vec![FeePoint { timestamp: 1000, fee: 100, ledger: 1, is_spike: false }] } #[test] @@ -144,4 +143,4 @@ mod tests { assert!(json.starts_with('[')); assert!(json.ends_with(']')); } -} \ No newline at end of file +} diff --git a/packages/devkit/src/cli/mod.rs b/packages/devkit/src/cli/mod.rs index d8ea2a6..6e27d8b 100644 --- a/packages/devkit/src/cli/mod.rs +++ b/packages/devkit/src/cli/mod.rs @@ -2,6 +2,8 @@ pub mod benchmark; pub mod export; pub mod replay; +use clap::{Parser, Subcommand}; + /// Arguments for the `simulate` subcommand. pub struct SimulateArgs { /// Base fee floor in stroops. @@ -30,6 +32,8 @@ impl SimulateArgs { self.duration, self.base_fee, self.spike_prob ); } +} + /// Arguments for the `mock` subcommand. pub struct MockArgs { /// Scenario to load (e.g. "normal", "congested", "spike"). @@ -55,7 +59,7 @@ impl MockArgs { self.port, self.scenario ); } -use clap::{Parser, Subcommand}; +} /// Developer toolkit for the Stellar fee tracker. #[derive(Parser)] diff --git a/packages/devkit/src/cli/replay.rs b/packages/devkit/src/cli/replay.rs index ef0cd5e..122b156 100644 --- a/packages/devkit/src/cli/replay.rs +++ b/packages/devkit/src/cli/replay.rs @@ -8,6 +8,24 @@ pub struct ReplayArgs { pub db: PathBuf, /// Show a progress bar during replay. pub progress: bool, + /// Playback speed multiplier (1.0 = real-time). + pub speed: f32, + /// Start of the replay window (ISO-8601 timestamp). + pub from: Option, + /// End of the replay window (ISO-8601 timestamp). + pub to: Option, +} + +impl Default for ReplayArgs { + fn default() -> Self { + Self { + db: PathBuf::from("stellar_fees.db"), + progress: false, + speed: 1.0, + from: None, + to: None, + } + } } impl ReplayArgs { @@ -27,27 +45,10 @@ impl ReplayArgs { bar.inc(1); } bar.finish_with_message("replay complete"); -use clap::Args; - -/// Arguments for the `replay` subcommand. -#[derive(Args)] -pub struct ReplayArgs { - /// Path to the SQLite database file. - pub db: PathBuf, - /// Playback speed multiplier (1.0 = real-time). - #[arg(long, default_value = "1.0")] - pub speed: f32, - /// Start of the replay window (ISO-8601 timestamp). - #[arg(long)] - pub from: Option, - /// End of the replay window (ISO-8601 timestamp). - #[arg(long)] - pub to: Option, -} + } -impl ReplayArgs { /// Replays fee records filtered by the given time window. - pub fn run(&self) { + pub fn run_windowed(&self) { eprintln!( "Replaying from {} at {:.1}x speed, window {:?}..{:?}", self.db.display(), @@ -55,26 +56,10 @@ impl ReplayArgs { self.from, self.to ); - /// Path to the SQLite database file containing recorded fee data. - pub db: PathBuf, - /// Playback speed multiplier (1.0 = real-time, 10.0 = 10x faster). - #[arg(long, default_value = "1.0")] - pub speed: f32, -} - -impl ReplayArgs { - /// Replays fee records at the specified speed multiplier. - pub fn run(&self) { - eprintln!( - "Replaying from {} at {:.1}x speed", - self.db.display(), - self.speed - ); -} + } -impl ReplayArgs { /// Replays fee records from the database to stdout as a JSON stream. - pub fn run(&self) { + pub fn run_json(&self) { eprintln!("Replaying fee records from {}", self.db.display()); println!("[]"); } diff --git a/packages/devkit/src/harness/horizon_mock.rs b/packages/devkit/src/harness/horizon_mock.rs index dfeddde..4404e1f 100644 --- a/packages/devkit/src/harness/horizon_mock.rs +++ b/packages/devkit/src/harness/horizon_mock.rs @@ -12,6 +12,8 @@ pub struct HorizonMock { /// Optional canned JSON response for `GET /fee_stats`. When set, takes /// precedence over `scenario_path` and the convention-based file path. pub fee_stats_response: Option, + /// Unix timestamp when this mock was created (for uptime calculation). + pub start_time: u64, } impl HorizonMock { @@ -22,6 +24,7 @@ impl HorizonMock { scenario_path: None, error_rate: 0.0, fee_stats_response: None, + start_time: current_unix_secs(), } } @@ -80,7 +83,11 @@ impl HorizonMock { /// Returns the JSON body for `GET /health`. pub fn health_payload(&self) -> String { - format!(r#"{{"status":"ok","scenario":"{}"}}"#, self.scenario) + let uptime = current_unix_secs().saturating_sub(self.start_time); + format!( + r#"{{"status":"ok","scenario":"{}","uptime_secs":{}}}"#, + self.scenario, uptime + ) } /// Loads and returns the scenario JSON to be served at `GET /fee_stats`. @@ -125,6 +132,10 @@ pub struct HorizonMockConfig { pub delay_ms: u64, /// Probability [0.0, 1.0] of injecting a 500 error response. pub error_rate: f64, + /// Interval in seconds between automatic scenario rotations (0 = disabled). + pub rotate_secs: u64, + /// Ordered list of scenario names to rotate through (used when rotate_secs > 0). + pub rotation_scenarios: Vec, } impl Default for HorizonMockConfig { @@ -134,6 +145,8 @@ impl Default for HorizonMockConfig { scenario_path: std::path::PathBuf::from("src/harness/scenarios/normal.json"), delay_ms: 0, error_rate: 0.0, + rotate_secs: 0, + rotation_scenarios: Vec::new(), } } } @@ -156,6 +169,7 @@ impl HorizonMock { scenario_path: Some(config.scenario_path), error_rate: config.error_rate, fee_stats_response: None, + start_time: current_unix_secs(), } } } @@ -163,8 +177,8 @@ impl HorizonMock { /// Starts an axum HTTP server serving mock Horizon responses. /// /// Routes: -/// - `GET /fee_stats` — returns scenario fee stats JSON -/// - `GET /health` — returns `{"status":"ok","scenario":""}` +/// - `GET /fee_stats` — returns scenario fee stats JSON, with optional delay and error injection +/// - `GET /health` — returns `{"status":"ok","scenario":"","uptime_secs":N}` /// /// Binds to `0.0.0.0:port`. Returns when the server shuts down. pub async fn serve(mock: std::sync::Arc, port: u16) -> std::io::Result<()> { @@ -180,6 +194,14 @@ pub async fn serve(mock: std::sync::Arc, port: u16) -> std::io::Res get(move || { let m = m1.clone(); async move { + m.apply_delay(); + if m.should_inject_error() { + return ( + axum::http::StatusCode::SERVICE_UNAVAILABLE, + [(axum::http::header::CONTENT_TYPE, "application/json")], + r#"{"error":"service unavailable"}"#.to_string(), + ); + } match m.fee_stats_payload() { Ok(json) => ( axum::http::StatusCode::OK, @@ -212,6 +234,14 @@ pub async fn serve(mock: std::sync::Arc, port: u16) -> std::io::Res .map_err(std::io::Error::other) } +/// Returns the current Unix timestamp in seconds. +fn current_unix_secs() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + /// Minimal pseudo-random float in [0.0, 1.0) using system time as entropy. fn rand_f64() -> f64 { let nanos = std::time::SystemTime::now() diff --git a/packages/devkit/src/harness/scenarios/mod.rs b/packages/devkit/src/harness/scenarios/mod.rs index 916fca3..3ddf9fa 100644 --- a/packages/devkit/src/harness/scenarios/mod.rs +++ b/packages/devkit/src/harness/scenarios/mod.rs @@ -68,6 +68,10 @@ pub fn load_from_file(path: &Path) -> std::io::Result { pub struct ScenarioRotator { scenarios: Vec, index: usize, + /// How often (in seconds) to advance to the next scenario. 0 = manual only. + pub interval_secs: u64, + /// Unix timestamp of the last rotation. + last_rotated: u64, } impl ScenarioRotator { @@ -78,6 +82,18 @@ impl ScenarioRotator { Self { scenarios, index: 0, + interval_secs: 0, + last_rotated: current_unix_secs(), + } + } + + /// Creates a rotator that automatically advances every `interval_secs` seconds. + pub fn with_interval(scenarios: Vec, interval_secs: u64) -> Self { + Self { + scenarios, + index: 0, + interval_secs, + last_rotated: current_unix_secs(), } } @@ -88,6 +104,27 @@ impl ScenarioRotator { } let current = self.scenarios[self.index].as_str(); self.index = (self.index + 1) % self.scenarios.len(); + self.last_rotated = current_unix_secs(); Some(current) } + + /// Advances if the rotation interval has elapsed. Returns the new scenario name if rotated. + pub fn advance_if_due(&mut self) -> Option<&str> { + if self.interval_secs == 0 { + return None; + } + let elapsed = current_unix_secs().saturating_sub(self.last_rotated); + if elapsed >= self.interval_secs { + self.advance() + } else { + None + } + } +} + +fn current_unix_secs() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() } diff --git a/packages/devkit/src/test_helpers/mod.rs b/packages/devkit/src/test_helpers/mod.rs index b601a87..cedb970 100644 --- a/packages/devkit/src/test_helpers/mod.rs +++ b/packages/devkit/src/test_helpers/mod.rs @@ -1,8 +1,10 @@ -use crate::simulation::fee_model::{FeeModel, FeeModelConfig}; -use crate::types::FeeRecord; +use crate::simulation::fee_model::{FeeModel, FeeModelConfig, FeePoint}; + +use rand::rngs::SmallRng; +use rand::{Rng, SeedableRng}; /// Returns a deterministic fee sequence of `count` records seeded by `seed`. -pub fn make_fee_sequence(count: usize, seed: u64) -> Vec { +pub fn make_fee_sequence(count: usize, seed: u64) -> Vec { let config = FeeModelConfig { seed: Some(seed), ..Default::default() @@ -11,7 +13,7 @@ pub fn make_fee_sequence(count: usize, seed: u64) -> Vec { } /// Returns a fee sequence where every record is flagged as a spike. -pub fn make_spike_sequence(count: usize) -> Vec { +pub fn make_spike_sequence(count: usize) -> Vec { let config = FeeModelConfig { spike_probability: 1.0, seed: Some(0), @@ -21,7 +23,7 @@ pub fn make_spike_sequence(count: usize) -> Vec { } /// Returns a fee sequence with no spikes (baseline load only). -pub fn make_baseline_sequence(count: usize) -> Vec { +pub fn make_baseline_sequence(count: usize) -> Vec { let config = FeeModelConfig { spike_probability: 0.0, seed: Some(1), @@ -29,10 +31,6 @@ pub fn make_baseline_sequence(count: usize) -> Vec { }; FeeModel::new(config).generate(count, 0) } -//! Test helpers: deterministic fee sequence generator and SQLite fixture builder. - -use rand::{Rng, SeedableRng}; -use rand::rngs::SmallRng; /// Generates a deterministic fee sequence from a seed for repeatable tests. pub struct FeeGenerator { @@ -58,21 +56,21 @@ impl FeeGenerator { /// A simple in-memory fee record for fixture use. #[derive(Debug, Clone, PartialEq)] -pub struct FeeRecord { +pub struct FixtureFeeRecord { pub timestamp: u64, pub fee_amount: u64, pub ledger_sequence: u64, pub tx_hash: String, } -/// Builds a vec of FeeRecord fixtures for testing. +/// Builds a vec of FixtureFeeRecord fixtures for testing. pub struct FixtureBuilder; impl FixtureBuilder { /// Build `n` sequential fee records starting at `base_timestamp`. - pub fn build(n: usize, base_timestamp: u64, base_fee: u64) -> Vec { + pub fn build(n: usize, base_timestamp: u64, base_fee: u64) -> Vec { (0..n) - .map(|i| FeeRecord { + .map(|i| FixtureFeeRecord { timestamp: base_timestamp + i as u64, fee_amount: base_fee, ledger_sequence: 1000 + i as u64, @@ -97,6 +95,9 @@ mod tests { let records = FixtureBuilder::build(3, 1000, 100); assert_eq!(records[0].timestamp, 1000); assert_eq!(records[2].timestamp, 1002); + } + + #[test] fn same_seed_produces_same_sequence() { let a = FeeGenerator::new(42).generate(10, 100, 1000); let b = FeeGenerator::new(42).generate(10, 100, 1000); From 5a0a34d2dbcb6de501aca094a1bedc1bf0631a02 Mon Sep 17 00:00:00 2001 From: tolulopedd26 Date: Mon, 1 Jun 2026 11:45:06 +0100 Subject: [PATCH 2/2] fix: apply cargo fmt and rename Window::from_str to parse (clippy) --- Cargo.lock | 71 ++++++++++++++++++++++++ packages/devkit/src/cli/benchmark.rs | 11 +++- packages/devkit/src/cli/export.rs | 25 +++++++-- packages/devkit/src/test_helpers/mod.rs | 8 ++- packages/devkit/tests/cli_export.rs | 15 ++++- packages/devkit/tests/cli_replay.rs | 10 +++- packages/devkit/tests/percentile_prop.rs | 4 +- packages/devkit/tests/spike_prop.rs | 4 +- 8 files changed, 129 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 856e6de..f6b972d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -402,6 +402,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -616,6 +629,12 @@ dependencies = [ "serde", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1292,6 +1311,19 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + [[package]] name = "infer" version = "0.2.3" @@ -1571,6 +1603,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "once_cell" version = "1.21.3" @@ -1750,6 +1788,12 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.4" @@ -2573,7 +2617,9 @@ name = "stellar-devkit" version = "0.1.0" dependencies = [ "axum", + "clap", "criterion", + "indicatif", "rand 0.8.5", "serde", "serde_json", @@ -3006,6 +3052,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -3191,6 +3243,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "whoami" version = "1.6.1" @@ -3287,6 +3349,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" diff --git a/packages/devkit/src/cli/benchmark.rs b/packages/devkit/src/cli/benchmark.rs index 52cb660..67db741 100644 --- a/packages/devkit/src/cli/benchmark.rs +++ b/packages/devkit/src/cli/benchmark.rs @@ -1,4 +1,4 @@ -/// Runs benchmarks against the fee tracker pipeline. +/// Runs benchmarks against the fee tracker pipeline. pub struct Benchmark; impl Benchmark { @@ -27,7 +27,12 @@ impl Benchmark { /// Run all analysis benchmarks and print a summary table. pub fn run_all(fees: &[f64], window: usize, alpha: f64) { println!("=== Benchmark Results ==="); - println!("Input: {} data points, window={}, alpha={}", fees.len(), window, alpha); + println!( + "Input: {} data points, window={}, alpha={}", + fees.len(), + window, + alpha + ); println!(); Self::compare_spike(fees, window, alpha); } @@ -42,4 +47,4 @@ mod tests { let fees: Vec = (1..=10).map(|x| x as f64 * 100.0).collect(); Benchmark::run_all(&fees, 3, 0.3); } -} \ No newline at end of file +} diff --git a/packages/devkit/src/cli/export.rs b/packages/devkit/src/cli/export.rs index 4202a2c..8052ad7 100644 --- a/packages/devkit/src/cli/export.rs +++ b/packages/devkit/src/cli/export.rs @@ -30,7 +30,7 @@ pub enum Window { } impl Window { - pub fn from_str(s: &str) -> Option { + pub fn parse(s: &str) -> Option { match s { "1h" => Some(Self::OneHour), "6h" => Some(Self::SixHours), @@ -55,7 +55,7 @@ pub struct Export; impl Export { /// Filter points by window relative to the latest timestamp. - pub fn filter_window<'a>(points: &'a [FeePoint], window: Window) -> &'a [FeePoint] { + pub fn filter_window(points: &[FeePoint], window: Window) -> &[FeePoint] { match window.cutoff_seconds() { None => points, Some(secs) => { @@ -108,13 +108,28 @@ mod tests { fn pts() -> Vec { vec![ - FeePoint { timestamp: 0, fee: 100, ledger: 1, is_spike: false }, - FeePoint { timestamp: 7200, fee: 200, ledger: 2, is_spike: true }, + FeePoint { + timestamp: 0, + fee: 100, + ledger: 1, + is_spike: false, + }, + FeePoint { + timestamp: 7200, + fee: 200, + ledger: 2, + is_spike: true, + }, ] } fn sample() -> Vec { - vec![FeePoint { timestamp: 1000, fee: 100, ledger: 1, is_spike: false }] + vec![FeePoint { + timestamp: 1000, + fee: 100, + ledger: 1, + is_spike: false, + }] } #[test] diff --git a/packages/devkit/src/test_helpers/mod.rs b/packages/devkit/src/test_helpers/mod.rs index cedb970..cd3c835 100644 --- a/packages/devkit/src/test_helpers/mod.rs +++ b/packages/devkit/src/test_helpers/mod.rs @@ -40,12 +40,16 @@ pub struct FeeGenerator { impl FeeGenerator { /// Create a generator with the given seed. pub fn new(seed: u64) -> Self { - Self { rng: SmallRng::seed_from_u64(seed) } + Self { + rng: SmallRng::seed_from_u64(seed), + } } /// Generate `n` fee values in the range [min_fee, max_fee]. pub fn generate(&mut self, n: usize, min_fee: u64, max_fee: u64) -> Vec { - (0..n).map(|_| self.rng.gen_range(min_fee..=max_fee)).collect() + (0..n) + .map(|_| self.rng.gen_range(min_fee..=max_fee)) + .collect() } /// Generate a flat sequence of `n` identical fees (useful for baseline tests). diff --git a/packages/devkit/tests/cli_export.rs b/packages/devkit/tests/cli_export.rs index 4b10b94..22c8c6b 100644 --- a/packages/devkit/tests/cli_export.rs +++ b/packages/devkit/tests/cli_export.rs @@ -3,7 +3,10 @@ use stellar_devkit::simulation::fee_model::{FeeModel, FeeModelConfig}; #[test] fn export_csv_has_correct_header() { - let config = FeeModelConfig { seed: Some(1), ..Default::default() }; + let config = FeeModelConfig { + seed: Some(1), + ..Default::default() + }; let mut model = FeeModel::new(config); let points = model.generate(5, 0); let csv = Export::to_csv(&points); @@ -15,7 +18,10 @@ fn export_csv_has_correct_header() { #[test] fn export_csv_row_count_matches_input() { - let config = FeeModelConfig { seed: Some(2), ..Default::default() }; + let config = FeeModelConfig { + seed: Some(2), + ..Default::default() + }; let mut model = FeeModel::new(config); let points = model.generate(10, 0); let csv = Export::to_csv(&points); @@ -30,7 +36,10 @@ fn export_csv_row_count_matches_input() { #[test] fn export_csv_columns_are_parseable() { - let config = FeeModelConfig { seed: Some(3), ..Default::default() }; + let config = FeeModelConfig { + seed: Some(3), + ..Default::default() + }; let mut model = FeeModel::new(config); let points = model.generate(1, 1_000); let csv = Export::to_csv(&points); diff --git a/packages/devkit/tests/cli_replay.rs b/packages/devkit/tests/cli_replay.rs index efa1599..17a57df 100644 --- a/packages/devkit/tests/cli_replay.rs +++ b/packages/devkit/tests/cli_replay.rs @@ -2,7 +2,10 @@ use stellar_devkit::simulation::fee_model::{FeeModel, FeeModelConfig}; #[test] fn replay_generates_expected_record_count() { - let config = FeeModelConfig { seed: Some(42), ..Default::default() }; + let config = FeeModelConfig { + seed: Some(42), + ..Default::default() + }; let mut model = FeeModel::new(config); let records = model.generate(100, 0); assert_eq!( @@ -35,7 +38,10 @@ fn replay_records_have_sequential_timestamps() { #[test] fn replay_is_deterministic_with_same_seed() { let make = || { - let cfg = FeeModelConfig { seed: Some(77), ..Default::default() }; + let cfg = FeeModelConfig { + seed: Some(77), + ..Default::default() + }; FeeModel::new(cfg).generate(100, 0) }; let a = make(); diff --git a/packages/devkit/tests/percentile_prop.rs b/packages/devkit/tests/percentile_prop.rs index 83e981d..bb50a4a 100644 --- a/packages/devkit/tests/percentile_prop.rs +++ b/packages/devkit/tests/percentile_prop.rs @@ -1,4 +1,4 @@ -//! Property-style tests for percentile functions. +//! Property-style tests for percentile functions. //! Verifies invariants on arbitrary fee sequences without external proptest crate. use stellar_devkit::analysis::percentile::Percentile; @@ -49,4 +49,4 @@ fn interpolation_p50_of_two_elements_is_midpoint() { let mid = Percentile::linear_interpolation(&data, 50); assert_eq!(mid, (a + b) / 2); } -} \ No newline at end of file +} diff --git a/packages/devkit/tests/spike_prop.rs b/packages/devkit/tests/spike_prop.rs index 7d306c3..494b9b2 100644 --- a/packages/devkit/tests/spike_prop.rs +++ b/packages/devkit/tests/spike_prop.rs @@ -1,4 +1,4 @@ -//! Property-style tests for spike classifier — no false positives on flat sequences. +//! Property-style tests for spike classifier — no false positives on flat sequences. use stellar_devkit::analysis::spike_classifier::SpikeClassifier; @@ -45,4 +45,4 @@ fn spike_detected_when_fee_exceeds_threshold() { let fees = vec![100u64, 200, 100]; let events = SpikeClassifier::detect(&fees, 100); assert!(!events.is_empty()); -} \ No newline at end of file +}