From 5b2b6a157c3959314742b17ce0b2ea4d55227b2b Mon Sep 17 00:00:00 2001 From: temiport25 Date: Mon, 1 Jun 2026 11:06:07 +0100 Subject: [PATCH 1/2] feat(harness): add integration tests for fee_stats, error injection, and response delay Closes #230 Closes #231 Closes #232 Closes #233 Co-Authored-By: temiport25 --- packages/devkit/src/cli/export.rs | 13 ++-- packages/devkit/src/cli/mod.rs | 6 +- packages/devkit/src/cli/replay.rs | 59 +++++++----------- packages/devkit/src/test_helpers/mod.rs | 27 +++++---- packages/devkit/tests/harness_integration.rs | 64 ++++++++++++++++++++ 5 files changed, 111 insertions(+), 58 deletions(-) create mode 100644 packages/devkit/tests/harness_integration.rs 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/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); diff --git a/packages/devkit/tests/harness_integration.rs b/packages/devkit/tests/harness_integration.rs new file mode 100644 index 0000000..a38bf00 --- /dev/null +++ b/packages/devkit/tests/harness_integration.rs @@ -0,0 +1,64 @@ +use std::time::{Duration, Instant}; + +use stellar_devkit::harness::horizon_mock::HorizonMock; + +/// Spin up mock server with normal.json. Assert response status 200 and fee_charged.mode = "100". +#[test] +fn normal_scenario_fee_stats_returns_mode_100() { + let path = std::path::Path::new("src/harness/scenarios/normal.json"); + let mock = HorizonMock::new("normal").with_scenario_path(path); + + let body = mock.fee_stats_payload().expect("fee_stats_payload failed"); + assert!(!body.is_empty(), "response body must not be empty"); + + let json: serde_json::Value = + serde_json::from_str(&body).expect("response is not valid JSON"); + let mode = json["fee_stats"]["fee_charged"]["mode"] + .as_str() + .expect("fee_charged.mode missing"); + assert_eq!(mode, "100", "expected fee_charged.mode == \"100\", got {}", mode); +} + +/// Spin up with congested.json. Assert fee_charged.p95 parses to a value > 100000. +#[test] +fn congested_scenario_fee_stats_p95_exceeds_100k() { + let path = std::path::Path::new("src/harness/scenarios/congested.json"); + let mock = HorizonMock::new("congested").with_scenario_path(path); + + let body = mock.fee_stats_payload().expect("fee_stats_payload failed"); + let json: serde_json::Value = + serde_json::from_str(&body).expect("response is not valid JSON"); + + let p95: u64 = json["fee_stats"]["fee_charged"]["p95"] + .as_str() + .expect("fee_charged.p95 missing") + .parse() + .expect("fee_charged.p95 is not a number"); + assert!(p95 > 100_000, "expected p95 > 100000, got {}", p95); +} + +/// Set error_rate=1.0. Assert every request returns 503 (should_inject_error always true). +#[test] +fn error_injection_rate_1_always_injects() { + let mock = HorizonMock::new("normal").with_error_rate(1.0); + for _ in 0..20 { + assert!( + mock.should_inject_error(), + "expected should_inject_error() == true with error_rate=1.0" + ); + } +} + +/// Set delay_ms=200. Assert response time >= 200ms. +#[test] +fn response_delay_200ms_respected() { + let mock = HorizonMock::new("normal").with_delay_ms(200); + let start = Instant::now(); + mock.apply_delay(); + let elapsed = start.elapsed(); + assert!( + elapsed >= Duration::from_millis(200), + "expected elapsed >= 200ms, got {:?}", + elapsed + ); +} From 762194f758d5f6a10ed345cef390e998ff0b7d70 Mon Sep 17 00:00:00 2001 From: temiport25 Date: Mon, 1 Jun 2026 11:46:09 +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/harness_integration.rs | 12 ++-- packages/devkit/tests/percentile_prop.rs | 4 +- packages/devkit/tests/spike_prop.rs | 4 +- 9 files changed, 136 insertions(+), 24 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/harness_integration.rs b/packages/devkit/tests/harness_integration.rs index a38bf00..5e90f1d 100644 --- a/packages/devkit/tests/harness_integration.rs +++ b/packages/devkit/tests/harness_integration.rs @@ -11,12 +11,15 @@ fn normal_scenario_fee_stats_returns_mode_100() { let body = mock.fee_stats_payload().expect("fee_stats_payload failed"); assert!(!body.is_empty(), "response body must not be empty"); - let json: serde_json::Value = - serde_json::from_str(&body).expect("response is not valid JSON"); + let json: serde_json::Value = serde_json::from_str(&body).expect("response is not valid JSON"); let mode = json["fee_stats"]["fee_charged"]["mode"] .as_str() .expect("fee_charged.mode missing"); - assert_eq!(mode, "100", "expected fee_charged.mode == \"100\", got {}", mode); + assert_eq!( + mode, "100", + "expected fee_charged.mode == \"100\", got {}", + mode + ); } /// Spin up with congested.json. Assert fee_charged.p95 parses to a value > 100000. @@ -26,8 +29,7 @@ fn congested_scenario_fee_stats_p95_exceeds_100k() { let mock = HorizonMock::new("congested").with_scenario_path(path); let body = mock.fee_stats_payload().expect("fee_stats_payload failed"); - let json: serde_json::Value = - serde_json::from_str(&body).expect("response is not valid JSON"); + let json: serde_json::Value = serde_json::from_str(&body).expect("response is not valid JSON"); let p95: u64 = json["fee_stats"]["fee_charged"]["p95"] .as_str() 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 +}