use async_trait::async_trait;
use moonpool_sim::{
ExplorationConfig, SimContext, SimulationBuilder, SimulationReport, SimulationResult, Workload,
};
fn run_simulation(builder: SimulationBuilder) -> SimulationReport {
builder.run()
}
struct AssertOnceWorkload {
message: &'static str,
}
#[async_trait(?Send)]
impl Workload for AssertOnceWorkload {
fn name(&self) -> &str {
"client"
}
async fn run(&mut self, _ctx: &SimContext) -> SimulationResult<()> {
moonpool_sim::assert_sometimes!(true, self.message);
Ok(())
}
}
struct ChildBugWorkload;
#[async_trait(?Send)]
impl Workload for ChildBugWorkload {
fn name(&self) -> &str {
"client"
}
async fn run(&mut self, _ctx: &SimContext) -> SimulationResult<()> {
moonpool_sim::assert_sometimes!(true, "triggers fork");
if moonpool_explorer::explorer_is_child() {
return Err(moonpool_sim::SimulationError::InvalidState(
"simulated bug in child".to_string(),
));
}
Ok(())
}
}
struct TwoGateWorkload;
#[async_trait(?Send)]
impl Workload for TwoGateWorkload {
fn name(&self) -> &str {
"client"
}
async fn run(&mut self, _ctx: &SimContext) -> SimulationResult<()> {
moonpool_sim::assert_sometimes!(true, "first gate");
moonpool_sim::assert_sometimes!(true, "second gate");
Ok(())
}
}
struct ThreeGateWorkload;
#[async_trait(?Send)]
impl Workload for ThreeGateWorkload {
fn name(&self) -> &str {
"client"
}
async fn run(&mut self, _ctx: &SimContext) -> SimulationResult<()> {
moonpool_sim::assert_sometimes!(true, "gate 1");
moonpool_sim::assert_sometimes!(true, "gate 2");
moonpool_sim::assert_sometimes!(true, "gate 3");
Ok(())
}
}
struct PlantedBugWorkload;
#[async_trait(?Send)]
impl Workload for PlantedBugWorkload {
fn name(&self) -> &str {
"client"
}
async fn run(&mut self, _ctx: &SimContext) -> SimulationResult<()> {
moonpool_sim::assert_sometimes!(true, "gate 1");
let gate2 = moonpool_sim::sim_random_range(0u32..10) == 0;
moonpool_sim::assert_sometimes!(gate2, "gate 2");
if gate2 {
let gate3 = moonpool_sim::sim_random_range(0u32..10) == 0;
moonpool_sim::assert_sometimes!(gate3, "gate 3");
if gate3 {
return Err(moonpool_sim::SimulationError::InvalidState(
"planted bug found: all gates passed".to_string(),
));
}
}
Ok(())
}
}
struct EachBucketWorkload;
#[async_trait(?Send)]
impl Workload for EachBucketWorkload {
fn name(&self) -> &str {
"client"
}
async fn run(&mut self, _ctx: &SimContext) -> SimulationResult<()> {
for lock in 0..2 {
for depth in 1..3 {
moonpool_sim::assert_sometimes_each!(
"each_gate",
[("lock", lock as i64), ("depth", depth as i64)]
);
}
}
Ok(())
}
}
#[test]
fn test_exploration_disabled_default() {
let report = run_simulation(SimulationBuilder::new().set_iterations(1).workload(
AssertOnceWorkload {
message: "always passes",
},
));
assert_eq!(report.successful_runs, 1);
assert!(!moonpool_explorer::explorer_is_child());
assert!(
report.exploration.is_none(),
"exploration report should be None when disabled"
);
}
#[test]
fn test_fork_basic() {
let report = run_simulation(
SimulationBuilder::new()
.set_iterations(1)
.enable_exploration(ExplorationConfig {
max_depth: 1,
timelines_per_split: 2,
global_energy: 10,
adaptive: None,
parallelism: None,
})
.workload(AssertOnceWorkload {
message: "triggers fork",
}),
);
assert_eq!(report.successful_runs, 1);
let exp = report.exploration.expect("exploration report missing");
assert!(
exp.total_timelines > 0,
"expected forked children, got total_timelines={}",
exp.total_timelines
);
assert!(
exp.fork_points > 0,
"expected fork points, got fork_points={}",
exp.fork_points
);
}
#[test]
fn test_child_exit_code() {
let report = run_simulation(
SimulationBuilder::new()
.set_iterations(1)
.enable_exploration(ExplorationConfig {
max_depth: 1,
timelines_per_split: 2,
global_energy: 10,
adaptive: None,
parallelism: None,
})
.workload(ChildBugWorkload),
);
assert_eq!(report.successful_runs, 1);
let exp = report.exploration.expect("exploration report missing");
assert!(
exp.bugs_found > 0,
"expected bugs_found > 0, got {}",
exp.bugs_found
);
}
#[test]
fn test_depth_limit() {
let report = run_simulation(
SimulationBuilder::new()
.set_iterations(1)
.enable_exploration(ExplorationConfig {
max_depth: 1,
timelines_per_split: 2,
global_energy: 100,
adaptive: None,
parallelism: None,
})
.workload(TwoGateWorkload),
);
assert_eq!(report.successful_runs, 1);
let exp = report.exploration.expect("exploration report missing");
assert!(
exp.total_timelines <= 4,
"depth limit exceeded: total_timelines={} (expected <= 4)",
exp.total_timelines
);
}
#[test]
fn test_energy_limit() {
let report = run_simulation(
SimulationBuilder::new()
.set_iterations(1)
.enable_exploration(ExplorationConfig {
max_depth: 3,
timelines_per_split: 8,
global_energy: 2,
adaptive: None,
parallelism: None,
})
.workload(ThreeGateWorkload),
);
assert_eq!(report.successful_runs, 1);
let exp = report.exploration.expect("exploration report missing");
assert!(
exp.total_timelines <= 2,
"energy limit exceeded: total_timelines={} (expected <= 2)",
exp.total_timelines
);
}
#[test]
fn test_replay_matches_fork() {
let recipe = vec![(42, 12345), (17, 67890)];
let formatted = moonpool_sim::format_timeline(&recipe);
assert_eq!(formatted, "42@12345 -> 17@67890");
let parsed = moonpool_sim::parse_timeline(&formatted).expect("parse failed");
assert_eq!(recipe, parsed);
let empty = moonpool_sim::format_timeline(&[]);
assert_eq!(empty, "");
let parsed_empty = moonpool_sim::parse_timeline("").expect("parse failed");
assert!(parsed_empty.is_empty());
}
#[test]
fn test_planted_bug() {
let report = run_simulation(
SimulationBuilder::new()
.set_iterations(1)
.enable_exploration(ExplorationConfig {
max_depth: 3,
timelines_per_split: 4,
global_energy: 50,
adaptive: None,
parallelism: None,
})
.workload(PlantedBugWorkload),
);
assert_eq!(report.successful_runs, 1);
let exp = report.exploration.expect("exploration report missing");
assert!(
exp.total_timelines > 0,
"expected some exploration, got total_timelines=0"
);
}
#[test]
fn test_sometimes_each_triggers_fork() {
let report = run_simulation(
SimulationBuilder::new()
.set_iterations(1)
.enable_exploration(ExplorationConfig {
max_depth: 2,
timelines_per_split: 2,
global_energy: 20,
adaptive: None,
parallelism: None,
})
.workload(EachBucketWorkload),
);
assert_eq!(report.successful_runs, 1);
let exp = report.exploration.expect("exploration report missing");
assert!(
exp.total_timelines > 0,
"expected forked children from assert_sometimes_each!, got total_timelines={}",
exp.total_timelines
);
}