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