use clap::{ Arg, Command, Error, Parser, builder::{PossibleValue, TypedValueParser, ValueParserFactory}, command, error::{ContextKind, ContextValue, ErrorKind}, }; use serde::{Deserialize, Serialize}; use crate::profiles::ProfileType; #[derive(Parser, Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)] #[command(name = "limbo-simulator")] #[command(author, version, about, long_about = None)] pub struct SimulatorCLI { #[clap( short, long, help = "set seed for reproducible runs", conflicts_with = "load" )] pub seed: Option, #[clap( short, long, help = "enable doublechecking, run the simulator with the plan twice and check output equality", conflicts_with = "differential" )] pub doublecheck: bool, #[clap( short = 'n', long, help = "change the maximum size of the randomly generated sequence of interactions", default_value_t = normal_or_miri(5000, 50), value_parser = clap::value_parser!(u32).range(1..) )] pub maximum_tests: u32, #[clap( short = 'k', long, help = "change the minimum size of the randomly generated sequence of interactions", default_value_t = normal_or_miri(1000, 10), value_parser = clap::value_parser!(u32).range(1..) )] pub minimum_tests: u32, #[clap( short = 't', long, help = "change the maximum time of the simulation(in seconds)", default_value_t = 60 * 60 // default to 1 hour )] pub maximum_time: usize, #[clap( short = 'l', long, help = "load plan from the bug base", conflicts_with = "seed" )] pub load: Option, #[clap( short = 'w', long, help = "enable watch mode that reruns the simulation on file changes" )] pub watch: bool, #[clap( long, help = "run differential testing between sqlite and Limbo", conflicts_with = "doublecheck" )] pub differential: bool, #[clap( long, help = "enable brute force shrink (warning: it might take a long time)" )] pub enable_brute_force_shrinking: bool, #[clap(subcommand)] pub subcommand: Option, #[clap(long, help = "disable BugBase")] pub disable_bugbase: bool, #[clap(long, help = "disable heuristic shrinking")] pub disable_heuristic_shrinking: bool, #[clap(long, help = "disable UPDATE Statement")] pub disable_update: bool, #[clap(long, help = "disable DELETE Statement")] pub disable_delete: bool, #[clap(long, help = "disable CREATE Statement")] pub disable_create: bool, #[clap(long, help = "disable CREATE INDEX Statement")] pub disable_create_index: bool, #[clap(long, help = "disable DROP Statement")] pub disable_drop: bool, #[clap( long, help = "disable Insert-Values-Select Property", default_value_t = false )] pub disable_insert_values_select: bool, #[clap( long, help = "disable Double-Create-Failure Property", default_value_t = false )] pub disable_double_create_failure: bool, #[clap(long, help = "disable Select-Limit Property")] pub disable_select_limit: bool, #[clap(long, help = "disable Delete-Select Property")] pub disable_delete_select: bool, #[clap(long, help = "disable Drop-Select Property")] pub disable_drop_select: bool, #[clap( long, help = "disable Select-Select-Optimizer Property", default_value_t = false )] pub disable_select_optimizer: bool, #[clap( long, help = "disable Where-True-False-Null Property", default_value_t = false )] pub disable_where_true_false_null: bool, #[clap( long, help = "disable UNION ALL preserves cardinality Property", default_value_t = false )] pub disable_union_all_preserves_cardinality: bool, #[clap(long, help = "disable FsyncNoWait Property", default_value_t = true)] pub disable_fsync_no_wait: bool, #[clap(long, help = "disable FaultyQuery Property")] pub disable_faulty_query: bool, #[clap(long, help = "disable Reopen-Database fault")] pub disable_reopen_database: bool, #[clap(long = "latency-prob", help = "added IO latency probability", value_parser = clap::value_parser!(u8).range(0..=100))] pub latency_probability: Option, #[clap(long, help = "Minimum tick time in microseconds for simulated time")] pub min_tick: Option, #[clap(long, help = "Maximum tick time in microseconds for simulated time")] pub max_tick: Option, #[clap(long, help = "Enable experimental MVCC feature")] pub experimental_mvcc: Option, #[clap(long, help = "Disable experimental indexing feature")] pub disable_experimental_indexes: Option, #[clap( long, help = "Keep all database and plan files", default_value_t = false )] pub keep_files: bool, #[clap( long, help = "Disable the SQLite integrity check at the end of a simulation", default_value_t = normal_or_miri(false, true) )] pub disable_integrity_check: bool, #[clap( long, help = "Use memory IO for complex simulations", default_value_t = false )] pub memory_io: bool, #[clap(long, default_value_t = ProfileType::Default)] /// Profile selector for Simulation run pub profile: ProfileType, } #[derive(Parser, Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)] pub enum SimulatorCommand { #[clap(about = "run the simulator in a loop")] Loop { #[clap( short = 'n', long, help = "number of iterations to run the simulator", default_value_t = 5 )] n: usize, #[clap( short = 's', long, help = "short circuit the simulator, stop on the first failure", default_value_t = false )] short_circuit: bool, }, #[clap(about = "list all the bugs in the base")] List, #[clap(about = "run the simulator against a specific bug")] Test { #[clap( short = 'b', long, help = "run the simulator with previous buggy runs for the specific filter" )] filter: String, }, /// Print profile Json Schema PrintSchema, } impl SimulatorCLI { pub fn validate(&mut self) -> anyhow::Result<()> { if self.watch { anyhow::bail!("watch mode is disabled for now"); } if self.minimum_tests > self.maximum_tests { tracing::warn!( "minimum size '{}' is greater than '{}' maximum size, setting both to '{}'", self.minimum_tests, self.maximum_tests, self.maximum_tests ); self.minimum_tests = self.maximum_tests; } Ok(()) } } #[derive(Clone)] pub struct ProfileTypeParser; impl TypedValueParser for ProfileTypeParser { type Value = ProfileType; fn parse_ref( &self, cmd: &Command, arg: Option<&Arg>, value: &std::ffi::OsStr, ) -> Result { let s = value .to_str() .ok_or_else(|| Error::new(ErrorKind::InvalidUtf8).with_cmd(cmd))?; ProfileType::parse(s).map_err(|_| { let mut err = Error::new(ErrorKind::InvalidValue).with_cmd(cmd); if let Some(arg) = arg { err.insert( ContextKind::InvalidArg, ContextValue::String(arg.to_string()), ); } err.insert( ContextKind::InvalidValue, ContextValue::String(s.to_string()), ); err.insert( ContextKind::ValidValue, ContextValue::Strings( self.possible_values() .unwrap() .map(|s| s.get_name().to_string()) .collect(), ), ); err }) } fn possible_values(&self) -> Option + '_>> { use strum::VariantNames; Some(Box::new( Self::Value::VARIANTS .iter() .map(|variant| { // Custom variant should be listed as a Custom path if variant.eq_ignore_ascii_case("custom") { "CUSTOM_PATH" } else { variant } }) .map(PossibleValue::new), )) } } impl ValueParserFactory for ProfileType { type Parser = ProfileTypeParser; fn value_parser() -> Self::Parser { ProfileTypeParser } } const fn normal_or_miri(normal_val: T, miri_val: T) -> T { if cfg!(miri) { miri_val } else { normal_val } }