Skip to main content

forge/mutation/
orchestrator.rs

1//! Mutation testing orchestrator.
2//!
3//! This module coordinates the mutation testing workflow, including:
4//! - Filtering source files for mutation
5//! - Managing mutation handlers per file
6//! - Running mutations in parallel with caching
7//! - Aggregating results and reporting
8
9use std::{
10    collections::{BTreeMap, BTreeSet, HashSet},
11    path::{Path, PathBuf},
12    sync::{
13        Arc,
14        atomic::{AtomicBool, Ordering},
15    },
16    time::Instant,
17};
18
19use alloy_primitives::keccak256;
20use eyre::{Result, WrapErr};
21use foundry_cli::utils::FoundryPathExt;
22use foundry_common::{compile::ProjectCompiler, sh_println};
23use foundry_compilers::{
24    Language, ProjectCompileOutput,
25    compilers::multi::{MultiCompiler, MultiCompilerLanguage},
26    utils::source_files_iter,
27};
28use foundry_config::{Config, filter::GlobMatcher};
29use foundry_evm::opts::EvmOpts;
30
31use crate::{
32    cmd::test::{FilterArgs, RerunFailure},
33    mutation::{
34        MutationHandler, MutationProgress, MutationReporter, MutationsSummary,
35        mutant::{Mutant, MutationResult},
36        runner::run_mutations_parallel_with_progress,
37        type_analysis::{collect_mutation_exclusions, normalize_path},
38    },
39};
40
41#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Serialize)]
42struct ArtifactCacheFingerprint {
43    source: String,
44    name: String,
45    version: String,
46    build_id: String,
47    profile: String,
48}
49
50#[derive(serde::Serialize)]
51struct ExecutionCacheFingerprint<'a> {
52    schema: &'static str,
53    config: &'a Config,
54    evm_opts: &'a EvmOpts,
55    filter_args: FilterArgsFingerprint<'a>,
56    rerun_failures: Option<&'a [RerunFailure]>,
57    num_workers: usize,
58    artifacts: &'a [ArtifactCacheFingerprint],
59}
60
61#[derive(serde::Serialize)]
62struct FilterArgsFingerprint<'a> {
63    test_pattern: Option<&'a str>,
64    test_pattern_inverse: Option<&'a str>,
65    contract_pattern: Option<&'a str>,
66    contract_pattern_inverse: Option<&'a str>,
67    path_pattern: Option<&'a str>,
68    path_pattern_inverse: Option<&'a str>,
69}
70
71/// Configuration for mutation testing run.
72pub struct MutationRunConfig {
73    /// Paths to mutate (if empty, use all source files).
74    pub mutate_paths: Vec<PathBuf>,
75    /// Optional glob pattern to filter paths.
76    pub mutate_path_pattern: Option<GlobMatcher>,
77    /// Optional contract regex pattern to filter contracts.
78    pub mutate_contract_pattern: Option<regex::Regex>,
79    /// Number of parallel workers (0 = auto-detect).
80    pub num_workers: usize,
81    /// Whether to show progress display.
82    pub show_progress: bool,
83    /// Whether to output JSON (suppress all other output).
84    pub json_output: bool,
85    /// Test filter (`--match-test`, `--match-contract`, `--match-path`, ...)
86    /// applied identically to baseline and every mutant run so they exercise
87    /// the same test set.
88    pub filter_args: FilterArgs,
89    /// Exact contract/test pairs selected by `--rerun`, if present. These are
90    /// not representable by `FilterArgs` alone because rerun stores precise
91    /// suite identifiers in addition to a test-name regex.
92    pub rerun_failures: Option<Vec<RerunFailure>>,
93    /// Project-relative source files selected for the baseline compile.
94    /// Re-rooted into each per-mutant workspace so compilation and execution
95    /// honor the same filtered test universe.
96    pub selected_sources_relative: Vec<PathBuf>,
97    /// EVM isolation flag — mirrors the canonical `forge test` runner so
98    /// baseline and mutant runs use the same execution model.
99    pub isolate: bool,
100}
101
102impl MutationRunConfig {
103    /// Determine number of workers, using auto-detection if 0.
104    pub fn effective_workers(&self) -> usize {
105        if self.num_workers == 0 {
106            std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
107        } else {
108            self.num_workers
109        }
110    }
111}
112
113/// Result of a mutation testing run.
114pub struct MutationRunResult {
115    /// Summary of all mutations across all files.
116    pub summary: MutationsSummary,
117    /// Whether the run was cancelled (e.g., Ctrl+C).
118    pub cancelled: bool,
119    /// Duration of the mutation testing run in seconds.
120    pub duration_secs: f64,
121}
122
123/// Run mutation testing on the project.
124///
125/// This function encapsulates the mutation testing logic that was previously
126/// in the test command. It handles:
127/// - Filtering source files based on patterns
128/// - Per-file mutation handling with caching
129/// - Parallel mutation execution
130/// - Result aggregation and reporting
131pub async fn run_mutation_testing(
132    config: Arc<Config>,
133    output: &ProjectCompileOutput<MultiCompiler>,
134    mut evm_opts: EvmOpts,
135    mutation_config: MutationRunConfig,
136) -> Result<MutationRunResult> {
137    let fork_block = evm_opts.pin_fork_block().await?;
138    let create2_deployer_available = evm_opts.can_use_create2_deployer(fork_block).await?;
139    let num_workers = mutation_config.effective_workers();
140    let json_output = mutation_config.json_output;
141    let artifact_link_references = output.artifact_ids().filter_map(|(id, artifact)| {
142        let source = project_relative_path(&config.root, &id.source)?;
143        let links = artifact
144            .all_link_references()
145            .into_keys()
146            .filter_map(|file| project_relative_path(&config.root, Path::new(&file)))
147            .collect::<BTreeSet<_>>();
148        Some((source, links))
149    });
150    let selected_sources_relative = mutation_compile_sources(
151        mutation_config.selected_sources_relative.iter().cloned(),
152        artifact_link_references,
153    );
154
155    // Determine which paths to mutate
156    let mutate_paths = resolve_mutate_paths(&config, output, &mutation_config)?;
157    let execution_cache_output = ProjectCompiler::new()
158        .dynamic_test_linking(config.dynamic_test_linking)
159        .quiet(json_output)
160        .files(
161            selected_sources_relative
162                .iter()
163                .map(|path| config.root.join(path))
164                .filter(|path| path.exists())
165                .collect::<Vec<_>>(),
166        )
167        .compile(&config.project()?)?;
168    let execution_cache_key = mutation_execution_cache_key(
169        &config,
170        &execution_cache_output,
171        &evm_opts,
172        &mutation_config.filter_args,
173        mutation_config.rerun_failures.as_deref(),
174        num_workers,
175    )?;
176    let mut mutation_exclusions = collect_mutation_exclusions(&config, output).unwrap_or_default();
177
178    if !mutation_config.show_progress && !json_output {
179        sh_println!("Running mutation tests with {} parallel workers...", num_workers)?;
180    }
181
182    let mut mutation_summary = MutationsSummary::new();
183    let mut cancelled = false;
184    let start_time = Instant::now();
185    let cancellation_requested = Arc::new(AtomicBool::new(false));
186    let ctrlc_handle = {
187        let cancellation_requested = Arc::clone(&cancellation_requested);
188        tokio::spawn(async move {
189            if tokio::signal::ctrl_c().await.is_ok() {
190                cancellation_requested.store(true, Ordering::SeqCst);
191            }
192        })
193    };
194
195    for path in mutate_paths {
196        if cancellation_requested.load(Ordering::SeqCst) {
197            cancelled = true;
198            break;
199        }
200
201        if !mutation_config.show_progress && !json_output {
202            sh_println!("Running mutation tests for {}", path.display())?;
203        }
204
205        // Create handler for this file, optionally restricting to a subset of
206        // contracts by name when --mutate-contract is provided.
207        let mut handler = MutationHandler::new(path.clone(), config.clone());
208        if let Some(mutations) = mutation_exclusions.remove(&normalize_path(&path)) {
209            handler = handler.with_mutation_exclusions(mutations);
210        }
211        if let Some(filter) = &mutation_config.mutate_contract_pattern {
212            handler = handler.with_contract_filter(filter.clone());
213        }
214        handler.read_source_contract()?;
215
216        // Get build ID for caching
217        let build_id = output
218            .artifact_ids()
219            .find_map(|(id, _)| (id.source == path).then_some(id.build_id))
220            .unwrap_or_default();
221
222        // Load persisted survived spans before generating/loading mutants so
223        // resumed runs can retain adaptively skipped points as Skipped results
224        // while only executing mutants whose spans still need coverage.
225        handler.retrieve_survived_spans(&build_id, &execution_cache_key);
226
227        // Generate or load cached mutants. Adaptive resume happens after the
228        // full mutant set is known so skipped points are still counted and
229        // reported as Skipped instead of disappearing from totals.
230        let mut mutants = if let Some(ms) = handler.retrieve_cached_mutants(&build_id) {
231            ms
232        } else {
233            handler.generate_ast().await?;
234            handler.mutations.clone()
235        };
236
237        if mutants.is_empty() {
238            if !mutation_config.show_progress && !json_output {
239                sh_println!("  No mutants generated for {}", path.display())?;
240            }
241            continue;
242        }
243
244        // Check for cached results only after the current mutant set is known.
245        // The result cache carries a count/hash of that set so stale or partial
246        // caches cannot suppress newly generated mutants.
247        if let Some(prior) =
248            handler.retrieve_cached_mutant_results(&build_id, &execution_cache_key, &mutants)
249        {
250            if !mutation_config.show_progress && !json_output {
251                sh_println!("  Using cached results for {} mutants", prior.len())?;
252            }
253            for (mutant, status) in prior {
254                match status {
255                    MutationResult::Dead => handler.add_dead_mutant(mutant),
256                    MutationResult::Alive => handler.add_survived_mutant(mutant),
257                    MutationResult::Invalid => handler.add_invalid_mutant(mutant),
258                    MutationResult::Skipped => handler.add_skipped_mutant(mutant),
259                    MutationResult::TimedOut => handler.add_timed_out_mutant(mutant),
260                }
261            }
262            mutation_summary.merge(handler.get_report());
263            continue;
264        }
265
266        // Sort mutations by span for optimal adaptive testing
267        mutants.sort_by(|a, b| {
268            a.span.lo().0.cmp(&b.span.lo().0).then_with(|| b.span.hi().0.cmp(&a.span.hi().0))
269        });
270
271        let (mutants_to_test, skipped_results) =
272            partition_adaptively_skipped_mutants(&mut handler, &mutants);
273
274        // Create progress display if enabled (not in JSON mode)
275        let progress = if mutation_config.show_progress && !json_output {
276            let p = MutationProgress::with_timeout(
277                mutants_to_test.len(),
278                num_workers,
279                config.mutation.timeout,
280            );
281            // Show relative path from project root
282            let display_path =
283                path.strip_prefix(&config.root).unwrap_or(&path).display().to_string();
284            p.set_current_file(&display_path);
285            Some(p)
286        } else if !json_output {
287            sh_println!(
288                "  Generated {} mutants; testing {}, adaptively skipped {}",
289                mutants.len(),
290                mutants_to_test.len(),
291                skipped_results.len()
292            )?;
293            None
294        } else {
295            None
296        };
297
298        // Run mutations in parallel using isolated workspaces
299        let batch = run_mutations_parallel_with_progress(
300            mutants_to_test.clone(),
301            path.clone(),
302            handler.src.clone(),
303            config.clone(),
304            evm_opts.clone(),
305            create2_deployer_available,
306            num_workers,
307            progress.clone(),
308            json_output,
309            mutation_config.filter_args.clone(),
310            mutation_config.rerun_failures.clone(),
311            Arc::new(selected_sources_relative.clone()),
312            mutation_config.isolate,
313            Arc::clone(&cancellation_requested),
314        )?;
315        let file_cancelled = batch.cancelled;
316
317        // Collect results for caching
318        let mut results_vec = Vec::with_capacity(skipped_results.len() + batch.results.len());
319        results_vec.extend(skipped_results);
320        for result in batch.results {
321            results_vec.push((result.mutant.clone(), result.result.clone()));
322            match result.result {
323                MutationResult::Dead => handler.add_dead_mutant(result.mutant),
324                MutationResult::Alive => {
325                    handler.mark_span_survived(result.mutant.span);
326                    handler.add_survived_mutant(result.mutant);
327                }
328                MutationResult::Invalid => handler.add_invalid_mutant(result.mutant),
329                MutationResult::Skipped => handler.add_skipped_mutant(result.mutant),
330                MutationResult::TimedOut => handler.add_timed_out_mutant(result.mutant),
331            }
332        }
333
334        // Detect cancellation early so we can decide whether the result set is
335        // complete before persisting it. Without this guard a Ctrl+C mid-run
336        // would write a *partial* results vector to the cache and the next run
337        // would treat that subset as the full answer for this file.
338        let complete_run = !file_cancelled && results_vec.len() == mutants.len();
339
340        // Persist results for caching only when the run for this file is
341        // complete. Partial caches are silent correctness bugs:
342        //   - cancelled runs would be reloaded as authoritative
343        //   - non-cancelled-but-short result vectors indicate a bug, not a hit
344        // The mutants list itself is fine to persist (it's deterministic from
345        // the AST + operator set) and so are survived spans (best-effort hint).
346        //
347        // Sort the persisted result vector by mutant span so the on-disk
348        // cache is independent of rayon worker completion order; otherwise
349        // the cache file changes content-hash run-to-run even when the
350        // outcomes are identical, defeating diffing and reproducibility.
351        results_vec.sort_by(|(a, _), (b, _)| {
352            a.span.lo().0.cmp(&b.span.lo().0).then_with(|| a.span.hi().0.cmp(&b.span.hi().0))
353        });
354        if !mutants.is_empty() && !build_id.is_empty() {
355            let _ = handler.persist_cached_mutants(&build_id, &mutants);
356            if complete_run {
357                let _ = handler.persist_cached_results(
358                    &build_id,
359                    &execution_cache_key,
360                    &mutants,
361                    &results_vec,
362                );
363            }
364            let _ = handler.persist_survived_spans(&build_id, &execution_cache_key);
365        }
366
367        mutation_summary.merge(handler.get_report());
368
369        // If cancelled, break out of the loop
370        if file_cancelled {
371            cancelled = true;
372            break;
373        }
374    }
375    cancelled |= cancellation_requested.load(Ordering::SeqCst);
376
377    // Report results
378    let duration = start_time.elapsed();
379    let duration_secs = duration.as_secs_f64();
380
381    // Only show human-readable report if not in JSON mode
382    if !json_output {
383        MutationReporter::new().report(&mutation_summary, duration);
384    }
385
386    ctrlc_handle.abort();
387
388    Ok(MutationRunResult { summary: mutation_summary, cancelled, duration_secs })
389}
390
391/// Build the cache discriminator for mutation *results*.
392///
393/// Mutant generation only depends on the source build + selected mutators, but
394/// result correctness depends on the compiled test universe and execution
395/// settings. Hashing the full serialized config intentionally includes fuzz /
396/// invariant settings, test filters, fs permissions, sender/balance/env values,
397/// and future config fields unless explicitly skipped by `Config` itself. The
398/// artifact fingerprint covers the same filter-selected source and test build
399/// IDs that baseline and mutant runs compile. Worker count is included because
400/// adaptive span skipping is concurrency-sensitive.
401fn mutation_execution_cache_key(
402    config: &Config,
403    output: &ProjectCompileOutput<MultiCompiler>,
404    evm_opts: &EvmOpts,
405    filter_args: &FilterArgs,
406    rerun_failures: Option<&[RerunFailure]>,
407    num_workers: usize,
408) -> Result<String> {
409    let artifacts = output
410        .artifact_ids()
411        .map(|(id, _)| ArtifactCacheFingerprint {
412            source: id.source.display().to_string(),
413            name: id.name,
414            version: id.version.to_string(),
415            build_id: id.build_id,
416            profile: id.profile,
417        })
418        .collect::<Vec<_>>();
419    mutation_execution_cache_key_from_parts_with_rerun_failures(
420        config,
421        evm_opts,
422        filter_args,
423        rerun_failures,
424        num_workers,
425        artifacts,
426    )
427}
428
429#[cfg(test)]
430fn mutation_execution_cache_key_from_parts(
431    config: &Config,
432    evm_opts: &EvmOpts,
433    filter_args: &FilterArgs,
434    num_workers: usize,
435    artifacts: Vec<ArtifactCacheFingerprint>,
436) -> Result<String> {
437    mutation_execution_cache_key_from_parts_with_rerun_failures(
438        config,
439        evm_opts,
440        filter_args,
441        None,
442        num_workers,
443        artifacts,
444    )
445}
446
447fn mutation_execution_cache_key_from_parts_with_rerun_failures(
448    config: &Config,
449    evm_opts: &EvmOpts,
450    filter_args: &FilterArgs,
451    rerun_failures: Option<&[RerunFailure]>,
452    num_workers: usize,
453    mut artifacts: Vec<ArtifactCacheFingerprint>,
454) -> Result<String> {
455    artifacts.sort();
456    let fingerprint = ExecutionCacheFingerprint {
457        schema: "mutation-results-v1",
458        config,
459        evm_opts,
460        filter_args: filter_args_fingerprint(filter_args),
461        rerun_failures,
462        num_workers,
463        artifacts: &artifacts,
464    };
465    let encoded = serde_json::to_vec(&fingerprint)
466        .wrap_err("failed to encode mutation execution cache key")?;
467
468    Ok(keccak256(encoded).to_string())
469}
470
471fn filter_args_fingerprint(filter_args: &FilterArgs) -> FilterArgsFingerprint<'_> {
472    FilterArgsFingerprint {
473        test_pattern: filter_args.test_pattern.as_ref().map(|re| re.as_str()),
474        test_pattern_inverse: filter_args.test_pattern_inverse.as_ref().map(|re| re.as_str()),
475        contract_pattern: filter_args.contract_pattern.as_ref().map(|re| re.as_str()),
476        contract_pattern_inverse: filter_args
477            .contract_pattern_inverse
478            .as_ref()
479            .map(|re| re.as_str()),
480        path_pattern: filter_args.path_pattern.as_ref().map(|glob| glob.as_str()),
481        path_pattern_inverse: filter_args.path_pattern_inverse.as_ref().map(|glob| glob.as_str()),
482    }
483}
484
485fn project_relative_path(root: &Path, path: &Path) -> Option<PathBuf> {
486    if path.is_relative() {
487        return Some(path.to_path_buf());
488    }
489
490    if let Ok(stripped) = path.strip_prefix(root) {
491        return Some(stripped.to_path_buf());
492    }
493
494    path.canonicalize().ok()?.strip_prefix(root.canonicalize().ok()?).ok().map(PathBuf::from)
495}
496
497fn mutation_compile_sources(
498    selected_sources: impl IntoIterator<Item = PathBuf>,
499    artifact_link_references: impl IntoIterator<Item = (PathBuf, BTreeSet<PathBuf>)>,
500) -> Vec<PathBuf> {
501    let link_edges = artifact_link_references.into_iter().collect::<BTreeMap<_, _>>();
502    let mut selected_sources_relative = selected_sources.into_iter().collect::<BTreeSet<_>>();
503    let mut queue = selected_sources_relative.iter().cloned().collect::<Vec<_>>();
504
505    while let Some(source) = queue.pop() {
506        if let Some(links) = link_edges.get(&source) {
507            for link in links {
508                if selected_sources_relative.insert(link.clone()) {
509                    queue.push(link.clone());
510                }
511            }
512        }
513    }
514
515    selected_sources_relative.into_iter().collect()
516}
517
518fn partition_adaptively_skipped_mutants(
519    handler: &mut MutationHandler,
520    mutants: &[Mutant],
521) -> (Vec<Mutant>, Vec<(Mutant, MutationResult)>) {
522    let mut skipped_results = Vec::new();
523    let mutants_to_test = mutants
524        .iter()
525        .filter_map(|mutant| {
526            if handler.should_skip_span(mutant.span) {
527                handler.add_skipped_mutant(mutant.clone());
528                skipped_results.push((mutant.clone(), MutationResult::Skipped));
529                None
530            } else {
531                Some(mutant.clone())
532            }
533        })
534        .collect();
535
536    (mutants_to_test, skipped_results)
537}
538
539/// Resolve which paths to mutate based on configuration.
540///
541/// Resolution order:
542/// 1. Pick the *base* set of candidate files:
543///    - `--mutate-path <GLOB>` → all source files matching the glob, OR
544///    - explicit `--mutate PATH...` → those validated files, OR
545///    - default → every Solidity file under `config.src`.
546/// 2. If `--mutate-contract <REGEX>` is set, intersect the base set with files that contain at
547///    least one contract whose name matches the regex. The per-file contract filter still
548///    re-applies inside the handler.
549fn resolve_mutate_paths(
550    config: &Config,
551    output: &ProjectCompileOutput<MultiCompiler>,
552    mutation_config: &MutationRunConfig,
553) -> Result<Vec<PathBuf>> {
554    // 1. Base path set.
555    let base: Vec<PathBuf> = if let Some(pattern) = &mutation_config.mutate_path_pattern {
556        let paths: Vec<_> = source_files_iter(&config.src, MultiCompilerLanguage::FILE_EXTENSIONS)
557            .filter(|entry| entry.is_sol() && !entry.is_sol_test() && pattern.is_match(entry))
558            .collect();
559        if paths.is_empty() {
560            eyre::bail!("no source matched --mutate-path pattern `{pattern}`");
561        }
562        paths
563    } else if !mutation_config.mutate_paths.is_empty() {
564        let root_canon =
565            config.root.canonicalize().wrap_err("failed to canonicalize project root")?;
566        let mut validated = Vec::with_capacity(mutation_config.mutate_paths.len());
567        for path in &mutation_config.mutate_paths {
568            let resolved = if path.is_relative() { config.root.join(path) } else { path.clone() };
569            if !resolved.exists() {
570                eyre::bail!("mutate path does not exist: {}", resolved.display());
571            }
572            if !resolved.is_file() {
573                eyre::bail!("mutate path is not a file: {}", resolved.display());
574            }
575            let canon = resolved
576                .canonicalize()
577                .wrap_err_with(|| format!("failed to canonicalize: {}", resolved.display()))?;
578            if !canon.starts_with(&root_canon) {
579                eyre::bail!("mutate path is outside the project root: {}", resolved.display());
580            }
581            if !canon.is_sol() {
582                eyre::bail!("mutate path is not a Solidity file: {}", resolved.display());
583            }
584            if canon.is_sol_test() {
585                eyre::bail!(
586                    "mutate path is a test file, not a source file: {}",
587                    resolved.display()
588                );
589            }
590            validated.push(canon);
591        }
592        validated
593    } else {
594        source_files_iter(&config.src, MultiCompilerLanguage::FILE_EXTENSIONS)
595            .filter(|entry| entry.is_sol() && !entry.is_sol_test())
596            .collect()
597    };
598
599    // 2. Intersect with `--mutate-contract` if set, so explicit `--mutate <paths>` combined with
600    //    `--mutate-contract <regex>` does the principled thing (the listed files, restricted to
601    //    those containing a matching contract) instead of silently expanding to every source file.
602    let paths = if let Some(contract_pattern) = &mutation_config.mutate_contract_pattern {
603        let matching_sources: HashSet<PathBuf> = output
604            .artifact_ids()
605            .filter_map(|(id, _)| contract_pattern.is_match(&id.name).then_some(id.source.clone()))
606            .collect();
607        let paths: Vec<_> =
608            base.into_iter().filter(|entry| matching_sources.contains(entry)).collect();
609        if paths.is_empty() {
610            if mutation_config.mutate_paths.is_empty()
611                && mutation_config.mutate_path_pattern.is_none()
612            {
613                eyre::bail!("no source matched --mutate-contract pattern `{contract_pattern}`");
614            }
615            eyre::bail!("no source matched --mutate-contract within the selected mutation paths");
616        }
617        paths
618    } else {
619        base
620    };
621
622    Ok(paths)
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628    use std::str::FromStr;
629
630    use crate::mutation::mutant::MutationType;
631    use solar::{ast::Span, interface::BytePos};
632
633    fn artifact(build_id: &str) -> ArtifactCacheFingerprint {
634        ArtifactCacheFingerprint {
635            source: "src/Counter.sol".to_string(),
636            name: "Counter".to_string(),
637            version: "0.8.30".to_string(),
638            build_id: build_id.to_string(),
639            profile: "default".to_string(),
640        }
641    }
642
643    fn filter_args() -> FilterArgs {
644        FilterArgs {
645            test_pattern: None,
646            test_pattern_inverse: None,
647            contract_pattern: None,
648            contract_pattern_inverse: None,
649            path_pattern: None,
650            path_pattern_inverse: None,
651            coverage_pattern_inverse: None,
652        }
653    }
654
655    fn mutant(lo: u32, hi: u32) -> Mutant {
656        Mutant {
657            path: PathBuf::from("src/Counter.sol"),
658            span: Span::new(BytePos(lo), BytePos(hi)),
659            mutation: MutationType::DeleteExpression,
660            original: "number++".to_string(),
661            source_line: "number++;".to_string(),
662            line_number: 1,
663            column_number: 1,
664        }
665    }
666
667    #[test]
668    fn execution_cache_key_changes_when_fuzz_config_changes() {
669        let first = Config::default();
670        let mut second = first.clone();
671        second.fuzz.runs += 1;
672
673        let evm_opts = EvmOpts::default();
674        let filter_args = filter_args();
675        let artifacts = vec![artifact("build-a")];
676
677        let first_key = mutation_execution_cache_key_from_parts(
678            &first,
679            &evm_opts,
680            &filter_args,
681            1,
682            artifacts.clone(),
683        )
684        .unwrap();
685        let second_key =
686            mutation_execution_cache_key_from_parts(&second, &evm_opts, &filter_args, 1, artifacts)
687                .unwrap();
688
689        assert_ne!(first_key, second_key);
690    }
691
692    #[test]
693    fn execution_cache_key_changes_when_evm_options_change() {
694        let config = Config::default();
695        let first = EvmOpts::default();
696        let mut second = first.clone();
697        second.memory_limit = first.memory_limit + 1;
698
699        let filter_args = filter_args();
700        let artifacts = vec![artifact("build-a")];
701
702        let first_key = mutation_execution_cache_key_from_parts(
703            &config,
704            &first,
705            &filter_args,
706            1,
707            artifacts.clone(),
708        )
709        .unwrap();
710        let second_key =
711            mutation_execution_cache_key_from_parts(&config, &second, &filter_args, 1, artifacts)
712                .unwrap();
713
714        assert_ne!(first_key, second_key);
715    }
716
717    #[test]
718    fn execution_cache_key_changes_when_compiled_artifacts_change() {
719        let config = Config::default();
720        let evm_opts = EvmOpts::default();
721        let filter_args = filter_args();
722
723        let first_key = mutation_execution_cache_key_from_parts(
724            &config,
725            &evm_opts,
726            &filter_args,
727            1,
728            vec![artifact("build-a")],
729        )
730        .unwrap();
731        let second_key = mutation_execution_cache_key_from_parts(
732            &config,
733            &evm_opts,
734            &filter_args,
735            1,
736            vec![artifact("build-b")],
737        )
738        .unwrap();
739
740        assert_ne!(first_key, second_key);
741    }
742
743    #[test]
744    fn execution_cache_key_sorts_artifacts_before_hashing() {
745        let config = Config::default();
746        let evm_opts = EvmOpts::default();
747        let filter_args = filter_args();
748
749        let first = vec![artifact("build-a"), artifact("build-b")];
750        let second = vec![artifact("build-b"), artifact("build-a")];
751
752        let first_key =
753            mutation_execution_cache_key_from_parts(&config, &evm_opts, &filter_args, 1, first)
754                .unwrap();
755        let second_key =
756            mutation_execution_cache_key_from_parts(&config, &evm_opts, &filter_args, 1, second)
757                .unwrap();
758
759        assert_eq!(first_key, second_key);
760    }
761
762    #[test]
763    fn execution_cache_key_changes_when_worker_count_changes() {
764        let config = Config::default();
765        let evm_opts = EvmOpts::default();
766        let filter_args = filter_args();
767        let artifacts = vec![artifact("build-a")];
768
769        let first_key = mutation_execution_cache_key_from_parts(
770            &config,
771            &evm_opts,
772            &filter_args,
773            1,
774            artifacts.clone(),
775        )
776        .unwrap();
777        let second_key =
778            mutation_execution_cache_key_from_parts(&config, &evm_opts, &filter_args, 4, artifacts)
779                .unwrap();
780
781        assert_ne!(first_key, second_key);
782    }
783
784    #[test]
785    fn execution_cache_key_changes_when_match_test_filter_changes() {
786        let config = Config::default();
787        let evm_opts = EvmOpts::default();
788        let mut first_filter = filter_args();
789        let mut second_filter = filter_args();
790        first_filter.test_pattern = Some(regex::Regex::new("testA|testAlpha").unwrap());
791        second_filter.test_pattern = Some(regex::Regex::new("testB|testBeta").unwrap());
792        let artifacts = vec![artifact("build-a")];
793
794        let first_key = mutation_execution_cache_key_from_parts(
795            &config,
796            &evm_opts,
797            &first_filter,
798            1,
799            artifacts.clone(),
800        )
801        .unwrap();
802        let second_key = mutation_execution_cache_key_from_parts(
803            &config,
804            &evm_opts,
805            &second_filter,
806            1,
807            artifacts,
808        )
809        .unwrap();
810
811        assert_ne!(first_key, second_key);
812    }
813
814    #[test]
815    fn execution_cache_key_changes_when_match_path_filter_changes() {
816        let config = Config::default();
817        let evm_opts = EvmOpts::default();
818        let mut first_filter = filter_args();
819        let mut second_filter = filter_args();
820        first_filter.path_pattern = Some(GlobMatcher::from_str("test/A.t.sol").unwrap());
821        second_filter.path_pattern = Some(GlobMatcher::from_str("test/B.t.sol").unwrap());
822        let artifacts = vec![artifact("build-a")];
823
824        let first_key = mutation_execution_cache_key_from_parts(
825            &config,
826            &evm_opts,
827            &first_filter,
828            1,
829            artifacts.clone(),
830        )
831        .unwrap();
832        let second_key = mutation_execution_cache_key_from_parts(
833            &config,
834            &evm_opts,
835            &second_filter,
836            1,
837            artifacts,
838        )
839        .unwrap();
840
841        assert_ne!(first_key, second_key);
842    }
843
844    #[test]
845    fn execution_cache_key_changes_when_rerun_failures_change() {
846        let config = Config::default();
847        let evm_opts = EvmOpts::default();
848        let filter_args = filter_args();
849        let first_failures = vec![RerunFailure {
850            contract: "test/Counter.t.sol:WeakTest".to_string(),
851            test: "test_increment()".to_string(),
852        }];
853        let second_failures = vec![RerunFailure {
854            contract: "test/Counter.t.sol:StrongTest".to_string(),
855            test: "test_increment()".to_string(),
856        }];
857        let artifacts = vec![artifact("build-a")];
858
859        let first_key = mutation_execution_cache_key_from_parts_with_rerun_failures(
860            &config,
861            &evm_opts,
862            &filter_args,
863            Some(&first_failures),
864            1,
865            artifacts.clone(),
866        )
867        .unwrap();
868        let second_key = mutation_execution_cache_key_from_parts_with_rerun_failures(
869            &config,
870            &evm_opts,
871            &filter_args,
872            Some(&second_failures),
873            1,
874            artifacts,
875        )
876        .unwrap();
877
878        assert_ne!(first_key, second_key);
879    }
880
881    #[test]
882    fn mutation_compile_sources_only_include_selected_link_reference_closure() {
883        let sources = mutation_compile_sources(
884            [PathBuf::from("test/Selected.t.sol")],
885            [
886                (
887                    PathBuf::from("test/Selected.t.sol"),
888                    BTreeSet::from([PathBuf::from("test/SelectedLinkedHelper.sol")]),
889                ),
890                (
891                    PathBuf::from("test/SelectedLinkedHelper.sol"),
892                    BTreeSet::from([PathBuf::from("test/TransitiveLinkedHelper.sol")]),
893                ),
894                (
895                    PathBuf::from("test/Unrelated.t.sol"),
896                    BTreeSet::from([PathBuf::from("test/UnusedLinkedHelper.sol")]),
897                ),
898            ],
899        );
900
901        assert_eq!(
902            sources,
903            vec![
904                PathBuf::from("test/Selected.t.sol"),
905                PathBuf::from("test/SelectedLinkedHelper.sol"),
906                PathBuf::from("test/TransitiveLinkedHelper.sol"),
907            ]
908        );
909    }
910
911    #[test]
912    fn resumed_adaptive_skips_are_reported_as_skipped_results() {
913        let mut handler =
914            MutationHandler::new(PathBuf::from("src/Counter.sol"), Arc::new(Config::default()));
915        handler.mark_span_survived(Span::new(BytePos(10), BytePos(20)));
916
917        let exact_survivor = mutant(10, 20);
918        let skipped_child = mutant(12, 18);
919        let unrelated = mutant(30, 40);
920        let (mutants_to_test, skipped_results) = partition_adaptively_skipped_mutants(
921            &mut handler,
922            &[exact_survivor.clone(), skipped_child.clone(), unrelated.clone()],
923        );
924
925        assert_eq!(mutants_to_test.len(), 2);
926        assert_eq!(mutants_to_test[0].span, exact_survivor.span);
927        assert_eq!(mutants_to_test[1].span, unrelated.span);
928        assert_eq!(skipped_results.len(), 1);
929        assert!(matches!(skipped_results[0].1, MutationResult::Skipped));
930        assert_eq!(skipped_results[0].0.span, skipped_child.span);
931        assert_eq!(handler.get_report().total_skipped(), 1);
932        assert_eq!(handler.get_report().total_mutants(), 1);
933    }
934}