1use std::collections::HashMap;
7use std::ops::Range;
8use std::time::{Duration, Instant};
9use tracing::instrument;
10
11use crate::chaos::invariant_trait::Invariant;
12use crate::runner::fault_injector::{FaultInjector, PhaseConfig};
13use crate::runner::workload::Workload;
14
15use super::orchestrator::{IterationManager, MetricsCollector, WorkloadOrchestrator};
16use super::report::{SimulationMetrics, SimulationReport};
17
18#[derive(Debug, Clone)]
22pub enum IterationControl {
23 FixedCount(usize),
25 TimeLimit(Duration),
27}
28
29#[derive(Debug, Clone)]
44pub enum WorkloadCount {
45 Fixed(usize),
47 Random(Range<usize>),
50}
51
52impl WorkloadCount {
53 fn resolve(&self) -> usize {
56 match self {
57 WorkloadCount::Fixed(n) => *n,
58 WorkloadCount::Random(range) => crate::sim::sim_random_range(range.clone()),
59 }
60 }
61}
62
63enum WorkloadEntry {
65 Instance(Option<Box<dyn Workload>>),
67 Factory {
69 count: WorkloadCount,
70 factory: Box<dyn Fn(usize) -> Box<dyn Workload>>,
71 },
72}
73
74pub struct SimulationBuilder {
76 iteration_control: IterationControl,
77 entries: Vec<WorkloadEntry>,
78 seeds: Vec<u64>,
79 use_random_config: bool,
80 invariants: Vec<Box<dyn Invariant>>,
81 fault_injectors: Vec<Box<dyn FaultInjector>>,
82 phase_config: Option<PhaseConfig>,
83 exploration_config: Option<moonpool_explorer::ExplorationConfig>,
84 replay_recipe: Option<super::report::BugRecipe>,
85}
86
87impl Default for SimulationBuilder {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl SimulationBuilder {
94 pub fn new() -> Self {
96 Self {
97 iteration_control: IterationControl::FixedCount(1),
98 entries: Vec::new(),
99 seeds: Vec::new(),
100 use_random_config: false,
101 invariants: Vec::new(),
102 fault_injectors: Vec::new(),
103 phase_config: None,
104 exploration_config: None,
105 replay_recipe: None,
106 }
107 }
108
109 pub fn workload(mut self, w: impl Workload) -> Self {
114 self.entries
115 .push(WorkloadEntry::Instance(Some(Box::new(w))));
116 self
117 }
118
119 pub fn workloads(
137 mut self,
138 count: WorkloadCount,
139 factory: impl Fn(usize) -> Box<dyn Workload> + 'static,
140 ) -> Self {
141 self.entries.push(WorkloadEntry::Factory {
142 count,
143 factory: Box::new(factory),
144 });
145 self
146 }
147
148 pub fn invariant(mut self, i: impl Invariant) -> Self {
150 self.invariants.push(Box::new(i));
151 self
152 }
153
154 pub fn invariant_fn(
156 mut self,
157 name: impl Into<String>,
158 f: impl Fn(&crate::chaos::StateHandle, u64) + 'static,
159 ) -> Self {
160 self.invariants.push(crate::chaos::invariant_fn(name, f));
161 self
162 }
163
164 pub fn fault(mut self, f: impl FaultInjector) -> Self {
166 self.fault_injectors.push(Box::new(f));
167 self
168 }
169
170 pub fn phases(mut self, config: PhaseConfig) -> Self {
172 self.phase_config = Some(config);
173 self
174 }
175
176 pub fn set_iterations(mut self, iterations: usize) -> Self {
178 self.iteration_control = IterationControl::FixedCount(iterations);
179 self
180 }
181
182 pub fn set_iteration_control(mut self, control: IterationControl) -> Self {
184 self.iteration_control = control;
185 self
186 }
187
188 pub fn set_time_limit(mut self, duration: Duration) -> Self {
190 self.iteration_control = IterationControl::TimeLimit(duration);
191 self
192 }
193
194 pub fn set_debug_seeds(mut self, seeds: Vec<u64>) -> Self {
196 self.seeds = seeds;
197 self
198 }
199
200 pub fn random_network(mut self) -> Self {
202 self.use_random_config = true;
203 self
204 }
205
206 pub fn enable_exploration(mut self, config: moonpool_explorer::ExplorationConfig) -> Self {
211 self.exploration_config = Some(config);
212 self
213 }
214
215 pub fn replay_recipe(mut self, recipe: super::report::BugRecipe) -> Self {
220 self.replay_recipe = Some(recipe);
221 self
222 }
223
224 fn resolve_entries(&mut self) -> (Vec<Box<dyn Workload>>, Vec<Option<usize>>) {
229 let mut workloads = Vec::new();
230 let mut return_map = Vec::new();
231
232 for (entry_idx, entry) in self.entries.iter_mut().enumerate() {
233 match entry {
234 WorkloadEntry::Instance(opt) => {
235 if let Some(w) = opt.take() {
236 return_map.push(Some(entry_idx));
237 workloads.push(w);
238 }
239 }
240 WorkloadEntry::Factory { count, factory } => {
241 let n = count.resolve();
242 for i in 0..n {
243 return_map.push(None);
244 workloads.push(factory(i));
245 }
246 }
247 }
248 }
249
250 (workloads, return_map)
251 }
252
253 fn return_entries(
255 &mut self,
256 workloads: Vec<Box<dyn Workload>>,
257 return_map: Vec<Option<usize>>,
258 ) {
259 for (w, slot) in workloads.into_iter().zip(return_map) {
260 if let Some(entry_idx) = slot
261 && let WorkloadEntry::Instance(opt) = &mut self.entries[entry_idx]
262 {
263 *opt = Some(w);
264 }
265 }
267 }
268
269 #[instrument(skip_all)]
270 pub async fn run(mut self) -> SimulationReport {
272 if self.entries.is_empty() {
273 return SimulationReport {
274 iterations: 0,
275 successful_runs: 0,
276 failed_runs: 0,
277 metrics: SimulationMetrics::default(),
278 individual_metrics: Vec::new(),
279 seeds_used: Vec::new(),
280 seeds_failing: Vec::new(),
281 assertion_results: HashMap::new(),
282 assertion_violations: Vec::new(),
283 coverage_violations: Vec::new(),
284 exploration: None,
285 assertion_details: Vec::new(),
286 bucket_summaries: Vec::new(),
287 };
288 }
289
290 let mut iteration_manager =
292 IterationManager::new(self.iteration_control.clone(), self.seeds.clone());
293 let mut metrics_collector = MetricsCollector::new();
294
295 let mut total_exploration_timelines: u64 = 0;
297 let mut total_exploration_fork_points: u64 = 0;
298 let mut total_exploration_bugs: u64 = 0;
299 let mut bug_recipes: Vec<super::report::BugRecipe> = Vec::new();
300
301 if let Err(e) = moonpool_explorer::init_assertions() {
303 tracing::error!("Failed to initialize assertion table: {}", e);
304 }
305
306 if let Some(ref config) = self.exploration_config {
308 moonpool_explorer::set_rng_hooks(crate::sim::get_rng_call_count, |seed| {
309 crate::sim::set_sim_seed(seed);
310 crate::sim::reset_rng_call_count();
311 });
312 if let Err(e) = moonpool_explorer::init(config.clone()) {
313 tracing::error!("Failed to initialize exploration: {}", e);
314 }
315 }
316
317 while iteration_manager.should_continue() {
318 let seed = iteration_manager.next_iteration();
319 let iteration_count = iteration_manager.current_iteration();
320
321 if iteration_count > 1 {
325 if let Some(ref config) = self.exploration_config {
326 moonpool_explorer::prepare_next_seed(config.global_energy);
327 }
328 crate::chaos::assertions::skip_next_assertion_reset();
329 }
330
331 crate::sim::reset_sim_rng();
333 crate::sim::set_sim_seed(seed);
334 crate::chaos::reset_always_violations();
335
336 crate::chaos::buggify_init(0.5, 0.25);
339
340 let (workloads, return_map) = self.resolve_entries();
343
344 let workload_info: Vec<(String, String)> = workloads
346 .iter()
347 .enumerate()
348 .map(|(i, w)| (w.name().to_string(), format!("10.0.0.{}", i + 1)))
349 .collect();
350
351 let network_config = if self.use_random_config {
353 crate::NetworkConfiguration::random_for_seed()
354 } else {
355 crate::NetworkConfiguration::default()
356 };
357
358 let sim = crate::sim::SimWorld::new_with_network_config_and_seed(network_config, seed);
360
361 if let Some(ref br) = self.replay_recipe {
363 crate::sim::set_rng_breakpoints(br.recipe.clone());
364 }
365
366 let start_time = Instant::now();
367
368 let fault_injectors = std::mem::take(&mut self.fault_injectors);
370
371 let orchestration_result = WorkloadOrchestrator::orchestrate_workloads(
373 workloads,
374 fault_injectors,
375 &self.invariants,
376 &workload_info,
377 seed,
378 sim,
379 self.phase_config.as_ref(),
380 iteration_count,
381 )
382 .await;
383
384 match orchestration_result {
385 Ok((returned_workloads, returned_injectors, all_results, sim_metrics)) => {
386 self.return_entries(returned_workloads, return_map);
388 self.fault_injectors = returned_injectors;
389
390 let wall_time = start_time.elapsed();
391 let has_violations = crate::chaos::has_always_violations();
392
393 metrics_collector.record_iteration(
394 seed,
395 wall_time,
396 &all_results,
397 has_violations,
398 sim_metrics,
399 );
400 }
401 Err((faulty_seeds_from_deadlock, failed_count)) => {
402 metrics_collector.add_faulty_seeds(faulty_seeds_from_deadlock);
404 metrics_collector.add_failed_runs(failed_count);
405
406 let assertion_results = crate::chaos::get_assertion_results();
408 let (assertion_violations, coverage_violations) =
409 crate::chaos::validate_assertion_contracts();
410 crate::chaos::buggify_reset();
411
412 return metrics_collector.generate_report(
413 iteration_count,
414 iteration_manager.seeds_used().to_vec(),
415 assertion_results,
416 assertion_violations,
417 coverage_violations,
418 None,
419 Vec::new(),
420 Vec::new(),
421 );
422 }
423 }
424
425 if self.exploration_config.is_some() {
427 if let Some(stats) = moonpool_explorer::get_exploration_stats() {
428 total_exploration_timelines += stats.total_timelines;
429 total_exploration_fork_points += stats.fork_points;
430 total_exploration_bugs += stats.bug_found;
431 }
432 if let Some(recipe) = moonpool_explorer::get_bug_recipe() {
433 bug_recipes.push(super::report::BugRecipe { seed, recipe });
434 }
435 }
436
437 crate::chaos::buggify_reset();
439 }
440
441 let exploration_report = if self.exploration_config.is_some() {
449 let final_stats = moonpool_explorer::get_exploration_stats();
450 let coverage_bits = moonpool_explorer::explored_map_bits_set().unwrap_or(0);
453
454 Some(super::report::ExplorationReport {
455 total_timelines: total_exploration_timelines,
456 fork_points: total_exploration_fork_points,
457 bugs_found: total_exploration_bugs,
458 bug_recipes,
459 energy_remaining: final_stats.as_ref().map(|s| s.global_energy).unwrap_or(0),
460 realloc_pool_remaining: final_stats
461 .as_ref()
462 .map(|s| s.realloc_pool_remaining)
463 .unwrap_or(0),
464 coverage_bits,
465 coverage_total: (moonpool_explorer::coverage::COVERAGE_MAP_SIZE * 8) as u32,
466 })
467 } else {
468 None
469 };
470
471 let assertion_results = crate::chaos::get_assertion_results();
473 let (assertion_violations, coverage_violations) =
474 crate::chaos::validate_assertion_contracts();
475 let raw_assertion_slots = moonpool_explorer::assertion_read_all();
476 let raw_each_buckets = moonpool_explorer::each_bucket_read_all();
477
478 if self.exploration_config.is_some() {
480 moonpool_explorer::cleanup();
481 } else {
482 moonpool_explorer::cleanup_assertions();
483 }
484
485 let assertion_details = build_assertion_details(&raw_assertion_slots);
487
488 let bucket_summaries = build_bucket_summaries(&raw_each_buckets);
490
491 if let Some(ref exp) = exploration_report {
493 eprintln!(
494 "\n--- Exploration ---\n timelines: {} | fork points: {} | bugs: {} | energy left: {} | coverage: {}/{} ({:.1}%)",
495 exp.total_timelines,
496 exp.fork_points,
497 exp.bugs_found,
498 exp.energy_remaining,
499 exp.coverage_bits,
500 exp.coverage_total,
501 if exp.coverage_total > 0 {
502 (exp.coverage_bits as f64 / exp.coverage_total as f64) * 100.0
503 } else {
504 0.0
505 }
506 );
507 for br in &exp.bug_recipes {
508 eprintln!(
509 " bug recipe: {}",
510 moonpool_explorer::format_timeline(&br.recipe)
511 );
512 }
513 }
514
515 let iteration_count = iteration_manager.current_iteration();
517 let (successful_runs, failed_runs) = metrics_collector.current_stats();
518 tracing::info!(
519 "Simulation completed: {}/{} iterations successful",
520 successful_runs,
521 iteration_count
522 );
523 tracing::info!("Seeds used: {:?}", iteration_manager.seeds_used());
524 if failed_runs > 0 {
525 tracing::warn!(
526 "{} iterations failed - check logs above for failing seeds",
527 failed_runs
528 );
529 }
530
531 crate::chaos::buggify_reset();
533
534 metrics_collector.generate_report(
535 iteration_count,
536 iteration_manager.seeds_used().to_vec(),
537 assertion_results,
538 assertion_violations,
539 coverage_violations,
540 exploration_report,
541 assertion_details,
542 bucket_summaries,
543 )
544 }
545}
546
547fn build_assertion_details(
549 slots: &[moonpool_explorer::AssertionSlotSnapshot],
550) -> Vec<super::report::AssertionDetail> {
551 use super::report::{AssertionDetail, AssertionStatus};
552 use moonpool_explorer::AssertKind;
553
554 slots
555 .iter()
556 .filter_map(|slot| {
557 let kind = AssertKind::from_u8(slot.kind)?;
558 let total = slot.pass_count.saturating_add(slot.fail_count);
559
560 if total == 0 && slot.frontier == 0 {
562 return None;
563 }
564
565 let status = match kind {
566 AssertKind::Always
567 | AssertKind::AlwaysOrUnreachable
568 | AssertKind::NumericAlways => {
569 if slot.fail_count > 0 {
570 AssertionStatus::Fail
571 } else {
572 AssertionStatus::Pass
573 }
574 }
575 AssertKind::Sometimes | AssertKind::NumericSometimes => {
576 if slot.pass_count > 0 {
577 AssertionStatus::Pass
578 } else {
579 AssertionStatus::Miss
580 }
581 }
582 AssertKind::Reachable => {
583 if slot.pass_count > 0 {
584 AssertionStatus::Pass
585 } else {
586 AssertionStatus::Miss
587 }
588 }
589 AssertKind::Unreachable => {
590 if slot.pass_count > 0 {
591 AssertionStatus::Fail
592 } else {
593 AssertionStatus::Pass
594 }
595 }
596 AssertKind::BooleanSometimesAll => {
597 if slot.frontier > 0 {
598 AssertionStatus::Pass
599 } else {
600 AssertionStatus::Miss
601 }
602 }
603 };
604
605 Some(AssertionDetail {
606 msg: slot.msg.clone(),
607 kind,
608 pass_count: slot.pass_count,
609 fail_count: slot.fail_count,
610 watermark: slot.watermark,
611 frontier: slot.frontier,
612 status,
613 })
614 })
615 .collect()
616}
617
618fn build_bucket_summaries(
620 buckets: &[moonpool_explorer::EachBucket],
621) -> Vec<super::report::BucketSiteSummary> {
622 use super::report::BucketSiteSummary;
623 use std::collections::HashMap;
624
625 let mut sites: HashMap<u32, BucketSiteSummary> = HashMap::new();
626
627 for bucket in buckets {
628 let entry = sites
629 .entry(bucket.site_hash)
630 .or_insert_with(|| BucketSiteSummary {
631 msg: bucket.msg_str().to_string(),
632 buckets_discovered: 0,
633 total_hits: 0,
634 });
635
636 entry.buckets_discovered += 1;
637 entry.total_hits += bucket.pass_count as u64;
638 }
639
640 let mut summaries: Vec<_> = sites.into_values().collect();
641 summaries.sort_by(|a, b| b.total_hits.cmp(&a.total_hits));
642 summaries
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648 use async_trait::async_trait;
649 use moonpool_core::RandomProvider;
650
651 use crate::SimulationResult;
652 use crate::runner::context::SimContext;
653
654 struct BasicWorkload;
655
656 #[async_trait(?Send)]
657 impl Workload for BasicWorkload {
658 fn name(&self) -> &str {
659 "test_workload"
660 }
661
662 async fn run(&mut self, _ctx: &SimContext) -> SimulationResult<()> {
663 Ok(())
664 }
665 }
666
667 #[test]
668 fn test_simulation_builder_basic() {
669 let local_runtime = tokio::runtime::Builder::new_current_thread()
670 .enable_io()
671 .enable_time()
672 .build_local(Default::default())
673 .expect("Failed to build local runtime");
674
675 let report = local_runtime.block_on(async move {
676 SimulationBuilder::new()
677 .workload(BasicWorkload)
678 .set_iterations(3)
679 .set_debug_seeds(vec![1, 2, 3])
680 .run()
681 .await
682 });
683
684 assert_eq!(report.iterations, 3);
685 assert_eq!(report.successful_runs, 3);
686 assert_eq!(report.failed_runs, 0);
687 assert_eq!(report.success_rate(), 100.0);
688 assert_eq!(report.seeds_used, vec![1, 2, 3]);
689 }
690
691 struct FailingWorkload;
692
693 #[async_trait(?Send)]
694 impl Workload for FailingWorkload {
695 fn name(&self) -> &str {
696 "failing_workload"
697 }
698
699 async fn run(&mut self, ctx: &SimContext) -> SimulationResult<()> {
700 let random_num: u32 = ctx.random().random_range(0..100);
702 if random_num % 2 == 0 {
703 return Err(crate::SimulationError::InvalidState(
704 "Test failure".to_string(),
705 ));
706 }
707 Ok(())
708 }
709 }
710
711 #[test]
712 fn test_simulation_builder_with_failures() {
713 let local_runtime = tokio::runtime::Builder::new_current_thread()
714 .enable_io()
715 .enable_time()
716 .build_local(Default::default())
717 .expect("Failed to build local runtime");
718
719 let report = local_runtime.block_on(async move {
720 SimulationBuilder::new()
721 .workload(FailingWorkload)
722 .set_iterations(10)
723 .run()
724 .await
725 });
726
727 assert_eq!(report.iterations, 10);
728 assert_eq!(
729 report.successful_runs + report.failed_runs,
730 10,
731 "all iterations should be accounted for"
732 );
733 assert!(
734 report.failed_runs > 0,
735 "expected at least one failure across 10 seeds"
736 );
737 assert!(
738 report.successful_runs > 0,
739 "expected at least one success across 10 seeds"
740 );
741 }
742}