diff --git a/Cargo.toml b/Cargo.toml index 125321a..c5925e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ log = "0.4.0" env_logger = "0.9.0" num_cpus = "1.13.1" indicatif = {version = "0.16.2", features = ["rayon"]} -rand = "0.8.4" +rand = "0.8.5" syntect = "4.6.0" handlebars = "4.2.0" md5 = "0.7.0" diff --git a/src/cliarguments.rs b/src/cliarguments.rs index 7df77bb..da605fe 100644 --- a/src/cliarguments.rs +++ b/src/cliarguments.rs @@ -70,6 +70,10 @@ pub enum CLICommand { #[clap(short, long, default_value = "wasmut-report")] output: String, + /// The percentage of all mutants which should be executed + #[clap(short, long, default_value_t = 100)] + sample_threshold: i32, + /// Path to the wasm module wasmfile: String, }, diff --git a/src/main.rs b/src/main.rs index c854ae1..eadfd47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,11 +98,12 @@ fn mutate( config: &Config, report_type: &Output, output_directory: &str, + sample_threshold: i32, ) -> Result<()> { let start = Instant::now(); let module = WasmModule::from_file(wasmfile)?; - let mutator = MutationEngine::new(config)?; + let mutator = MutationEngine::new(config, sample_threshold)?; let mutations = mutator.discover_mutation_positions(&module)?; let executor = Executor::new(config); @@ -234,12 +235,13 @@ fn run_main(cli: CLIArguments) -> Result<()> { wasmfile, threads, config_samedir, + sample_threshold, report, output, } => { let config = load_config(config.as_deref(), Some(&wasmfile), config_samedir)?; init_rayon(threads); - mutate(&wasmfile, &config, &report, &output)?; + mutate(&wasmfile, &config, &report, &output, sample_threshold)?; } CLICommand::NewConfig { path } => { new_config(path)?; diff --git a/src/mutation.rs b/src/mutation.rs index 3128d51..d556acd 100644 --- a/src/mutation.rs +++ b/src/mutation.rs @@ -6,6 +6,7 @@ use crate::{config::Config, policy::MutationPolicy, wasmmodule::WasmModule}; use anyhow::Result; use atomic_counter::AtomicCounter; use atomic_counter::RelaxedCounter; +use rand::distributions::{Distribution, Uniform}; /// Definition of a position where and how a module is mutated. #[derive(Debug, Clone)] @@ -41,14 +42,18 @@ pub struct MutationEngine { /// A list of all operators that are to be enabled. enabled_operators: Vec, + + /// Percentage of mutants that are to be executed + sample_threshold: i32, } impl MutationEngine { /// Create a new `MutationEngine`, based on a configuration. - pub fn new(config: &Config) -> Result { + pub fn new(config: &Config, sample_threshold: i32) -> Result { Ok(Self { mutation_policy: MutationPolicy::from_config(config)?, enabled_operators: config.operators().enabled_operators(), + sample_threshold, }) } @@ -80,6 +85,12 @@ impl MutationEngine { let mutations: Vec = registry .mutants_for_instruction(instruction, &context) .into_iter() + .filter(|_| { + let mut rng = rand::thread_rng(); + let die = Uniform::from(0..=100i32); + let roll = die.sample(&mut rng); + roll <= self.sample_threshold + }) .map(|operator| Mutation { id: id_counter.inc() as i64, operator, @@ -166,7 +177,7 @@ mod tests { let module = WasmModule::from_file("testdata/simple_add/test.wasm")?; let config = Config::default(); - let engine = MutationEngine::new(&config)?; + let engine = MutationEngine::new(&config, 100)?; let positions = engine.discover_mutation_positions(&module).unwrap(); assert!(!positions.is_empty()); @@ -177,7 +188,7 @@ mod tests { fn test_mutation() -> Result<()> { let module = WasmModule::from_file("testdata/simple_add/test.wasm")?; let config = Config::default(); - let engine = MutationEngine::new(&config)?; + let engine = MutationEngine::new(&config, 100)?; let locations = engine.discover_mutation_positions(&module).unwrap(); dbg!(&locations); @@ -196,7 +207,7 @@ mod tests { fn check_number_of_mutants(config: &str) -> usize { let module = WasmModule::from_file("testdata/count_words/test.wasm").unwrap(); let config = Config::parse_file(format!("testdata/count_words/{config}")).unwrap(); - let engine = MutationEngine::new(&config).unwrap(); + let engine = MutationEngine::new(&config, 100).unwrap(); engine.discover_mutation_positions(&module).unwrap().len() } @@ -205,4 +216,23 @@ mod tests { assert_eq!(check_number_of_mutants("wasmut.toml"), 23); Ok(()) } + + #[test] + fn test_sample_threshold() -> Result<()> { + fn check_number_of_mutants(threshold: i32) -> usize { + let module = WasmModule::from_file("testdata/count_words/test.wasm").unwrap(); + let config = Config::parse_file("testdata/count_words/wasmut.toml").unwrap(); + let engine = MutationEngine::new(&config, threshold).unwrap(); + engine.discover_mutation_positions(&module).unwrap().len() + } + + assert_eq!(check_number_of_mutants(100), 23); + + // There is a extremely low probability that this might fail... + // but I don't really see a way to test this without having + // a seedable RNG - and this requires a bit of refactoring + // TODO ! + assert!(check_number_of_mutants(50) < 23); + Ok(()) + } }